Viewsets¶
Full CRUD¶
ServiceViewSet ¶
Bases: ServiceCreateMixin, ServiceUpdateMixin, ServiceDestroyMixin, SelectorListMixin, SelectorRetrieveMixin, ActionSerializerResolver, GenericViewSet
Router-compatible viewset wiring services and selectors.
Composes :class:ServiceCreateMixin, :class:ServiceUpdateMixin,
:class:ServiceDestroyMixin, :class:SelectorListMixin,
:class:SelectorRetrieveMixin, and :class:ActionSerializerResolver
over :class:~rest_framework.viewsets.GenericViewSet. See those classes
for the configurable attributes.
SelectorViewSet ¶
Bases: SelectorListMixin, SelectorRetrieveMixin, ActionSerializerResolver, GenericViewSet
Read-only viewset for list + retrieve.
Composes :class:SelectorListMixin, :class:SelectorRetrieveMixin, and
:class:ActionSerializerResolver over
:class:~rest_framework.viewsets.GenericViewSet.
Per-action mixins¶
ServiceCreateMixin ¶
Bases: MutationFlowMixin, _ActionSpecsMixin
Provides the create action; reads its config from action_specs.
Set action_specs["create"] to a
:class:~rest_framework_services.types.service_spec.ServiceSpec.
When the "create" key is absent the action raises
:exc:~rest_framework.exceptions.MethodNotAllowed. A non-ServiceSpec
entry (e.g. a :class:~rest_framework_services.types.selector_spec.SelectorSpec)
raises :exc:~django.core.exceptions.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
:exc:~rest_framework.exceptions.MethodNotAllowed). When neither key
resolves, both actions raise
:exc:~rest_framework.exceptions.MethodNotAllowed. A non-ServiceSpec
entry raises :exc:~django.core.exceptions.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
:exc:~rest_framework.exceptions.MethodNotAllowed. A non-ServiceSpec
entry raises :exc:~django.core.exceptions.ImproperlyConfigured.
SelectorListMixin ¶
Bases: ListModelMixin, _ActionSpecsMixin
Compose with :class:~rest_framework.viewsets.GenericViewSet.
When action_specs["list"] is a
:class:~rest_framework_services.types.selector_spec.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
:exc:~django.core.exceptions.ImproperlyConfigured.
get_selector_kwargs ¶
Hook for additional kwargs available to the selector signature.
SelectorRetrieveMixin ¶
Bases: RetrieveModelMixin, _ActionSpecsMixin
Compose with :class:~rest_framework.viewsets.GenericViewSet.
When action_specs["retrieve"] is a
:class:~rest_framework_services.types.selector_spec.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
:exc:~django.core.exceptions.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().
Action-serializer dispatch¶
ActionSerializerResolver ¶
Bases: _ActionSpecsMixin
Resolve get_serializer_class() from action_specs.
Consults the action_specs map for the active action's entry:
- :class:
SelectorSpec— usesspec.output_serializerdirectly. - :class:
ServiceSpec— usesspec.output_selector_spec.output_serializer(the output pipeline is collapsed into the nested selector spec).
Falls back to DRF's standard serializer_class attribute (and raises
the usual DRF AssertionError if 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,
*,
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 :class:ServiceSpec for the service wiring. detail,
methods, url_path, url_name, and any extra **action_kwargs
are forwarded to DRF's @action.
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 that @selector_action can attach
DRF @action metadata and pick up the action name from __name__.
Pass a :class:SelectorSpec for the selector wiring. The dispatch shape
and the DRF URL shape are both driven by spec.kind — kind is
the single source of truth, with no separate detail= parameter to
keep in sync:
- :attr:
SelectorKind.LIST— collection action (detail=False); the selector is expected to return an iterable. The result flows throughself.paginate_queryset/self.get_paginated_responseif pagination is configured, otherwise it's serialized many=True. - :attr:
SelectorKind.RETRIEVE— detail action (detail=True); the selector is expected to return a single object (orNone/ raise :exc:~django.core.exceptions.ObjectDoesNotExist, both of which surface as 404).
If you need a URL shape that doesn't match the response shape (a
detail action that returns a list, or a collection action that returns
a single resource), fall back to DRF's plain @action and write
the dispatch yourself.
Output serialization resolves to spec.output_serializer when set,
falling back to self.get_serializer(...) otherwise.