Skip to content

API

init module.

in_this_repo module-attribute

in_this_repo = Path.cwd().name == snake_to_kebab_case(
    winidjango.__name__
)

installed_apps module-attribute

installed_apps = [tests_package.__name__]

logger module-attribute

logger = logging.getLogger(__name__)

tests_package module-attribute

tests_package = import_module_with_file_fallback(
    path=ProjectTester.I.package_root(),
    name=ProjectTester.I.package_name(),
)

ProjectTester

Bases: ProjectTester

Subclass of ProjectTester for customizing pyrig behavior.

dev_dependencies

dev_dependencies() -> tuple[str, ...]

Get the dev dependencies.

Source code in src/winidjango/rig/tools/tools.py
def dev_dependencies(self) -> tuple[str, ...]:
    """Get the dev dependencies."""
    return (*super().dev_dependencies(), "pytest-django")

core

src package.

This package exposes the project's internal modules used by the command-line utilities and database helpers. It exists primarily so that code under winidjango/src can be imported using the winidjango.core package path in other modules and tests.

The package itself contains the following subpackages: - commands - management command helpers and base classes - db - database utilities and model helpers

Consumers should import the specific submodules they need rather than relying on side effects from this package's import-time execution.

commands

Utilities and base classes for management commands.

The commands package contains base command classes and helpers used to implement Django management commands in the project. Subpackages and modules under commands provide reusable patterns for argument handling, logging and common command behaviors so that individual commands can focus on their business logic.

base

Base helpers and abstractions for management commands.

This package provides a common abstract base class used by project management commands. The base class centralizes argument handling, standard options (dry-run, batching, timeouts, etc.) and integrates logging behavior used throughout the commands package.

base

Utilities and an abstract base class for Django management commands.

This module defines :class:ABCBaseCommand, a reusable abstract base that combines Django's BaseCommand with the project's logging mixins and standard argument handling. The base class implements a template method pattern so concrete commands only need to implement the abstract extension points for providing command-specific arguments and business logic.

ABCBaseCommand

Bases: ABCLoggingMixin, BaseCommand

Abstract base class for management commands with logging and standard options.

The class wires common behavior such as base arguments (dry-run, batching, timeouts) and provides extension points that concrete commands must implement: :meth:add_command_arguments and :meth:handle_command.

Notes
  • Inheritance order matters: the logging mixin must precede BaseCommand so mixin initialization occurs as expected.
  • The base class follows the template method pattern; concrete commands should not override :meth:add_arguments or :meth:handle but implement the abstract hooks instead.
Options

Just a container class for hard coding the option keys.

add_arguments
add_arguments(parser: ArgumentParser) -> None

Configure command-line arguments for the command.

Adds common base arguments (dry-run, force, delete, timeout, batching and concurrency options) and then delegates to :meth:add_command_arguments for command-specific options.

Parameters:

Name Type Description Default
parser ArgumentParser

The argument parser passed by Django.

required
Source code in src/winidjango/core/commands/base/base.py
def add_arguments(self, parser: ArgumentParser) -> None:
    """Configure command-line arguments for the command.

    Adds common base arguments (dry-run, force, delete, timeout,
    batching and concurrency options) and then delegates to
    :meth:`add_command_arguments` for command-specific options.

    Args:
        parser (ArgumentParser): The argument parser passed by Django.
    """
    # add base args that are used in most commands
    self.base_add_arguments(parser)

    # add additional args that are specific to the command
    self.add_command_arguments(parser)
add_command_arguments abstractmethod
add_command_arguments(parser: ArgumentParser) -> None

Define command-specific arguments.

Implement this hook to add options and positional arguments that are specific to the concrete management command.

Parameters:

Name Type Description Default
parser ArgumentParser

The argument parser passed by Django.

required
Source code in src/winidjango/core/commands/base/base.py
@abstractmethod
def add_command_arguments(self, parser: ArgumentParser) -> None:
    """Define command-specific arguments.

    Implement this hook to add options and positional arguments that are
    specific to the concrete management command.

    Args:
        parser (ArgumentParser): The argument parser passed by Django.
    """
base_add_arguments
base_add_arguments(parser: ArgumentParser) -> None

Add the project's standard command-line arguments to parser.

Parameters:

Name Type Description Default
parser ArgumentParser

The argument parser passed by Django.

required
Source code in src/winidjango/core/commands/base/base.py
def base_add_arguments(self, parser: ArgumentParser) -> None:
    """Add the project's standard command-line arguments to ``parser``.

    Args:
        parser (ArgumentParser): The argument parser passed by Django.
    """
    parser.add_argument(
        f"--{self.Options.DRY_RUN}",
        action="store_true",
        help="Show what would be done without actually executing the changes",
    )

    parser.add_argument(
        f"--{self.Options.FORCE}",
        action="store_true",
        help="Force an action in a command",
    )

    parser.add_argument(
        f"--{self.Options.DELETE}",
        action="store_true",
        help="Deleting smth in a command",
    )

    parser.add_argument(
        f"--{self.Options.YES}",
        action="store_true",
        help="Answer yes to all prompts",
        default=False,
    )

    parser.add_argument(
        f"--{self.Options.TIMEOUT}",
        type=int,
        help="Timeout for a command",
        default=None,
    )

    parser.add_argument(
        f"--{self.Options.BATCH_SIZE}",
        type=int,
        default=None,
        help="Number of items to process in each batch",
    )

    parser.add_argument(
        f"--{self.Options.THREADS}",
        type=int,
        default=None,
        help="Number of threads to use for processing",
    )

    parser.add_argument(
        f"--{self.Options.PROCESSES}",
        type=int,
        default=None,
        help="Number of processes to use for processing",
    )
base_handle
base_handle(*args: Any, **options: Any) -> None

Perform common pre-processing for commands.

Stores the incoming arguments and options on the instance for use by :meth:handle_command and subclasses.

Parameters:

Name Type Description Default
*args Any

Positional arguments forwarded from Django.

()
**options Any

Parsed command-line options.

{}
Source code in src/winidjango/core/commands/base/base.py
def base_handle(self, *args: Any, **options: Any) -> None:  # noqa: ANN401
    """Perform common pre-processing for commands.

    Stores the incoming arguments and options on the instance for use
    by :meth:`handle_command` and subclasses.

    Args:
        *args: Positional arguments forwarded from Django.
        **options: Parsed command-line options.
    """
    self.args = args
    self.options = options
get_option
get_option(option: str) -> Any

Retrieve a parsed command option by key.

Parameters:

Name Type Description Default
option str

The option key to retrieve from self.options.

required

Returns:

Name Type Description
Any Any

The value for the requested option. If the option is not present a KeyError will be raised (matching how Django exposes options in management commands).

Source code in src/winidjango/core/commands/base/base.py
def get_option(self, option: str) -> Any:  # noqa: ANN401
    """Retrieve a parsed command option by key.

    Args:
        option (str): The option key to retrieve from ``self.options``.

    Returns:
        Any: The value for the requested option. If the option is not
            present a ``KeyError`` will be raised (matching how Django
            exposes options in management commands).
    """
    return self.options[option]
handle
handle(*args: Any, **options: Any) -> None

Orchestrate command execution.

Performs shared pre-processing by calling :meth:base_handle and then delegates to :meth:handle_command which must be implemented by subclasses.

Parameters:

Name Type Description Default
*args Any

Positional arguments forwarded from Django.

()
**options Any

Parsed command-line options.

{}
Source code in src/winidjango/core/commands/base/base.py
def handle(self, *args: Any, **options: Any) -> None:  # noqa: ANN401
    """Orchestrate command execution.

    Performs shared pre-processing by calling :meth:`base_handle` and
    then delegates to :meth:`handle_command` which must be implemented
    by subclasses.

    Args:
        *args: Positional arguments forwarded from Django.
        **options: Parsed command-line options.
    """
    self.base_handle(*args, **options)
    self.handle_command()
handle_command abstractmethod
handle_command() -> None

Run the command-specific behavior.

This abstract hook should be implemented by concrete commands to perform the command's main work. Implementations should read self.args and self.options which were set in :meth:base_handle.

Source code in src/winidjango/core/commands/base/base.py
@abstractmethod
def handle_command(self) -> None:
    """Run the command-specific behavior.

    This abstract hook should be implemented by concrete commands to
    perform the command's main work. Implementations should read
    ``self.args`` and ``self.options`` which were set in
    :meth:`base_handle`.
    """

import_data

Import command base class and utilities.

This module defines a reusable base command for importing tabular data into Django models. Implementations should provide a concrete source ingestion (for example, reading from CSV or an external API), a cleaning/normalization step implemented by a CleaningDF subclass, and mapping logic that groups cleaned data into model instances that can be bulk-created.

The base command centralizes the typical flow: 1. Read raw data (handle_import) 2. Wrap and clean the data using a CleaningDF subclass 3. Convert the cleaned frame into per-model bulks 4. Persist bulks using the project's bulk create helpers

Using this base class ensures a consistent import lifecycle and reduces duplicated boilerplate across different import implementations.

ImportDataBaseCommand

Bases: ABCBaseCommand

Abstract base for data-import Django management commands.

Subclasses must implement the ingestion, cleaning-class selection, and mapping of cleaned rows to Django model instances. The base implementation wires these pieces together and calls the project's bulk creation helper to persist the data.

Implementors typically only need to override the three abstract methods documented below.

get_bulks_by_model abstractmethod
get_bulks_by_model(
    df: DataFrame,
) -> dict[type[Model], Iterable[Model]]

Map the cleaned DataFrame to model-instance bulks.

The implementation should inspect the cleaned DataFrame and return a mapping where keys are Django model classes and values are iterables of unsaved model instances (or dataclass-like objects accepted by the project's bulk-creation utility).

Parameters:

Name Type Description Default
df DataFrame

The cleaned and normalized DataFrame.

required

Returns:

Type Description
dict[type[Model], Iterable[Model]]

dict[type[Model], Iterable[Model]]: Mapping from model classes to iterables of instances that should be created.

Source code in src/winidjango/core/commands/import_data.py
@abstractmethod
def get_bulks_by_model(
    self,
    df: pl.DataFrame,
) -> dict[type[Model], Iterable[Model]]:
    """Map the cleaned DataFrame to model-instance bulks.

    The implementation should inspect the cleaned DataFrame and
    return a mapping where keys are Django model classes and values
    are iterables of unsaved model instances (or dataclass-like
    objects accepted by the project's bulk-creation utility).

    Args:
        df (pl.DataFrame): The cleaned and normalized DataFrame.

    Returns:
        dict[type[Model], Iterable[Model]]: Mapping from model classes
            to iterables of instances that should be created.
    """
get_cleaning_df_cls abstractmethod
get_cleaning_df_cls() -> type[CleaningDF]

Return the CleaningDF subclass used to normalize the data.

The returned class will be instantiated with the raw DataFrame returned from :meth:handle_import and must provide the transformations required to prepare data for mapping into model instances.

Returns:

Type Description
type[CleaningDF]

type[CleaningDF]: A subclass of CleaningDF that performs the necessary normalization and validation.

Source code in src/winidjango/core/commands/import_data.py
@abstractmethod
def get_cleaning_df_cls(self) -> type[CleaningDF]:
    """Return the `CleaningDF` subclass used to normalize the data.

    The returned class will be instantiated with the raw DataFrame
    returned from :meth:`handle_import` and must provide the
    transformations required to prepare data for mapping into model
    instances.

    Returns:
        type[CleaningDF]: A subclass of ``CleaningDF`` that performs
            the necessary normalization and validation.
    """
handle_command
handle_command() -> None

Execute the full import lifecycle.

This template method reads raw data via :meth:handle_import, wraps it with the cleaning class returned by :meth:get_cleaning_df_cls and then persists the resulting model bulks returned by :meth:get_bulks_by_model.

Source code in src/winidjango/core/commands/import_data.py
def handle_command(self) -> None:
    """Execute the full import lifecycle.

    This template method reads raw data via :meth:`handle_import`,
    wraps it with the cleaning class returned by
    :meth:`get_cleaning_df_cls` and then persists the resulting
    model bulks returned by :meth:`get_bulks_by_model`.
    """
    data_df = self.handle_import()

    cleaning_df_cls = self.get_cleaning_df_cls()
    self.cleaning_df = cleaning_df_cls(data_df)

    self.import_to_db()
handle_import abstractmethod
handle_import() -> DataFrame

Read raw data from the import source.

This method should read data from whatever source the concrete command targets (files, remote APIs, etc.) and return it as a polars.DataFrame. No cleaning should be performed here; cleaning is handled by the cleaning CleaningDF returned from get_cleaning_df_cls.

Returns:

Type Description
DataFrame

pl.DataFrame: Raw (uncleaned) tabular data to be cleaned and mapped to model instances.

Source code in src/winidjango/core/commands/import_data.py
@abstractmethod
def handle_import(self) -> pl.DataFrame:
    """Read raw data from the import source.

    This method should read data from whatever source the concrete
    command targets (files, remote APIs, etc.) and return it as a
    ``polars.DataFrame``. No cleaning should be performed here;
    cleaning is handled by the cleaning `CleaningDF` returned from
    ``get_cleaning_df_cls``.

    Returns:
        pl.DataFrame: Raw (uncleaned) tabular data to be cleaned and
            mapped to model instances.
    """
import_to_db
import_to_db() -> None

Persist prepared model bulks to the database.

Calls the project's bulk_create_bulks_in_steps helper with the mapping returned from :meth:get_bulks_by_model.

Source code in src/winidjango/core/commands/import_data.py
def import_to_db(self) -> None:
    """Persist prepared model bulks to the database.

    Calls the project's `bulk_create_bulks_in_steps` helper with the
    mapping returned from :meth:`get_bulks_by_model`.
    """
    bulks_by_model = self.get_bulks_by_model(df=self.cleaning_df.df)

    bulk_create_bulks_in_steps(bulks_by_model)

db

Database utilities and common model helpers used in the project.

This package contains helpers for working with Django models, such as topological sorting of model classes according to foreign-key dependencies, a lightweight model hashing helper, and a project-wide BaseModel that adds common timestamp fields.

bulk

Utilities for performing bulk operations on Django models.

This module centralizes helpers used by importers and maintenance commands to create, update and delete large collections of model instances efficiently. It provides batching, optional concurrent execution, dependency-aware ordering and simulation helpers for previewing cascade deletions.

bulk_create_bulks_in_steps
bulk_create_bulks_in_steps(
    bulk_by_class: dict[type[Model], Iterable[Model]],
    step: int = STANDARD_BULK_SIZE,
) -> dict[type[Model], list[Model]]

Create multiple model-type bulks in dependency order.

The function topologically sorts the provided model classes so that models referenced by foreign keys are created before models that reference them. Each class' instances are created in batches using :func:bulk_create_in_steps.

Parameters:

Name Type Description Default
bulk_by_class dict[type[Model], Iterable[Model]]

Mapping from model class to iterable of instances to create.

required
step int

Chunk size for each model's batched creation.

STANDARD_BULK_SIZE

Returns:

Type Description
dict[type[Model], list[Model]]

Mapping from model class to list of created instances.

Source code in src/winidjango/core/db/bulk.py
def bulk_create_bulks_in_steps(
    bulk_by_class: dict[type[Model], Iterable[Model]],
    step: int = STANDARD_BULK_SIZE,
) -> dict[type[Model], list[Model]]:
    """Create multiple model-type bulks in dependency order.

    The function topologically sorts the provided model classes so that
    models referenced by foreign keys are created before models that
    reference them. Each class' instances are created in batches using
    :func:`bulk_create_in_steps`.

    Args:
        bulk_by_class: Mapping from model class to iterable of instances to create.
        step: Chunk size for each model's batched creation.

    Returns:
        Mapping from model class to list of created instances.
    """
    # order the bulks in order of creation depending how they depend on each other
    models_ = list(bulk_by_class.keys())
    ordered_models = topological_sort_models(models=models_)

    results: dict[type[Model], list[Model]] = {}
    for model_ in ordered_models:
        bulk = bulk_by_class[model_]
        result = bulk_create_in_steps(model=model_, bulk=bulk, step=step)
        results[model_] = result

    return results
bulk_create_in_steps
bulk_create_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int = STANDARD_BULK_SIZE,
) -> list[TModel]

