Skip to content

Views

Mutation views

ServiceCreateView

Bases: MutationFlowMixin, GenericAPIView

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

Configure by setting spec to a ServiceSpec. The spec'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 when set — no queryset / lookup_field required on the subclass — falling back to DRF's get_object() (set queryset and lookup_field, or override get_object()).

Configure by setting spec to a ServiceSpec. The spec'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 when set — no queryset / lookup_field required on the subclass — falling back to DRF's get_object().

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

Selector views

SelectorListView

Bases: ListModelMixin, GenericAPIView

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

Set spec to a SelectorSpec to configure the selector and/or the output serializer. Both fields are optional:

  • spec.selector overrides get_queryset(); None falls back to the inherited queryset attribute.
  • spec.output_serializer overrides get_serializer_class(); None falls back to DRF's standard serializer_class attribute.

spec = None (the default) keeps both as vanilla DRF.

The rest of the list flow — filter backends, pagination, response rendering — is the standard DRF ListModelMixin.

get_selector_kwargs

get_selector_kwargs() -> dict[str, Any]

Hook for additional kwargs available to the selector signature.

SelectorRetrieveView

Bases: RetrieveModelMixin, GenericAPIView

GET endpoint that returns a single object.

Set spec to a SelectorSpec to configure the selector and/or the output serializer. Both fields are optional:

  • spec.selector overrides get_object(); None falls back to self.get_object() — standard DRF lookup using queryset and lookup_field. Returning None or raising Model.DoesNotExist results in a 404 — or, when the spec sets allow_none=True, a 200 with a JSON null body (the nullable-resource contract; the output serializer is skipped).
  • spec.output_serializer overrides get_serializer_class(); None falls back to DRF's standard serializer_class attribute.

spec = None (the default) keeps both as vanilla DRF.

get_object

get_object() -> Any

Resolve the target row through spec.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

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_kwargsget_<action>_service_kwargsServiceSpec.kwargs.
  • the serializer's input dict, merged on top of request.data before validation: get_input_dataget_<action>_input_dataServiceSpec.input_data.
  • the input serializer's context=: get_serializer_context (DRF's own) → get_input_serializer_contextget_<action>_input_serializer_contextServiceSpec.input_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.

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.

get_permissions

get_permissions() -> list[Any]

Honor spec.permission_classes on standalone mutation views.

Standalone Service*View subclasses carry spec as a class attribute; when it sets permission_classes those win over the view's class-level ones, and None falls through.

ServiceView Protocol

ServiceView

Bases: Protocol

Minimal structural shape of a view as exposed to a kwargs provider.

Per-spec kwargs providers (ServiceSpec.kwargs / SelectorSpec.kwargs) receive the calling view typed as ServiceView. The Protocol pins only the attributes a provider can rely on across both the standalone Service*View classes and the viewset mixins:

  • request — the current DRF Request.
  • kwargs — the URL kwargs dict resolved by the URLconf (e.g. {"pk": 7}). Empty dict on routes without captured groups.
  • action — the viewset action name ("create", "list", etc.) on viewsets; None on standalone single-purpose views.

Keep the surface narrow on purpose: providers should translate view state into typed kwargs for the service, not reach for view internals.

OfflineServiceView

OfflineServiceView dataclass

A concrete ServiceView for dispatching a spec outside an HTTP request.

Spec kwargs / extend_queryset / context providers are typed against ServiceView — the structural request / kwargs / action surface. When a spec is dispatched off the HTTP path (a Pydantic-AI toolset, the MCP server, a management command) there is no DRF view, so this frozen value stands in:

  • request — the synthetic DRF Request built by build_offline_context.
  • action — an optional label identifying the operation; None when the caller has no meaningful action name.
  • kwargs — the URL-kwargs equivalent (e.g. parent ids), empty by default.

Kwarg resolution

utils

Cross-cutting view helpers used by both mutation and query views.

resolve_extra_kwargs

resolve_extra_kwargs(
    view: Any,
    request: Request,
    *,
    spec_kwargs: Callable[..., dict[str, Any]] | None,
    action_hook: str | None,
    catch_all_hook: str,
) -> dict[str, Any]

Collect the extras that should be merged into a service/selector pool.

Three layers, merged with dict.update in increasing specificity so the spec-level provider has the final say on overlapping keys. Each is invoked through the framework's provider convention, receiving only the subset of {view, request} it declares (or the whole pool via **kwargs).

Parameters:

