Metadata-Version: 2.3
Name: cs-djutils
Version: 20250111
Summary: My collection of things for working with Django.
Keywords: python3
Author-email: Cameron Simpson <cs@cskk.id.au>
Description-Content-Type: text/markdown
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Requires-Dist: cs.cmdutils>=20250103
Requires-Dist: cs.gimmicks>=20240316
Requires-Dist: cs.lex>=20250103
Requires-Dist: django
Requires-Dist: typeguard
Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/djutils.py

My collection of things for working with Django.

*Latest release 20250111*:
New model_batches_qs() generator yielding QuerySets for batches of a Model.

## <a name="BaseCommand"></a>Class `BaseCommand(cs.cmdutils.BaseCommand, django.core.management.base.BaseCommand)`

A drop in class for `django.core.management.base.BaseCommand`
which subclasses `cs.cmdutils.BaseCommand`.

This lets me write management commands more easily, particularly
if there are subcommands.

This is a drop in in the sense that you still make a management command
in nearly the same way:

    from cs.djutils import BaseCommand

    class Command(BaseCommand):

and `manage.py` will find it and run it as normal.
But from that point on the style is as for `cs.cmdutils.BaseCommand`:
- no `aegparse` setup
- direct support for subcommands as methods
- succinct option parsing, if you want command line options

A simple command looks like this:

    class Command(BaseCommand):

        def main(self, argv):
            ... do stuff based on the CLI args `argv` ...

A command with subcommands looks like this:

    class Command(BaseCommand):

        def cmd_this(self, argv):
            ... do the "this" subcommand ...

        def cmd_that(self, argv):
            ... do the "that" subcommand ...

If want some kind of app/client specific "overcommand" composed
from other management commands you can import them and make
them subcommands of the overcommand:

    from .other_command import Command as OtherCommand

    class Command(BaseCommand):

        # provide it as the "other" subcommand
        cmd_other = OtherCommand

Option parsing is inline in the command. `self` comes
presupplied with a `.options` attribute which is an instance
of `cs.cmdutils.BaseCommandOptions` (or some subclass).

Parsing options is simple:

    class Command(BaseCommand):

        def cmd_this(self, argv):
            options = self.options
            # parsing options:
            #
            # boolean -x option, makes options.x
            #
            # --thing-limit n option taking an int
            # makes options.thing_limit
            # help text is "Thing limit."
            #
            # a --mode foo option taking a string
            # makes options.mode
            # help text is "The run mode."
            options.popopts(
                argv,
                x=None,
                thing_limit_=int,
                mode_='The run mode.',
            )
            ... now consult options.x or whatever
            ... argv is now the remaining arguments after the options

*`BaseCommand.Options`*

*`BaseCommand.SubCommandClass`*

*`BaseCommand.add_arguments(self, parser)`*:
Add the `Options.COMMON_OPT_SPECS` to the `argparse` parser.
This is basicly to support the Django `call_command` function.

*`BaseCommand.handle(*, argv, **options)`*:
The Django `BaseComand.handle` method.
This creates another instance for `argv` and runs it.

*`BaseCommand.run_from_argv(argv)`*:
Intercept `django.core.management.base.BaseCommand.run_from_argv`.
Construct an instance of `cs.djutils.DjangoBaseCommand` and run it.

## <a name="DjangoSpecificSubCommand"></a>Class `DjangoSpecificSubCommand(cs.cmdutils.SubCommand)`

A subclass of `cs.cmdutils.SubCOmmand` with additional support
for Django's `BaseCommand`.

*`DjangoSpecificSubCommand.__call__(self, argv: List[str])`*:
Run this `SubCommand` with `argv`.
This calls Django's `BaseCommand.run_from_argv` for pure Django commands.

*`DjangoSpecificSubCommand.is_pure_django_command`*:
Whether this subcommand is a pure Django `BaseCommand`.

*`DjangoSpecificSubCommand.usage_text(self, *, cmd=None, **kw)`*:
Return the usage text for this subcommand.

## <a name="model_batches_qs"></a>`model_batches_qs(model, field_name='pk', *, chunk_size=1024, desc=False) -> Iterable[django.db.models.query.QuerySet]`

A generator yielding `QuerySet`s which produce nonoverlapping
batches of model instances.

Efficient behaviour requires the field to be indexed.
Correct behaviour requires the field values to be unique.

Parameters:
* `model`: the `Model` to query
* `field_name`: default `'pk'`, the name of the field on which
  to order the batches
* `chunk_size`: the maximum size of each chunk
* `desc`: default `False`; if true the order the batches in
  descending order instead of ascending order

Example iteration of a `Model` would look like:

    from itertools import chain
    from cs.djutils import model_batches_qs
    for instance in chain.from_iterable(model_batches_qs(MyModel)):
        ... work with instance ...

By returning `QuerySet`s it is possible to further alter each query:

    from cs.djutils import model_batches_qs
    for batch_qs in model_batches_qs(MyModel):
        for result in batch_qs.filter(
            some_field__gt=10
        ).select_related(.......):
            ... work with each result in the batch ...

or:

    from itertools import chain
    from cs.djutils import model_batches_qs
    for result in chain.from_iterable(
        batch_qs.filter(
            some_field__gt=10
        ).select_related(.......)
        for batch_qs in model_batches_qs(MyModel)
    ):
            ... work with each result ...

# Release Log



*Release 20250111*:
New model_batches_qs() generator yielding QuerySets for batches of a Model.

*Release 20241222.3*:
Autocall settings.configure() if required because Django's settings object is a royal PITA.

*Release 20241222.2*:
BaseCommand.Options.settings: call settings.configure() on init if that has not already been done.

*Release 20241222.1*:
Placate the dataclass - upgrade BaseCommand.Options.settings to be a field() with a default_factory.

*Release 20241222*:
BaseCommand.Options: include .settings with the public django.conf.settings names, mostly for cmd_info and cmd_repl.

*Release 20241119*:
New DjangoSpecificSubCommand(CSBaseCommand.SubCommandClass) to include support for pure Django BaseCommands.

*Release 20241111*:
Rename DjangoBaseCommand to just BaseCommand so that we go `from cs.djutils import BaseCommand`. Less confusing.

*Release 20241110*:
Initial PyPI release with DjangoBaseCommand, cs.cmdutils.BaseCommand subclass suppplanting django.core.management.base.BaseCommand.