Create objects in batches and return created instances.

Breaks bulk into chunks of size step and calls the project's batched bulk-create helper for each chunk. Execution is performed using the concurrent utility where configured for throughput.

Parameters:

Name Type Description Default
model type[TModel]

Django model class to create instances for.

required
bulk Iterable[TModel]

Iterable of unsaved model instances.

required
step int

Number of instances to create per chunk.

STANDARD_BULK_SIZE

Returns:

Type Description
list[TModel]

List of created model instances (flattened across chunks).

Source code in src/winidjango/core/db/bulk.py
def bulk_create_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int = STANDARD_BULK_SIZE,
) -> list[TModel]:
    """Create objects in batches and return created instances.

    Breaks ``bulk`` into chunks of size ``step`` and calls the project's
    batched bulk-create helper for each chunk. Execution is performed
    using the concurrent utility where configured for throughput.

    Args:
        model: Django model class to create instances for.
        bulk: Iterable of unsaved model instances.
        step: Number of instances to create per chunk.

    Returns:
        List of created model instances (flattened across chunks).
    """
    return cast(
        "list[TModel]",
        bulk_method_in_steps(model=model, bulk=bulk, step=step, mode=MODE_CREATE),
    )
bulk_delete
bulk_delete(
    model: type[Model], objs: Iterable[Model], **_: Any
) -> tuple[int, dict[str, int]]

Delete the provided objects and return Django's delete summary.

Accepts either a QuerySet or an iterable of model instances. When an iterable of instances is provided it is converted to a QuerySet by filtering on primary keys before calling QuerySet.delete().

Parameters:

Name Type Description Default
model type[Model]

Django model class.

required
objs Iterable[Model]

Iterable of model instances or a QuerySet.

required

Returns:

Type Description
tuple[int, dict[str, int]]

Tuple(total_deleted, per_model_counts) as returned by delete().

Source code in src/winidjango/core/db/bulk.py
def bulk_delete(
    model: type[Model],
    objs: Iterable[Model],
    **_: Any,  # noqa: ANN401
) -> tuple[int, dict[str, int]]:
    """Delete the provided objects and return Django's delete summary.

    Accepts either a QuerySet or an iterable of model instances. When an
    iterable of instances is provided it is converted to a QuerySet by
    filtering on primary keys before calling ``QuerySet.delete()``.

    Args:
        model: Django model class.
        objs: Iterable of model instances or a QuerySet.

    Returns:
        Tuple(total_deleted, per_model_counts) as returned by ``delete()``.
    """
    return model.objects.filter(pk__in=(obj.pk for obj in objs)).delete()
bulk_delete_in_steps
bulk_delete_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int = STANDARD_BULK_SIZE,
) -> tuple[int, dict[str, int]]

Delete objects in batches and return deletion statistics.

Each chunk is deleted using Django's QuerySet delete which returns a (count, per-model-counts) tuple. Results are aggregated across chunks and returned as a consolidated tuple.

Parameters:

Name Type Description Default
model type[TModel]

Django model class.

required
bulk Iterable[TModel]

Iterable of model instances to delete.

required
step int

Chunk size for deletions.

STANDARD_BULK_SIZE

Returns:

Type Description
int

A tuple containing the total number of deleted objects and a

dict[str, int]

mapping from model label to deleted count (including cascaded

tuple[int, dict[str, int]]

deletions).

Source code in src/winidjango/core/db/bulk.py
def bulk_delete_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int = STANDARD_BULK_SIZE,
) -> tuple[int, dict[str, int]]:
    """Delete objects in batches and return deletion statistics.

    Each chunk is deleted using Django's QuerySet ``delete`` which
    returns a (count, per-model-counts) tuple. Results are aggregated
    across chunks and returned as a consolidated tuple.

    Args:
        model: Django model class.
        bulk: Iterable of model instances to delete.
        step: Chunk size for deletions.

    Returns:
        A tuple containing the total number of deleted objects and a
        mapping from model label to deleted count (including cascaded
        deletions).
    """
    return cast(
        "tuple[int, dict[str, int]]",
        bulk_method_in_steps(
            model=model,
            bulk=bulk,
            step=step,
            mode=MODE_DELETE,
        ),
    )
bulk_method_in_steps
bulk_method_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["create"],
    **kwargs: Any,
) -> list[TModel]
bulk_method_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["update"],
    **kwargs: Any,
) -> int
bulk_method_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["delete"],
    **kwargs: Any,
) -> tuple[int, dict[str, int]]
bulk_method_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: MODE_TYPES,
    **kwargs: Any,
) -> int | tuple[int, dict[str, int]] | list[TModel]

Run a batched bulk operation (create/update/delete) on bulk.

This wrapper warns if called from within an existing transaction and delegates actual work to :func:bulk_method_in_steps_atomic which is executed inside an atomic transaction. The return type depends on mode (see :mod:winidjango.core.db.bulk constants).

Parameters:

Name Type Description Default
model type[TModel]

Django model class to operate on.

required
bulk Iterable[TModel]

Iterable of model instances.

required
step int

Chunk size for processing.

required
mode MODE_TYPES

One of 'create', 'update' or 'delete'.

required
**kwargs Any

Additional keyword arguments forwarded to the underlying bulk methods (for example update_fields for updates).

{}

Returns:

Type Description
int | tuple[int, dict[str, int]] | list[TModel]

For create: list of created instances.

int | tuple[int, dict[str, int]] | list[TModel]

For update: integer number of updated rows.

int | tuple[int, dict[str, int]] | list[TModel]

For delete: tuple(total_deleted, per_model_counts).

Source code in src/winidjango/core/db/bulk.py
def bulk_method_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: MODE_TYPES,
    **kwargs: Any,
) -> int | tuple[int, dict[str, int]] | list[TModel]:
    """Run a batched bulk operation (create/update/delete) on ``bulk``.

    This wrapper warns if called from within an existing transaction and
    delegates actual work to :func:`bulk_method_in_steps_atomic` which is
    executed inside an atomic transaction. The return type depends on
    ``mode`` (see :mod:`winidjango.core.db.bulk` constants).

    Args:
        model: Django model class to operate on.
        bulk: Iterable of model instances.
        step: Chunk size for processing.
        mode: One of ``'create'``, ``'update'`` or ``'delete'``.
        **kwargs: Additional keyword arguments forwarded to the underlying
            bulk methods (for example ``update_fields`` for updates).

    Returns:
        For ``create``: list of created instances.
        For ``update``: integer number of updated rows.
        For ``delete``: tuple(total_deleted, per_model_counts).
    """
    # check if we are inside a transaction.atomic block
    _in_atomic_block = transaction.get_connection().in_atomic_block
    if _in_atomic_block:
        logger.info(
            "BE CAREFUL USING BULK OPERATIONS INSIDE A BROADER TRANSACTION BLOCK. "
            "BULKING WITH BULKS THAT DEPEND ON EACH OTHER CAN CAUSE "
            "INTEGRITY ERRORS OR POTENTIAL OTHER ISSUES.",
        )
    return bulk_method_in_steps_atomic(
        model=model,
        bulk=bulk,
        step=step,
        mode=mode,
        **kwargs,
    )
bulk_method_in_steps_atomic
bulk_method_in_steps_atomic[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["create"],
    **kwargs: Any,
) -> list[TModel]
bulk_method_in_steps_atomic[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["update"],
    **kwargs: Any,
) -> int
bulk_method_in_steps_atomic[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["delete"],
    **kwargs: Any,
) -> tuple[int, dict[str, int]]
bulk_method_in_steps_atomic[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: MODE_TYPES,
    **kwargs: Any,
) -> int | tuple[int, dict[str, int]] | list[TModel]

Atomic implementation of the batched bulk operation.

This function is decorated with transaction.atomic so each call to this function runs in a database transaction. Note that nesting additional, outer atomic blocks that also include dependent bulk operations can cause integrity issues for operations that depend on each other's side-effects; callers should be careful about atomic decorator placement in higher-level code.

Parameters:

Name Type Description Default
model type[TModel]

Django model class.

required
bulk Iterable[TModel]

Iterable of model instances.

required
step int

Chunk size for processing.

required
mode MODE_TYPES

One of 'create', 'update' or 'delete'.

required
**kwargs Any

Forwarded to the underlying bulk method.

{}

Returns:

Name Type Description
See int | tuple[int, dict[str, int]] | list[TModel]

func:bulk_method_in_steps for return value semantics.

Source code in src/winidjango/core/db/bulk.py
@transaction.atomic
def bulk_method_in_steps_atomic[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: MODE_TYPES,
    **kwargs: Any,
) -> int | tuple[int, dict[str, int]] | list[TModel]:
    """Atomic implementation of the batched bulk operation.

    This function is decorated with ``transaction.atomic`` so each call
    to this function runs in a database transaction. Note that nesting
    additional, outer atomic blocks that also include dependent bulk
    operations can cause integrity issues for operations that depend on
    each other's side-effects; callers should be careful about atomic
    decorator placement in higher-level code.

    Args:
        model: Django model class.
        bulk: Iterable of model instances.
        step: Chunk size for processing.
        mode: One of ``'create'``, ``'update'`` or ``'delete'``.
        **kwargs: Forwarded to the underlying bulk method.

    Returns:
        See :func:`bulk_method_in_steps` for return value semantics.
    """
    bulk_method = get_bulk_method(model=model, mode=mode, **kwargs)

    chunks = get_step_chunks(bulk=bulk, step=step)

    # multithreading significantly increases speed
    result = multithread_loop(
        process_function=bulk_method,
        process_args=chunks,
    )

    return flatten_bulk_in_steps_result(result=result, mode=mode)
bulk_update_in_steps
bulk_update_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    update_fields: list[str],
    step: int = STANDARD_BULK_SIZE,
) -> int

Update objects in batches and return total updated count.

Parameters:

Name Type Description Default
model type[TModel]

Django model class.

required
bulk Iterable[TModel]

Iterable of model instances to update (must have PKs set).

required
update_fields list[str]

Fields to update on each instance when calling bulk_update.

required
step int

Chunk size for batched updates.

STANDARD_BULK_SIZE

Returns:

Type Description
int

Total number of rows updated across all chunks.

Source code in src/winidjango/core/db/bulk.py
def bulk_update_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    update_fields: list[str],
    step: int = STANDARD_BULK_SIZE,
) -> int:
    """Update objects in batches and return total updated count.

    Args:
        model: Django model class.
        bulk: Iterable of model instances to update (must have PKs set).
        update_fields: Fields to update on each instance when calling
            ``bulk_update``.
        step: Chunk size for batched updates.

    Returns:
        Total number of rows updated across all chunks.
    """
    return cast(
        "int",
        bulk_method_in_steps(
            model=model,
            bulk=bulk,
            step=step,
            mode=MODE_UPDATE,
            fields=update_fields,
        ),
    )
flatten_bulk_in_steps_result
flatten_bulk_in_steps_result[TModel: Model](
    result: list[list[TModel]], mode: Literal["create"]
) -> list[TModel]
flatten_bulk_in_steps_result[TModel: Model](
    result: list[int], mode: Literal["update"]
) -> int
flatten_bulk_in_steps_result[TModel: Model](
    result: list[tuple[int, dict[str, int]]],
    mode: Literal["delete"],
) -> tuple[int, dict[str, int]]
flatten_bulk_in_steps_result[TModel: Model](
    result: list[int]
    | list[tuple[int, dict[str, int]]]
    | list[list[TModel]],
    mode: str,
) -> int | tuple[int, dict[str, int]] | list[TModel]

