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, a list or one object.

The loosest description of a SelectorSpec's selector: dispatch binds it from the keywords it declares, drawn from the keyword pool and the reads the spec declares, so no signature is imposed and conforming is optional. For a typed result, use ListSelector or RetrieveSelector.

Runtime-checkable, and an isinstance check asks only whether the object is callable: an async def passes it too, so it cannot tell the two kinds apart.

AsyncSelector

Bases: Protocol

The async def sibling of Selector.

A SelectorSpec's selector may be async def, and adispatch awaits it. To a type checker the two Protocols differ in the return, an awaitable here. An isinstance check cannot tell them apart, since both ask only whether the object is callable; is_async is the run-time question.

ListSelector

Bases: Protocol[ResultT]

The structural shape of the selector of a LIST SelectorSpec.

It returns the rows, a queryset or any other iterable; a queryset is what the spec's shaping (extend_queryset and its peers) can act on. Every keyword arrives through **extras.

See CreateService for **extras and strictly typed extras.

RetrieveSelector

Bases: Protocol[ResultT]

The structural shape of the selector of a RETRIEVE SelectorSpec.

It returns the one row, or None for a row that is not there, which dispatch answers as not-found unless the spec declares allow_none. A selector returning a queryset, which dispatch collapses to its one row, binds ResultT to the queryset's type. The lookup the spec reads - typically pk - arrives through **extras with every other keyword.

See CreateService for **extras and strictly typed extras.

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, and view is the view DRF recorded on the request it built (a plain HttpRequest records none). extras are spread last, so a key in them replaces either seed; call_service drops such a key instead. 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).

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, through the kernel's shape_queryset.

drfs' signature over django-service-specs' shaping, for a caller holding a view and a request rather than a spec: the fields become a kernel SelectorSpec (with a stand-in selector it never runs), and view, request and the filter's arguments its pool. The order is the kernel's: 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
extend_queryset Any

Called as extend_queryset(qs, view, request), by position, as it always was; the kernel binds its own by name.

required
filter_set Any

A django-filter FilterSet, validated before .qs is read, and handed request through its constructor, so a request-scoped one sees the self.request it would behind DjangoFilterBackend.

None
filter_data Any

The mapping the filter's arguments are read from, each of its filters' names and nothing else. None falls back to request.query_params, the HTTP view path. A QueryDict is read flat as dispatch reads a query string: a repeated key as a list where the filter takes several values, and each value coerced to the type the filter declares.

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; filter_set is not a django-filter FilterSet (one duck-typed on (data, queryset) -> .qs is refused); or extend_queryset answered with an awaitable.

InvalidArguments

The kernel's, where filter_set refused its data or a value no integer column can hold, keyed as the FilterSet keys its errors. It reaches the caller as the kernel raised it, not as DRF's ValidationError: nothing here translates it, so a caller inside a DRF view that wants a 400 translates it itself.

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.

The request is no argument mapping. Off HTTP the flat params mapping is the argument channel; over HTTP it is empty, and a selector's arguments come from what the spec declares it reads: a route capture of one of its reads or a declared UrlKwarg from view.kwargs, a declared QueryParam from the query string, and the hook chain's values. The query string reaches the filter_set as filter_data, restricted to the names the filter reads, so it feeds the filter without widening the argument channel.

There is deliberately no extra_url_kwargs parameter: the core reads view.kwargs itself, for the names the spec reads and the ones its callables mark NotClientInput, and a capture named like a seed of the call never replaces the seed, so a nested route like /users/<user>/posts/ cannot let the captured value shadow the authenticated user (test_url_kwarg_cannot_shadow_the_authenticated_user_over_http). 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.