Name Type Description Default
spec_kwargs Callable[..., dict[str, Any]] | None

The spec's own ServiceSpec.kwargs / SelectorSpec.kwargs provider — the most specific layer.

required
action_hook str | None

Per-action view method, e.g. get_create_service_kwargs / get_list_selector_kwargs. None (standalone single-purpose views) or an absent method skips the layer.

required
catch_all_hook str

View-wide fallback, e.g. get_service_kwargs / get_selector_kwargs. An absent method skips the layer.

required

resolve_input_extras

resolve_input_extras(
    view: Any,
    request: Request,
    *,
    spec_input_data: Callable[..., Mapping[str, Any]] | None,
    action_hook: str | None,
    catch_all_hook: str,
    extras: Mapping[str, Any] | None = None,
) -> dict[str, Any]

Collect the extras to merge into the serializer input dict.

resolve_extra_kwargs's layering — catch_all_hook view method, then action_hook, then the spec's ServiceSpec.input_data provider — applied to the input_serializer-bound data rather than the service-call pool.

Parameters:

Name Type Description Default
extras Mapping[str, Any] | None

The resolved data available before validation — currently the mutation target instance, None on create — offered to each provider by keyword only when it declares the name.

None

layer_serializer_context

layer_serializer_context(
    base: Mapping[str, Any],
    view: Any,
    request: Request,
    *,
    direction_hook: str | None,
    action_hook: str | None,
    spec_provider: Callable[..., Mapping[str, Any]] | None = None,
    extras: Mapping[str, Any] | None = None,
) -> dict[str, Any]

Layer the directional, action, and spec context hooks onto base.

resolve_serializer_context's precedence rules with layer 1 handed in rather than read from view.get_serializer_context() — for overrides of that method which need to extend super()'s result without recursing. See there for action_hook / spec_provider / extras.

Parameters:

Name Type Description Default
base Mapping[str, Any]

The layer-1 context to build on.

required
direction_hook str | None

Directional view hook name; None skips the layer. That is what _ActionSpecsMixin.get_serializer_context needs: MutationFlowMixin's default get_output_serializer_context would recurse back into get_serializer_context.

required

resolve_serializer_context

resolve_serializer_context(
    view: Any,
    request: Request,
    *,
    direction_hook: str,
    action_hook: str | None,
    spec_provider: Callable[..., Mapping[str, Any]] | None = None,
    extras: Mapping[str, Any] | None = None,
) -> dict[str, Any]

Build the serializer context dict for one direction (input or output).