Aggregate per-chunk results returned by concurrent bulk execution.

Depending on mode the function reduces a list of per-chunk results into a single consolidated return value:

  • create: flattens a list of lists into a single list of objects
  • update: sums integer counts returned per chunk
  • delete: aggregates (count, per-model-dict) tuples into a single total and combined per-model counts

Parameters:

Name Type Description Default
result list[int] | list[tuple[int, dict[str, int]]] | list[list[TModel]]

List of per-chunk results returned by the chunk function.

required
mode str

One of the supported modes.

required

Returns:

Type Description
int | tuple[int, dict[str, int]] | list[TModel]

Aggregated result corresponding to mode.

Source code in src/winidjango/core/db/bulk.py
def flatten_bulk_in_steps_result[TModel: Model](
    result: list[int] | list[tuple[int, dict[str, int]]] | list[list[TModel]],
    mode: str,
) -> int | tuple[int, dict[str, int]] | list[TModel]:
    """Aggregate per-chunk results returned by concurrent bulk execution.

    Depending on ``mode`` the function reduces a list of per-chunk
    results into a single consolidated return value:

    - ``create``: flattens a list of lists into a single list of objects
    - ``update``: sums integer counts returned per chunk
    - ``delete``: aggregates (count, per-model-dict) tuples into a single
      total and combined per-model counts

    Args:
        result: List of per-chunk results returned by the chunk function.
        mode: One of the supported modes.

    Returns:
        Aggregated result corresponding to ``mode``.
    """
    if mode == MODE_UPDATE:
        # formatted as [1000, 1000, ...]
        # since django 4.2 bulk_update returns the count of updated objects
        result = cast("list[int]", result)
        return int(sum(result))
    if mode == MODE_DELETE:
        # formatted as [(count, {model_name: count, model_cascade_name: count}), ...]
        # join the results to get the total count of deleted objects
        result = cast("list[tuple[int, dict[str, int]]]", result)
        total_count = 0
        count_sum_by_model: defaultdict[str, int] = defaultdict(int)
        for count_sum, count_by_model in result:
            total_count += count_sum
            for model_name, count in count_by_model.items():
                count_sum_by_model[model_name] += count
        return (total_count, dict(count_sum_by_model))
    if mode == MODE_CREATE:
        # formatted as [[obj1, obj2, ...], [obj1, obj2, ...], ...]
        result = cast("list[list[TModel]]", result)
        return [item for sublist in result for item in sublist]

    msg = f"Invalid method. Must be one of {MODES}"
    raise ValueError(msg)
get_bulk_method
get_bulk_method(
    model: type[Model],
    mode: Literal["create"],
    **kwargs: Any,
) -> Callable[[list[Model]], list[Model]]
get_bulk_method(
    model: type[Model],
    mode: Literal["update"],
    **kwargs: Any,
) -> Callable[[list[Model]], int]
get_bulk_method(
    model: type[Model],
    mode: Literal["delete"],
    **kwargs: Any,
) -> Callable[[list[Model]], tuple[int, dict[str, int]]]
get_bulk_method(
    model: type[Model], mode: MODE_TYPES, **kwargs: Any
) -> Callable[
    [list[Model]],
    list[Model] | int | tuple[int, dict[str, int]],
]

Return a callable that performs the requested bulk operation on a chunk.

The returned function accepts a single argument (a list of model instances) and returns the per-chunk result for the chosen mode.

Parameters:

Name Type Description Default
model type[Model]

Django model class.

required
mode MODE_TYPES

One of 'create', 'update' or 'delete'.

required
**kwargs Any

Forwarded to the underlying ORM bulk methods.

{}

Raises:

Type Description
ValueError

If mode is invalid.

Returns:

Type Description
Callable[[list[Model]], list[Model] | int | tuple[int, dict[str, int]]]

Callable that accepts a list of model instances and returns the

Callable[[list[Model]], list[Model] | int | tuple[int, dict[str, int]]]

result for that chunk.

Source code in src/winidjango/core/db/bulk.py
def get_bulk_method(
    model: type[Model],
    mode: MODE_TYPES,
    **kwargs: Any,
) -> Callable[[list[Model]], list[Model] | int | tuple[int, dict[str, int]]]:
    """Return a callable that performs the requested bulk operation on a chunk.

    The returned function accepts a single argument (a list of model
    instances) and returns the per-chunk result for the chosen mode.

    Args:
        model: Django model class.
        mode: One of ``'create'``, ``'update'`` or ``'delete'``.
        **kwargs: Forwarded to the underlying ORM bulk methods.

    Raises:
        ValueError: If ``mode`` is invalid.

    Returns:
        Callable that accepts a list of model instances and returns the
        result for that chunk.
    """
    bulk_method: Callable[[list[Model]], list[Model] | int | tuple[int, dict[str, int]]]
    if mode == MODE_CREATE:

        def bulk_create_chunk(chunk: list[Model]) -> list[Model]:
            return model.objects.bulk_create(objs=chunk, **kwargs)

        bulk_method = bulk_create_chunk
    elif mode == MODE_UPDATE:

        def bulk_update_chunk(chunk: list[Model]) -> int:
            return model.objects.bulk_update(objs=chunk, **kwargs)

        bulk_method = bulk_update_chunk
    elif mode == MODE_DELETE:

        def bulk_delete_chunk(chunk: list[Model]) -> tuple[int, dict[str, int]]:
            return bulk_delete(model=model, objs=chunk, **kwargs)

        bulk_method = bulk_delete_chunk
    else:
        msg = f"Invalid method. Must be one of {MODES}"
        raise ValueError(msg)

    return bulk_method
get_differences_between_bulks
get_differences_between_bulks[
    TModel1: Model,
    TModel2: Model,
](
    bulk1: list[TModel1],
    bulk2: list[TModel2],
    fields: list[
        Field[Any, Any]
        | ForeignObjectRel
        | GenericForeignKey
    ],
) -> tuple[
    list[TModel1],
    list[TModel2],
    list[TModel1],
    list[TModel2],
]

Return differences and intersections between two bulks of the same model.

Instances are compared using :func:hash_model_instance over the provided fields. The function maintains the original ordering for returned lists so that callers can preserve deterministic ordering when applying diffs.

Parameters:

Name Type Description Default
bulk1 list[TModel1]

First list of model instances.

required
bulk2 list[TModel2]

Second list of model instances.

required
fields list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]

Fields to include when hashing instances.

required

Raises:

Type Description
ValueError

If bulks are empty or contain different model types.

Returns:

Type Description
tuple[list[TModel1], list[TModel2], list[TModel1], list[TModel2]]

Four lists in the order: (in_1_not_2, in_2_not_1, in_both_from_1, in_both_from_2).

Source code in src/winidjango/core/db/bulk.py
def get_differences_between_bulks[TModel1: Model, TModel2: Model](
    bulk1: list[TModel1],
    bulk2: list[TModel2],
    fields: "list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]",
) -> tuple[list[TModel1], list[TModel2], list[TModel1], list[TModel2]]:
    """Return differences and intersections between two bulks of the same model.

    Instances are compared using :func:`hash_model_instance` over the
    provided ``fields``. The function maintains the original ordering
    for returned lists so that callers can preserve deterministic
    ordering when applying diffs.

    Args:
        bulk1: First list of model instances.
        bulk2: Second list of model instances.
        fields: Fields to include when hashing instances.

    Raises:
        ValueError: If bulks are empty or contain different model types.

    Returns:
        Four lists in the order:
            (in_1_not_2, in_2_not_1, in_both_from_1, in_both_from_2).
    """
    if not bulk1 or not bulk2:
        return bulk1, bulk2, [], []

    if type(bulk1[0]) is not type(bulk2[0]):
        msg = "Both bulks must be of the same model type."
        raise ValueError(msg)

    hash_model_instance_with_fields = partial(
        hash_model_instance,
        fields=fields,
    )
    # Precompute hashes and map them directly to models in a single pass for both bulks
    hashes1 = list(map(hash_model_instance_with_fields, bulk1))
    hashes2 = list(map(hash_model_instance_with_fields, bulk2))

    # Convert keys to sets for difference operations
    set1, set2 = set(hashes1), set(hashes2)

    # Calculate differences between sets
    # Find differences and intersection with original order preserved
    # Important, we need to return the original objects that are the same in memory,
    # so in_1_not_2 and in_2_not_1
    in_1_not_2 = set1 - set2
    in_1_not_2_list = [
        model
        for model, hash_ in zip(bulk1, hashes1, strict=False)
        if hash_ in in_1_not_2
    ]

    in_2_not_1 = set2 - set1
    in_2_not_1_list = [
        model
        for model, hash_ in zip(bulk2, hashes2, strict=False)
        if hash_ in in_2_not_1
    ]

    in_1_and_2 = set1 & set2
    in_1_and_2_from_1 = [
        model
        for model, hash_ in zip(bulk1, hashes1, strict=False)
        if hash_ in in_1_and_2
    ]
    in_1_and_2_from_2 = [
        model
        for model, hash_ in zip(bulk2, hashes2, strict=False)
        if hash_ in in_1_and_2
    ]

    return in_1_not_2_list, in_2_not_1_list, in_1_and_2_from_1, in_1_and_2_from_2
get_step_chunks
get_step_chunks(
    bulk: Iterable[Model], step: int
) -> Generator[tuple[list[Model]], None, None]

Yield consecutive chunks of at most step items from bulk.

The function yields a single-tuple containing the chunk (a list of model instances) because the concurrent execution helper expects a tuple of positional arguments for the target function.

Parameters:

Name Type Description Default
bulk Iterable[Model]

Iterable of model instances.

required
step int

Maximum number of instances per yielded chunk.

required

Yields:

Type Description
tuple[list[Model]]

Tuples where the first element is a list of model instances.

Source code in src/winidjango/core/db/bulk.py
def get_step_chunks(
    bulk: Iterable[Model],
    step: int,
) -> Generator[tuple[list[Model]], None, None]:
    """Yield consecutive chunks of at most ``step`` items from ``bulk``.

    The function yields a single-tuple containing the chunk (a list of
    model instances) because the concurrent execution helper expects a
    tuple of positional arguments for the target function.

    Args:
        bulk: Iterable of model instances.
        step: Maximum number of instances per yielded chunk.

    Yields:
        Tuples where the first element is a list of model instances.
    """
    bulk = iter(bulk)
    while True:
        chunk = list(islice(bulk, step))
        if not chunk:
            break
        yield (chunk,)  # bc concurrent_loop expects a tuple of args
multi_simulate_bulk_deletion
multi_simulate_bulk_deletion(
    entries: dict[type[Model], list[Model]],
) -> dict[type[Model], set[Model]]

Simulate deletions for multiple model classes and merge results.

Runs :func:simulate_bulk_deletion for each provided model and returns a unified mapping of all models that would be deleted.

Parameters:

Name Type Description Default
entries dict[type[Model], list[Model]]

Mapping from model class to list of instances to simulate.

required

Returns:

Type Description
dict[type[Model], set[Model]]

Mapping from model class to set of instances that would be deleted.

Source code in src/winidjango/core/db/bulk.py
def multi_simulate_bulk_deletion(
    entries: dict[type[Model], list[Model]],
) -> dict[type[Model], set[Model]]:
    """Simulate deletions for multiple model classes and merge results.

    Runs :func:`simulate_bulk_deletion` for each provided model and
    returns a unified mapping of all models that would be deleted.

    Args:
        entries: Mapping from model class to list of instances to simulate.

    Returns:
        Mapping from model class to set of instances that would be deleted.
    """
    deletion_summaries = [
        simulate_bulk_deletion(model, entry) for model, entry in entries.items()
    ]
    # join the dicts to get the total count of deleted objects
    joined_deletion_summary: defaultdict[type[Model], set[Model]] = defaultdict(set)
    for deletion_summary in deletion_summaries:
        for model, objects in deletion_summary.items():
            joined_deletion_summary[model].update(objects)

    return dict(joined_deletion_summary)
simulate_bulk_deletion
simulate_bulk_deletion[TModel: Model](
    model_class: type[TModel], entries: list[TModel]
) -> dict[type[Model], set[Model]]

Simulate Django's delete cascade and return affected objects.

Uses :class:django.db.models.deletion.Collector to determine which objects (including cascaded related objects) would be removed if the provided entries were deleted. No database writes are performed.

Parameters:

Name Type Description Default
model_class type[TModel]

Model class of the provided entries.

required
entries list[TModel]

Instances to simulate deletion for.

required

Returns:

Type Description
dict[type[Model], set[Model]]

Mapping from model class to set of instances that would be deleted.

Source code in src/winidjango/core/db/bulk.py
def simulate_bulk_deletion[TModel: Model](
    model_class: type[TModel],
    entries: list[TModel],
) -> dict[type[Model], set[Model]]:
    """Simulate Django's delete cascade and return affected objects.

    Uses :class:`django.db.models.deletion.Collector` to determine which
    objects (including cascaded related objects) would be removed if the
    provided entries were deleted. No database writes are performed.

    Args:
        model_class: Model class of the provided entries.
        entries: Instances to simulate deletion for.

    Returns:
        Mapping from model class to set of instances that would be deleted.
    """
    if not entries:
        return {}

    # Initialize the Collector
    using = router.db_for_write(model_class)
    collector = Collector(using)

    # Collect deletion cascade for all entries
    collector.collect(entries)  # ty:ignore[invalid-argument-type]

    # Prepare the result dictionary
    deletion_summary: defaultdict[type[Model], set[Model]] = defaultdict(set)

    # Add normal deletes
    for model, objects in collector.data.items():
        deletion_summary[model].update(objects)  # objects is already iterable

    # Add fast deletes (explicitly expand querysets)
    for queryset in collector.fast_deletes:
        deletion_summary[queryset.model].update(list(queryset))

    return deletion_summary

