Skip to content

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

get_selector_kwargs() -> dict[str, Any]

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 — uses spec.output_serializer directly.
  • :class:ServiceSpec — uses spec.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.kindkind 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 through self.paginate_queryset / self.get_paginated_response if 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 (or None / 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.