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
package — run_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 ¶
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 ¶
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 ¶
Call fn(**kwargs), optionally inside transaction.atomic().
arun_service¶
arun_service
async
¶
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
DataclassSerializeron the fly;validated_datais a dataclass instance; - a
DataclassSerializersubclass — instantiated directly;validated_datais a dataclass instance; - any other
Serializersubclass (e.g.ModelSerializer) — instantiated directly;validated_datais adict.
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 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 ¶
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 ¶
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").