fields

Utilities for inspecting Django model fields.

This module provides small helpers that make it easier to introspect Django model fields and metadata in a type-friendly way. The helpers are used across the project's database utilities to implement operations like topological sorting and deterministic hashing of model instances.

get_field_names
get_field_names(
    fields: list[
        Field[Any, Any]
        | ForeignObjectRel
        | GenericForeignKey
    ],
) -> list[str]

Return the name attribute for a list of Django field objects.

Parameters:

Name Type Description Default
fields list[Field | ForeignObjectRel | GenericForeignKey]

Field objects obtained from a model's _meta.get_fields().

required

Returns:

Type Description
list[str]

list[str]: List of field names in the same order as fields.

Source code in src/winidjango/core/db/fields.py
def get_field_names(
    fields: "list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]",
) -> list[str]:
    """Return the ``name`` attribute for a list of Django field objects.

    Args:
        fields (list[Field | ForeignObjectRel | GenericForeignKey]):
            Field objects obtained from a model's ``_meta.get_fields()``.

    Returns:
        list[str]: List of field names in the same order as ``fields``.
    """
    return [field.name for field in fields]
get_fields
get_fields[TModel: Model](
    model: type[TModel],
) -> list[
    Field[Any, Any] | ForeignObjectRel | GenericForeignKey
]

Return all field objects for a Django model.

This wraps model._meta.get_fields() and is typed to include relationship fields so callers can handle both regular and related fields uniformly.

Parameters:

Name Type Description Default
model type[Model]

Django model class.

required

Returns:

Type Description
list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]

list[Field | ForeignObjectRel | GenericForeignKey]: All field objects associated with the model.

Source code in src/winidjango/core/db/fields.py
def get_fields[TModel: Model](
    model: type[TModel],
) -> "list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]":
    """Return all field objects for a Django model.

    This wraps ``model._meta.get_fields()`` and is typed to include
    relationship fields so callers can handle both regular and related
    fields uniformly.

    Args:
        model (type[Model]): Django model class.

    Returns:
        list[Field | ForeignObjectRel | GenericForeignKey]: All field
            objects associated with the model.
    """
    return get_model_meta(model).get_fields()
get_model_meta
get_model_meta(model: type[Model]) -> Options[Model]

Return a model class' _meta options object.

This small wrapper exists to make typing clearer at call sites where the code needs the model Options object.

Parameters:

Name Type Description Default
model type[Model]

Django model class.

required

Returns:

Name Type Description
Options Options[Model]

The model's _meta options object.

Source code in src/winidjango/core/db/fields.py
def get_model_meta(model: type[Model]) -> "Options[Model]":
    """Return a model class' ``_meta`` options object.

    This small wrapper exists to make typing clearer at call sites where
    the code needs the model Options object.

    Args:
        model (type[Model]): Django model class.

    Returns:
        Options: The model's ``_meta`` options object.
    """
    return model._meta  # noqa: SLF001

models

Database utilities and lightweight model helpers.

This module provides helpers used across the project when manipulating Django models: ordering models by foreign-key dependencies, creating a deterministic hash for unsaved instances, and a project-wide BaseModel that exposes common timestamp fields.

BaseModel

Bases: Model

Abstract base model containing common fields and helpers.

Concrete models can inherit from this class to get consistent created_at and updated_at timestamp fields and convenient string representations.

meta property
meta: Options[Self]

Return the model's _meta options object.

This property is a small convenience wrapper used to make access sites slightly more explicit in code and improve typing in callers.

Meta

Mark the model as abstract.

__repr__
__repr__() -> str

Base representation of a model.

Source code in src/winidjango/core/db/models.py
def __repr__(self) -> str:
    """Base representation of a model."""
    return str(self)
__str__
__str__() -> str

Return a concise human-readable representation.

The default shows the model class name and primary key which is useful for logging and interactive debugging.

Returns:

Name Type Description
str str

Short representation, e.g. MyModel(123).

Source code in src/winidjango/core/db/models.py
def __str__(self) -> str:
    """Return a concise human-readable representation.

    The default shows the model class name and primary key which is
    useful for logging and interactive debugging.

    Returns:
        str: Short representation, e.g. ``MyModel(123)``.
    """
    return f"{self.__class__.__name__}({self.pk})"
hash_model_instance
hash_model_instance(
    instance: Model,
    fields: list[
        Field[Any, Any]
        | ForeignObjectRel
        | GenericForeignKey
    ],
) -> int

Compute a deterministic hash for a model instance.

The function returns a hash suitable for comparing unsaved model instances by their field values. If the instance has a primary key (instance.pk) that key is hashed and returned immediately; this keeps comparisons cheap for persisted objects.

Parameters:

Name Type Description Default
instance Model

The Django model instance to hash.

required
fields list[Field | ForeignObjectRel | GenericForeignKey]

Field objects that should be included when computing the hash.

required

Returns:

Name Type Description
int int

Deterministic integer hash of the instance. For persisted instances this is hash(instance.pk).

Notes
  • The returned hash is intended for heuristic comparisons (e.g. deduplication in import pipelines) and is not cryptographically secure. Use with care when relying on absolute uniqueness.
Source code in src/winidjango/core/db/models.py
def hash_model_instance(
    instance: Model,
    fields: "list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]",
) -> int:
    """Compute a deterministic hash for a model instance.

    The function returns a hash suitable for comparing unsaved model
    instances by their field values. If the instance has a primary key
    (``instance.pk``) that key is hashed and returned immediately; this
    keeps comparisons cheap for persisted objects.

    Args:
        instance (Model): The Django model instance to hash.
        fields (list[Field | ForeignObjectRel | GenericForeignKey]):
            Field objects that should be included when computing the hash.

    Returns:
        int: Deterministic integer hash of the instance. For persisted
            instances this is ``hash(instance.pk)``.

    Notes:
        - The returned hash is intended for heuristic comparisons (e.g.
          deduplication in import pipelines) and is not cryptographically
          secure. Use with care when relying on absolute uniqueness.
    """
    if instance.pk:
        return hash(instance.pk)

    field_names = get_field_names(fields)
    model_dict = model_to_dict(instance, fields=field_names)
    sorted_dict = dict(sorted(model_dict.items()))
    values = (type(instance), tuple(sorted_dict.items()))
    return hash(values)
topological_sort_models
topological_sort_models(
    models: list[type[Model]],
) -> list[type[Model]]

Sort Django models in dependency order using topological sorting.

Analyzes foreign key relationships between Django models and returns them in an order where dependencies come before dependents. This ensures that when performing operations like bulk creation or deletion, models are processed in the correct order to avoid foreign key constraint violations.

The function uses Python's graphlib.TopologicalSorter to perform the sorting based on ForeignKey relationships between the provided models. Only relationships between models in the input list are considered.

Parameters:

Name Type Description Default
models list[type[Model]]

A list of Django model classes to sort based on their foreign key dependencies.

required

Returns:

Type Description
list[type[Model]]

list[type[Model]]: The input models sorted in dependency order, where models that are referenced by foreign keys appear before models that reference them. Self-referential relationships are ignored.

Raises:

Type Description
CycleError

If there are circular dependencies between models that cannot be resolved.

Example
Assuming Author model has no dependencies
and Book model has ForeignKey to Author

models = [Book, Author] sorted_models = topological_sort_models(models) sorted_models [, ]

Note
  • Only considers ForeignKey relationships, not other field types
  • Self-referential foreign keys are ignored to avoid self-loops
  • Only relationships between models in the input list are considered
Source code in src/winidjango/core/db/models.py
def topological_sort_models(
    models: list[type[Model]],
) -> list[type[Model]]:
    """Sort Django models in dependency order using topological sorting.

    Analyzes foreign key relationships between Django models and returns them
    in an order where dependencies come before dependents. This ensures that
    when performing operations like bulk creation or deletion, models are
    processed in the correct order to avoid foreign key constraint violations.

    The function uses Python's graphlib.TopologicalSorter to perform the sorting
    based on ForeignKey relationships between the provided models. Only
    relationships between models in the input list are considered.

    Args:
        models (list[type[Model]]): A list of Django model classes to sort
            based on their foreign key dependencies.

    Returns:
        list[type[Model]]: The input models sorted in dependency order, where
            models that are referenced by foreign keys appear before models
            that reference them. Self-referential relationships are ignored.

    Raises:
        graphlib.CycleError: If there are circular dependencies between models
            that cannot be resolved.

    Example:
        >>> # Assuming Author model has no dependencies
        >>> # and Book model has ForeignKey to Author
        >>> models = [Book, Author]
        >>> sorted_models = topological_sort_models(models)
        >>> sorted_models
        [<class 'Author'>, <class 'Book'>]

    Note:
        - Only considers ForeignKey relationships, not other field types
        - Self-referential foreign keys are ignored to avoid self-loops
        - Only relationships between models in the input list are considered
    """
    ts: TopologicalSorter[type[Model]] = TopologicalSorter()

    for model in models:
        deps = {
            field.related_model
            for field in get_fields(model)
            if isinstance(field, ForeignKey)
            and isinstance(field.related_model, type)
            and field.related_model in models
            and field.related_model is not model
        }
        ts.add(model, *deps)

    return list(ts.static_order())

setup

Concurrency-safe Django database setup helpers.

Django's migration recorder checks whether its bookkeeping table exists and, if not, creates it. That check-then-create is not atomic, so when multiple processes call migrate against the same database at once (for example, several concurrent invocations of the same pre-commit hook), more than one process can decide the table is missing and race to create it, crashing with "table already exists". migrate_safely serializes migration across processes with a file lock so only one process ever runs it at a time; the rest simply find the schema already up to date once they acquire the lock.

migrate_safely
migrate_safely(db_path: Path) -> None

Apply Django migrations without racing other concurrent processes.

Must be called after django.setup(). Serializes access with a file lock next to db_path so concurrent processes targeting the same database apply migrations one at a time instead of racing to create the migration bookkeeping table.

Parameters:

Name Type Description Default
db_path Path

Path to the database file the migrations apply to.

required
Source code in src/winidjango/core/db/setup.py
def migrate_safely(db_path: Path) -> None:
    """Apply Django migrations without racing other concurrent processes.

    Must be called after ``django.setup()``. Serializes access with a
    file lock next to ``db_path`` so concurrent processes targeting the
    same database apply migrations one at a time instead of racing to
    create the migration bookkeeping table.

    Args:
        db_path: Path to the database file the migrations apply to.
    """
    lock_path = db_path.with_suffix(f"{db_path.suffix}.lock")
    lock_path.parent.mkdir(parents=True, exist_ok=True)
    with FileLock(lock_path):
        call_command("migrate")

sql

Low-level helper to execute raw SQL against Django's database.

This module exposes :func:execute_sql which runs a parameterized SQL query using Django's database connection and returns column names and rows. It is intended for one-off queries where ORM abstractions are insufficient or when reading complex reports from the database.

The helper uses Django's connection cursor context manager to ensure resources are cleaned up correctly. Results are fetched into memory so avoid using it for very large result sets.

execute_sql
execute_sql(
    sql: str, params: dict[str, Any] | None = None
) -> tuple[list[str], list[tuple[Any, ...]]]

Execute a SQL statement and return column names and rows.

Parameters:

Name Type Description Default
sql str

SQL statement possibly containing named placeholders (%(name)s) for database binding.

required
params dict[str, Any] | None

Optional mapping of parameters to bind to the query.

None

Returns:

Type Description
list[str]

Tuple[List[str], List[Tuple[Any, ...]]]: A tuple where the first

list[tuple[Any, ...]]

element is the list of column names (empty list if the statement

tuple[list[str], list[tuple[Any, ...]]]

returned no rows) and the second element is a list of row tuples.

Raises:

Type Description
Error

Propagates underlying database errors raised by Django's database backend.

Source code in src/winidjango/core/db/sql.py
def execute_sql(
    sql: str,
    params: dict[str, Any] | None = None,
) -> tuple[list[str], list[tuple[Any, ...]]]:
    """Execute a SQL statement and return column names and rows.

    Args:
        sql (str): SQL statement possibly containing named placeholders
            (``%(name)s``) for database binding.
        params (dict[str, Any] | None): Optional mapping of parameters to
            bind to the query.

    Returns:
        Tuple[List[str], List[Tuple[Any, ...]]]: A tuple where the first
        element is the list of column names (empty list if the statement
        returned no rows) and the second element is a list of row tuples.

    Raises:
        django.db.Error: Propagates underlying database errors raised by
            Django's database backend.
    """
    with connection.cursor() as cursor:
        cursor.execute(sql=sql, params=params)
        rows = cursor.fetchall()
        column_names = (
            [col[0] for col in cursor.description] if cursor.description else []
        )

    return column_names, rows

rig

init module.

configs

Package initialization.

pyproject

Manage pyproject.toml (PEP 518, 621, 660).

Handles metadata, dependencies, build config (uv), tool configs (ruff, ty, pytest, bandit, rumdl). Enforces opinionated defaults: all ruff rules (except D203, D213, COM812, ANN401), Google docstrings, strict ty, bandit security, coverage threshold. Validation uses subset checking (users can add extra configs). Priority 20 (created early for other configs to read).

Utility methods: project info, dependencies, Python versions, license detection, classifiers.

DjangoPyprojectConfigFile

Bases: PyprojectConfigFile, PyprojectConfigFile

Overrides base to resolve conflicts.

Only needed when developing Winidjango itself.

PyprojectConfigFile

Bases: PyprojectConfigFile

You can override methods from the base class to customize behavior.

resources

init module.

tools

Tool wrappers for CLI tools used in development workflows.

Tools are subclasses of Tool providing methods that return Args objects for type-safe command construction and execution.

tools

Override pyrig tools.

ProjectTester

Bases: ProjectTester

Subclass of ProjectTester for customizing pyrig behavior.

