Skip to content

Dispatch (stable surface)

The stable dispatch surface: the primitives an alternate transport (such as djangorestframework-mcp-server) builds on instead of re-implementing the "how to call a service / selector" rules. Every symbol here is importable from the top-level package, follows semantic versioning, and will not move or change signature within a major version. Blessed in 0.17, which also removed the private _compat packagerun_service / arun_service now live in services/ and is_async at the package root; downstreams re-point their imports here when they bump past 0.17.

Deliberately not part of this surface: dispatch_mutation_for_spec and dispatch_selector_for_spec. They are view-coupled orchestrators (they take a view, read its URL kwargs, and walk the get_<action>_*_kwargs hook chains); a transport-neutral spec dispatcher is future work that will compose the leaves below.

Shared

resolve_callable_kwargs

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.

is_async

is_async

is_async(fn: Callable[..., Any]) -> bool

Return True if calling fn(...) produces a coroutine.

Handles plain async def functions, functools.partial wrapping one (via inspect.iscoroutinefunction's built-in unwrapping), and any callable whose __call__ is a coroutine function.

Service side

run_service

run_service

run_service(fn: Callable[..., Any], kwargs: dict[str, Any], *, atomic: bool) -> Any

Call fn(**kwargs), optionally inside transaction.atomic().

arun_service

arun_service async

arun_service(
    fn: Callable[..., Awaitable[Any]], kwargs: dict[str, Any], *, atomic: bool
) -> Any

Await fn(**kwargs), optionally inside transaction.atomic().

build_input_serializer

build_input_serializer

build_input_serializer(
    request: Request,
    input_serializer: type | None,
    *,
    partial: bool = False,
    extra_data: Mapping[str, Any] | None = None,
    context: dict[str, Any] | None = None,
    instance: Any = None,
) -> Serializer | None

Construct + validate the bound input serializer; None if absent.

input_serializer may be:

  • a bare dataclass type — wrapped in a DataclassSerializer on the fly; validated_data is a dataclass instance;
  • a DataclassSerializer subclass — instantiated directly; validated_data is a dataclass instance;
  • any other Serializer subclass (e.g. ModelSerializer) — instantiated directly; validated_data is a dict.

extra_data (when supplied) is merged on top of request.data before the serializer instantiates — server-provided keys win on overlap. This is the seam used by the input_data resolver chain to lift URL kwargs into serializer input.

context (when supplied) is forwarded to the serializer's context= kwarg so DRF-style self.context["request"] / ["view"] lookups work inside validators and fields.

instance (when supplied) is the resolved mutation target on update / destroy flows. The serializer is constructed DRF-style — serializer(instance, data=data, partial=partial) — so self.instance is populated inside validate() / field validators and instance-aware validators (e.g. UniqueValidator excluding the current row) behave as they do under DRF's own update flow.

The serializer is returned validated (is_valid(raise_exception=True) has run) but never saved; the service owns persistence.

validate_input

validate_input

validate_input(
    request: Request,
    input_serializer: type | None,
    *,
    partial: bool = False,
    extra_data: Mapping[str, Any] | None = None,
    context: dict[str, Any] | None = None,
    instance: Any = None,
) -> Any

Validate request.data against input_serializer; None if absent.

Thin wrapper over :func:build_input_serializer (see there for the parameter semantics) returning only validated_data — kept for callers that don't need the bound serializer itself.

resolve_mutation_instance

resolve_mutation_instance

resolve_mutation_instance(view: Any, spec: ServiceSpec[Any, Any, Any]) -> Any

Resolve the instance an update / destroy / detail action targets.

Precedence: spec.instance_selector_spec (when set with a selector) → the view's get_object() chain (an action_specs["retrieve"] selector via :class:SelectorRetrieveMixin, else DRF's default queryset / lookup_field lookup, else a user get_object() override). Used by the update / destroy viewset mixins, the standalone update / delete views, and @service_action detail actions so the precedence lives in one place.

The spec path dispatches through :func:dispatch_selector_for_spec (the standard selector call shape: {request, user} + the view's URL kwargs + the selector extras chain, queryset shaping applied, RETRIEVE materialization via .first()). The nested spec's allow_none flag is ignored — a mutation against a missing row is always a 404, so a None resolution raises :exc:~rest_framework.exceptions.NotFound regardless. Object-level permissions run against the resolved instance (view.check_object_permissions), matching DRF's own get_object() contract.

Selector side

run_selector

run_selector

run_selector(fn: Callable[..., Any], kwargs: dict[str, Any]) -> Any

Call a selector from sync code, transparently bridging async ones.

arun_selector

arun_selector async

arun_selector(
    fn: Callable[..., Any] | Callable[..., Awaitable[Any]], kwargs: dict[str, Any]
) -> Any

Call a selector from async code; sync ones run inline.

is_queryset

is_queryset

is_queryset(obj: Any) -> bool

True for Django QuerySet objects and Manager instances.

These are the queryset-shaping targets: the things the four shaping fields can be applied to, and the things a RETRIEVE selector / output selector should be materialized from via .first(). Centralizes the "is this a queryset?" decision so the selector and mutation dispatch paths agree on one definition instead of duck-typing on a method name (hasattr(..., "first")), which would also match an unrelated domain object that happens to expose first. QuerySet subclasses (.values(), .values_list(), polymorphic querysets, …) all pass.

apply_queryset_shaping

apply_queryset_shaping

apply_queryset_shaping(
    qs: Any,
    view: Any,
    request: Request,
    *,
    select_related: Any,
    prefetch_related: Any,
    annotations: Any,
    extend_queryset: Any,
    source_label: str,
) -> Any

Apply the four shaping fields to qs.

Declarative fields apply first (in declaration order), then extend_queryset runs so the user callable always sees the fully statically-shaped queryset. Returns qs unchanged when no shaping is configured.

Raises :exc:ImproperlyConfigured when shaping is configured but qs is not a Django QuerySet (no annotate method) — loud failure beats a stale AttributeError deep in DRF rendering. source_label is included in the error to point at the misuse ("SelectorSpec.selector" vs "ServiceSpec.output_selector_spec.selector").