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.
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.
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. 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(...).