dev_dependencies
dev_dependencies() -> tuple[str, ...]

Get the dev dependencies.

Source code in src/winidjango/rig/tools/tools.py
def dev_dependencies(self) -> tuple[str, ...]:
    """Get the dev dependencies."""
    return (*super().dev_dependencies(), "pytest-django")

winidjango

init module.

core

src package.

This package exposes the project's internal modules used by the command-line utilities and database helpers. It exists primarily so that code under winidjango/src can be imported using the winidjango.core package path in other modules and tests.

The package itself contains the following subpackages: - commands - management command helpers and base classes - db - database utilities and model helpers

Consumers should import the specific submodules they need rather than relying on side effects from this package's import-time execution.

commands

Utilities and base classes for management commands.

The commands package contains base command classes and helpers used to implement Django management commands in the project. Subpackages and modules under commands provide reusable patterns for argument handling, logging and common command behaviors so that individual commands can focus on their business logic.

base

Base helpers and abstractions for management commands.

This package provides a common abstract base class used by project management commands. The base class centralizes argument handling, standard options (dry-run, batching, timeouts, etc.) and integrates logging behavior used throughout the commands package.

base

Utilities and an abstract base class for Django management commands.

This module defines :class:ABCBaseCommand, a reusable abstract base that combines Django's BaseCommand with the project's logging mixins and standard argument handling. The base class implements a template method pattern so concrete commands only need to implement the abstract extension points for providing command-specific arguments and business logic.

ABCBaseCommand

Bases: ABCLoggingMixin, BaseCommand

Abstract base class for management commands with logging and standard options.

The class wires common behavior such as base arguments (dry-run, batching, timeouts) and provides extension points that concrete commands must implement: :meth:add_command_arguments and :meth:handle_command.

Notes
  • Inheritance order matters: the logging mixin must precede BaseCommand so mixin initialization occurs as expected.
  • The base class follows the template method pattern; concrete commands should not override :meth:add_arguments or :meth:handle but implement the abstract hooks instead.
Options

Just a container class for hard coding the option keys.

add_arguments
add_arguments(parser: ArgumentParser) -> None

Configure command-line arguments for the command.

Adds common base arguments (dry-run, force, delete, timeout, batching and concurrency options) and then delegates to :meth:add_command_arguments for command-specific options.

Parameters:

Name Type Description Default
parser ArgumentParser

The argument parser passed by Django.

required
Source code in src/winidjango/core/commands/base/base.py
def add_arguments(self, parser: ArgumentParser) -> None:
    """Configure command-line arguments for the command.

    Adds common base arguments (dry-run, force, delete, timeout,
    batching and concurrency options) and then delegates to
    :meth:`add_command_arguments` for command-specific options.

    Args:
        parser (ArgumentParser): The argument parser passed by Django.
    """
    # add base args that are used in most commands
    self.base_add_arguments(parser)

    # add additional args that are specific to the command
    self.add_command_arguments(parser)
add_command_arguments abstractmethod
add_command_arguments(parser: ArgumentParser) -> None

Define command-specific arguments.

Implement this hook to add options and positional arguments that are specific to the concrete management command.

Parameters:

Name Type Description Default
parser ArgumentParser

The argument parser passed by Django.

required
Source code in src/winidjango/core/commands/base/base.py
@abstractmethod
def add_command_arguments(self, parser: ArgumentParser) -> None:
    """Define command-specific arguments.

    Implement this hook to add options and positional arguments that are
    specific to the concrete management command.

    Args:
        parser (ArgumentParser): The argument parser passed by Django.
    """
base_add_arguments
base_add_arguments(parser: ArgumentParser) -> None

Add the project's standard command-line arguments to parser.

Parameters:

Name Type Description Default
parser ArgumentParser

The argument parser passed by Django.

required
Source code in src/winidjango/core/commands/base/base.py
def base_add_arguments(self, parser: ArgumentParser) -> None:
    """Add the project's standard command-line arguments to ``parser``.

    Args:
        parser (ArgumentParser): The argument parser passed by Django.
    """
    parser.add_argument(
        f"--{self.Options.DRY_RUN}",
        action="store_true",
        help="Show what would be done without actually executing the changes",
    )

    parser.add_argument(
        f"--{self.Options.FORCE}",
        action="store_true",
        help="Force an action in a command",
    )

    parser.add_argument(
        f"--{self.Options.DELETE}",
        action="store_true",
        help="Deleting smth in a command",
    )

    parser.add_argument(
        f"--{self.Options.YES}",
        action="store_true",
        help="Answer yes to all prompts",
        default=False,
    )

    parser.add_argument(
        f"--{self.Options.TIMEOUT}",
        type=int,
        help="Timeout for a command",
        default=None,
    )

    parser.add_argument(
        f"--{self.Options.BATCH_SIZE}",
        type=int,
        default=None,
        help="Number of items to process in each batch",
    )

    parser.add_argument(
        f"--{self.Options.THREADS}",
        type=int,
        default=None,
        help="Number of threads to use for processing",
    )

    parser.add_argument(
        f"--{self.Options.PROCESSES}",
        type=int,
        default=None,
        help="Number of processes to use for processing",
    )
base_handle
base_handle(*args: Any, **options: Any) -> None

Perform common pre-processing for commands.

Stores the incoming arguments and options on the instance for use by :meth:handle_command and subclasses.

Parameters:

Name Type Description Default
*args Any

Positional arguments forwarded from Django.

()
**options Any

Parsed command-line options.

{}
Source code in src/winidjango/core/commands/base/base.py
def base_handle(self, *args: Any, **options: Any) -> None:  # noqa: ANN401
    """Perform common pre-processing for commands.

    Stores the incoming arguments and options on the instance for use
    by :meth:`handle_command` and subclasses.

    Args:
        *args: Positional arguments forwarded from Django.
        **options: Parsed command-line options.
    """
    self.args = args
    self.options = options
get_option
get_option(option: str) -> Any

Retrieve a parsed command option by key.

Parameters:

Name Type Description Default
option str

The option key to retrieve from self.options.

required

Returns:

Name Type Description
Any Any

The value for the requested option. If the option is not present a KeyError will be raised (matching how Django exposes options in management commands).

Source code in src/winidjango/core/commands/base/base.py
def get_option(self, option: str) -> Any:  # noqa: ANN401
    """Retrieve a parsed command option by key.

    Args:
        option (str): The option key to retrieve from ``self.options``.

    Returns:
        Any: The value for the requested option. If the option is not
            present a ``KeyError`` will be raised (matching how Django
            exposes options in management commands).
    """
    return self.options[option]
handle
handle(*args: Any, **options: Any) -> None

Orchestrate command execution.

Performs shared pre-processing by calling :meth:base_handle and then delegates to :meth:handle_command which must be implemented by subclasses.

Parameters:

Name Type Description Default
*args Any

Positional arguments forwarded from Django.

()
**options Any

Parsed command-line options.

{}
Source code in src/winidjango/core/commands/base/base.py
def handle(self, *args: Any, **options: Any) -> None:  # noqa: ANN401
    """Orchestrate command execution.

    Performs shared pre-processing by calling :meth:`base_handle` and
    then delegates to :meth:`handle_command` which must be implemented
    by subclasses.

    Args:
        *args: Positional arguments forwarded from Django.
        **options: Parsed command-line options.
    """
    self.base_handle(*args, **options)
    self.handle_command()
handle_command abstractmethod
handle_command() -> None

Run the command-specific behavior.

This abstract hook should be implemented by concrete commands to perform the command's main work. Implementations should read self.args and self.options which were set in :meth:base_handle.

Source code in src/winidjango/core/commands/base/base.py
@abstractmethod
def handle_command(self) -> None:
    """Run the command-specific behavior.

    This abstract hook should be implemented by concrete commands to
    perform the command's main work. Implementations should read
    ``self.args`` and ``self.options`` which were set in
    :meth:`base_handle`.
    """
import_data

Import command base class and utilities.

This module defines a reusable base command for importing tabular data into Django models. Implementations should provide a concrete source ingestion (for example, reading from CSV or an external API), a cleaning/normalization step implemented by a CleaningDF subclass, and mapping logic that groups cleaned data into model instances that can be bulk-created.

The base command centralizes the typical flow: 1. Read raw data (handle_import) 2. Wrap and clean the data using a CleaningDF subclass 3. Convert the cleaned frame into per-model bulks 4. Persist bulks using the project's bulk create helpers

Using this base class ensures a consistent import lifecycle and reduces duplicated boilerplate across different import implementations.

ImportDataBaseCommand

Bases: ABCBaseCommand

Abstract base for data-import Django management commands.

Subclasses must implement the ingestion, cleaning-class selection, and mapping of cleaned rows to Django model instances. The base implementation wires these pieces together and calls the project's bulk creation helper to persist the data.

Implementors typically only need to override the three abstract methods documented below.

get_bulks_by_model abstractmethod
get_bulks_by_model(
    df: DataFrame,
) -> dict[type[Model], Iterable[Model]]

Map the cleaned DataFrame to model-instance bulks.

The implementation should inspect the cleaned DataFrame and return a mapping where keys are Django model classes and values are iterables of unsaved model instances (or dataclass-like objects accepted by the project's bulk-creation utility).

Parameters:

Name Type Description Default
df DataFrame

The cleaned and normalized DataFrame.

required

Returns:

Type Description
dict[type[Model], Iterable[Model]]

dict[type[Model], Iterable[Model]]: Mapping from model classes to iterables of instances that should be created.

Source code in src/winidjango/core/commands/import_data.py
@abstractmethod
def get_bulks_by_model(
    self,
    df: pl.DataFrame,
) -> dict[type[Model], Iterable[Model]]:
    """Map the cleaned DataFrame to model-instance bulks.

    The implementation should inspect the cleaned DataFrame and
    return a mapping where keys are Django model classes and values
    are iterables of unsaved model instances (or dataclass-like
    objects accepted by the project's bulk-creation utility).

    Args:
        df (pl.DataFrame): The cleaned and normalized DataFrame.

    Returns:
        dict[type[Model], Iterable[Model]]: Mapping from model classes
            to iterables of instances that should be created.
    """
get_cleaning_df_cls abstractmethod
get_cleaning_df_cls() -> type[CleaningDF]

Return the CleaningDF subclass used to normalize the data.

The returned class will be instantiated with the raw DataFrame returned from :meth:handle_import and must provide the transformations required to prepare data for mapping into model instances.

Returns:

Type Description
type[CleaningDF]

type[CleaningDF]: A subclass of CleaningDF that performs the necessary normalization and validation.

Source code in src/winidjango/core/commands/import_data.py
@abstractmethod
def get_cleaning_df_cls(self) -> type[CleaningDF]:
    """Return the `CleaningDF` subclass used to normalize the data.

    The returned class will be instantiated with the raw DataFrame
    returned from :meth:`handle_import` and must provide the
    transformations required to prepare data for mapping into model
    instances.

    Returns:
        type[CleaningDF]: A subclass of ``CleaningDF`` that performs
            the necessary normalization and validation.
    """
handle_command
handle_command() -> None

Execute the full import lifecycle.

This template method reads raw data via :meth:handle_import, wraps it with the cleaning class returned by :meth:get_cleaning_df_cls and then persists the resulting model bulks returned by :meth:get_bulks_by_model.

Source code in src/winidjango/core/commands/import_data.py
def handle_command(self) -> None:
    """Execute the full import lifecycle.

    This template method reads raw data via :meth:`handle_import`,
    wraps it with the cleaning class returned by
    :meth:`get_cleaning_df_cls` and then persists the resulting
    model bulks returned by :meth:`get_bulks_by_model`.
    """
    data_df = self.handle_import()

    cleaning_df_cls = self.get_cleaning_df_cls()
    self.cleaning_df = cleaning_df_cls(data_df)

    self.import_to_db()
handle_import abstractmethod
handle_import() -> DataFrame

Read raw data from the import source.

This method should read data from whatever source the concrete command targets (files, remote APIs, etc.) and return it as a polars.DataFrame. No cleaning should be performed here; cleaning is handled by the cleaning CleaningDF returned from get_cleaning_df_cls.

Returns:

Type Description
DataFrame

pl.DataFrame: Raw (uncleaned) tabular data to be cleaned and mapped to model instances.

Source code in src/winidjango/core/commands/import_data.py
@abstractmethod
def handle_import(self) -> pl.DataFrame:
    """Read raw data from the import source.

    This method should read data from whatever source the concrete
    command targets (files, remote APIs, etc.) and return it as a
    ``polars.DataFrame``. No cleaning should be performed here;
    cleaning is handled by the cleaning `CleaningDF` returned from
    ``get_cleaning_df_cls``.

    Returns:
        pl.DataFrame: Raw (uncleaned) tabular data to be cleaned and
            mapped to model instances.
    """
import_to_db
import_to_db() -> None

Persist prepared model bulks to the database.

Calls the project's bulk_create_bulks_in_steps helper with the mapping returned from :meth:get_bulks_by_model.

Source code in src/winidjango/core/commands/import_data.py
def import_to_db(self) -> None:
    """Persist prepared model bulks to the database.

    Calls the project's `bulk_create_bulks_in_steps` helper with the
    mapping returned from :meth:`get_bulks_by_model`.
    """
    bulks_by_model = self.get_bulks_by_model(df=self.cleaning_df.df)

    bulk_create_bulks_in_steps(bulks_by_model)

db

Database utilities and common model helpers used in the project.

This package contains helpers for working with Django models, such as topological sorting of model classes according to foreign-key dependencies, a lightweight model hashing helper, and a project-wide BaseModel that adds common timestamp fields.

bulk

Utilities for performing bulk operations on Django models.

This module centralizes helpers used by importers and maintenance commands to create, update and delete large collections of model instances efficiently. It provides batching, optional concurrent execution, dependency-aware ordering and simulation helpers for previewing cascade deletions.

bulk_create_bulks_in_steps
bulk_create_bulks_in_steps(
    bulk_by_class: dict[type[Model], Iterable[Model]],
    step: int = STANDARD_BULK_SIZE,
) -> dict[type[Model], list[Model]]

Create multiple model-type bulks in dependency order.

The function topologically sorts the provided model classes so that models referenced by foreign keys are created before models that reference them. Each class' instances are created in batches using :func:bulk_create_in_steps.

Parameters:

Name Type Description Default
bulk_by_class dict[type[Model], Iterable[Model]]

Mapping from model class to iterable of instances to create.

required
step int

Chunk size for each model's batched creation.

STANDARD_BULK_SIZE

Returns:

Type Description
dict[type[Model], list[Model]]

Mapping from model class to list of created instances.

Source code in src/winidjango/core/db/bulk.py
def bulk_create_bulks_in_steps(
    bulk_by_class: dict[type[Model], Iterable[Model]],
    step: int = STANDARD_BULK_SIZE,
) -> dict[type[Model], list[Model]]:
    """Create multiple model-type bulks in dependency order.

    The function topologically sorts the provided model classes so that
    models referenced by foreign keys are created before models that
    reference them. Each class' instances are created in batches using
    :func:`bulk_create_in_steps`.

    Args:
        bulk_by_class: Mapping from model class to iterable of instances to create.
        step: Chunk size for each model's batched creation.

    Returns:
        Mapping from model class to list of created instances.
    """
    # order the bulks in order of creation depending how they depend on each other
    models_ = list(bulk_by_class.keys())
    ordered_models = topological_sort_models(models=models_)

    results: dict[type[Model], list[Model]] = {}
    for model_ in ordered_models:
        bulk = bulk_by_class[model_]
        result = bulk_create_in_steps(model=model_, bulk=bulk, step=step)
        results[model_] = result

    return results
bulk_create_in_steps
bulk_create_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int = STANDARD_BULK_SIZE,
) -> list[TModel]

