Skip to content

Selectors

Protocols

Each Protocol is parameterised on input / instance / result types only; **extras is typed Any. Strict-typed extras live on the user's function signature via **extras: Unpack[YourKw] — see Typing services and selectors for the full pattern.

Selector

Bases: Protocol

Any callable returning a queryset, list, or single object.

Used as a structural type for documentation purposes; the views accept any plain callable, so satisfying this protocol is optional.

AsyncSelector

Bases: Protocol

Async sibling of Selector.

ListSelector

Bases: Protocol[ResultT]

Structural shape for a list-action selector callable.

The framework calls this from get_queryset(); the returned iterable flows through DRF's filter backends, pagination, and output_serializer.

See CreateService for the extras-typing notes.

RetrieveSelector

Bases: Protocol[ResultT]

Structural shape for a retrieve-action selector callable.

The framework calls this from get_object(). Returning None (or raising Model.DoesNotExist) results in a 404. The URL lookup field (typically pk) is delivered via **extras.

See CreateService for the extras-typing notes.

Helpers

call_selector

call_selector

call_selector(
    selector: Callable[..., ResultT], *, request: Request, **extras: Any
) -> ResultT

Invoke selector with the framework's kwargs pool.

request is required; user is derived from request.user. Async selectors are bridged via async_to_sync.

acall_selector

acall_selector async

acall_selector(
    selector: Callable[..., ResultT] | Callable[..., Awaitable[ResultT]],
    *,
    request: Request,
    **extras: Any,
) -> ResultT

Invoke selector from async code with the framework's kwargs pool.

Async selectors are awaited; sync selectors are called inline.

Dispatch

utils

Internal selector dispatch helpers (sync + async).

is_queryset

is_queryset(obj: Any) -> bool

True for Django QuerySet objects and Manager instances.

The one definition of a queryset-shaping target: what the shaping fields may be applied to, and what a RETRIEVE selector materializes from. Tested by type rather than by hasattr(…, "first"), which would also match a domain object that happens to expose first. QuerySet subclasses all pass.

run_selector

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

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

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.

apply_queryset_shaping

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

Apply the five shaping fields to qs.

The order is fixed: the declarative fields first (in declaration order), then extend_queryset, so the user callable always sees the fully statically-shaped queryset, and finally filter_set, so filtering composes with shaping and runs before the retrieve .first() materialization the caller does next.

Parameters:

Name Type Description Default
request Request

Passed to extend_queryset, and into the filter_set constructor when it declares a request parameter — so a request-scoped FilterSet sees the same self.request it would behind DjangoFilterBackend. A bare (data, queryset) stand-in is called exactly as before.

required
filter_set Any

Applied by duck typing as filter_set(data=filter_data, queryset=qs).qs, after validation (mirroring DjangoFilterBackend's 400 on invalid filter input).

None
filter_data Any

The flat {field: value} mapping the FilterSet reads. None falls back to request.query_params — the HTTP view path; a transport-neutral caller passes its own params.

None
source_label str

Named in the misconfiguration error to point at the offending callable ("SelectorSpec.selector" vs "ServiceSpec.output_selector_spec.selector").

required

Returns:

Type Description
Any

The shaped queryset, or qs unchanged when nothing is configured.

Raises:

Type Description
ImproperlyConfigured

Shaping is configured but qs is not a Django queryset — loud failure beats a stray AttributeError deep in DRF rendering.

ValidationError

filter_set rejected filter_data.

materialize_retrieve

materialize_retrieve(result: Any) -> Any

Collapse a RETRIEVE selector's return to the single instance, or None.

The one definition of what kind=RETRIEVE means once the selector has run: a queryset materializes through .first() — so an author can write selector=lambda *, pk: Model.objects.filter(pk=pk) and still get the spec's shaping applied first — and anything else passes through as the resolved object. What each caller does with a None differs; how the value is arrived at does not.

amaterialize_retrieve async

amaterialize_retrieve(result: Any) -> Any

Async twin of materialize_retrieve, awaiting .afirst().

Separate because the materialization is the query — .first() would block the event loop. Same rule otherwise; keep the two in step.

check_view_object_permissions

check_view_object_permissions(spec: Any, context: Any, *, instance: Any = None) -> None

TargetGuard running DRF's object-permission check for an HTTP view.

The HTTP counterpart of enforce_permissions: off HTTP a transport enforces spec.permission_classes itself, while a DRF view has already instantiated them and exposes check_object_permissions. Both plug into the same on_target_resolved seam, so the core stays authz-agnostic on every transport.

Gated on Model, like enforce_permissions: the core fires this hook on the LIST branch too, with the resolved queryset, and object permissions are per-row. None — a create, or a bulk list payload — is skipped too.

dispatch_selector_for_spec

dispatch_selector_for_spec(view: Any, spec: SelectorSpec[Any, Any]) -> Any

End-to-end dispatch for one SelectorSpec call from a DRF view.

Resolves the view's get_selector_kwargs / get_<action>_selector_kwargs chain into a ViewHooks carrier, hands off to the one dispatch_spec core, and translates the neutral DispatchResult into the view-layer contract: a RETRIEVE that resolved nothing raises NotFound, unless the spec sets allow_none=True (then None, and the retrieve views render 200 + JSON null). SelectorKind.LIST returns the shaped queryset.

argument_binding=BUNDLE is what keeps HTTP semantics. Off HTTP the flat params mapping is the argument channel; over HTTP a selector's kwargs come from route captures plus the hook chain, and the query string belongs to filter_set / the filter backends. BUNDLE spreads nothing, so passing query_params feeds the filter without widening the argument channel.

There is deliberately no extra_url_kwargs parameter: the core reads view.kwargs itself via view_url_kwargs, which strips the reserved pool seeds, so a nested route like /users/<user>/posts/ cannot let the captured value shadow the authenticated user. Taking the mapping from a caller would reopen that.

The caller must check spec.selector is not None first and fall back to vanilla DRF otherwise.