Skip to content

Views

Each view takes a kernel spec on spec. See Concepts for what the edge reads from the request and how a refusal is answered. Each also takes pool_seeds, the seeds its spec's callables are resolved with (on MutationFlowMixin below for the mutation views); see Registering a seed.

Mutation views

ServiceCreateView

Bases: MutationFlowMixin, GenericAPIView

POST endpoint that runs a service callable to create a resource.

Configure by setting spec to a kernel ServiceSpec. The view's success_status defaults to 201 Created when unset.

ServiceUpdateView

Bases: MutationFlowMixin, GenericAPIView

PUT / PATCH endpoint that runs a service callable.

The instance to update is resolved via spec.instance_selector_spec — no queryset / lookup_field required on the subclass. A spec never falls back to DRF's get_object(), its target being its own lookup's, so as_view() refuses one that declares neither a target selector nor many.

Configure by setting spec to a kernel ServiceSpec. The view's success_status defaults to 200 OK when unset. Both verbs share the one spec, so a forced spec.partial applies to PUT and PATCH — set http_method_names = ["patch"] for a PATCH-only endpoint.

ServiceDeleteView

Bases: MutationFlowMixin, GenericAPIView

DELETE endpoint that runs a service callable.

The instance to delete is resolved via spec.instance_selector_spec — no queryset / lookup_field required on the subclass. A spec never falls back to DRF's get_object(), its target being its own lookup's, so as_view() refuses one that declares neither a target selector nor many.

Configure by setting spec to a kernel ServiceSpec. Its validator is optional (for delete-with-payload patterns such as a deletion reason); the view's success_status defaults to 204 No Content; set output_selector_spec with a presenter on the spec to render a body instead.

Selector views

SelectorListView

Bases: _ServesRaisedSchema, ListModelMixin, GenericAPIView

GET endpoint that delegates to a selector spec or to get_queryset().

A kernel SelectorSpec on spec runs through the kernel's own dispatch and is presented by its own presenter, the page's rows alone when the view paginates, with the affordance answers it declares; a spec declaring neither presenter nor affordances is rendered by the view's serializer_class, as DRF renders a queryset.

get_queryset() answers the spec's set to a GET or HEAD alone, from the one dispatch list presents, so a permission class asking it first does not run the selector twice; to an OPTIONS or any other method it is DRF's own, the view's queryset.

spec = None (the default) keeps get_queryset() and the rest of the flow as vanilla DRF. The rest of the list flow (filter backends, pagination, response rendering) is the standard DRF ListModelMixin.

pool_seeds class-attribute instance-attribute

pool_seeds: PoolSeeds = DEFAULT_POOL_SEEDS

The seeds the spec's callables are resolved with: the kernel's DEFAULT_POOL_SEEDS unless the view registers others, as a class attribute or as as_view(pool_seeds=...). One registry for the view's life: as_view() checks the spec against it, refusing a callable that requires a seed it does not register, and every call's checks, target lookup and dispatch are handed that object (test_pool_seeds).

SelectorRetrieveView

Bases: _ServesRaisedSchema, RetrieveModelMixin, GenericAPIView

GET endpoint that returns a single object.

A kernel SelectorSpec on spec runs through the kernel's own dispatch and is presented by its own presenter, with the affordance answers it declares. Its selector replaces get_object(): a miss is a 404, or, when the spec sets allow_none=True, a 200 with a JSON null body presented by nothing (the nullable-resource contract). A spec declaring neither presenter nor affordances is rendered by the view's serializer_class.

spec = None (the default) keeps get_object(), standard DRF lookup using queryset and lookup_field, as vanilla DRF.

pool_seeds class-attribute instance-attribute

pool_seeds: PoolSeeds = DEFAULT_POOL_SEEDS

The seeds the spec's callables are resolved with: the kernel's DEFAULT_POOL_SEEDS unless the view registers others, as a class attribute or as as_view(pool_seeds=...). One registry for the view's life: as_view() checks the spec against it, refusing a callable that requires a seed it does not register, and every call's checks, target lookup and dispatch are handed that object (test_pool_seeds).

get_object

get_object() -> Any

Resolve the target row through the spec's selector, if one is set.

filter_backends do not apply here. DRF runs filter_queryset() inside its own get_object(), so overriding that method - this view, or any hand-written override - drops them. A tenant-scoping backend in DEFAULT_FILTER_BACKENDS scopes a sibling list view and not this lookup. Scope the selector's own queryset, or declare the rule as SelectorSpec.filter_set, which the dispatcher applies on both the list and the retrieve path.

Mutation flow mixin

MutationFlowMixin

Bases: _ServesRaisedSchema

Provides _run_mutation for service-backed views and viewset mixins.

The flow itself lives in dispatch_mutation_for_spec, so @service_action can reach it without being a class; this mixin is the OO entry point that the per-action mixins (ServiceCreateMixin and friends) and the standalone single-purpose views compose, calling self._run_mutation(...) after resolving their per-action spec.