Create objects in batches and return created instances.

Breaks bulk into chunks of size step and calls the project's batched bulk-create helper for each chunk. Execution is performed using the concurrent utility where configured for throughput.

Parameters:

Name Type Description Default
model type[TModel]

Django model class to create instances for.

required
bulk Iterable[TModel]

Iterable of unsaved model instances.

required
step int

Number of instances to create per chunk.

STANDARD_BULK_SIZE

Returns:

Type Description
list[TModel]

List of created model instances (flattened across chunks).

Source code in src/winidjango/core/db/bulk.py
def bulk_create_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int = STANDARD_BULK_SIZE,
) -> list[TModel]:
    """Create objects in batches and return created instances.

    Breaks ``bulk`` into chunks of size ``step`` and calls the project's
    batched bulk-create helper for each chunk. Execution is performed
    using the concurrent utility where configured for throughput.

    Args:
        model: Django model class to create instances for.
        bulk: Iterable of unsaved model instances.
        step: Number of instances to create per chunk.

    Returns:
        List of created model instances (flattened across chunks).
    """
    return cast(
        "list[TModel]",
        bulk_method_in_steps(model=model, bulk=bulk, step=step, mode=MODE_CREATE),
    )
bulk_delete
bulk_delete(
    model: type[Model], objs: Iterable[Model], **_: Any
) -> tuple[int, dict[str, int]]

Delete the provided objects and return Django's delete summary.

Accepts either a QuerySet or an iterable of model instances. When an iterable of instances is provided it is converted to a QuerySet by filtering on primary keys before calling QuerySet.delete().

Parameters:

Name Type Description Default
model type[Model]

Django model class.

required
objs Iterable[Model]

Iterable of model instances or a QuerySet.

required

Returns:

Type Description
tuple[int, dict[str, int]]

Tuple(total_deleted, per_model_counts) as returned by delete().

Source code in src/winidjango/core/db/bulk.py
def bulk_delete(
    model: type[Model],
    objs: Iterable[Model],
    **_: Any,  # noqa: ANN401
) -> tuple[int, dict[str, int]]:
    """Delete the provided objects and return Django's delete summary.

    Accepts either a QuerySet or an iterable of model instances. When an
    iterable of instances is provided it is converted to a QuerySet by
    filtering on primary keys before calling ``QuerySet.delete()``.

    Args:
        model: Django model class.
        objs: Iterable of model instances or a QuerySet.

    Returns:
        Tuple(total_deleted, per_model_counts) as returned by ``delete()``.
    """
    return model.objects.filter(pk__in=(obj.pk for obj in objs)).delete()
bulk_delete_in_steps
bulk_delete_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int = STANDARD_BULK_SIZE,
) -> tuple[int, dict[str, int]]

Delete objects in batches and return deletion statistics.

Each chunk is deleted using Django's QuerySet delete which returns a (count, per-model-counts) tuple. Results are aggregated across chunks and returned as a consolidated tuple.

Parameters:

Name Type Description Default
model type[TModel]

Django model class.

required
bulk Iterable[TModel]

Iterable of model instances to delete.

required
step int

Chunk size for deletions.

STANDARD_BULK_SIZE

Returns:

Type Description
int

A tuple containing the total number of deleted objects and a

dict[str, int]

mapping from model label to deleted count (including cascaded

tuple[int, dict[str, int]]

deletions).

Source code in src/winidjango/core/db/bulk.py
def bulk_delete_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int = STANDARD_BULK_SIZE,
) -> tuple[int, dict[str, int]]:
    """Delete objects in batches and return deletion statistics.

    Each chunk is deleted using Django's QuerySet ``delete`` which
    returns a (count, per-model-counts) tuple. Results are aggregated
    across chunks and returned as a consolidated tuple.

    Args:
        model: Django model class.
        bulk: Iterable of model instances to delete.
        step: Chunk size for deletions.

    Returns:
        A tuple containing the total number of deleted objects and a
        mapping from model label to deleted count (including cascaded
        deletions).
    """
    return cast(
        "tuple[int, dict[str, int]]",
        bulk_method_in_steps(
            model=model,
            bulk=bulk,
            step=step,
            mode=MODE_DELETE,
        ),
    )
bulk_method_in_steps
bulk_method_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["create"],
    **kwargs: Any,
) -> list[TModel]
bulk_method_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["update"],
    **kwargs: Any,
) -> int
bulk_method_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["delete"],
    **kwargs: Any,
) -> tuple[int, dict[str, int]]
bulk_method_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: MODE_TYPES,
    **kwargs: Any,
) -> int | tuple[int, dict[str, int]] | list[TModel]

Run a batched bulk operation (create/update/delete) on bulk.

This wrapper warns if called from within an existing transaction and delegates actual work to :func:bulk_method_in_steps_atomic which is executed inside an atomic transaction. The return type depends on mode (see :mod:winidjango.core.db.bulk constants).

Parameters:

Name Type Description Default
model type[TModel]

Django model class to operate on.

required
bulk Iterable[TModel]

Iterable of model instances.

required
step int

Chunk size for processing.

required
mode MODE_TYPES

One of 'create', 'update' or 'delete'.

required
**kwargs Any

Additional keyword arguments forwarded to the underlying bulk methods (for example update_fields for updates).

{}

Returns:

Type Description
int | tuple[int, dict[str, int]] | list[TModel]

For create: list of created instances.

int | tuple[int, dict[str, int]] | list[TModel]

For update: integer number of updated rows.

int | tuple[int, dict[str, int]] | list[TModel]

For delete: tuple(total_deleted, per_model_counts).

Source code in src/winidjango/core/db/bulk.py
def bulk_method_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: MODE_TYPES,
    **kwargs: Any,
) -> int | tuple[int, dict[str, int]] | list[TModel]:
    """Run a batched bulk operation (create/update/delete) on ``bulk``.

    This wrapper warns if called from within an existing transaction and
    delegates actual work to :func:`bulk_method_in_steps_atomic` which is
    executed inside an atomic transaction. The return type depends on
    ``mode`` (see :mod:`winidjango.core.db.bulk` constants).

    Args:
        model: Django model class to operate on.
        bulk: Iterable of model instances.
        step: Chunk size for processing.
        mode: One of ``'create'``, ``'update'`` or ``'delete'``.
        **kwargs: Additional keyword arguments forwarded to the underlying
            bulk methods (for example ``update_fields`` for updates).

    Returns:
        For ``create``: list of created instances.
        For ``update``: integer number of updated rows.
        For ``delete``: tuple(total_deleted, per_model_counts).
    """
    # check if we are inside a transaction.atomic block
    _in_atomic_block = transaction.get_connection().in_atomic_block
    if _in_atomic_block:
        logger.info(
            "BE CAREFUL USING BULK OPERATIONS INSIDE A BROADER TRANSACTION BLOCK. "
            "BULKING WITH BULKS THAT DEPEND ON EACH OTHER CAN CAUSE "
            "INTEGRITY ERRORS OR POTENTIAL OTHER ISSUES.",
        )
    return bulk_method_in_steps_atomic(
        model=model,
        bulk=bulk,
        step=step,
        mode=mode,
        **kwargs,
    )
bulk_method_in_steps_atomic
bulk_method_in_steps_atomic[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["create"],
    **kwargs: Any,
) -> list[TModel]
bulk_method_in_steps_atomic[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["update"],
    **kwargs: Any,
) -> int
bulk_method_in_steps_atomic[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: Literal["delete"],
    **kwargs: Any,
) -> tuple[int, dict[str, int]]
bulk_method_in_steps_atomic[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: MODE_TYPES,
    **kwargs: Any,
) -> int | tuple[int, dict[str, int]] | list[TModel]

Atomic implementation of the batched bulk operation.

This function is decorated with transaction.atomic so each call to this function runs in a database transaction. Note that nesting additional, outer atomic blocks that also include dependent bulk operations can cause integrity issues for operations that depend on each other's side-effects; callers should be careful about atomic decorator placement in higher-level code.

Parameters:

Name Type Description Default
model type[TModel]

Django model class.

required
bulk Iterable[TModel]

Iterable of model instances.

required
step int

Chunk size for processing.

required
mode MODE_TYPES

One of 'create', 'update' or 'delete'.

required
**kwargs Any

Forwarded to the underlying bulk method.

{}

Returns:

Name Type Description
See int | tuple[int, dict[str, int]] | list[TModel]

func:bulk_method_in_steps for return value semantics.

Source code in src/winidjango/core/db/bulk.py
@transaction.atomic
def bulk_method_in_steps_atomic[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    step: int,
    mode: MODE_TYPES,
    **kwargs: Any,
) -> int | tuple[int, dict[str, int]] | list[TModel]:
    """Atomic implementation of the batched bulk operation.

    This function is decorated with ``transaction.atomic`` so each call
    to this function runs in a database transaction. Note that nesting
    additional, outer atomic blocks that also include dependent bulk
    operations can cause integrity issues for operations that depend on
    each other's side-effects; callers should be careful about atomic
    decorator placement in higher-level code.

    Args:
        model: Django model class.
        bulk: Iterable of model instances.
        step: Chunk size for processing.
        mode: One of ``'create'``, ``'update'`` or ``'delete'``.
        **kwargs: Forwarded to the underlying bulk method.

    Returns:
        See :func:`bulk_method_in_steps` for return value semantics.
    """
    bulk_method = get_bulk_method(model=model, mode=mode, **kwargs)

    chunks = get_step_chunks(bulk=bulk, step=step)

    # multithreading significantly increases speed
    result = multithread_loop(
        process_function=bulk_method,
        process_args=chunks,
    )

    return flatten_bulk_in_steps_result(result=result, mode=mode)
bulk_update_in_steps
bulk_update_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    update_fields: list[str],
    step: int = STANDARD_BULK_SIZE,
) -> int

Update objects in batches and return total updated count.

Parameters:

Name Type Description Default
model type[TModel]

Django model class.

required
bulk Iterable[TModel]

Iterable of model instances to update (must have PKs set).

required
update_fields list[str]

Fields to update on each instance when calling bulk_update.

required
step int

Chunk size for batched updates.

STANDARD_BULK_SIZE

Returns:

Type Description
int

Total number of rows updated across all chunks.

Source code in src/winidjango/core/db/bulk.py
def bulk_update_in_steps[TModel: Model](
    model: type[TModel],
    bulk: Iterable[TModel],
    update_fields: list[str],
    step: int = STANDARD_BULK_SIZE,
) -> int:
    """Update objects in batches and return total updated count.

    Args:
        model: Django model class.
        bulk: Iterable of model instances to update (must have PKs set).
        update_fields: Fields to update on each instance when calling
            ``bulk_update``.
        step: Chunk size for batched updates.

    Returns:
        Total number of rows updated across all chunks.
    """
    return cast(
        "int",
        bulk_method_in_steps(
            model=model,
            bulk=bulk,
            step=step,
            mode=MODE_UPDATE,
            fields=update_fields,
        ),
    )
flatten_bulk_in_steps_result
flatten_bulk_in_steps_result[TModel: Model](
    result: list[list[TModel]], mode: Literal["create"]
) -> list[TModel]
flatten_bulk_in_steps_result[TModel: Model](
    result: list[int], mode: Literal["update"]
) -> int
flatten_bulk_in_steps_result[TModel: Model](
    result: list[tuple[int, dict[str, int]]],
    mode: Literal["delete"],
) -> tuple[int, dict[str, int]]
flatten_bulk_in_steps_result[TModel: Model](
    result: list[int]
    | list[tuple[int, dict[str, int]]]
    | list[list[TModel]],
    mode: str,
) -> int | tuple[int, dict[str, int]] | list[TModel]

Aggregate per-chunk results returned by concurrent bulk execution.

Depending on mode the function reduces a list of per-chunk results into a single consolidated return value:

  • create: flattens a list of lists into a single list of objects
  • update: sums integer counts returned per chunk
  • delete: aggregates (count, per-model-dict) tuples into a single total and combined per-model counts

