M .hgtags +2 -0
@@ 1341,3 1341,5 @@ 8c0b3a5ee50a4ea14c9598442883843191b32ebc
5e2dad99624454df502d77d5d3a1bed20887d209 cs.djutils-20250113.2
afb3a471221e72416f08b81b73507d643b886546 cs.fsm-20250120
8d2f3ed2cef226ffcfe1ed17242686d5ccc5bf8b cs.taskqueue-20250120
+d7680b5a801ead0698a1ecbe9ed7aa916d368132 cs.djutils-20250213
+2bc7c8ecc8ef239d35f23fc3c5804b2b9b91c385 cs.djutils-20250219
M TODO.txt +9 -2
@@ 1,8 1,15 @@
-default: wpr RENEW to scan the spaces configs for the random directories?
+default: sqlite worker - use the queue get_batch mode to collect requests, open sqlite db in bursts
+default: generic sqlite dbm with thread worker to be pulled from ContentCache, into a dbmutils or mappings class
+cs.urlutils: URL: use cs.rfc2616.content_type, content_length
+default: cs.debug.stack_dump vs cs.py/stack.stack_dump
+ContextManagerMixin: document robust recipe for classws which subclass 2 or more classes which subclass ContextManagerMixin, see cs.Later.Later for example
+cs.rfc2616: use cornucopybuffer, update read_chunked
+spaces: try using tilde in spaces_pathfor (tried it, does not work with ~/ or /~/)
+spaces: wpr RENEW to scan the spaces configs for the random directories?
PipeLineSpec.make_stage_funcs: each input (item,P) should become (item,P=P.copy_with_vars(_=item)) so as to have its own set of variables
default: wpr RENEW, fast mode
RunState.__enter__exit__: also set runstate.cancel to the asyncio event loop SIGINT handler?
-cs.binary: genberate .5 manpage entries from classes? maybe just those built from struct formats initially
+cs.binary: generate .5 manpage entries from classes? maybe just those built from struct formats initially
setvar: not expanding leading $MANPATH etc
bin-cs/brew: pass all HOMEBREW_* envvars through to brewexe
cs.jsonutils: merge the various JSON utility functions?
M bin-cs/wpr +7 -7
@@ 23,12 23,11 @@ from icontract import require
from typeguard import typechecked
from cs.app.osx.spaces import Spaces
-from cs.cmdutils import BaseCommand
from cs.fs import needdir, scandirtree, shortpath
from cs.lastvalue import LastValue
from cs.lex import cutprefix
from cs.mappings import missingdict
-from cs.pfx import Pfx, pfx_call
+from cs.pfx import pfx_call
# cached autopopulating mapping of to list of re.Pattern
word_re_map = missingdict(partial(re.compile, flags=re.I))
@@ 103,13 102,13 @@ def main(argv=None):
for wpn, word_list in sorted(last_wp_values(lv).items()):
word_tuple = tuple(word_list)
if word_tuple not in seen_word_tuples:
- wpdir,_=update_random_wpdir(wplinkdirpath, word_tuple, wppath, nwp)
+ wpdir, _ = update_random_wpdir(wplinkdirpath, word_tuple, wppath, nwp)
seen_word_tuples.add(word_tuple)
- spaces.set_wp_dirpath(wpn-1,wpdir)
+ spaces.set_wp_fspath(wpn - 1, wpdir)
else:
- wpdir,_=update_random_wpdir(wplinkdirpath, word_tuple, wppath, nwp)
+ wpdir, _ = update_random_wpdir(wplinkdirpath, word_tuple, wppath, nwp)
for wpn in space_list:
- spaces.set_wp_dirpath(wpn-1,wpdir)
+ spaces.set_wp_fspath(wpn - 1, wpdir)
def last_wp_values(lv) -> dict[int, list[str]]:
''' Return a `dict` mapping space number to a list of words.
@@ 133,7 132,7 @@ def update_random_wpdir(
''' Randomly `count` image paths found in `wppath` matching `words`.
Choose a subdirectory of `wplinkdirpath` based on `words`, making it at need.
Update its contents with symlinks to the chosen paths.
- Return 2-tuple of (wpsublinkdirpath,namemap)` being the
+ Return 2-tuple of `(wpsublinkdirpath,namemap)` being the
subdirectory path and a mapping of names in `linkdirpath` to
absolute forms of `paths`.
'''
@@ 170,6 169,7 @@ def update_linkdir(linkdirpath: str, pat
'''
# TODO: deal with paths which conflict by basename
# TODO: hard link mode?
+ # TODO: move to cs.fs
name_map = {basename(path): abspath(path) for path in paths}
for name, linkpath in sorted(name_map.items()):
linkpath_short = shortpath(linkpath)
M bin/pf-tshow +5 -2
@@ 9,8 9,11 @@ set -ue
cmd=$0
usage="Usage: $cmd [-a|table...]"
+trace=
+[ -t 2 ] && trace=set-x
+
# no table name? recite tables
-[ $# -gt 0 ] || exec pfctl -sT
+[ $# -gt 0 ] || exec $trace pfctl -sT
if [ "x$*" = x-a ]
then
@@ 30,7 33,7 @@ fi
# must be exactly one table name
table=$1; shift
-pfctl -t "$table" -T show -v \
+$trace pfctl -t "$table" -T show -v \
| awk '/^ *[1-9]/ { ip=$1 }
/^\tCleared:/ {
mon=$3;
M bin/pft +17 -9
@@ 1,12 1,12 @@
#!/bin/sh
#
# pft - convenience wrapper for "pfctl(8) -t".
-# - Cameron Simpson <cs@cskk.id.au> 24may2014
+# - Cameron Simpson <cs@cskk.id.au> 24may2014
#
set -ue
-: ${PFT_EXPORT:=$HOME/var/pf/tables}
+: "${PFT_EXPORT:=$HOME/var/pf/tables}"
trace=
[ -t 2 ] && trace=set-x
@@ 14,11 14,19 @@ trace=
cmd=`basename "$0"`
usage="Usage:
$cmd [-a anchor] [op [args...]]
-Operations:
- $cmd cat table...
- $cmd export [-d export_base] [table...]
- $cmd import [-d export_base] [-m {add|replace}] table...
- $cmd [ls] List tables."
+ The default op is \"ls\".
+ -a anchor Specify the pf anchor, default \"\" (the root).
+ Operations:
+ $cmd show table...
+ Show table contents.
+ $cmd export [-d export_base] [table...]
+ Export table contents to files in $PFT_EXPORT (from \$PFT_EXPORT).
+ -d export_base Specify an alterantive export directory.
+ $cmd import [-d export_base] [-m {add|replace}] table...
+ Import table contents from files in $PFT_EXPORT (from \$PFT_EXPORT).
+ -d export_base Specify an alterantive export directory.
+ $cmd [ls]
+ List tables."
badopts=
@@ 48,7 56,7 @@ shift
}
case "$op" in
- cat)
+ show)
for t
do
$trace pfctl -a "$anchor" -t "$t" -T show
@@ 74,7 82,7 @@ case "$op" in
for t
do
tfile=$export_dir/$t.txt
- _pft cat "$t" >"$tfile"
+ _pft show "$t" >"$tfile"
done
;;
import)
M lib/python/cs/app/osx/spaces.py +93 -63
@@ 9,11 9,13 @@ from getopt import GetoptError
import os
from os.path import (
abspath,
+ dirname,
exists as existspath,
isdir as isdirpath,
+ isfile as isfilepath,
join as joinpath,
)
-from pprint import pprint
+from pprint import pformat, pprint
from random import choice as random_choice
import sys
from typing import Optional
@@ 25,11 27,11 @@ from typeguard import typechecked
from cs.cmdutils import BaseCommand
from cs.context import stackattrs
from cs.delta import monitor
-from cs.logutils import warning
-from cs.pfx import Pfx, pfx_call
+from cs.fs import shortpath
+from cs.lex import tabulate
+from cs.pfx import Pfx, pfx_call, pfx_method
from .misc import macos_version
-
from .objc import apple, cg
__version__ = '20250108-post'
@@ 64,6 66,9 @@ DISTINFO = {
CG = apple.CoreGraphics
HI = apple.HIServices
+DEFAULT_BACKGROUND_RGB = 0, 0, 0 # black background
+VALID_IMAGE_SUFFIXES = '.jpg', '.png'
+
def main(argv=None):
''' cs.app.osx.spaces command line mode.
'''
@@ 160,7 165,7 @@ class Spaces:
if space_num < 1:
raise GetoptError("space# counts from 1")
if space_num > len(self):
- raise GetoptError("only %d spaces" % (len(self),))
+ raise GetoptError(f'only {len(self)} spaces')
space_indices = (space_num - 1,)
return space_indices
@@ 197,6 202,7 @@ class Spaces:
self.display_id, 0, space["uuid"]
)
+ @pfx_method
@typechecked
def set_wp_config(self, space_index: int, wp_config: dict):
''' Set the desktop picture configuration of the space at
@@ 213,67 219,93 @@ class Spaces:
space["uuid"],
)
- def set_wp_dirpath(
+ @staticmethod
+ def spaces_pathfor(fspath: str):
+ ''' Return `fspath` adjusted for use in a spaces configuration.
+
+ Prior to MacOS Sonoma (14.5), this just returns the absolute path.
+
+ In MacOS Sonoma there's some hideous bug in the
+ DesktopPictureSetDisplayForSpace library where it seems to
+ see a leading home directory path and replace it with `/~`
+ (instead of something plausible like '~'), perhaps intended
+ for making paths track homedir moves. It turns out that
+ providing a _relative_ path from '/' does The Right Thing.
+ Ugh.
+ '''
+ fspath = abspath(fspath)
+ if macos_version < (14, 5):
+ spaces_path = fspath
+ else:
+ spaces_path = fspath[1:]
+ # a cut at seeing if the ~ _is_ meaningful, but it doesn't work
+ ##home = os.environ.get('HOME')
+ ##if home and isdirpath(home):
+ ## home_prefix = f'{home}/'
+ ## if fspath.startswith(home_prefix):
+ ## spaces_path = f'~/{cutprefix(fspath,home_prefix)}'
+ return spaces_path
+
+ @pfx_method
+ def set_wp_fspath(
self,
space_index: int,
- dirpath: str,
+ fspath: str,
*,
+ background_color=DEFAULT_BACKGROUND_RGB,
random=True,
change_duration=5.0,
placement='SizeToFit',
):
- print("spaces set_wp_dirpath", space_index, dirpath)
- images = [
- filename for filename in os.listdir(dirpath)
- if not filename.startswith('.') and '.' in filename
- ]
- if not images:
- warning("no *.* files in %r", dirpath)
- return 1
- lastname = random_choice(images)
- imagepath = abspath(joinpath(dirpath, lastname))
- if macos_version < (14, 5):
- # This worked before I upgraded to Sonoma, MacOS 14.5.
+ ''' Set the Space configuration of `space_index` to use images from `fspath`.
+ '''
+ print("spaces set_wp_fspath", space_index, fspath)
+ if isfilepath(fspath):
+ # an image file
+ if not fspath.endswith(VALID_IMAGE_SUFFIXES):
+ raise ValueError(
+ f'invalid image path suffix, expected one of {VALID_IMAGE_SUFFIXES!r}'
+ )
+ space_imagepath = self.spaces_pathfor(fspath)
+ dirpath = dirname(abspath(fspath))
+ spaces_dirpath = self.spaces_pathfor(dirpath)
+ wp_config = dict(
+ BackgroundColor=background_color,
+ ChangePath=spaces_dirpath,
+ NewChangePath=spaces_dirpath,
+ ImageFilePath=space_imagepath,
+ Placement=placement,
+ )
+ elif isdirpath(fspath):
+ spaces_dirpath = self.spaces_pathfor(fspath)
+ images = [
+ filename for filename in os.listdir(fspath)
+ if not filename.startswith('.') and '.' in filename
+ and filename.endswith(VALID_IMAGE_SUFFIXES)
+ ]
+ if not images:
+ raise ValueError(
+ f'no *.{{{",".join(VALID_IMAGE_SUFFIXES)}}} files in {fspath}'
+ )
+ lastname = random_choice(images)
+ spaces_imagepath = self.spaces_pathfor(joinpath(fspath, lastname))
# pylint: disable=use-dict-literal
wp_config = dict(
- BackgroundColor=(0, 0, 0),
+ BackgroundColor=background_color,
Change='TimeInterval',
- ChangePath=abspath(dirpath),
- NewChangePath=abspath(dirpath),
+ ChangeDuration=change_duration,
+ ChangePath=spaces_dirpath,
+ NewChangePath=spaces_dirpath,
ChangeTime=change_duration,
DynamicStyle=0,
- ImageFilePath=imagepath,
- NewImageFilePath=imagepath,
- LastName=lastname,
+ ImageFilePath=spaces_imagepath,
+ ##NewImageFilePath=spaces_imagepath,
+ ##LastName=lastname,
Placement=placement,
Random=random,
) # pylint: disable=use-dict-literal
else:
- # MacOS Sonoma onward
- # There's some hideous bug in the DesktopPictureSetDisplayForSpace
- # library where it seems to see a leading home directory path
- # and replace it with '/~' (instead of something plausible like '~'),
- # perhaps intended for making paths track homedir moves.
- # It turns out that providing a _relative_ path from '/'
- # does The Right Thing. Ugh.
- dirpath = abspath(dirpath)
- dirpath = dirpath[1:]
- ##rdirpath = relpath(dirpath, os.environ['HOME'])
- ##if not rdirpath.startswith('../'):
- ## dirpath = rdirpath
- wp_config = dict(
- BackgroundColor=(0, 0, 0),
- Change='TimeInterval',
- ChangePath=dirpath,
- NewChangePath=dirpath,
- ChangeDuration=change_duration,
- DynamicStyle=0,
- ##ImageFilePath=imagepath,
- ##NewImageFilePath=imagepath,
- LastName=lastname,
- Placement=placement,
- Random=random,
- ) # pylint: disable=use-dict-literal
+ raise ValueError('unsupported fspath, neither image file nor directory')
self.set_wp_config(space_index, wp_config)
def monitor_current(self, **kw):
@@ 317,7 349,7 @@ class SpacesCommand(BaseCommand):
Print the current space number.
'''
if argv:
- raise GetoptError("extra arguments: %r" % (argv,))
+ raise GetoptError(f'extra arguments: {argv!r}')
print(self.options.spaces.current_index + 1)
def cmd_monitor(self, argv):
@@ 325,7 357,7 @@ class SpacesCommand(BaseCommand):
Monitor space switches.
'''
if argv:
- raise GetoptError("extra arguments: %r" % (argv,))
+ raise GetoptError(f'extra arguments: {argv!r}')
spaces = self.options.spaces
for old, new, changes in monitor(
lambda: (spaces.forget(), {'index': spaces.current_index})[-1],
@@ 349,26 381,24 @@ class SpacesCommand(BaseCommand):
if not existspath(wp_path):
raise GetoptError("not a file")
if argv:
- raise GetoptError("extra aguments: %r" % (argv,))
+ raise GetoptError(f'extra aguments: {argv!r}')
if wp_path is None:
if space_indices is None:
space_indices = list(range(len(spaces)))
+ report_lines = []
for space_index in space_indices:
+ report_lines.append([f'Space {space_index + 1}'])
space_num = space_index + 1
- print("Space", space_num)
for k, v in sorted(spaces.get_wp_config(space_index).items()):
- print(" ", k, "=", str(v).replace("\n", ""))
+ report_lines.append([f' {k}', pformat(v)])
+ for line in tabulate(*report_lines):
+ print(line)
else:
if space_indices is None:
space_indices = [spaces.current_index]
for space_index in space_indices:
- with Pfx("%d <- %r", space_index + 1, wp_path):
- if isdirpath(wp_path):
- spaces.set_wp_dir(wp_path)
- else:
- # pylint: disable=use-dict-literal
- wp_config = dict(ImageFilePath=abspath(wp_path),)
- spaces.set_wp_config(space_index, wp_config)
+ with Pfx("%d <- %s", space_index + 1, shortpath(wp_path)):
+ spaces.set_wp_fspath(space_index, wp_path)
return 0
def cmd_wpm(self, argv):
@@ 386,7 416,7 @@ class SpacesCommand(BaseCommand):
except ValueError:
# pylint: disable=raise-missing-from
raise GetoptError(
- "expected exactly one space index, got: %r" % (space_indices,)
+ f'expected exactly one space index, got: {space_indices!r}'
)
for old, new, changes in spaces.monitor_wp_config(
space_index=space_index,
M lib/python/cs/app/playon.py +12 -12
@@ 105,6 105,15 @@ DEFAULT_FILENAME_FORMAT = (
'{series_prefix}{series_episode_name}--{resolution}--{playon.ProviderID}--playon--{playon.ID}'
)
+# default "ls" output format
+LS_FORMAT = (
+ '{playon.ID} {playon.HumanSize} {resolution}'
+ ' {nice_name} {playon.ProviderID} {status:upper}'
+)
+
+# default "queue" output format
+QUEUE_FORMAT = '{playon.ID} {playon.Series} {playon.Name} {playon.ProviderID}'
+
# download parallelism
DEFAULT_DL_PARALLELISM = 2
@@ 117,16 126,6 @@ def main(argv=None):
class PlayOnCommand(BaseCommand):
''' Playon command line implementation.
'''
-
- # default "ls" output format
- LS_FORMAT = (
- '{playon.ID} {playon.HumanSize} {resolution}'
- ' {nice_name} {playon.ProviderID} {status:upper}'
- )
-
- # default "queue" output format
- QUEUE_FORMAT = '{playon.ID} {playon.Series} {playon.Name} {playon.ProviderID}'
-
USAGE_KEYWORDS = {
'DEFAULT_DL_PARALLELISM': DEFAULT_DL_PARALLELISM,
'DEFAULT_FILENAME_FORMAT': DEFAULT_FILENAME_FORMAT,
@@ 291,6 290,7 @@ class PlayOnCommand(BaseCommand):
'''
options = self.options
dl_jobs = options.dl_jobs
+ no_download = options.dry_run
sqltags = options.sqltags
if not argv:
argv = ['pending']
@@ 467,7 467,7 @@ class PlayOnCommand(BaseCommand):
-o format Format string for each entry.
Default format: {LS_FORMAT}
'''
- return self._list(argv, self.options, ['available'], self.LS_FORMAT)
+ return self._list(argv, self.options, ['available'], LS_FORMAT)
def cmd_poll(self, argv):
if argv:
@@ 482,7 482,7 @@ class PlayOnCommand(BaseCommand):
-o format Format string for each entry.
Default format: {QUEUE_FORMAT}
'''
- return self._list(argv, self.options, ['queued'], self.QUEUE_FORMAT)
+ return self._list(argv, self.options, ['queued'], QUEUE_FORMAT)
cmd_q = cmd_queue
M lib/python/cs/djutils.py +40 -6
@@ 10,15 10,17 @@
from dataclasses import dataclass, field
from inspect import isclass
+from itertools import chain
import os
import sys
-from typing import Iterable, List
+from typing import Iterable, List, Mapping
from django.conf import settings
from django.core.management.base import (
BaseCommand as DjangoBaseCommand,
CommandError as DjangoCommandError,
)
+from django.db.models import Model
from django.db.models.query import QuerySet
from django.utils.functional import empty as djf_empty
@@ 28,7 30,7 @@ from cs.cmdutils import BaseCommand as C
from cs.gimmicks import warning
from cs.lex import cutprefix, stripped_dedent
-__version__ = '20250113.2-post'
+__version__ = '20250219-post'
DISTINFO = {
'keywords': ["python3"],
@@ 228,20 230,25 @@ class BaseCommand(CSBaseCommand, DjangoB
parser.add_argument('argv', nargs='*')
def model_batches_qs(
- model,
+ model: Model,
field_name='pk',
*,
chunk_size=1024,
desc=False,
exclude=None,
filter=None,
+ only=None,
) -> Iterable[QuerySet]:
''' A generator yielding `QuerySet`s which produce nonoverlapping
- batches of model instances.
+ batches of `Model` instances.
Efficient behaviour requires the field to be indexed.
Correct behaviour requires the field values to be unique.
+ See `model_instances` for an iterable of instances wrapper
+ of this function, where you have no need to further amend the
+ `QuerySet`s or to be aware of the batches.
+
Parameters:
* `model`: the `Model` to query
* `field_name`: default `'pk'`, the name of the field on which
@@ 251,6 258,7 @@ def model_batches_qs(
descending order instead of ascending order
* `exclude`: optional mapping of Django query terms to exclude by
* `filter`: optional mapping of Django query terms to filter by
+ * `only`: optional sequence of field names for a Django query `.only()`
Example iteration of a `Model` would look like:
@@ 289,8 297,18 @@ def model_batches_qs(
qs0 = mgr.all()
if exclude:
qs0 = qs0.exclude(**exclude)
- if filter:
- qs0 = qs0.filter(**filter)
+ if exclude is not None:
+ if isinstance(exclude, Mapping):
+ qs0 = qs0.exclude(**exclude)
+ else:
+ qs0 = qs0.exclude(exclude)
+ if filter is not None:
+ if isinstance(filter, Mapping):
+ qs0 = qs0.filter(**filter)
+ else:
+ qs0 = qs0.filter(filter)
+ if only is not None:
+ qs0 = qs0.only(*only)
qs = qs0.order_by(ordering)[:chunk_size]
while True:
key_list = list(qs.only(field_name).values_list(field_name, flat=True))
@@ 301,3 319,19 @@ def model_batches_qs(
qs = qs0.filter(**{
after_condition: end_key
}).order_by(ordering)[:chunk_size]
+
+def model_instances(
+ model: Model,
+ field_name='pk',
+ only=None,
+ **mbqs_kw,
+) -> Iterable[Model]:
+ ''' A generator yielding Model instances.
+ This is a wrapper for `model_batches_qs` and accepts the same arguments.
+
+ Efficient behaviour requires the field to be indexed.
+ Correct behaviour requires the field values to be unique.
+ '''
+ return chain.from_iterable(
+ model_batches_qs(model, field_name=field_name, **mbqs_kw)
+ )
M pkg_tags +2 -1
@@ 34,7 34,7 @@ cs.dateutils ok_revision="8d0cd6ae028e8f
cs.debug pypi.release="20241005" ok_revision=fb49ab3bf51884d4c570c46375fe17fba3a742e9 features={"20230613":["patch_builtins"],"20241005":["CS_DEBUG_TRACE","abrk","log_via_print"]}
cs.deco pypi.release="20250103" ok_revision="2eb65f1a9304f2443a2433cb3b376306bb12b695" features={"20220905":["ALL","default_params"],"20221106":["promote"],"20221106.1":["promote_optional"],"20230210":["promote_dot_as_typename"],"20230212":["Promotable"],"20241003":["uses_cmd_option"],"20241109":["uses_doit","uses_force","uses_quiet","uses_verbose"]}
cs.delta ok_revision=b5ee8df7f2f919ad4881022e26959dfc74653967 pypi.release="20240622"
-cs.djutils ok_revision=b5f554963d8aec8658f6704e0f1c0f9832f3d71f features={"20241110":["DjangoBaseCommand"],"20241119":["DjangoSpecificSubCommand"],"20241222":["options_settings"],"20250111":["model_batches_qs"]} pypi.release="20250113.2"
+cs.djutils ok_revision=a3397b32d6df1d7c07c9530c905da6dfc303b8fa features={"20241110":["DjangoBaseCommand"],"20241119":["DjangoSpecificSubCommand"],"20241222":["options_settings"],"20250111":["model_batches_qs"],"20250213":["model_instances"]} pypi.release="20250219"
cs.dockerutils ok_revision=f846ece6e9f9353b443df14f81512ae6438681e6 pypi.release="20240519" features={"20240305":["DockerRun__network"]}
cs.ebooks ok_revision="3379fcbf1eb008df3cd38e9afd40f1d0db9afbd4" pypi.release="20241007" features={"20230110":["dedrm"],"20240201.4":["cbz","kobo","pdf"]}
cs.edit pypi.release="20220429" ok_revision="515bc888f37576690b3eb672b69e1a573a0d8b65" features={"20220429":["edit_obj"]}
@@ 103,6 103,7 @@ cs.threads ok_revision=b3fb539982274055e
cs.timeseries ok_revision="795e4c4be59054f58d2c7e32e8a24f652c51cfb8" pypi.release="20240316"
cs.timeutils
cs.typingutils ok_revision="3654853d2facea604cfef62988be6ba53a262166" features={"20230331":["subtype"]} pypi.release="20230331"
+cs.units
cs.upd ok_revision="08f7d62b98d3c1b2532c4b8c9199a0c756914c6c" pypi.release="20240630" features={"20230212":["with_upd_proxy"],"20230217":["Upd_MultiOpenMixin"],"20240216":["without"],"20240412":["without"]}
cs.urlutils ok_revision="7eeaa1d7ef5d4e82b88e9d614e8646d80a5344e7" pypi.release="20231129"
cs.x ok_revision="68e0c6027afe808d188c53ab5ef6c9285f29067b" pypi.release="20240630" features={"20231129":["via_logger"]}
A => release/cs.djutils-20250213/CHANGES.txt +1 -0
@@ 0,0 1,1 @@
+lib/python/cs/djutils.py: cs.djutils: new model_instances() wrapper for model_batches_qs() returning an iterable of Model instance
A => release/cs.djutils-20250213/SUMMARY.txt +1 -0
@@ 0,0 1,1 @@
+New model_instances() wrapper for model_batches_qs() returning an iterable of Model instances.
A => release/cs.djutils-20250219/CHANGES.txt +2 -0
@@ 0,0 1,2 @@
+lib/python/cs/djutils.py: cs.djutils: model_batches_qs: accept a nonmapping for exclude= or filter= eg a Q() function
+lib/python/cs/djutils.py: cs.djutils: model_batches_qs: new optional only= parameter
A => release/cs.djutils-20250219/SUMMARY.txt +2 -0
@@ 0,0 1,2 @@
+model_batches_qs: accept a nonmapping for exclude= or filter= eg a Q() function.
+model_batches_qs: new optional only= parameter.