Four layers, merged with dict.update in increasing specificity so the spec-level provider has the final say on overlapping keys: view.get_serializer_context() (DRF's own) → view.<direction_hook>()view.<action_hook>()spec_provider. An absent view method skips its layer, so plain DRF viewsets work unchanged.

Parameters:

Name Type Description Default
direction_hook str

get_input_serializer_context / get_output_serializer_context.

required
action_hook str | None

Per-action override, e.g. get_create_input_serializer_context. None on standalone single-purpose views.

required
spec_provider Callable[..., Mapping[str, Any]] | None

The spec's own *_serializer_context provider.

None
extras Mapping[str, Any] | None

The resolved data about to be serialized (a mutation's result, the retrieved instance, or the list page), offered to the last three layers by keyword only when declared.

None

get_class_attr

get_class_attr(view: Any, name: str) -> Any

Return the named class attribute without instance binding.

A function stored as a plain class attribute (service = my_fn) would otherwise be wrapped in a bound method when read via self.

resolve_callable_kwargs

resolve_callable_kwargs(fn: Callable[..., Any], pool: dict[str, Any]) -> dict[str, Any]

Pick the subset of pool matching fn's declared parameters.

If fn declares **kwargs, the entire pool is passed. Otherwise only parameters present in the signature are forwarded.

resolve_progress_hook

resolve_progress_hook(view: Any, request: Request, *, action: str | None) -> Any

First progress reporter the view offers: per-action hook, then catch-all.

Most-specific wins and there is no merging — a reporter is a single sink, not a set of keys, so layering two of them would mean silently fanning out; a view that wants that composes them itself with combine_progress. None when the view offers neither, which leaves the seed as null_progress.

resolve_view_hooks

resolve_view_hooks(
    view: Any, request: Request, *, chain: str = "service", instance: Any = None
) -> ViewHooks

Resolve the calling view's hook chains into a ViewHooks carrier.

View layers only — every spec_* argument below is deliberately None. The chains run view.get_<x>view.get_<action>_<x>spec.<x>, and dispatch_spec owns that last layer; resolving the spec provider here too would invoke it twice, which is not safe for a provider that queries the database. See ViewHooks.

Parameters:

Name Type Description Default
chain str

Which kwargs chain to collect — "service" for mutations, "selector" for reads. Only the view-method names differ, which is why one carrier serves both. The input-phase fields are mutation-only and stay unset for a selector: a read has no payload to merge into and no input serializer to give context to.

'service'
instance Any

The resolved mutation target (None on create and on every bulk path), offered to the input_data providers that declare it.

None

Spec validation

spec_validation

Fail-fast validation of service / selector signatures at view setup time.

resolve_callable_kwargs forwards pool∩signature, so a required parameter the framework cannot supply is omitted rather than rejected and the call dies as a bare TypeError deep in dispatch, at the first request. These helpers surface the same misconfiguration at as_view() time with a precise message.

Deliberately lenient on extras: a kwargs provider or an overridden get_*_kwargs may be feeding the callable keys the validator cannot see.

validate_callable_signature

validate_callable_signature(
    fn: Callable[..., Any],
    *,
    spec_label: str,
    has_data: bool,
    has_instance: bool,
    has_result: bool,
    spec_kwargs: Callable[..., Any] | None,
    permissive_extras: bool,
    extra_known_keys: Iterable[str] = (),
) -> None

Raise ImproperlyConfigured on a misconfigured service / selector.

has_data / has_instance / has_result say whether those framework-injected keys exist at this call site; requiring one that does not always fails, since no user override can supply it. Every other required parameter is checked only when nothing could be feeding it (permissive_extras false and no spec_kwargs). extra_known_keys extends the allowed set for call sites that seed additional names.

is_overridden

is_overridden(view_cls: type, base_cls: type, method_name: str) -> bool

Return True if view_cls overrides base_cls's method_name.

Drives permissive_extras: an overridden get_*_kwargs is assumed to contribute keys the validator cannot see.

validate_filter_set_no_backend_conflict

validate_filter_set_no_backend_conflict(
    view_cls: type, spec: SelectorSpec[Any, Any], *, label: str
) -> None

Reject a list selector that sets filter_set and wires DjangoFilterBackend.

On the list path DRF's list() runs filter_queryset() while the dispatcher also applies spec.filter_set — equivalent operations, so the queryset would be filtered twice. Callers must gate this on the list path only: the selector retrieve path overrides get_object() and never calls filter_queryset, so there is no conflict there.

validate_service_spec

validate_service_spec(
    spec: ServiceSpec[Any, Any, Any],
    *,
    label: str,
    has_instance: bool,
    permissive_extras: bool,
) -> None

Validate a ServiceSpec's service and nested selector specs.

Shared between standalone mutation views, viewset mixins, and @service_action. has_instance is fixed by the action context (False for create, True for update / destroy / detail actions).

validate_polymorphic_service_spec

validate_polymorphic_service_spec(
    poly: PolymorphicServiceSpec,
    *,
    label: str,
    has_instance: bool,
    permissive_extras: bool,
) -> None

Validate every variant of a PolymorphicServiceSpec + its strategy.

Each variant is validated with the same has_instance / permissive_extras as a plain entry would be.

validate_selector_spec

validate_selector_spec(
    spec: SelectorSpec[Any, Any],
    *,
    label: str,
    expected_kind: SelectorKind | None = None,
) -> None

Validate a SelectorSpec's selector.

Selectors are always permissive on extras (URL kwargs and get_selector_kwargs are dynamic), so the only fatal misuses are framework-only keys absent from the selector pool. expected_kind catches a spec mounted on the wrong view — a LIST spec on SelectorRetrieveView would otherwise misbehave only at runtime.

validate_mutation_view_spec

validate_mutation_view_spec(view_cls: type, *, has_instance: bool) -> None

Validate view_cls.spec on a standalone mutation view.

No-op when spec is unset — the base classes inherit a None placeholder so as_view() itself doesn't trip.

validate_selector_view_spec

validate_selector_view_spec(view_cls: type, *, expected_kind: SelectorKind) -> None

Validate view_cls.spec on a standalone selector view.

No-op when spec is unset — that means "use vanilla DRF". expected_kind is the kind the view is shaped for (LIST for SelectorListView, RETRIEVE for SelectorRetrieveView).