Parameters:

Name Type Description Default
result list[int] | list[tuple[int, dict[str, int]]] | list[list[TModel]]

List of per-chunk results returned by the chunk function.

required
mode str

One of the supported modes.

required

Returns:

Type Description
int | tuple[int, dict[str, int]] | list[TModel]

Aggregated result corresponding to mode.

Source code in src/winidjango/core/db/bulk.py
def flatten_bulk_in_steps_result[TModel: Model](
    result: list[int] | list[tuple[int, dict[str, int]]] | list[list[TModel]],
    mode: str,
) -> int | tuple[int, dict[str, int]] | list[TModel]:
    """Aggregate per-chunk results returned by concurrent bulk execution.

    Depending on ``mode`` the function reduces a list of per-chunk
    results into a single consolidated return value:

    - ``create``: flattens a list of lists into a single list of objects
    - ``update``: sums integer counts returned per chunk
    - ``delete``: aggregates (count, per-model-dict) tuples into a single
      total and combined per-model counts

    Args:
        result: List of per-chunk results returned by the chunk function.
        mode: One of the supported modes.

    Returns:
        Aggregated result corresponding to ``mode``.
    """
    if mode == MODE_UPDATE:
        # formatted as [1000, 1000, ...]
        # since django 4.2 bulk_update returns the count of updated objects
        result = cast("list[int]", result)
        return int(sum(result))
    if mode == MODE_DELETE:
        # formatted as [(count, {model_name: count, model_cascade_name: count}), ...]
        # join the results to get the total count of deleted objects
        result = cast("list[tuple[int, dict[str, int]]]", result)
        total_count = 0
        count_sum_by_model: defaultdict[str, int] = defaultdict(int)
        for count_sum, count_by_model in result:
            total_count += count_sum
            for model_name, count in count_by_model.items():
                count_sum_by_model[model_name] += count
        return (total_count, dict(count_sum_by_model))
    if mode == MODE_CREATE:
        # formatted as [[obj1, obj2, ...], [obj1, obj2, ...], ...]
        result = cast("list[list[TModel]]", result)
        return [item for sublist in result for item in sublist]

    msg = f"Invalid method. Must be one of {MODES}"
    raise ValueError(msg)
get_bulk_method
get_bulk_method(
    model: type[Model],
    mode: Literal["create"],
    **kwargs: Any,
) -> Callable[[list[Model]], list[Model]]
get_bulk_method(
    model: type[Model],
    mode: Literal["update"],
    **kwargs: Any,
) -> Callable[[list[Model]], int]
get_bulk_method(
    model: type[Model],
    mode: Literal["delete"],
    **kwargs: Any,
) -> Callable[[list[Model]], tuple[int, dict[str, int]]]
get_bulk_method(
    model: type[Model], mode: MODE_TYPES, **kwargs: Any
) -> Callable[
    [list[Model]],
    list[Model] | int | tuple[int, dict[str, int]],
]

Return a callable that performs the requested bulk operation on a chunk.

The returned function accepts a single argument (a list of model instances) and returns the per-chunk result for the chosen mode.

Parameters:

Name Type Description Default
model type[Model]

Django model class.

required
mode MODE_TYPES

One of 'create', 'update' or 'delete'.

required
**kwargs Any

Forwarded to the underlying ORM bulk methods.

{}

Raises:

Type Description
ValueError

If mode is invalid.

Returns:

Type Description
Callable[[list[Model]], list[Model] | int | tuple[int, dict[str, int]]]

Callable that accepts a list of model instances and returns the

Callable[[list[Model]], list[Model] | int | tuple[int, dict[str, int]]]

result for that chunk.

Source code in src/winidjango/core/db/bulk.py
def get_bulk_method(
    model: type[Model],
    mode: MODE_TYPES,
    **kwargs: Any,
) -> Callable[[list[Model]], list[Model] | int | tuple[int, dict[str, int]]]:
    """Return a callable that performs the requested bulk operation on a chunk.

    The returned function accepts a single argument (a list of model
    instances) and returns the per-chunk result for the chosen mode.

    Args:
        model: Django model class.
        mode: One of ``'create'``, ``'update'`` or ``'delete'``.
        **kwargs: Forwarded to the underlying ORM bulk methods.

    Raises:
        ValueError: If ``mode`` is invalid.

    Returns:
        Callable that accepts a list of model instances and returns the
        result for that chunk.
    """
    bulk_method: Callable[[list[Model]], list[Model] | int | tuple[int, dict[str, int]]]
    if mode == MODE_CREATE:

        def bulk_create_chunk(chunk: list[Model]) -> list[Model]:
            return model.objects.bulk_create(objs=chunk, **kwargs)

        bulk_method = bulk_create_chunk
    elif mode == MODE_UPDATE:

        def bulk_update_chunk(chunk: list[Model]) -> int:
            return model.objects.bulk_update(objs=chunk, **kwargs)

        bulk_method = bulk_update_chunk
    elif mode == MODE_DELETE:

        def bulk_delete_chunk(chunk: list[Model]) -> tuple[int, dict[str, int]]:
            return bulk_delete(model=model, objs=chunk, **kwargs)

        bulk_method = bulk_delete_chunk
    else:
        msg = f"Invalid method. Must be one of {MODES}"
        raise ValueError(msg)

    return bulk_method
get_differences_between_bulks
get_differences_between_bulks[
    TModel1: Model,
    TModel2: Model,
](
    bulk1: list[TModel1],
    bulk2: list[TModel2],
    fields: list[
        Field[Any, Any]
        | ForeignObjectRel
        | GenericForeignKey
    ],
) -> tuple[
    list[TModel1],
    list[TModel2],
    list[TModel1],
    list[TModel2],
]

Return differences and intersections between two bulks of the same model.

Instances are compared using :func:hash_model_instance over the provided fields. The function maintains the original ordering for returned lists so that callers can preserve deterministic ordering when applying diffs.

Parameters:

Name Type Description Default
bulk1 list[TModel1]

First list of model instances.

required
bulk2 list[TModel2]

Second list of model instances.

required
fields list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]

Fields to include when hashing instances.

required

Raises:

Type Description
ValueError

If bulks are empty or contain different model types.

Returns:

Type Description
tuple[list[TModel1], list[TModel2], list[TModel1], list[TModel2]]

Four lists in the order: (in_1_not_2, in_2_not_1, in_both_from_1, in_both_from_2).

Source code in src/winidjango/core/db/bulk.py
def get_differences_between_bulks[TModel1: Model, TModel2: Model](
    bulk1: list[TModel1],
    bulk2: list[TModel2],
    fields: "list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]",
) -> tuple[list[TModel1], list[TModel2], list[TModel1], list[TModel2]]:
    """Return differences and intersections between two bulks of the same model.

    Instances are compared using :func:`hash_model_instance` over the
    provided ``fields``. The function maintains the original ordering
    for returned lists so that callers can preserve deterministic
    ordering when applying diffs.

    Args:
        bulk1: First list of model instances.
        bulk2: Second list of model instances.
        fields: Fields to include when hashing instances.

    Raises:
        ValueError: If bulks are empty or contain different model types.

    Returns:
        Four lists in the order:
            (in_1_not_2, in_2_not_1, in_both_from_1, in_both_from_2).
    """
    if not bulk1 or not bulk2:
        return bulk1, bulk2, [], []

    if type(bulk1[0]) is not type(bulk2[0]):
        msg = "Both bulks must be of the same model type."
        raise ValueError(msg)

    hash_model_instance_with_fields = partial(
        hash_model_instance,
        fields=fields,
    )
    # Precompute hashes and map them directly to models in a single pass for both bulks
    hashes1 = list(map(hash_model_instance_with_fields, bulk1))
    hashes2 = list(map(hash_model_instance_with_fields, bulk2))

    # Convert keys to sets for difference operations
    set1, set2 = set(hashes1), set(hashes2)

    # Calculate differences between sets
    # Find differences and intersection with original order preserved
    # Important, we need to return the original objects that are the same in memory,
    # so in_1_not_2 and in_2_not_1
    in_1_not_2 = set1 - set2
    in_1_not_2_list = [
        model
        for model, hash_ in zip(bulk1, hashes1, strict=False)
        if hash_ in in_1_not_2
    ]

    in_2_not_1 = set2 - set1
    in_2_not_1_list = [
        model
        for model, hash_ in zip(bulk2, hashes2, strict=False)
        if hash_ in in_2_not_1
    ]

    in_1_and_2 = set1 & set2
    in_1_and_2_from_1 = [
        model
        for model, hash_ in zip(bulk1, hashes1, strict=False)
        if hash_ in in_1_and_2
    ]
    in_1_and_2_from_2 = [
        model
        for model, hash_ in zip(bulk2, hashes2, strict=False)
        if hash_ in in_1_and_2
    ]

    return in_1_not_2_list, in_2_not_1_list, in_1_and_2_from_1, in_1_and_2_from_2
get_step_chunks
get_step_chunks(
    bulk: Iterable[Model], step: int
) -> Generator[tuple[list[Model]], None, None]

Yield consecutive chunks of at most step items from bulk.

The function yields a single-tuple containing the chunk (a list of model instances) because the concurrent execution helper expects a tuple of positional arguments for the target function.

Parameters:

Name Type Description Default
bulk Iterable[Model]

Iterable of model instances.

required
step int

Maximum number of instances per yielded chunk.

required

Yields:

Type Description
tuple[list[Model]]

Tuples where the first element is a list of model instances.

Source code in src/winidjango/core/db/bulk.py
def get_step_chunks(
    bulk: Iterable[Model],
    step: int,
) -> Generator[tuple[list[Model]], None, None]:
    """Yield consecutive chunks of at most ``step`` items from ``bulk``.

    The function yields a single-tuple containing the chunk (a list of
    model instances) because the concurrent execution helper expects a
    tuple of positional arguments for the target function.

    Args:
        bulk: Iterable of model instances.
        step: Maximum number of instances per yielded chunk.

    Yields:
        Tuples where the first element is a list of model instances.
    """
    bulk = iter(bulk)
    while True:
        chunk = list(islice(bulk, step))
        if not chunk:
            break
        yield (chunk,)  # bc concurrent_loop expects a tuple of args
multi_simulate_bulk_deletion
multi_simulate_bulk_deletion(
    entries: dict[type[Model], list[Model]],
) -> dict[type[Model], set[Model]]

Simulate deletions for multiple model classes and merge results.

Runs :func:simulate_bulk_deletion for each provided model and returns a unified mapping of all models that would be deleted.

Parameters:

Name Type Description Default
entries dict[type[Model], list[Model]]

Mapping from model class to list of instances to simulate.

required

Returns:

Type Description
dict[type[Model], set[Model]]

Mapping from model class to set of instances that would be deleted.

Source code in src/winidjango/core/db/bulk.py
def multi_simulate_bulk_deletion(
    entries: dict[type[Model], list[Model]],
) -> dict[type[Model], set[Model]]:
    """Simulate deletions for multiple model classes and merge results.

    Runs :func:`simulate_bulk_deletion` for each provided model and
    returns a unified mapping of all models that would be deleted.

    Args:
        entries: Mapping from model class to list of instances to simulate.

    Returns:
        Mapping from model class to set of instances that would be deleted.
    """
    deletion_summaries = [
        simulate_bulk_deletion(model, entry) for model, entry in entries.items()
    ]
    # join the dicts to get the total count of deleted objects
    joined_deletion_summary: defaultdict[type[Model], set[Model]] = defaultdict(set)
    for deletion_summary in deletion_summaries:
        for model, objects in deletion_summary.items():
            joined_deletion_summary[model].update(objects)

    return dict(joined_deletion_summary)
simulate_bulk_deletion
simulate_bulk_deletion[TModel: Model](
    model_class: type[TModel], entries: list[TModel]
) -> dict[type[Model], set[Model]]

Simulate Django's delete cascade and return affected objects.

Uses :class:django.db.models.deletion.Collector to determine which objects (including cascaded related objects) would be removed if the provided entries were deleted. No database writes are performed.

Parameters:

Name Type Description Default
model_class type[TModel]

Model class of the provided entries.

required
entries list[TModel]

Instances to simulate deletion for.

required

Returns:

Type Description
dict[type[Model], set[Model]]

Mapping from model class to set of instances that would be deleted.

Source code in src/winidjango/core/db/bulk.py
def simulate_bulk_deletion[TModel: Model](
    model_class: type[TModel],
    entries: list[TModel],
) -> dict[type[Model], set[Model]]:
    """Simulate Django's delete cascade and return affected objects.

    Uses :class:`django.db.models.deletion.Collector` to determine which
    objects (including cascaded related objects) would be removed if the
    provided entries were deleted. No database writes are performed.

    Args:
        model_class: Model class of the provided entries.
        entries: Instances to simulate deletion for.

    Returns:
        Mapping from model class to set of instances that would be deleted.
    """
    if not entries:
        return {}

    # Initialize the Collector
    using = router.db_for_write(model_class)
    collector = Collector(using)

    # Collect deletion cascade for all entries
    collector.collect(entries)  # ty:ignore[invalid-argument-type]

    # Prepare the result dictionary
    deletion_summary: defaultdict[type[Model], set[Model]] = defaultdict(set)

    # Add normal deletes
    for model, objects in collector.data.items():
        deletion_summary[model].update(objects)  # objects is already iterable

    # Add fast deletes (explicitly expand querysets)
    for queryset in collector.fast_deletes:
        deletion_summary[queryset.model].update(list(queryset))

    return deletion_summary
fields

Utilities for inspecting Django model fields.

This module provides small helpers that make it easier to introspect Django model fields and metadata in a type-friendly way. The helpers are used across the project's database utilities to implement operations like topological sorting and deterministic hashing of model instances.

get_field_names
get_field_names(
    fields: list[
        Field[Any, Any]
        | ForeignObjectRel
        | GenericForeignKey
    ],
) -> list[str]

