django_utils package¶
Subpackages¶
- django_utils.admin package
- Submodules
- django_utils.admin.export module
- django_utils.admin.filters module
- django_utils.admin.mixins module
- django_utils.admin.widgets module
- Module contents
- django_utils.management package
- Subpackages
- Module contents
- django_utils.templatetags package
Submodules¶
django_utils.aggregates module¶
Aggregates computed in a subquery, immune to JOIN fan-out.
The classic footgun: annotate(Count('review'), Count('topping'))
implements each aggregate as a JOIN, so aggregating two one-to-many
relations together counts the cartesian product: a sandwich with 2
reviews and 3 toppings reports 6 of each. Each helper here runs its
aggregate in an independent (SELECT ... FROM (subquery)) instead, so
combining any number of them stays correct:
from django.db.models import OuterRef
from django_utils.aggregates import SubqueryCount, SubquerySum
Sandwich.objects.annotate(
reviews=SubqueryCount(Review.objects.filter(sandwich=OuterRef('pk'))),
topping_total=SubquerySum(
Topping.objects.filter(sandwich=OuterRef('pk')), 'price'
),
)
Annotations built this way support filter() and order_by() like
any other. SubqueryCount of an empty set is 0; the column
aggregates return NULL (Python None) for an empty set, matching
SQL. Wrap in django.db.models.functions.Coalesce for a default.
Backend honesty: the correlated subquery lives inside a FROM-clause
derived table (FROM (SELECT ...) _agg/_count). MySQL earlier
than 8.0.14 cannot reference the outer query's columns from within a
derived table and fails loudly with an unknown-column error. Verified
on SQLite (this repo's CI); expected to work on PostgreSQL and on
MySQL/MariaDB 8.0.14+, but not CI-verified on either.
The column aggregates (SubquerySum, SubqueryAvg, SubqueryMin,
SubqueryMax) reserve agg_value as the inner annotation alias.
A queryset whose model has a real field or annotation named
agg_value raises Django's annotation-conflict error.
- class django_utils.aggregates.SubqueryAvg(queryset: models.QuerySet[Any] | str, column: str, *, output_field: models.Field[Any, Any] | None = None, **extra: Any)[source]¶
Bases:
_SubqueryColumnAggregateAVG(column); defaults toFloatFieldoutput because SQL AVG of integers is fractional.- function = 'AVG'¶
- class django_utils.aggregates.SubqueryCount(queryset: QuerySet | str, **extra: Any)[source]¶
Bases:
_RelationNameMixin,SubqueryCOUNT(*)overqueryset, evaluated as its own subquery.Unsliced inner querysets get their ordering stripped (pointless in an aggregate); sliced ones keep it (a top-N slice needs its ordering).
querysetmay also be a relation name on the model being annotated (SubqueryCount('review')), resolved atresolve_expressiontime against reverse FK, reverse M2M, or forward M2M relations.- template = '(SELECT COUNT(*) FROM (%(subquery)s) _count)'¶
- class django_utils.aggregates.SubqueryMax(queryset: models.QuerySet[Any] | str, column: str, *, output_field: models.Field[Any, Any] | None = None, **extra: Any)[source]¶
Bases:
_SubqueryColumnAggregateMAX(column);Nonefor an empty set.- function = 'MAX'¶
django_utils.auth module¶
The auth helpers Django is missing.
login_required and permission_required ship with Django;
superuser_required/staff_required do not (django/new-features
#47) and get re-implemented in nearly every project. Likewise the
'app_label.action_modelname' string every user.has_perm() call
needs is hand-formatted everywhere (django/new-features #137);
permission_string builds it from the model.
- django_utils.auth.permission_string(model: type[Model], action: str) str[source]¶
Return the
'app_label.action_modelname'stringhas_permwants.>>> from tests.test_app import models as test_models >>> permission_string(test_models.Sandwich, 'change') 'test_app.change_sandwich'
- django_utils.auth.staff_required(view_func: _View | None = None, *, login_url: str | None = None, raise_exception: bool = False) _View | Callable[[_View], _View][source]¶
Allow only users with
is_staff. Seesuperuser_required.Works on both sync and async views.
- django_utils.auth.superuser_required(view_func: _View | None = None, *, login_url: str | None = None, raise_exception: bool = False) _View | Callable[[_View], _View][source]¶
Allow only users with
is_superuser.Mirrors
login_required's shape: use bare (@superuser_required) or parameterized (@superuser_required(raise_exception=True)). Failing users are redirected to login (login_urlorsettings.LOGIN_URL); withraise_exception=Truethey getPermissionDenied(HTTP 403) instead, matchingpermission_required's option of the same name. Works on both sync and async views.
django_utils.base_models module¶
- class django_utils.base_models.CreatedAtModelBase(*args, **kwargs)[source]¶
Bases:
ModelBase- class Meta[source]¶
Bases:
object- abstract = False¶
- db_table = 'django_utils_created_at_model_base'¶
- created_at¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- get_next_by_created_at(*, field=<django.db.models.fields.DateTimeField: created_at>, is_next=True, **kwargs)¶
- get_next_by_updated_at(*, field=<django.db.models.fields.DateTimeField: updated_at>, is_next=True, **kwargs)¶
- get_previous_by_created_at(*, field=<django.db.models.fields.DateTimeField: created_at>, is_next=False, **kwargs)¶
- get_previous_by_updated_at(*, field=<django.db.models.fields.DateTimeField: updated_at>, is_next=False, **kwargs)¶
- updated_at¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- class django_utils.base_models.ModelBaseMeta(name: str, bases: tuple[type, ...], attrs: dict[str, Any], **kwargs: Any)[source]¶
Bases:
ModelBaseModel base with more readable naming convention
Example: Assuming the model is called app.FooBarObject
Default Django table name: app_foobarobject Table name with this base: app_foo_bar_object
- class django_utils.base_models.NameCreatedAtModelBase(*args, **kwargs)[source]¶
Bases:
NameModelBase,CreatedAtModelBase- class Meta[source]¶
Bases:
object- abstract = False¶
- db_table = 'django_utils_name_created_at_model_base'¶
- created_at¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- get_next_by_created_at(*, field=<django.db.models.fields.DateTimeField: created_at>, is_next=True, **kwargs)¶
- get_next_by_updated_at(*, field=<django.db.models.fields.DateTimeField: updated_at>, is_next=True, **kwargs)¶
- get_previous_by_created_at(*, field=<django.db.models.fields.DateTimeField: created_at>, is_next=False, **kwargs)¶
- get_previous_by_updated_at(*, field=<django.db.models.fields.DateTimeField: updated_at>, is_next=False, **kwargs)¶
- name: Any¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- updated_at¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- class django_utils.base_models.NameMixin[source]¶
Bases:
objectMixin to automatically get a unicode and repr string base on the name
>>> x = NameMixin() >>> x.pk = 123 >>> x.name = 'test' >>> repr(x) '<NameMixin[123]: test>' >>> str(x) 'test' >>> str(str(x)) 'test'
- class django_utils.base_models.SlugCreatedAtModelBase(*args, **kwargs)[source]¶
Bases:
SlugModelBase,CreatedAtModelBase- class Meta[source]¶
Bases:
object- abstract = False¶
- db_table = 'django_utils_slug_created_at_model_base'¶
- created_at¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- get_next_by_created_at(*, field=<django.db.models.fields.DateTimeField: created_at>, is_next=True, **kwargs)¶
- get_next_by_updated_at(*, field=<django.db.models.fields.DateTimeField: updated_at>, is_next=True, **kwargs)¶
- get_previous_by_created_at(*, field=<django.db.models.fields.DateTimeField: created_at>, is_next=False, **kwargs)¶
- get_previous_by_updated_at(*, field=<django.db.models.fields.DateTimeField: updated_at>, is_next=False, **kwargs)¶
- name: Any¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- slug: Any¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- updated_at¶
A wrapper for a deferred-loading field. When the value is read from this object the first time, the query is executed.
- class django_utils.base_models.SlugMixin[source]¶
Bases:
NameMixinMixin to automatically slugify the name and add both a name and slug to the model
>>> x = NameMixin() >>> x.pk = 123 >>> x.name = 'test' >>> repr(x) '<NameMixin[123]: test>' >>> str(x) 'test' >>> str(str(x)) 'test'
Note: this mixin does not add a unique constraint on
slug. The nestedMeta.unique_togetherbelow is inert --SlugMixinis not itself aModel, and a concrete subclass such asSlugModelBasedeclares its ownMeta, which does not inherit from this one. Models that need the slug to be enforced unique at the database level should declareunique=Trueon their own slug field.- get_unique_slug(base: str) str[source]¶
Return
base, suffixed with a counter if already taken.Probes with
_base_manager-- Django's documented unfiltered manager -- rather than_default_manager, so a model with a filtered default manager (soft-delete style:objects = ActiveManager()) still gets checked against every row in the table, not just the ones its default manager happens to expose. Otherwise two rows hidden from each other by the filter can silently collide onto the same slug.Note: the uniqueness check and the eventual insert are not atomic, so two concurrent saves can still race each other onto the same slug.
SlugMixindoes not add a unique constraint (see the class docstring), so callers with high write concurrency on the same name should declareunique=Trueon their own slug field and handle the resultingIntegrityError.
django_utils.bulk module¶
Bulk upsert built on Django's native conflict handling.
bulk_update_or_create wraps bulk_create(update_conflicts=True)
(Django 4.1+) with chunking and up-front validation. Unlike a loop of
update_or_create (2N queries, racy between check and write), the
upsert happens in one INSERT ... ON CONFLICT DO UPDATE statement per
batch, atomic per row on the database side.
Backend honesty: PostgreSQL and SQLite use ON CONFLICT with
unique_fields naming the conflict target; MySQL/MariaDB use
ON DUPLICATE KEY UPDATE, which ignores unique_fields and fires
on any unique constraint: identical behaviour when the model has one
unique constraint, subtly broader when it has several.
Version honesty: on Django 5.0+ the returned objects have their primary
keys populated (inserted and conflict-updated rows alike), where the
backend can return rows from a bulk insert at all: PostgreSQL and
SQLite can, MariaDB can, vanilla MySQL never can (its Django backend
disables row-returning inserts on every Django version). On Django
4.2, bulk_create(update_conflicts=True) cannot return IDs anywhere
(Django ticket #34698, fixed in 5.0), so every returned object has
pk=None even though its row was written.
Field names accept the same spellings bulk_create does: field names
(owner), foreign-key attnames (owner_id), and the 'pk'
alias.
- django_utils.bulk.bulk_update_or_create(objs: Iterable[M], *, unique_fields: Sequence[str], update_fields: Sequence[str], batch_size: int = 1000) list[M][source]¶
Insert
objs, updatingupdate_fieldson conflicts.Rows whose
unique_fieldsalready exist are updated instead of raisingIntegrityError; new rows are inserted. Returns the input objects as a list. All validation errors raiseValueErrorbefore any query runs -- including whenobjsis empty, so a badbatch_sizeor field list is never masked by an empty input.Each batch is committed independently; there is no transaction spanning all batches. That is deliberate -- a crash partway through a large run leaves the earlier batches durable and the operation resumable. Wrap the call in
django.db.transaction.atomic()yourself if you need all-or-nothing semantics instead.Writes always go through the model's default database (
Model._base_manager, nousingoverride); routing to a non-default alias is a known gap -- there is nousing=parameter.
django_utils.choices module¶
Usage¶
Create a Choices class and add Choice objects to the
class to define your choices.
Example with explicit values:¶
The normal Django version:
class Human(models.Model):
GENDER = (
('m', 'Male'),
('f', 'Female'),
('o', 'Other'),
)
gender = models.CharField(max_length=1, choices=GENDER)
The Django Utils Choices version:
from django_utils import choices
class Human(models.Model):
class Gender(choices.Choices):
Male = choices.Choice('m')
Female = choices.Choice('f')
Other = choices.Choice('o')
gender = models.CharField(max_length=1, choices=Gender)
To reference these properties:
Human.create(gender=Human.Gender.Male)
Example with implicit values:¶
The normal Django version:
class SomeModel(models.Model):
SOME_ENUM = (
(1, 'foo'),
(2, 'bar'),
(3, 'spam'),
(4, 'eggs'),
)
enum = models.IntegerField(choices=SOME_ENUM, default=1)
The Django Utils Choices version:
from django_utils import choices
class SomeModel(models.Model):
class Enum(choices.Choices):
Foo = choices.Choice()
Bar = choices.Choice()
Spam = choices.Choice()
Eggs = choices.Choice()
enum = models.IntegerField(choices=Enum, default=Enum.Foo)
To reference these properties:
SomeModel.create(enum=SomeModel.Enum.Spam)
Excluding constants¶
Any plain str, int or float class attribute becomes a choice.
To keep a constant alongside your choices, list it in _ignore_:
class Gender(choices.Choices):
_ignore_ = ('MAX_LENGTH',)
MAX_LENGTH = 1
Male = choices.Choice('m')
Female = choices.Choice('f')
Like the standard library's enum, _ignore_ also accepts a single
string of names separated by whitespace and/or commas, which is split for
you:
class Gender(choices.Choices):
_ignore_ = 'MAX_LENGTH'
MAX_LENGTH = 1
Male = choices.Choice('m')
Female = choices.Choice('f')
Attaching metadata to choices¶
Unlike Django's TextChoices, a Choice can carry arbitrary
extra data, reachable as attributes:
class Status(choices.Choices):
Active = choices.Choice('a', 'Active', color='green')
Closed = choices.Choice('c', 'Closed', color='red')
Status.choices['a'].color # 'green'
Grouped choices¶
Give choices a group and hand Django the nested structure it renders as
<optgroup>:
class Product(choices.Choices):
Apple = choices.Choice('ap', 'Apple', group='Fruit')
Carrot = choices.Choice('ca', 'Carrot', group='Vegetable')
field = models.CharField(max_length=2, choices=Product.choices.grouped())
Getting a real Enum¶
Members of a Choices class are raw values, so they can be passed
straight to Django fields. When you want isinstance checks or a
match on real enum members, ask for an enum:
class Gender(choices.Choices):
Male = choices.Choice('m', 'Male')
Female = choices.Choice('f', 'Female')
GenderEnum = Gender.as_enum()
GenderEnum('m') is GenderEnum.Male # True
- class django_utils.choices.Choice(value: Any = None, label: str | StrPromise | None = None, **metadata: Any)[source]¶
Bases:
objectThe choice object has an optional label and value. If the value is not given an autoincrementing id (starting from 1) will be used
>>> choice = Choice('value', 'label') >>> choice <Choice[1]:label> >>> str(choice) 'label'
>>> choice = Choice() >>> choice <Choice[2]:None> >>> str(choice) 'None'
- class django_utils.choices.Choices[source]¶
Bases:
objectThe choices class is what you should inherit in your Django models
>>> choices = Choices() >>> choices.choices[0] Traceback (most recent call last): ... KeyError: 'Key 0 does not exist' >>> choices.choices OrderedDict() >>> str(choices.choices) 'OrderedDict()' >>> choices.choices.items() [] >>> choices.choices.keys() [] >>> choices.choices.values() [] >>> list(choices) []
>>> class ChoiceTest(Choices): ... a = Choice() >>> choices = ChoiceTest() >>> choices.choices.items() [(0, <Choice[...]:a>)] >>> choices.a 0 >>> choices.choices['a'] <Choice[...]:a> >>> choices.choices[0] <Choice[...]:a> >>> choices.choices.keys() [0] >>> choices.choices.values() ['a'] >>> list(choices) [(0, <Choice[...]:a>)] >>> list(ChoiceTest) [(0, <Choice[...]:a>)]
- choices = OrderedDict()¶
- class django_utils.choices.ChoicesDict[source]¶
Bases:
objectThe choices dict is an object that stores a sorted representation of the values by key and database value
- by_key() OrderedDict[str, Choice][source]¶
The choices keyed by their declared attribute name.
- grouped() list[tuple[str | StrPromise, list[tuple[Any, str | StrPromise]]]][source]¶
The choices as Django's
<optgroup>structure.Choices declaring a
groupmetadata key are collected under it, in declaration order; ungrouped choices land under''.Labels (and group keys) are passed through unchanged, so a
gettext_lazylabel stays a lazy proxy -- resolved at render time, not whengrouped()is called. This matters because the module docstring's own example callsgrouped()at model-field definition time (import time), so eagerly resolving here would freeze translations in whatever language happened to be active at import.
- class django_utils.choices.ChoicesMeta(name: str, bases: tuple[type, ...], attrs: dict[str, Any])[source]¶
Bases:
typeThe choices metaclass is where all the magic happens, this automatically creates a ChoicesDict to get a sorted list of keys and values
- as_enum() type[Enum][source]¶
Build a real
enum.Enumfrom these choices.The result is memoised on the class: repeated calls return the same enum class, so
Gender.as_enum() is Gender.as_enum()andGender.as_enum().Male is Gender.as_enum().Maleboth hold, and the enum can be used as (or as part of) a dict/cache key. The cache is stored incls.__dict__and looked up there directly (never viagetattr, which walks the MRO), so a subclass builds and caches its own enum rather than inheriting its parent's.The returned class is built dynamically via
enum.Enum's functional API with its__module__set todjango_utils.choicesrather than the caller's module, so its members are not picklable with the default pickle protocol (pickling an enum member looks the class up by__module__+ qualified name, which won't resolve back to a class that was never assigned a name in that module).The original class is unchanged: its members stay raw values so they can be handed to Django fields. Use the enum where you want
isinstancechecks or amatchon real enum members.
- choices: ChoicesDict¶
- class django_utils.choices.LiteralChoices[source]¶
Bases:
ChoicesSpecial version of the Choices class that uses the label as the value
>>> class Role(LiteralChoices): ... admin = Choice() ... user = Choice() ... guest = Choice()
>>> Role.choices.values() ['admin', 'user', 'guest'] >>> Role.choices.keys() ['admin', 'user', 'guest']
>>> class RoleWithImplicitChoice(LiteralChoices): ... ADMIN = 'admin' ... USER = 'user' ... GUEST = 'guest'
>>> Role.choices.values() ['admin', 'user', 'guest'] >>> Role.choices.keys() ['admin', 'user', 'guest'] >>> Role.admin 'admin'
- choices = OrderedDict()¶
django_utils.context module¶
Current-request/current-user access, contextvars-native.
RequestContextMiddleware stores each request in a
contextvars.ContextVar for exactly the duration of its
request/response cycle, making the request reachable from code with no
request argument: model save() methods, signal handlers, log
filters, template-independent helpers.
Why not a thread-local (django-crum, django-currentuser)? Under ASGI a
single thread's event loop interleaves many requests, so thread-local
state leaks between them. A ContextVar is isolated per asyncio task
and propagated into sync_to_async threads by asgiref, which makes it
correct under WSGI and ASGI alike.
Everything here is opt-in: without the middleware (or the
current_request context manager) the getters simply return None.
- class django_utils.context.RequestContextMiddleware(get_response: Callable[[HttpRequest], HttpResponseBase] | Callable[[HttpRequest], Awaitable[HttpResponseBase]])[source]¶
Bases:
objectStore each request in the context variable for its whole cycle.
Add to
MIDDLEWARE(any position; before auth middleware is fine becauseget_current_userreads the user lazily). The variable is reset in afinallyso an exception anywhere downstream cannot leak one request into the next. That reset happens as soon as the view returns a response, before aStreamingHttpResponsebody iterator runs -- soget_current_request()called from inside one returnsNone.- async_capable = True¶
- sync_capable = True¶
- django_utils.context.current_request(request: HttpRequest) Generator[HttpRequest, None, None][source]¶
Make
requestcurrent for the duration of the block.For tests,
shellsessions and management commands, anywhere no middleware runs. Nests: the previous request is restored on exit.
- django_utils.context.get_current_request() HttpRequest | None[source]¶
Return the request currently being served, or
Noneoutside one.
- django_utils.context.get_current_user() _User | None[source]¶
Return
request.userfor the current request.Nonewhen there is no current request or when the request has nouserattribute (AuthenticationMiddlewareabsent). The user is read lazily off the stored request, so middleware order relative toRequestContextMiddlewaredoes not matter, only that auth middleware ran before this call.
django_utils.crypto_fields module¶
Fernet-encrypted model fields, behind the crypto extra.
EncryptedCharField, EncryptedTextField and EncryptedJSONField
store a base64 Fernet token in a plain TEXT column
(get_internal_type() returns 'TextField' for all three, so the
column is always sized for ciphertext, never plaintext). Encryption uses
cryptography's Fernet/MultiFernet exclusively -- nothing
hand-rolled, no hazmat primitives touched directly.
Keys come from settings.DJANGO_UTILS_FERNET_KEYS, a list of
urlsafe-base64 32-byte keys (Fernet.generate_key()). The FIRST key
encrypts; EVERY key is tried on decrypt (MultiFernet). Rotate by
prepending a new key and redeploying: old rows keep decrypting under the
old key until they are next saved, at which point they are re-encrypted
under the new (first) key. There is no bulk re-encryption command here --
touch (.save()) the rows you want migrated, or run your own
queryset_iterator-based pass over the table.
Non-goals, loudly: this is encryption at rest for values you never need
to query, sort or index by. Ordering is not blocked (Django has no
field-level hook for it) but sorts by ciphertext -- meaningless. And
anything reading through the ORM sees plaintext: an admin export action
(django_utils.admin.export.ExportMixin) on an encrypted model
streams decrypted values, and dumpdata writes plaintext fixtures.
There is no queryable/searchable mode, no
per-field keys, and no deterministic (same-plaintext-same-ciphertext)
mode -- Fernet salts every encryption, so even two rows with identical
plaintext get different ciphertext, and equality can never match at the
database level. Every lookup except isnull therefore raises
NotImplementedError; filter in Python after decrypting, or maintain a
separate searchable hash column alongside the encrypted one if you need
to find rows by value.
Requires the crypto extra (pip install 'django-utils2[crypto]'):
importing this module works without cryptography installed, but
instantiating any of the three fields raises ImproperlyConfigured
with that install hint. Fields instantiate as part of executing a
model's class body, so a model that declares one of them fails at
app-import/django.setup() time without the extra -- the whole app
fails to boot, loudly and immediately, not a deferred per-use error.
- class django_utils.crypto_fields.EncryptedCharField(*args: Any, **kwargs: Any)[source]¶
Bases:
_EncryptedFieldFernet-encrypted
str, stored as a TEXT column.max_lengthvalidates the PLAINTEXT via aMaxLengthValidator(mirroring plainCharField's own behaviour); it never sizes the column, which stores the necessarily-longer ciphertext instead.
django_utils.fields module¶
django_utils.middleware module¶
Standalone middleware.
FetchMetadataMiddleware implements modern, header-based CSRF
protection (the approach django/new-features #98 proposes for core and
Go's standard library ships): browsers have sent the Sec-Fetch-Site
request header on every request since roughly 2019, so cross-site
state-changing requests can be rejected without tokens, template tags or
AJAX header plumbing.
Deliberately defense-in-depth: run it alongside
CsrfViewMiddleware, never instead of it. A request carrying neither
Sec-Fetch-Site nor Origin (an old browser, curl, a server-side
client) is allowed through. Rejecting those is exactly the token
middleware's job, which is why this middleware alone is not sufficient
protection.
- class django_utils.middleware.FetchMetadataMiddleware(get_response: Callable[[HttpRequest], HttpResponseBase] | Callable[[HttpRequest], Awaitable[HttpResponseBase]])[source]¶
Bases:
objectReject cross-site state-changing requests by request headers.
Policy, in order:
Safe methods (GET/HEAD/OPTIONS/TRACE) always pass.
Views marked
@fetch_metadata_exemptpass.Sec-Fetch-Sitepresent: allowsame-origin/same-site/none(browser UI, e.g. the address bar); reject anything else with 403. Unknown values fail closed.No
Sec-Fetch-Site: compareOriginto this request'sscheme://host; mismatch is rejected.Neither header: allow. Token CSRF remains the backstop.
Behind a TLS-terminating proxy, step 4 can false-reject: without
SECURE_PROXY_SSL_HEADERconfigured,request.schemereadshttpwhile a legacy browser'sOriginheader (the client sees only the outer HTTPS connection) ishttps://..., so the exact-match comparison fails and the request is rejected as cross-site. Modern browsers are unaffected -- they sendSec-Fetch-Site, which step 3 handles first. ConfigureSECURE_PROXY_SSL_HEADERper the Django docs to fixrequest.schemeitself.- async_capable = True¶
- process_view(request: HttpRequest, callback: Callable[[...], Any], callback_args: tuple[Any, ...], callback_kwargs: dict[str, Any]) HttpResponseBase | None[source]¶
- sync_capable = True¶
django_utils.pg_enum module¶
PostgreSQL ENUM-backed model field, with explicit migration operations.
EnumField is a CharField wired to a django_utils.choices
Choices class: choices/max_length are derived from it, and on
PostgreSQL the column's real type is a native CREATE TYPE ... AS ENUM
type instead of VARCHAR -- the database itself then rejects a row
that doesn't hold one of the declared values, on top of (not instead of)
Django's own choice validation. On every other backend db_type()
falls back to plain VARCHAR, so a model using EnumField stays
portable -- SQLite never sees a Postgres-specific type name.
The field never issues DDL for the enum type itself -- that is the job
of three explicit migrations.Operation subclasses you add to a
migration BY HAND. Django's autodetector has no concept of "create this
standalone database object first", so these are never auto-generated by
makemigrations:
CreateEnumType(name, values)CREATE TYPE <name> AS ENUM (<values>). Add it to the same migration as -- and before -- theAddField/CreateModeloperation that introduces theEnumFieldcolumn using it; the type must exist before a column can reference it.AddEnumValue(name, value)ALTER TYPE <name> ADD VALUE IF NOT EXISTS <value>. PostgreSQL cannot run this statement inside a transaction block on versions before 12, and even on 12+ a value added in one transaction can't be used in that same transaction. It setsatomic = False, but that alone is NOT enough: Django's migration executor opens its schema editor -- and with it, the wrapping transaction -- keyed on the Migration'satomicattribute (defaultTrue), before any operation's ownatomicflag is ever consulted; the operation-level flag can only add extra wrapping inside that, never escape it. PutAddEnumValuein its own migration withatomic = Falseset as a CLASS attribute on thatMigration(same convention as Django's ownAddIndexConcurrently). Skip that anddatabase_forwardsraisesNotSupportedErrorinstead of running -- it never applies the value from inside a transaction. It is also irreversible -- PostgreSQL has noDROP VALUE-- so reversing it raisesIrreversibleError. The recreate-and-migrate escape hatch (create a new type with the reduced value list, migrate the column and data across, drop the old type) is documented in the README.DropEnumType(name, values)The reverse of
CreateEnumType. It takes the samevaluesasCreateEnumTypeso that ITS OWN reversal (recreating the type) has something to recreate.
All three are DB-only: state_forwards is a no-op (there is no model
state for a standalone database type -- EnumField doesn't need one
either, its deconstruct() carries the Choices class, not the
type's existence) and database_forwards/database_backwards are
no-ops on every non-PostgreSQL vendor, so a migration using them still
applies cleanly against SQLite or MySQL -- just without the enum type's
extra database-level integrity check. SQL is always built with
schema_editor.quote_name() for identifiers and
schema_editor.quote_value() for values, never raw string
interpolation of unquoted input.
- class django_utils.pg_enum.AddEnumValue(*args, **kwargs)[source]¶
Bases:
OperationAdd a value to an existing PostgreSQL ENUM type, by hand, in its own migration entry -- see the module docstring for why this is
atomic = Falseand irreversible.atomic = Falseon THIS operation is necessary but not sufficient: Django's migration executor opens its schema editor -- and with it, the wrapping transaction -- keyed on the Migration'satomicattribute (defaultTrue), before any operation's ownatomicflag is consulted. Put this operation in its own migration withatomic = Falseset as a CLASS attribute on thatMigration(same convention as Django's ownAddIndexConcurrently). Without that,database_forwardsraisesNotSupportedErrorrather than running inside a transaction it cannot safely run inside.- atomic = False¶
- database_backwards(app_label: str, schema_editor: BaseDatabaseSchemaEditor, from_state: ProjectState, to_state: ProjectState) None[source]¶
Perform the mutation on the database schema in the reverse direction - e.g. if this were CreateModel, it would in fact drop the model's table.
- class django_utils.pg_enum.CreateEnumType(*args, **kwargs)[source]¶
Bases:
OperationCreate a PostgreSQL ENUM type. Add to a migration BY HAND, before the operation that adds a column using it -- see the module docstring.
- database_backwards(app_label: str, schema_editor: BaseDatabaseSchemaEditor, from_state: ProjectState, to_state: ProjectState) None[source]¶
Perform the mutation on the database schema in the reverse direction - e.g. if this were CreateModel, it would in fact drop the model's table.
- class django_utils.pg_enum.DropEnumType(*args, **kwargs)[source]¶
Bases:
OperationDrop a PostgreSQL ENUM type. The reverse of
CreateEnumType-- takes the samevaluesso reversing THIS operation (recreating the type) has something to recreate.- database_backwards(app_label: str, schema_editor: BaseDatabaseSchemaEditor, from_state: ProjectState, to_state: ProjectState) None[source]¶
Perform the mutation on the database schema in the reverse direction - e.g. if this were CreateModel, it would in fact drop the model's table.
- class django_utils.pg_enum.EnumField(choices_class: type[Choices], *, enum_type: str | None = None, **kwargs: Any)[source]¶
Bases:
CharFieldCharFieldwhosechoices/max_lengthcome from adjango_utils.choices.Choicesclass, with a native PostgreSQL ENUM column type (plainVARCHARon every other backend).choicesis always derived fromchoices_class-- there is no override; passing achoices=keyword too raisesTypeError(this__init__rejects it explicitly, before deriving anything).max_lengthis derived as the longest value's length unless given explicitly.enum_typedefaults tochoices_class's name in snake_case (SandwichStatus->'sandwich_status') and names the PostgreSQL type this field's column references -- that type must already exist by the time this field's table/column is created; seeCreateEnumTypein the module docstring.- db_type(connection: BaseDatabaseWrapper) str | None[source]¶
Return the database column data type for this field, for the provided connection.
- deconstruct() tuple[str, str, Sequence[Any], dict[str, Any]][source]¶
Return enough information to recreate the field as a 4-tuple:
The name of the field on the model, if contribute_to_class() has been run.
The import path of the field, including the class, e.g. django.db.models.IntegerField. This should be the most portable version, so less specific may be better.
A list of positional arguments.
A dict of keyword arguments.
Note that the positional or keyword arguments must contain values of the following types (including inner values of collection types):
None, bool, str, int, float, complex, set, frozenset, list, tuple, dict
UUID
datetime.datetime (naive), datetime.date
top-level classes, top-level functions - will be referenced by their full import path
Storage instances - these have their own deconstruct() method
This is because the values here must be serialized into a text format (possibly new Python code, possibly JSON) and these are the only types with encoding handlers defined.
There's no need to return the exact way the field was instantiated this time, just ensure that the resulting field is the same - prefer keyword arguments over positional ones, and omit parameters with their default values.
django_utils.query_debug module¶
Query budgets that are safe to leave in production code paths.
query_budget counts every query a block executes and logs a
structured warning (warn_at) or raises (raise_at) when the block
exceeds its budget: an always-on guard against N+1 regressions, unlike
test-only tools (assertNumQueries) or dev-only profilers
(django-silk, django-debug-toolbar).
The load-bearing design decision: counting is implemented with
connection.execute_wrapper(), which is public, documented, and
active regardless
of DEBUG, and free of the per-connection query-log accumulation that
force_debug_cursor/connection.queries would cause in a
long-lived production process.
- exception django_utils.query_debug.QueryBudgetExceeded(count: int, raise_at: int, last_sql: str)[source]¶
Bases:
ExceptionA block exceeded its
raise_atquery budget.
- class django_utils.query_debug.query_budget(warn_at: int | None = None, raise_at: int | None = None, *, using: str | None = None)[source]¶
Bases:
ContextDecoratorCount queries in a block; warn and/or raise over budget.
Usable as a context manager or a decorator:
with query_budget(warn_at=20, raise_at=100): ... @query_budget(warn_at=20) def view(request): ...
warn_atlogs a single warning on exit when exceeded (with the most-repeated statement, the classic N+1 signature).raise_atraisesQueryBudgetExceededfrom the first over-budget query.usinglimits counting to one connection alias; the default counts every configured connection. Each decorated call gets a fresh budget; instances are single-use perwith(not reentrant). Instances are not thread-safe; the decorator form is safe because each call gets a fresh instance.
django_utils.queryset module¶
- django_utils.queryset.queryset_iterator(queryset: ~django.db.models.query.QuerySet, chunksize: int = 1000, getfunc: ~collections.abc.Callable[[~typing.Any, str], ~typing.Any] = <built-in function getattr>, *, pk_field: str = 'pk', start_after: ~typing.Any = None, gc_collect: bool = False) Generator[Any, None, None][source]¶
Iterate over a Django queryset in fixed-size chunks.
Uses keyset pagination (
WHERE <pk_field> > <cursor> LIMIT chunksize) instead of a database cursor: each chunk is fetched as its own independent query rather than as part of one long-lived cursor kept open for the whole iteration. The database driver therefore never has to hold more than one chunk's worth of rows at a time -- whatever the driver does internally, it does itchunksizerows at a time.- Why this function exists
This function was written after
QuerySet.iterator()exhausted memory on a very large table. Splitting one unbounded query into many boundedLIMITqueries fixed that, and the same query shape brings four distinct benefits along with it -- only the first of which is about the client driver.Bounded memory on the Python side. Only
chunksizemodel instances are ever alive at once, rather than the whole table. Core's ownQuerySet.iterator(chunk_size=...)makes the same promise, but it only holds if the driver streams: with PostgreSQL and Django's default server-side cursors, the database -- not the driver -- holds the unfetched rows, soiterator()is the better choice there. Where the driver buffers instead of streaming -- verified for MySQL with mysqlclient, whose default cursor callsstore_result()and materialises the entire result set client-side before Python sees the first row, in a C bufferchunk_sizenever touches -- the whole table is already sitting in the client process beforechunk_sizegets a chance to bound anything. Oracle (python-oracledb) and PostgreSQL withDISABLE_SERVER_SIDE_CURSORS = Trueare driver-dependent in the same way, though that is not established here the way mysqlclient's is. SQLite has no server-side cursor to disable either, but the stdlibsqlite3module steps rows lazily rather than buffering the whole result set (see the benchmark section below).Bounded memory on the database server. Each chunk is issued as
WHERE <pk_field> > cursor ORDER BY <pk_field> LIMIT chunksize-- an index range scan the planner can satisfy incrementally and stop as soon as it haschunksizerows. A single unboundedSELECT ... ORDER BYover a large table can instead force the server to materialise and sort the entire result set before returning the first row, consumingwork_mem(PostgreSQL) orsort_buffer_size(MySQL) and spilling to disk when it doesn't fit. The chunked form never asks the server to hold more than one chunk at a time. This follows from the query shape, not from measurement here.Read-replica distribution. Because each chunk is its own independent statement, a Django database router or a connection pooler can spread the N chunk queries across read replicas. A single long-running query pins one connection to one server for its whole duration and cannot be load-balanced mid-flight, so chunked iteration can scale horizontally in a way one cursor cannot -- again by construction, not measured here.
Operational blast radius. A single heavy query is a single point of failure: it can exhaust server memory, hit a statement timeout, and hold its transaction snapshot open for as long as it runs -- which on PostgreSQL keeps vacuum from reclaiming dead rows and causes table bloat, and on MySQL/InnoDB grows the undo history. Short per-chunk queries release their snapshot between chunks instead, so a slow consumer doesn't hold the database hostage. They also fail gracefully and resumably: a chunk that fails partway through a run can be retried from the last cursor seen instead of restarting the whole query, which is exactly what
start_afterbelow is for.None of points two through four were independently benchmarked in this repository -- they follow from the query shape described above. The only measured numbers here are the query counts and wall-clock figures below, and the cost is real: N small queries instead of 1, plus a modest wall-clock overhead.
- Benchmark
benchmarks/queryset_iterator.py, SQLite, 20,000 rows, chunksize=1000. Wall clock is close to parity withQuerySet.iterator()(~1.09x-1.14x across runs; query count and wall clock are measured in separate passes so that query-logging instrumentation doesn't inflate the many-query path's timing more than the single-query path's). An earlier version of this function calledgc.collect()after every chunk; that call alone added roughly 37-47% toqueryset_iterator()'s default-mode wall-clock time for no measured reduction in Python-level memory, so it is off by default here (seegc_collectbelow).The benchmark's
tracemallocpeak figures are not evidence about the memory story either way:tracemalloconly sees Python-level allocations, so it cannot see a driver's C-level result buffer -- the thing this function exists to bound. SQLite also has no server-side cursor to disable in the first place, and its stdlib driver does not buffer the full result set either, so it structurally cannot reproduce the failure mode this function guards against. That failure mode is verified on MySQL with mysqlclient, and may also occur -- driver-dependent, not established here -- on Oracle and on PostgreSQL withDISABLE_SERVER_SIDE_CURSORS = True; it does not occur on SQLite, and none of this shows up in atracemalloctrace on any backend. No OOM has been measured here; the claim is narrower: bounded driver-side memory by construction, not a demonstrated fix for a specific crash.- Choosing between the two
Prefer
QuerySet.iterator(chunk_size=...)on PostgreSQL with server-side cursors enabled (the default): one query, and the database -- not the driver -- holds the unfetched rows. Reach for this function when that guarantee isn't available (MySQL, Oracle, SQLite, or PostgreSQL with server-side cursors disabled), where a single unbounded query can hand the driver an unbounded buffer. It is also useful whenever a single long-lived query/cursor is undesirable for another reason: because each chunk is its own query, iteration can resume on a connection that was reset or recycled between chunks, which one open cursor cannot.
Keyword-only options
pk_fieldColumn to order and paginate by, instead of the primary key. Must be unique, indexed, totally ordered and non-nullable so keyset pagination doesn't skip or repeat rows -- e.g. a monotonic
created_ator integeridcolumn. Useful when the primary key itself is a randomly-ordered UUID that would make everyWHERE pk > cursorquery scan out of index order. ANULLvalue read frompk_fieldraisesValueError, since the keyset cursor cannot resume fromNULL. A non-unique value silently truncates: if several rows tie on the samepk_fieldvalue, only the ones that land in the current chunk are yielded and the rest of that tied group is skipped.start_afterResume from a previously-seen cursor value instead of starting from the beginning, so a batch job that died partway through can continue rather than restart. When given, the first chunk is filtered
> start_afterinstead of being unfiltered. The value must be a value ofpk_field(a plain pk by default):last_seen = None for row in queryset_iterator( MyModel.objects.all(), pk_field='created_at', start_after=last_seen, ): process(row) last_seen = row.created_at checkpoint(last_seen) # survives a restart
gc_collectCall
gc.collect()after every chunk. Off by default: in the SQLite benchmark above it adds roughly 37-47% to this function's default-mode wall-clock time and made no measured difference to peak Python-level memory. It is available for runs where reclaiming Python-level garbage more aggressively than the allocator would on its own is worth that cost -- e.g. very large runs where resident set size matters more than throughput.
Note that the results are always ordered by
pk_field.
django_utils.utils module¶
django_utils.view_decorators module¶
- class django_utils.view_decorators.EnvRequest[source]¶
Bases:
HttpRequestA request as seen inside an
env-decorated view.The
envdecorator injects these attributes at runtime; this subclass exists so type checkers know about them.
- django_utils.view_decorators.debug_allowed(request: HttpRequest) bool[source]¶
Whether the
?debug=1HTML view may render for this request.Rendering a response body inside an HTML document is a debugging aid, so it is restricted the way
django-debug-toolbarrestricts its own panels:settings.DEBUG, or an explicitly whitelisted client address.Note that behind a reverse proxy
REMOTE_ADDRis the proxy's address, not the client's, soINTERNAL_IPSwill match nothing (or everything, if the proxy's own address is listed). If your deployment sits behind a reverse proxy and needs the real client address (e.g. fromX-Forwarded-For), reassign this function (django_utils.view_decorators.debug_allowed = my_check) rather than trusting that header here, since it is only meaningful when the proxy topology is known.
- django_utils.view_decorators.env(function: Callable[[...], Any]) Callable[[...], HttpResponse][source]¶
- django_utils.view_decorators.env(function: None = None, login_required: bool = False, response_class: type[HttpResponse] = http.HttpResponse) Callable[[Callable[[...], Any]], Callable[[...], HttpResponse]]
View decorator that automatically adds context and renders response
Keyword arguments: login_required -- is everyone allowed or only authenticated users
Adds a RequestContext (request.context) with the following context items: name -- current function name
Stores the template in request.template and assumes it to be in <app>/<view>.html