Four hook chains feed a mutation, each layered view-wide → per-action → per-spec and merged with dict.update so the more specific hook wins on overlapping keys:

  • extra service kwargs: get_service_kwargs → get_<action>_service_kwargs → the spec's kwargs provider.
  • the serializer's input dict, merged on top of request.data before validation: get_input_data → get_<action>_input_data → the spec's input_data provider.
  • the input serializer's context=: get_serializer_context (DRF's own) → get_input_serializer_context → get_<action>_input_serializer_context → the SerializerValidator's serializer_context.
  • the output serializer's context=: the same chain with output in place of input, applied during response rendering.

The per-action layer reads self.action, so it applies to viewsets only.

A kernel spec declares neither HTTP answer, so the response's two HTTP hooks are the view's: success_status and response_finalizer below, read off the class unbound. On a viewset an action may declare its own, which outranks these: @service_action's keywords, or a get_<action>_success_status / get_<action>_response_finalizer hook (action_answer, test_per_action_answers).

success_status class-attribute

success_status: int | Callable[..., int] | None = None

The success status: an int, or a callable declaring any of result / instance / request / view, or None for the action's default (201 create, 200 update, 204 delete). A body never goes out under 204, so a status resolving to it answers 200 with one; set, it applies to an answer with nothing to present too (test_callable_success_status). On a DiscriminatedAction a callable may also declare variant, the key the discriminator chose.

response_finalizer class-attribute

response_finalizer: Callable[..., Response | None] | None = None

The post-presentation hook on a 2xx: declares any of response / result / request / view / instance / data and returns a Response to replace the one built, or None to keep it (test_response_finalizer). On a DiscriminatedAction it may also declare variant, the key the discriminator chose.

lenient_json_body class-attribute instance-attribute

lenient_json_body: bool | None = None

Whether this view's serializer alone answers a JSON body, as it answers a form body: {"title": 5} for a CharField is "5", and every refusal is DRF's, in DRF's words. None reads SPEC_REST_LENIENT_JSON_BODY, which is False unset, so the spec's declared types are checked first. Only what this view dispatches changes: the spec's schema, its tool schema and every other transport stay typed (test_a_lenient_json_body_over_http). A @service_action may declare its own, as a keyword of the decorator.

pool_seeds class-attribute instance-attribute

pool_seeds: PoolSeeds = DEFAULT_POOL_SEEDS

The seeds the spec's callables are resolved with: the kernel's DEFAULT_POOL_SEEDS unless the view registers others, as a class attribute or as as_view(pool_seeds=...). One registry for the view's life: as_view() checks the spec against it, refusing a callable that requires a seed it does not register, and every call's checks, target lookup and dispatch are handed that object (test_pool_seeds).

get_service_kwargs

get_service_kwargs() -> dict[str, Any]

Hook for additional kwargs available to every mutation service.

get_input_data

get_input_data(request: Request) -> Mapping[str, Any]

Hook for extras merged on top of request.data before validation.

get_input_serializer_context

get_input_serializer_context() -> dict[str, Any]

Hook for the context= dict passed to the input serializer.

Defaults to get_serializer_context, so overriding the DRF-standard hook flows into input validation automatically; override here for keys visible only during input validation.

get_output_serializer_context

get_output_serializer_context() -> dict[str, Any]

Hook for the context= dict passed to the output serializer.

Defaults to get_serializer_context, so overriding the DRF-standard hook flows into response rendering automatically; override here for keys visible only during response rendering.

Settings

lenient_json_body

lenient_json_body() -> bool

SPEC_REST_LENIENT_JSON_BODY: whether a view's serializer alone answers a JSON body.

False when the project sets nothing, so a JSON body is checked against the spec's declared types first. A view's own lenient_json_body outranks it.

Read on every call, never at import, as every setting the kernel reads is: a test's override_settings must reach a view built before it.

Raises:

Type Description
ImproperlyConfigured

anything but a bool, so a "false" is refused by name rather than read as true (test_a_setting_that_is_no_bool_is_refused_by_name).

The write's target

resolve_mutation_instance

resolve_mutation_instance(view: Any, spec: ServiceSpec) -> Any

Resolve the mutation target, or defer to the core.

A spec's target is its own target selector's, or none: the view's get_object() is never asked for one, because the kernel takes a target only where the spec declares the selector that would find it, and refuses one handed to a spec declaring none. A standalone update or delete view refuses such a spec at as_view(); a caller that check never reaches (a viewset's action, @service_action) is handed no row (test_service_update_view's test_a_kernel_spec_with_no_target_selector_is_handed_no_row).

``None`` for a **bulk** spec (``many=True`` or a

Type Description
Any

collection_selector_spec): there is no single instance. UNSET when

Any

the spec carries an instance_selector_spec: the core resolves it,

Any

with the right kwarg pool, error label and reserved-seed strip, and the

Any

view's object permissions are asked of the row it finds. None for a

Any

spec with no target at all.

filter_backends do not apply: DRF runs filter_queryset() inside its own get_object(), which a spec's lookup bypasses, exactly as a hand-written get_object() override does. A tenant-scoping backend in DEFAULT_FILTER_BACKENDS therefore does not narrow the row a PATCH or DELETE may reach here, while the sibling list action stays scoped. Put the scoping in the selector's own queryset.