Skip to content

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 — a TypedDict describing the keys returned by kwargs.

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:SelectorKind discriminator (LIST vs RETRIEVE). Drives the dispatcher: RETRIEVE materializes a QuerySet via .first() and raises :exc:~rest_framework.exceptions.NotFound on None / missing-object, LIST returns whatever the selector returns unchanged. Also drives the fail-fast check that the spec is mounted on a compatible view (a LIST spec on :class:SelectorRetrieveView raises at as_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 by get_queryset() (list) or get_object() (retrieve). None means "use the configured queryset / default DRF behaviour".
  • allow_noneRETRIEVE-only knob for the None / missing-object case. False (the default) keeps the standard behaviour: raise :exc:~rest_framework.exceptions.NotFound. True expresses a nullable-resource contract: the standalone retrieve view and the retrieve viewset mixin render 200 with a JSON null body, skipping the output serializer. The flag is ignored when the spec is nested — :attr:ServiceSpec.output_selector_spec keeps its authoritative-None → 204 contract, and :attr:ServiceSpec.instance_selector_spec always 404s (an update against a missing row is not a nullable read).
  • output_serializer — DRF Serializer subclass used by get_serializer_class() for this action. None falls back to DRF's standard serializer_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 — no if self.action == ... branching in a catch-all get_selector_kwargs.
  • 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 @selector_action decorator, and surfaced via get_permissions for 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's context= dict. Sits at the most-specific layer of the resolution chain (view.get_serializer_contextview.get_output_serializer_contextview.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 symmetrical input_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 by input_serializer. For dataclass-based serializers this is the dataclass; for plain ModelSerializer it is typically dict[str, Any].
  • ResultT — the value returned by the service callable, and (when output_selector_spec is set) the input to its selector.
  • ExtraT — a TypedDict describing the keys returned by kwargs.

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_contextview.get_input_serializer_contextview.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.

changed_fields property

changed_fields: tuple[str, ...]

Names of every field present in :attr:changes.

get_field_change

get_field_change(field_name: str) -> FieldChange | None

Return the :class:FieldChange for field_name, or None.

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]