Types¶
SelectorKind¶
SelectorKind ¶
Bases: str, Enum
Whether a :class:SelectorSpec returns many objects or a single one.
The kind is what tells the framework — and any future caller that
reuses a spec outside an HTTP request — whether to materialize the
selector's return as a collection (LIST) or as a single instance
with retrieve-flavoured 404 semantics (RETRIEVE). Mounting a spec
on a mismatched view (e.g. a LIST spec on a
:class:SelectorRetrieveView) raises
:exc:~django.core.exceptions.ImproperlyConfigured at as_view()
time.
Inheriting from str keeps the value JSON-serializable and
print-friendly while still behaving as a proper enum for is /
== checks.
SelectorSpec¶
SelectorSpec
dataclass
¶
Bases: Generic[ResultT, ExtraT]
All wiring for a single read action in one record.
Used as a value in action_specs on viewsets, as the spec=
argument to :class:SelectorListView / :class:SelectorRetrieveView,
and as the output_selector_spec field on :class:ServiceSpec
(where it describes the post-mutation re-fetch).
Generic parameters (both default to Any):
ResultT— the selector's return type.ExtraT— aTypedDictdescribing the keys returned bykwargs.
All fields are keyword-only: SelectorSpec(kind=SelectorKind.LIST,
selector=fn) rather than positional. kind is required and has no
default — see below.
Fields:
kind— required :class:SelectorKinddiscriminator (LISTvsRETRIEVE). Drives the dispatcher:RETRIEVEmaterializes a QuerySet via.first()and raises :exc:~rest_framework.exceptions.NotFoundonNone/ missing-object,LISTreturns whatever the selector returns unchanged. Also drives the fail-fast check that the spec is mounted on a compatible view (aLISTspec on :class:SelectorRetrieveViewraises atas_view()). Making it explicit lets a spec be reused outside a request — from a management command, a cron job, or any non-DRF caller — without the semantics living implicitly in the call site.selector— callable invoked byget_queryset()(list) orget_object()(retrieve).Nonemeans "use the configuredqueryset/ default DRF behaviour".allow_none—RETRIEVE-only knob for theNone/ missing-object case.False(the default) keeps the standard behaviour: raise :exc:~rest_framework.exceptions.NotFound.Trueexpresses a nullable-resource contract: the standalone retrieve view and the retrieve viewset mixin render200with a JSONnullbody, skipping the output serializer. The flag is ignored when the spec is nested — :attr:ServiceSpec.output_selector_speckeeps its authoritative-None→ 204 contract, and :attr:ServiceSpec.instance_selector_specalways 404s (an update against a missing row is not a nullable read).output_serializer— DRFSerializersubclass used byget_serializer_class()for this action.Nonefalls back to DRF's standardserializer_class.kwargs— callable returning extra kwargs to merge into the pool the selector receives. Co-locating it with the spec lets each action declare its own contract — noif self.action == ...branching in a catch-allget_selector_kwargs.permission_classes— overrides the calling view'spermission_classesfor the action the spec backs.None(the default) means "inherit the view's class-level permissions"; an empty sequence means "no permissions" explicitly. Forwarded through DRF's@action(permission_classes=...)for the@selector_actiondecorator, and surfaced viaget_permissionsfor the viewset mixins and standalone views. Ignored when the spec is nested under :attr:ServiceSpec.output_selector_spec— the surrounding mutation action's permissions apply.output_serializer_context— per-spec hook for the response serializer'scontext=dict. Sits at the most-specific layer of the resolution chain (view.get_serializer_context→view.get_output_serializer_context→view.get_<action>_output_serializer_context→ spec hook), so it wins on overlapping keys.None(the default) leaves the three earlier layers intact. Selectors don't validate input, so there's no symmetricalinput_serializer_context.
The provider is called with (view, request) positionally and may
additionally declare the resolved data being serialized as a keyword
parameter — page on a LIST spec (the paginated object list, or
the full queryset when pagination is off) or instance on a
RETRIEVE spec. It is passed only when declared (or when the
provider accepts **kwargs), so legacy (view, request)
providers are unaffected. This lets the provider run a single batched
query against the exact objects being serialized and propagate the
result through context — e.g.
lambda view, request, *, page: {"votes": tally(page)}. The hook
always runs after the data is resolved.
- select_related / prefetch_related / annotations
— declarative queryset shaping applied to the selector's return value
before it leaves :func:dispatch_selector_for_spec. select_related
is a sequence of relation names (forwarded as
qs.select_related(*spec.select_related)); prefetch_related is
a sequence of relation names or :class:Prefetch objects;
annotations is a mapping merged into a single .annotate(**...)
call. Use these for the common case where the same shaping applies
every request — they're introspectable for OpenAPI / future tooling.
- extend_queryset — dynamic escape hatch. A
Callable[[QuerySet, ServiceView, Request], QuerySet] invoked
after the declarative fields have applied, so it always sees the
fully statically-shaped queryset. Use it when the shaping depends on
the request (e.g. only prefetch when a query string opts in).
Synchronous only — it manipulates the queryset's lazy expression
tree, not the database.
All four shaping fields require selector to be set and the selector
to return a Django :class:QuerySet. Configuring shaping with no
selector raises :exc:ImproperlyConfigured at as_view() time; a
non-QuerySet return raises at request time.
ServiceSpec¶
ServiceSpec
dataclass
¶
Bases: Generic[InputT, ResultT, ExtraT]
All wiring for a single mutation action in one record.
Used as a value in ServiceViewSet.action_specs and as the spec=
argument to :func:service_action / :class:ServiceCreateView /
:class:ServiceUpdateView / :class:ServiceDeleteView.
Generic parameters are optional and purely informational for type checkers:
InputT— the validated-data type produced byinput_serializer. For dataclass-based serializers this is the dataclass; for plainModelSerializerit is typicallydict[str, Any].ResultT— the value returned by the service callable, and (whenoutput_selector_specis set) the input to itsselector.ExtraT— aTypedDictdescribing the keys returned bykwargs.
All three default to Any, so ServiceSpec(service=fn) keeps working
unchanged.
Fields are grouped by what they configure: the service callable itself,
the input pipeline (input_*), the output pipeline (a single nested
:class:SelectorSpec), and the cross-cutting concerns (kwargs,
permission_classes).
success_status is left as None so each consumer can supply its
own action-appropriate default (201 for create, 200 for update, 204
for destroy).
input_data is the symmetrical hook for the serializer's input.
Returns a mapping merged on top of request.data before the
input_serializer validates it — useful for lifting URL kwargs
(e.g. parent IDs from nested routes) into fields the serializer can
cross-validate. Server-provided keys win on conflict. The provider is
called with (view, request) positionally and may additionally
declare instance as a keyword parameter to receive the resolved
mutation target (None on create) — passed only when declared, so
pre-validation input mutation that depends on the current row has a
home. The same declare-to-receive rule applies to the
get_input_data / get_<action>_input_data view hooks.
input_serializer_context is a per-spec hook for the input
serializer's context= dict. It sits at the most-specific layer of
the resolution chain (view.get_serializer_context →
view.get_input_serializer_context →
view.get_<action>_input_serializer_context → spec hook), so the
spec wins on overlapping keys. None (the default) leaves the
three earlier layers intact. The symmetrical output hook lives on the
nested output_selector_spec.output_serializer_context; that hook may
additionally declare a result keyword to receive the final
(post-selector) instance being serialized — passed only when declared —
so it can run a single batched query against it and propagate the
outcome through context. The output hook always runs after the service
and output selector have resolved result.
output_selector_spec is the full output pipeline collapsed into a
single :class:SelectorSpec. Its kind must be
:attr:SelectorKind.RETRIEVE (the post-mutation re-fetch always
materializes a single instance). Set it to render the response through
a different shape than what the service returned (typical pattern: the
service returns a freshly created/updated instance, the
output_selector_spec.selector re-fetches it with the relations the
response serializer needs, and the spec's output_serializer
renders the result). None (the default) means "render the service's
return value directly". The nested spec's permission_classes and
kwargs are ignored — the surrounding mutation's permissions and
kwargs chain apply.
instance_selector_spec is the input-side twin of
output_selector_spec: a nested :class:SelectorSpec (kind must
be :attr:SelectorKind.RETRIEVE) that resolves the instance an
update / destroy / detail action targets, embedding the lookup in the
spec instead of relying on the view's queryset / get_object()
chain. The selector's kwarg pool is {request, user} plus the URL
kwargs (plus the standard selector extras chain), so
selector=lambda *, pk: Project.objects.filter(pk=pk) resolves the
row from the route. Resolution happens before input validation —
the resolved instance is handed to the input serializer
(DRF-style serializer(instance, data=..., partial=...)) and seeded
into the service kwarg pool as instance. A None / missing
resolution raises :exc:~rest_framework.exceptions.NotFound (the
nested spec's allow_none flag is ignored — an update against a
missing row is always a 404), and object-level permissions
(check_object_permissions) run against the resolved instance. The
queryset-shaping fields apply; permission_classes,
output_serializer, and output_serializer_context on the nested
spec are ignored. None (the default) keeps today's get_object()
chain.
partial overrides the transport-derived partial-validation flag.
None (the default) inherits the flag the calling surface derives
(False for PUT/POST, True for PATCH); True / False
forces it regardless of HTTP method — e.g. partial=False on an
action_specs["partial_update"] entry makes a PATCH endpoint
enforce required fields like a PUT. Applied once, at
dispatch_mutation_for_spec, so it is honoured uniformly by the
viewset mixins, the standalone views, and @service_action.
kwargs is a callable that returns extra kwargs to merge into the
pool the service receives. Co-locating it with the spec lets each action
declare its own contract — no if self.action == ... branching in a
catch-all get_service_kwargs. See :class:ServiceView for the
attributes available on the view argument.
permission_classes overrides the calling view's permission_classes
for the action the spec backs. None (the default) means "inherit the
view's class-level permissions"; an empty sequence means "no permissions"
explicitly. Forwarded through DRF's @action(permission_classes=...)
for the @service_action decorator, and surfaced via get_permissions
for the viewset mixins and standalone views.
ChangeResult¶
ChangeResult
dataclass
¶
Bases: Generic[ModelT]
Outcome of a mutation helper call.
instance is the model instance after the mutation. created is True
iff this came from :func:create_from_input / :func:acreate_from_input.
changes records every field whose value actually differed from its
prior value (or from UNSET for creates).
The class is generic over the concrete model type: callers that pass
Author into a mutation helper get back a ChangeResult[Author]
whose .instance is typed as Author. The bare name
ChangeResult (no parameter) resolves to ChangeResult[Model] and
keeps working for callers that don't care.
FieldChange¶
FieldChange
dataclass
¶
One field's before/after pair from a mutation.
old will be UNSET for fields populated as part of a create
(no prior value existed).
UNSET¶
unset ¶
The UNSET sentinel and its type.
Used to distinguish "field omitted from input" from "field explicitly set to
None". Critical for partial updates where None must not stomp on an
existing value.
UNSET is the singleton value you compare against (value is UNSET).
UnsetType is its type, exported so callers can spell it in annotations —
e.g. bio: str | None | UnsetType.
UnsetType ¶
Singleton sentinel type. Always falsy; identity-equal to itself only.
Don't instantiate this directly — use the module-level UNSET singleton.
UnsetType() returns that same instance, but the sentinel is the value
you compare against and UnsetType is only useful as a type annotation.
NoInput¶
NoInput ¶
Sentinel type for the InputT slot when a service expects no body.
Pair with :class:DeleteService when the spec has no input_serializer::
@implements(DeleteService[NoInput, Author, None])
def delete_author(
*,
instance: Author,
**extras: Any,
) -> None: ...
The class itself is never instantiated — it exists purely to bind the
InputT type variable in a way that is searchable in IDEs and docs.
HttpExtras¶
HttpExtras ¶
Bases: TypedDict, Generic[UserT]