Viewsets¶
Full CRUD¶
ServiceViewSet ¶
Bases: ServiceCreateMixin, ServiceUpdateMixin, ServiceDestroyMixin, SelectorListMixin, SelectorRetrieveMixin, ActionSerializerResolver, GenericViewSet
Router-compatible viewset wiring services and selectors.
Composes
ServiceCreateMixin,
ServiceUpdateMixin,
ServiceDestroyMixin,
SelectorListMixin,
SelectorRetrieveMixin,
and
ActionSerializerResolver
over GenericViewSet. See those classes for the configurable attributes.
SelectorViewSet ¶
Bases: SelectorListMixin, SelectorRetrieveMixin, ActionSerializerResolver, GenericViewSet
Read-only viewset for list + retrieve.
Composes
SelectorListMixin,
SelectorRetrieveMixin,
and
ActionSerializerResolver
over GenericViewSet.
Per-action mixins¶
ServiceCreateMixin ¶
Bases: MutationFlowMixin, _ActionSpecsMixin
Provides the create action; reads its config from action_specs.
Set action_specs["create"] to a
ServiceSpec. When the
"create" key is absent the action raises MethodNotAllowed. A
non-ServiceSpec entry (e.g. a
SelectorSpec) raises
ImproperlyConfigured.
ServiceUpdateMixin ¶
Bases: MutationFlowMixin, _ActionSpecsMixin
Provides update (PUT) and partial_update (PATCH) actions.
Looks up the instance via spec.instance_selector_spec when set,
falling back to DRF's get_object(). PUT reads its config from
action_specs["update"]; PATCH reads action_specs["partial_update"]
first and falls back to "update" — defining only
"partial_update" yields a PATCH-only endpoint (PUT raises
MethodNotAllowed). When neither key
resolves, both actions raise
MethodNotAllowed. A non-ServiceSpec
entry raises ImproperlyConfigured.
ServiceDestroyMixin ¶
Bases: MutationFlowMixin, _ActionSpecsMixin
Provides the destroy action.
Looks up the instance via spec.instance_selector_spec when set,
falling back to DRF's get_object(). Reads its config from
action_specs["destroy"]; when that key is absent the action raises
MethodNotAllowed. A non-ServiceSpec
entry raises ImproperlyConfigured.
SelectorListMixin ¶
Bases: ListModelMixin, _ActionSpecsMixin
Compose with GenericViewSet.
When action_specs["list"] is a
SelectorSpec with
a non-None selector, get_queryset() invokes it instead of
returning the configured queryset. The rest of DRF's list flow —
filter backends, pagination, serialization — is unchanged.
action_specs["list"] = SelectorSpec(selector=None) or an absent
"list" key both fall through to DRF's default get_queryset().
Any other entry type raises
ImproperlyConfigured.
get_selector_kwargs ¶
Hook for additional kwargs available to the selector signature.
SelectorRetrieveMixin ¶
Bases: RetrieveModelMixin, _ActionSpecsMixin
Compose with GenericViewSet.
When action_specs["retrieve"] is a
SelectorSpec with
a non-None selector, get_object() invokes it instead of
falling through to DRF's standard lookup. The selector receives the
URL kwargs plus the standard pool. 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).
action_specs["retrieve"] = SelectorSpec(selector=None) or an absent
"retrieve" key both fall through to DRF's default get_object().
Any other entry type raises
ImproperlyConfigured.
The selector applies wherever get_object() is called, including from
update/destroy actions composed alongside this mixin. If you need an
action-specific override, do it explicitly in your own get_object().
get_object ¶
Resolve the target row through the retrieve selector, if one is wired.
filter_backends do not apply here. DRF runs
filter_queryset() inside its own get_object(), so taking that
method over — as this mixin does, and as any hand-written
get_object() override does — is what drops them. A tenant-scoping
backend listed in DEFAULT_FILTER_BACKENDS therefore narrows the
sibling list action and not this lookup, which is what makes the
gap easy to miss. Scope the selector's own queryset, or declare the
rule as SelectorSpec.filter_set — the sanctioned seam, applied by
the dispatcher on both paths.
Action-serializer dispatch¶
ActionSerializerResolver ¶
Bases: _ActionSpecsMixin
Resolve get_serializer_class() from action_specs.
Reads the active action's entry: a
SelectorSpec supplies
output_serializer, a
ServiceSpec supplies
output_selector_spec.output_serializer. Falls back to DRF's
serializer_class, raising the usual AssertionError when neither is set.
OPTIONS is answered under DRF's implicit "metadata" action, once per
POST / PUT it describes (a real request to a view's own action named
metadata is not OPTIONS, and resolves like any other). A view that sets serializer_class is described
by it, as DRF always has. A view built only from specs has none, and there
the spec the described method routes to (from action_specs or stamped by
@service_action / @selector_action) answers with the body that
method accepts:
- a
ServiceSpecwith aninput_serializeranswers with it (a bare dataclass wrapped as it is for rendering); - a
PolymorphicServiceSpecanswers with its variants'input_serializerwhen they all declare the same one. Its discriminator is never asked: an OPTIONS request carries no body to discriminate. Variants that disagree leave the method out, unless the view resolves serializers itself; - a
ServiceSpecwith no input, and aSelectorSpec(a selector reads query parameters, never a body), answer with a serializer of no fields.
The input is built with DRF's base serializer context only: an
input_serializer whose fields depend on its input_serializer_context
cannot be described this way.
Every other action resolves exactly as it did without OPTIONS in the picture.
Example:
class InvoiceViewSet(ActionSerializerResolver, GenericViewSet):
action_specs = {
"list": SelectorSpec(
kind=SelectorKind.LIST, output_serializer=InvoiceListSerializer,
),
"retrieve": SelectorSpec(
kind=SelectorKind.RETRIEVE, output_serializer=InvoiceDetailSerializer,
),
}
Custom actions¶
service_action ¶
service_action(
spec: ServiceSpec | PolymorphicServiceSpec,
*,
detail: bool = False,
methods: list[str] | None = None,
url_path: str | None = None,
url_name: str | None = None,
**action_kwargs: Any,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]
Wrap a viewset method as a service-backed custom action.
The decorated method's body is not executed — the decorator supplies
the handler. The method exists so that @service_action can attach
DRF @action metadata and pick up the action name from __name__.
Pass a ServiceSpec (or a
PolymorphicServiceSpec
for one action accepting several payload shapes) for the service wiring. detail,
methods, url_path, url_name, and any extra **action_kwargs are
forwarded to DRF's @action.
A plain ServiceSpec forwards its permission_classes into DRF's
@action(permission_classes=...), so it is enforced on any viewset. A
PolymorphicServiceSpec
has no single list to forward — which list applies depends on the strategy,
and under "discriminate" on the body — so its per-variant
permission_classes are enforced by _ActionSpecsMixin.get_permissions,
which reads the spec stashed on the handler. That makes the mixin a
requirement, not a convenience: on a viewset without it, DRF's stock
get_permissions would apply the view's defaults and every variant rule
would go unchecked. The decorated class is unknown at decoration time, so
the dependency is checked on the first request through the action and the
request is refused rather than served unguarded. Compose ServiceViewSet
(or any mixin from this package) and the check never fires.
An AdditionalInputRequired
answers 422 with its schema as raised on any viewset, DRF's own
included. The restore runs inside the exception handler the view asks for,
so the configured EXCEPTION_HANDLER runs once, and a handle_exception
override on the viewset still has the last word on the body.
selector_action ¶
selector_action(
spec: SelectorSpec[Any, Any],
*,
methods: list[str] | None = None,
url_path: str | None = None,
url_name: str | None = None,
**action_kwargs: Any,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]
Wrap a viewset method as a selector-backed custom action.
The decorated method's body is not executed — the decorator supplies the
handler. The method exists so the decorator can attach DRF @action
metadata and take the action name from __name__.
spec.kind drives both the dispatch shape and the DRF URL shape, so there
is no separate detail= to keep in sync. SelectorKind.LIST is a
collection action (detail=False) whose selector returns an iterable,
flowing through self.paginate_queryset / self.get_paginated_response
when pagination is configured and serialized many=True otherwise;
SelectorKind.RETRIEVE is a detail action (detail=True) whose
selector returns a single object, or None / raises
ObjectDoesNotExist for a 404 -- or, when the spec sets
allow_none=True, a 200 with a JSON null body, the output
serializer skipped, as the retrieve view and mixin serve it. For a URL shape
that doesn't match the response shape, fall back to DRF's plain @action
and write the dispatch yourself.
Output serialization uses spec.output_serializer when set, falling back
to self.get_serializer(...).