Return the name attribute for a list of Django field objects.

Parameters:

Name Type Description Default
fields list[Field | ForeignObjectRel | GenericForeignKey]

Field objects obtained from a model's _meta.get_fields().

required

Returns:

Type Description
list[str]

list[str]: List of field names in the same order as fields.

Source code in src/winidjango/core/db/fields.py
def get_field_names(
    fields: "list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]",
) -> list[str]:
    """Return the ``name`` attribute for a list of Django field objects.

    Args:
        fields (list[Field | ForeignObjectRel | GenericForeignKey]):
            Field objects obtained from a model's ``_meta.get_fields()``.

    Returns:
        list[str]: List of field names in the same order as ``fields``.
    """
    return [field.name for field in fields]
get_fields
get_fields[TModel: Model](
    model: type[TModel],
) -> list[
    Field[Any, Any] | ForeignObjectRel | GenericForeignKey
]

Return all field objects for a Django model.

This wraps model._meta.get_fields() and is typed to include relationship fields so callers can handle both regular and related fields uniformly.

Parameters:

Name Type Description Default
model type[Model]

Django model class.

required

Returns:

Type Description
list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]

list[Field | ForeignObjectRel | GenericForeignKey]: All field objects associated with the model.

Source code in src/winidjango/core/db/fields.py
def get_fields[TModel: Model](
    model: type[TModel],
) -> "list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]":
    """Return all field objects for a Django model.

    This wraps ``model._meta.get_fields()`` and is typed to include
    relationship fields so callers can handle both regular and related
    fields uniformly.

    Args:
        model (type[Model]): Django model class.

    Returns:
        list[Field | ForeignObjectRel | GenericForeignKey]: All field
            objects associated with the model.
    """
    return get_model_meta(model).get_fields()
get_model_meta
get_model_meta(model: type[Model]) -> Options[Model]

Return a model class' _meta options object.

This small wrapper exists to make typing clearer at call sites where the code needs the model Options object.

Parameters:

Name Type Description Default
model type[Model]

Django model class.

required

Returns:

Name Type Description
Options Options[Model]

The model's _meta options object.

Source code in src/winidjango/core/db/fields.py
def get_model_meta(model: type[Model]) -> "Options[Model]":
    """Return a model class' ``_meta`` options object.

    This small wrapper exists to make typing clearer at call sites where
    the code needs the model Options object.

    Args:
        model (type[Model]): Django model class.

    Returns:
        Options: The model's ``_meta`` options object.
    """
    return model._meta  # noqa: SLF001
models

Database utilities and lightweight model helpers.

This module provides helpers used across the project when manipulating Django models: ordering models by foreign-key dependencies, creating a deterministic hash for unsaved instances, and a project-wide BaseModel that exposes common timestamp fields.

BaseModel

Bases: Model

Abstract base model containing common fields and helpers.

Concrete models can inherit from this class to get consistent created_at and updated_at timestamp fields and convenient string representations.

meta property
meta: Options[Self]

Return the model's _meta options object.

This property is a small convenience wrapper used to make access sites slightly more explicit in code and improve typing in callers.

Meta

Mark the model as abstract.

__repr__
__repr__() -> str

Base representation of a model.

Source code in src/winidjango/core/db/models.py
def __repr__(self) -> str:
    """Base representation of a model."""
    return str(self)
__str__
__str__() -> str

Return a concise human-readable representation.

The default shows the model class name and primary key which is useful for logging and interactive debugging.

Returns:

Name Type Description
str str

Short representation, e.g. MyModel(123).

Source code in src/winidjango/core/db/models.py
def __str__(self) -> str:
    """Return a concise human-readable representation.

    The default shows the model class name and primary key which is
    useful for logging and interactive debugging.

    Returns:
        str: Short representation, e.g. ``MyModel(123)``.
    """
    return f"{self.__class__.__name__}({self.pk})"
hash_model_instance
hash_model_instance(
    instance: Model,
    fields: list[
        Field[Any, Any]
        | ForeignObjectRel
        | GenericForeignKey
    ],
) -> int

Compute a deterministic hash for a model instance.

The function returns a hash suitable for comparing unsaved model instances by their field values. If the instance has a primary key (instance.pk) that key is hashed and returned immediately; this keeps comparisons cheap for persisted objects.

Parameters:

Name Type Description Default
instance Model

The Django model instance to hash.

required
fields list[Field | ForeignObjectRel | GenericForeignKey]

Field objects that should be included when computing the hash.

required

Returns:

Name Type Description
int int

Deterministic integer hash of the instance. For persisted instances this is hash(instance.pk).

Notes
  • The returned hash is intended for heuristic comparisons (e.g. deduplication in import pipelines) and is not cryptographically secure. Use with care when relying on absolute uniqueness.
Source code in src/winidjango/core/db/models.py
def hash_model_instance(
    instance: Model,
    fields: "list[Field[Any, Any] | ForeignObjectRel | GenericForeignKey]",
) -> int:
    """Compute a deterministic hash for a model instance.

    The function returns a hash suitable for comparing unsaved model
    instances by their field values. If the instance has a primary key
    (``instance.pk``) that key is hashed and returned immediately; this
    keeps comparisons cheap for persisted objects.

    Args:
        instance (Model): The Django model instance to hash.
        fields (list[Field | ForeignObjectRel | GenericForeignKey]):
            Field objects that should be included when computing the hash.

    Returns:
        int: Deterministic integer hash of the instance. For persisted
            instances this is ``hash(instance.pk)``.

    Notes:
        - The returned hash is intended for heuristic comparisons (e.g.
          deduplication in import pipelines) and is not cryptographically
          secure. Use with care when relying on absolute uniqueness.
    """
    if instance.pk:
        return hash(instance.pk)

    field_names = get_field_names(fields)
    model_dict = model_to_dict(instance, fields=field_names)
    sorted_dict = dict(sorted(model_dict.items()))
    values = (type(instance), tuple(sorted_dict.items()))
    return hash(values)
topological_sort_models
topological_sort_models(
    models: list[type[Model]],
) -> list[type[Model]]

Sort Django models in dependency order using topological sorting.

Analyzes foreign key relationships between Django models and returns them in an order where dependencies come before dependents. This ensures that when performing operations like bulk creation or deletion, models are processed in the correct order to avoid foreign key constraint violations.

The function uses Python's graphlib.TopologicalSorter to perform the sorting based on ForeignKey relationships between the provided models. Only relationships between models in the input list are considered.

Parameters:

Name Type Description Default
models list[type[Model]]

A list of Django model classes to sort based on their foreign key dependencies.

required

Returns:

Type Description
list[type[Model]]

list[type[Model]]: The input models sorted in dependency order, where models that are referenced by foreign keys appear before models that reference them. Self-referential relationships are ignored.

Raises:

Type Description
CycleError

If there are circular dependencies between models that cannot be resolved.

Example
Assuming Author model has no dependencies
and Book model has ForeignKey to Author

models = [Book, Author] sorted_models = topological_sort_models(models) sorted_models [, ]

Note
  • Only considers ForeignKey relationships, not other field types
  • Self-referential foreign keys are ignored to avoid self-loops
  • Only relationships between models in the input list are considered
Source code in src/winidjango/core/db/models.py
def topological_sort_models(
    models: list[type[Model]],
) -> list[type[Model]]:
    """Sort Django models in dependency order using topological sorting.

    Analyzes foreign key relationships between Django models and returns them
    in an order where dependencies come before dependents. This ensures that
    when performing operations like bulk creation or deletion, models are
    processed in the correct order to avoid foreign key constraint violations.

    The function uses Python's graphlib.TopologicalSorter to perform the sorting
    based on ForeignKey relationships between the provided models. Only
    relationships between models in the input list are considered.

    Args:
        models (list[type[Model]]): A list of Django model classes to sort
            based on their foreign key dependencies.

    Returns:
        list[type[Model]]: The input models sorted in dependency order, where
            models that are referenced by foreign keys appear before models
            that reference them. Self-referential relationships are ignored.

    Raises:
        graphlib.CycleError: If there are circular dependencies between models
            that cannot be resolved.

    Example:
        >>> # Assuming Author model has no dependencies
        >>> # and Book model has ForeignKey to Author
        >>> models = [Book, Author]
        >>> sorted_models = topological_sort_models(models)
        >>> sorted_models
        [<class 'Author'>, <class 'Book'>]

    Note:
        - Only considers ForeignKey relationships, not other field types
        - Self-referential foreign keys are ignored to avoid self-loops
        - Only relationships between models in the input list are considered
    """
    ts: TopologicalSorter[type[Model]] = TopologicalSorter()

    for model in models:
        deps = {
            field.related_model
            for field in get_fields(model)
            if isinstance(field, ForeignKey)
            and isinstance(field.related_model, type)
            and field.related_model in models
            and field.related_model is not model
        }
        ts.add(model, *deps)

    return list(ts.static_order())
setup

Concurrency-safe Django database setup helpers.

Django's migration recorder checks whether its bookkeeping table exists and, if not, creates it. That check-then-create is not atomic, so when multiple processes call migrate against the same database at once (for example, several concurrent invocations of the same pre-commit hook), more than one process can decide the table is missing and race to create it, crashing with "table already exists". migrate_safely serializes migration across processes with a file lock so only one process ever runs it at a time; the rest simply find the schema already up to date once they acquire the lock.

migrate_safely
migrate_safely(db_path: Path) -> None

Apply Django migrations without racing other concurrent processes.

Must be called after django.setup(). Serializes access with a file lock next to db_path so concurrent processes targeting the same database apply migrations one at a time instead of racing to create the migration bookkeeping table.

Parameters:

Name Type Description Default
db_path Path

Path to the database file the migrations apply to.

required
Source code in src/winidjango/core/db/setup.py
def migrate_safely(db_path: Path) -> None:
    """Apply Django migrations without racing other concurrent processes.

    Must be called after ``django.setup()``. Serializes access with a
    file lock next to ``db_path`` so concurrent processes targeting the
    same database apply migrations one at a time instead of racing to
    create the migration bookkeeping table.

    Args:
        db_path: Path to the database file the migrations apply to.
    """
    lock_path = db_path.with_suffix(f"{db_path.suffix}.lock")
    lock_path.parent.mkdir(parents=True, exist_ok=True)
    with FileLock(lock_path):
        call_command("migrate")
sql

Low-level helper to execute raw SQL against Django's database.

This module exposes :func:execute_sql which runs a parameterized SQL query using Django's database connection and returns column names and rows. It is intended for one-off queries where ORM abstractions are insufficient or when reading complex reports from the database.

The helper uses Django's connection cursor context manager to ensure resources are cleaned up correctly. Results are fetched into memory so avoid using it for very large result sets.

execute_sql
execute_sql(
    sql: str, params: dict[str, Any] | None = None
) -> tuple[list[str], list[tuple[Any, ...]]]

Execute a SQL statement and return column names and rows.

Parameters:

Name Type Description Default
sql str

SQL statement possibly containing named placeholders (%(name)s) for database binding.

required
params dict[str, Any] | None

Optional mapping of parameters to bind to the query.

None

Returns:

Type Description
list[str]

Tuple[List[str], List[Tuple[Any, ...]]]: A tuple where the first

list[tuple[Any, ...]]

element is the list of column names (empty list if the statement

tuple[list[str], list[tuple[Any, ...]]]

returned no rows) and the second element is a list of row tuples.

Raises:

Type Description
Error

Propagates underlying database errors raised by Django's database backend.

Source code in src/winidjango/core/db/sql.py
def execute_sql(
    sql: str,
    params: dict[str, Any] | None = None,
) -> tuple[list[str], list[tuple[Any, ...]]]:
    """Execute a SQL statement and return column names and rows.

    Args:
        sql (str): SQL statement possibly containing named placeholders
            (``%(name)s``) for database binding.
        params (dict[str, Any] | None): Optional mapping of parameters to
            bind to the query.

    Returns:
        Tuple[List[str], List[Tuple[Any, ...]]]: A tuple where the first
        element is the list of column names (empty list if the statement
        returned no rows) and the second element is a list of row tuples.

    Raises:
        django.db.Error: Propagates underlying database errors raised by
            Django's database backend.
    """
    with connection.cursor() as cursor:
        cursor.execute(sql=sql, params=params)
        rows = cursor.fetchall()
        column_names = (
            [col[0] for col in cursor.description] if cursor.description else []
        )

    return column_names, rows

rig

init module.

configs

Package initialization.

pyproject

Manage pyproject.toml (PEP 518, 621, 660).

Handles metadata, dependencies, build config (uv), tool configs (ruff, ty, pytest, bandit, rumdl). Enforces opinionated defaults: all ruff rules (except D203, D213, COM812, ANN401), Google docstrings, strict ty, bandit security, coverage threshold. Validation uses subset checking (users can add extra configs). Priority 20 (created early for other configs to read).

Utility methods: project info, dependencies, Python versions, license detection, classifiers.

DjangoPyprojectConfigFile

Bases: PyprojectConfigFile, PyprojectConfigFile

Overrides base to resolve conflicts.

Only needed when developing Winidjango itself.

PyprojectConfigFile

Bases: PyprojectConfigFile

You can override methods from the base class to customize behavior.

resources

init module.

tools

Tool wrappers for CLI tools used in development workflows.

Tools are subclasses of Tool providing methods that return Args objects for type-safe command construction and execution.

tools

Override pyrig tools.

ProjectTester

Bases: ProjectTester

Subclass of ProjectTester for customizing pyrig behavior.

dev_dependencies
dev_dependencies() -> tuple[str, ...]

Get the dev dependencies.

Source code in src/winidjango/rig/tools/tools.py
def dev_dependencies(self) -> tuple[str, ...]:
    """Get the dev dependencies."""
    return (*super().dev_dependencies(), "pytest-django")