Types¶
SelectorKind¶
SelectorKind ¶
Bases: str, Enum
Whether a :class:SelectorSpec returns many objects or a single one.
The kind is what tells the framework — and any future caller that
reuses a spec outside an HTTP request — whether to materialize the
selector's return as a collection (LIST) or as a single instance
with retrieve-flavoured 404 semantics (RETRIEVE). Mounting a spec
on a mismatched view (e.g. a LIST spec on a
:class:SelectorRetrieveView) raises
:exc:~django.core.exceptions.ImproperlyConfigured at as_view()
time.
Inheriting from str keeps the value JSON-serializable and
print-friendly while still behaving as a proper enum for is /
== checks.
SelectorSpec¶
SelectorSpec
dataclass
¶
Bases: Generic[ResultT, ExtraT]
All wiring for a single read action in one record.
Used as a value in action_specs on viewsets, as the spec=
argument to :class:SelectorListView / :class:SelectorRetrieveView,
and as the output_selector_spec field on :class:ServiceSpec
(where it describes the post-mutation re-fetch).
Generic parameters (both default to Any):
ResultT— the selector's return type.ExtraT— aTypedDictdescribing the keys returned bykwargs.
All fields are keyword-only: SelectorSpec(kind=SelectorKind.LIST,
selector=fn) rather than positional. kind is required and has no
default — see below.
Fields:
kind— required :class:SelectorKinddiscriminator (LISTvsRETRIEVE). Drives the dispatcher:RETRIEVEmaterializes a QuerySet via.first()and raises :exc:~rest_framework.exceptions.NotFoundonNone/ missing-object,LISTreturns whatever the selector returns unchanged. Also drives the fail-fast check that the spec is mounted on a compatible view (aLISTspec on :class:SelectorRetrieveViewraises atas_view()). Making it explicit lets a spec be reused outside a request — from a management command, a cron job, or any non-DRF caller — without the semantics living implicitly in the call site.selector— callable invoked byget_queryset()(list) orget_object()(retrieve).Nonemeans "use the configuredqueryset/ default DRF behaviour".allow_none—RETRIEVE-only knob for theNone/ missing-object case.False(the default) keeps the standard behaviour: raise :exc:~rest_framework.exceptions.NotFound.Trueexpresses a nullable-resource contract: the standalone retrieve view and the retrieve viewset mixin render200with a JSONnullbody, skipping the output serializer. The flag is ignored when the spec is nested — :attr:ServiceSpec.output_selector_speckeeps its authoritative-None→ 204 contract, and :attr:ServiceSpec.instance_selector_specalways 404s (an update against a missing row is not a nullable read).output_serializer— DRFSerializersubclass used byget_serializer_class()for this action.Nonefalls back to DRF's standardserializer_class.kwargs— callable returning extra kwargs to merge into the pool the selector receives. Co-locating it with the spec lets each action declare its own contract — noif self.action == ...branching in a catch-allget_selector_kwargs.permission_classes— overrides the calling view'spermission_classesfor the action the spec backs.None(the default) means "inherit the view's class-level permissions"; an empty sequence means "no permissions" explicitly. Forwarded through DRF's@action(permission_classes=...)for the@selector_actiondecorator, and surfaced viaget_permissionsfor the viewset mixins and standalone views. Ignored when the spec is nested under :attr:ServiceSpec.output_selector_spec— the surrounding mutation action's permissions apply.output_serializer_context— per-spec hook for the response serializer'scontext=dict. Sits at the most-specific layer of the resolution chain (view.get_serializer_context→view.get_output_serializer_context→view.get_<action>_output_serializer_context→ spec hook), so it wins on overlapping keys.None(the default) leaves the three earlier layers intact. Selectors don't validate input, so there's no symmetricalinput_serializer_context.
The provider is invoked through the framework's keyword-pool
convention, so it declares only what it needs: view, request,
and/or the resolved data being serialized — page on a LIST spec
(the paginated object list, or the full queryset when pagination is off)
or instance on a RETRIEVE spec — or **kwargs for the whole
pool. Any subset works (just view, just request, just page,
none). This lets the provider run a single batched query against the
exact objects being serialized and propagate the result through context
— e.g. lambda *, page: {"votes": tally(page)} or
lambda view, request, *, page: {"votes": tally(page)}. The hook
always runs after the data is resolved.
- select_related / prefetch_related / annotations
— declarative queryset shaping applied to the selector's return value
before it leaves :func:dispatch_selector_for_spec. select_related
is a sequence of relation names (forwarded as
qs.select_related(*spec.select_related)); prefetch_related is
a sequence of relation names or :class:Prefetch objects;
annotations is a mapping merged into a single .annotate(**...)
call. Use these for the common case where the same shaping applies
every request — they're introspectable for OpenAPI / future tooling.
- extend_queryset — dynamic escape hatch. A
Callable[[QuerySet, ServiceView, Request], QuerySet] invoked
after the declarative fields have applied, so it always sees the
fully statically-shaped queryset. Use it when the shaping depends on
the request (e.g. only prefetch when a query string opts in).
Synchronous only — it manipulates the queryset's lazy expression
tree, not the database.
- filter_set — transport-neutral filtering applied to the
selector's QuerySet. Holds a django-filter FilterSet class (or
any object honouring the same (data, queryset) -> .qs contract):
the dispatcher calls filter_set(data=request.query_params,
queryset=qs).qs, so the declaration of which fields are filterable
(with which lookups) lives on the spec while the values come from the
request. Applied after the four shaping fields above and before
the retrieve .first() materialization, so it composes with shaping
and narrows both LIST and RETRIEVE selectors — closing the
retrieve-path gap where RetrieveModelMixin runs no filter step.
Because the values are read off request.query_params, a
FilterSet here is exactly what
:class:~django_filters.rest_framework.DjangoFilterBackend applies
view-side, so on the list path it replaces that backend rather than
stacking with it — wiring both for one action raises at as_view().
Replacing it means keeping its contract, so invalid filter input is
rejected with a 400 rather than silently ignored: the FilterSet is
validated via is_valid() and its errors are raised as a DRF
:exc:~rest_framework.exceptions.ValidationError. (Reading .qs
without validating would return the unfiltered queryset in
django-filter's default non-strict mode — a bad ?field= value would
answer 200 with unfiltered rows.) Only enforced when the duck-typed
object actually exposes is_valid; a bare
(data, queryset) -> .qs stand-in that doesn't opt into validation
keeps its pass-through behaviour.
The dispatcher also forwards the request into the FilterSet when its
constructor declares one (as django-filter's does), so a request-scoped
FilterSet — self.request.user scoping, a request-aware
ModelChoiceFilter queryset — sees the same self.request it would
behind DjangoFilterBackend rather than None. That request is real on
the HTTP / MCP paths and a synthetic off-HTTP one whose user and
query_params are faithful (headers / session are best-effort there); a
bare (data, queryset) stand-in never receives it. None (the default)
applies no filtering. Reach for filter_set only when the selector returns
a QuerySet; when it returns an aggregate / computed object the ?param
values are computation inputs — use kwargs / get_selector_kwargs()
instead.
All five shaping fields (select_related / prefetch_related /
annotations / extend_queryset / filter_set) require
selector to be set and the selector to return a Django
:class:QuerySet. Configuring any of them with no selector raises
:exc:ImproperlyConfigured at as_view() time; a non-QuerySet return
raises at request time.
ServiceSpec¶
ServiceSpec
dataclass
¶
Bases: Generic[InputT, ResultT, ExtraT]
All wiring for a single mutation action in one record.
Used as a value in ServiceViewSet.action_specs and as the spec=
argument to :func:service_action / :class:ServiceCreateView /
:class:ServiceUpdateView / :class:ServiceDeleteView.
Generic parameters are optional and purely informational for type checkers:
InputT— the validated-data type produced byinput_serializer. For dataclass-based serializers this is the dataclass; for plainModelSerializerit is typicallydict[str, Any].ResultT— the value returned by the service callable, and (whenoutput_selector_specis set) the input to itsselector.ExtraT— aTypedDictdescribing the keys returned bykwargs.
All three default to Any, so ServiceSpec(service=fn) keeps working
unchanged.
Fields are grouped by what they configure: the service callable itself,
the input pipeline (input_*), the output pipeline (a single nested
:class:SelectorSpec), and the cross-cutting concerns (kwargs,
permission_classes).
success_status is left as None so each consumer can supply its
own action-appropriate default (201 for create, 200 for update, 204
for destroy). It may also be a callable resolved through the framework
keyword pool — declaring any subset of result / instance /
request / view (or **kwargs) — returning the status int.
This covers upserts whose code depends on the outcome (200 for an
existing row, 201 for a freshly created one); the callable sees the
service's return value as result. A None return from the field
itself is not meaningful — return an int. OpenAPI can't resolve a
callable statically, so the generated schema documents the mixin default
for the dynamic case.
input_data is the symmetrical hook for the serializer's input.
Returns a mapping merged on top of request.data before the
input_serializer validates it — useful for lifting URL kwargs
(e.g. parent IDs from nested routes) into fields the serializer can
cross-validate. Server-provided keys win on conflict. The provider is
invoked through the framework's keyword pool, so it declares any subset of
view / request plus instance (the resolved mutation target,
None on create) — or **kwargs — receiving only what it names, so
pre-validation input mutation that depends on the current row has a home.
The same declare-to-receive rule applies to the get_input_data /
get_<action>_input_data view hooks.
input_serializer_context is a per-spec hook for the input
serializer's context= dict. It sits at the most-specific layer of
the resolution chain (view.get_serializer_context →
view.get_input_serializer_context →
view.get_<action>_input_serializer_context → spec hook), so the
spec wins on overlapping keys. None (the default) leaves the
three earlier layers intact. The symmetrical output hook lives on the
nested output_selector_spec.output_serializer_context; that hook may
additionally declare a result keyword to receive the final
(post-selector) instance being serialized — passed only when declared —
so it can run a single batched query against it and propagate the
outcome through context. The output hook always runs after the service
and output selector have resolved result.
output_selector_spec is the full output pipeline collapsed into a
single :class:SelectorSpec. Its kind declares the response
cardinality: :attr:SelectorKind.RETRIEVE (the default) re-fetches a
single instance (typical pattern: the service returns a freshly
created/updated instance, the output_selector_spec.selector re-fetches
it with the relations the response serializer needs, and the spec's
output_serializer renders the result); :attr:SelectorKind.LIST
re-fetches and renders a set (many=True) and is valid only alongside
collection_selector_spec — the bulk-output twin that lets a
collection mutation return the affected set. None (the default) means
"render the service's return value directly". The nested spec's
permission_classes and kwargs are ignored — the surrounding
mutation's permissions and kwargs chain apply.
instance_selector_spec is the input-side twin of
output_selector_spec: a nested :class:SelectorSpec (kind must
be :attr:SelectorKind.RETRIEVE) that resolves the instance an
update / destroy / detail action targets, embedding the lookup in the
spec instead of relying on the view's queryset / get_object()
chain. The selector's kwarg pool is {request, user} plus the URL
kwargs (plus the standard selector extras chain), so
selector=lambda *, pk: Project.objects.filter(pk=pk) resolves the
row from the route. Resolution happens before input validation —
the resolved instance is handed to the input serializer
(DRF-style serializer(instance, data=..., partial=...)) and seeded
into the service kwarg pool as instance. A None / missing
resolution raises :exc:~rest_framework.exceptions.NotFound (the
nested spec's allow_none flag is ignored — an update against a
missing row is always a 404), and object-level permissions
(check_object_permissions) run against the resolved instance. The
queryset-shaping fields apply; permission_classes,
output_serializer, and output_serializer_context on the nested
spec are ignored. None (the default) keeps today's get_object()
chain.
partial overrides the transport-derived partial-validation flag.
None (the default) inherits the flag the calling surface derives
(False for PUT/POST, True for PATCH); True / False
forces it regardless of HTTP method — e.g. partial=False on an
action_specs["partial_update"] entry makes a PATCH endpoint
enforce required fields like a PUT. Applied once, at
dispatch_mutation_for_spec, so it is honoured uniformly by the
viewset mixins, the standalone views, and @service_action.
document_service_error is an OpenAPI-only flag controlling whether
the generated schema documents the 422 :class:ServiceError response
(it has no effect on runtime behaviour — a service may always raise
ServiceError). None (the default) gates the 422 on whether the
operation validates input (input_serializer is not None), so a
no-input mutation (e.g. a plain delete) doesn't carry a spurious 422 in
its schema. Set True to document it anyway (a no-input service that
does raise ServiceError) or False to drop it (an input-bearing
service that never raises one). Only consulted when the [spectacular]
extra is enabled via :func:~rest_framework_services.openapi.enable_openapi.
many and collection_selector_spec are the two bulk shapes,
mutually exclusive with each other. many=True validates the request
body as a list (input_serializer runs many=True) and renders the
result list the same way — the service receives the validated list as
data and loops itself (bulk_create / a comprehension), so one call
does the batch. collection_selector_spec is the LIST-kind twin of
instance_selector_spec: its resolved set (a queryset or any iterable,
scoped by the selector + filter_set) is seeded into the pool as
collection for the service to .delete() / .update() / iterate —
an instance-less "operate on the filtered set" action (bulk delete/update),
where an empty set is a harmless no-op. Its selector's kwarg pool carries
the request's query params and body plus the view's URL kwargs, so a
nested-route bulk (/parents/{parent_pk}/children/) can scope by
parent_pk — matching instance_selector_spec. Route captures win over
client query / body on a key conflict, so a filter value can't override the
route scope. Both run all-or-nothing under
atomic=True; authorization is per-set (the view / spec
permission_classes plus the scoped selector), with no per-row check.
kwargs is a callable that returns extra kwargs to merge into the
pool the service receives. Co-locating it with the spec lets each action
declare its own contract — no if self.action == ... branching in a
catch-all get_service_kwargs. See :class:ServiceView for the
attributes available on the view argument.
permission_classes overrides the calling view's permission_classes
for the action the spec backs. None (the default) means "inherit the
view's class-level permissions"; an empty sequence means "no permissions"
explicitly. Forwarded through DRF's @action(permission_classes=...)
for the @service_action decorator, and surfaced via get_permissions
for the viewset mixins and standalone views.
response_finalizer is a post-serialization hook for HTTP response side
effects (cookies, headers, a swapped response). It runs on the 2xx path
only, after the output serializer has produced the Response and
before it is returned (pre-render); error paths bypass it. Invoked through
the framework keyword pool, it declares any subset of
response / result / request / view / instance / data
(or **kwargs) and returns either a Response (which replaces the
built one) or None (which keeps it) — so lambda *, response:
response.set_cookie(...) or response attaches a cookie, and returning a
fresh Response swaps it wholesale. Unlike the service/selector pool it
does receive view (a documented exception — a response decision
legitimately needs view/request context). result is the service's
return value (the flags carrier), so the idiomatic pattern keeps services
DRF-free: the service returns domain flags on its result DTO and the
finalizer translates flags → transport effects. HTTP-only: it is skipped
on the transport-neutral path (dispatch_spec / call_service / MCP),
which builds no Response. On the bulk path instance / data are
absent and result is the dispatched value (post-output-selector).
PolymorphicServiceSpec¶
PolymorphicServiceSpec
dataclass
¶
A single action that accepts several mutually exclusive payload shapes.
Each variant has its own input serializer and service, bundled as a full
:class:ServiceSpec under a string key. A discriminator callable
inspects the request and returns the key; dispatch then proceeds through the
chosen spec exactly as a plain ServiceSpec would. Usable anywhere a
ServiceSpec is: an action_specs entry and the spec= of
@service_action.
PolymorphicServiceSpec(
discriminator=resolve_flow, # pool: {request, data, user, view} → key
specs={"email": email_spec, "token": token_spec},
)
discriminator is resolved through the framework keyword pool — it
declares any subset of request / data (the raw request.data) /
user / view (or **kwargs) — and returns a key present in
specs. For a no-match / rejected payload it should raise
(e.g. :exc:~rest_framework_services.exceptions.service_validation_error.ServiceValidationError,
which the view layer maps to a 400); returning a key absent from specs
is a configuration error and raises :exc:~django.core.exceptions.ImproperlyConfigured.
permission_strategy decides how get_permissions behaves, since DRF
runs permissions before the body is necessarily parsed:
"union"(the default) — require the union of every variant'spermission_classes; the request must satisfy them all. No early body read, and the conservative/secure failure mode (a mis-declared discriminator branch can't widen access). For the common "same auth, different payload shapes" case this is identical to"require_identical"."discriminate"— run the discriminator early (reading the raw body) and apply only the chosen variant'spermission_classes. Most precise, for the genuine "each variant is a different privilege" case; accepts the early-body-read tradeoff."require_identical"— validated atas_view()time to require every variant to declare the samepermission_classes; sidesteps ordering entirely.
The resolved concrete spec flows through the shared action→spec chain, so the chosen variant's serializer context, kwargs, and output pipeline all apply — the discriminator is resolved once per request and reused across dispatch, permissions, and serializer resolution.
ChangeResult¶
ChangeResult
dataclass
¶
Bases: Generic[ModelT]
Outcome of a mutation helper call.
instance is the model instance after the mutation. created is True
iff this came from :func:create_from_input / :func:acreate_from_input.
changes records every field whose value actually differed from its
prior value (or from UNSET for creates). children carries one
:class:ChildCollectionChange per reverse-FK collection written via the
children= argument — empty for the common no-nested-write case.
The class is generic over the concrete model type: callers that pass
Author into a mutation helper get back a ChangeResult[Author]
whose .instance is typed as Author. The bare name
ChangeResult (no parameter) resolves to ChangeResult[Model] and
keeps working for callers that don't care.
FieldChange¶
FieldChange
dataclass
¶
One field's before/after pair from a mutation.
old will be UNSET for fields populated as part of a create
(no prior value existed).
ChildSpec¶
ChildSpec
dataclass
¶
How to persist one reverse-FK ("one-to-many") child collection.
Passed in the children={relation_name: ChildSpec(...)} map of
:func:~rest_framework_services.create_from_input /
:func:~rest_framework_services.update_from_input (and their async
siblings), and forwarded by the default
:func:~rest_framework_services.create_model /
:func:~rest_framework_services.update_model /
:func:~rest_framework_services.delete_model services. The incoming child
rows are read from data[relation_name]; each child is persisted by
running it back through the same mutation helpers, so scalar / m2m / nested
semantics compose recursively.
Fields:
model— the child model class.fk— the name of the child's forward foreign-key field pointing at the parent (e.g."author"forBook.author). It is set automatically on created children and used to resolve the parent's reverse manager.match_key— the field used to pair an incoming row with an existing child (default"pk"). An incoming row whosematch_keymatches an existing child updates it; one with no match (or no key) is created. The same name is read off both the incoming mapping (item[match_key]) and the existing instance (getattr(child, match_key)), so serializers emitting"id"should setmatch_key="id".mode—"replace"(the default) matches incoming to existing, creates new, updates matched, and removes orphans (existing children absent from the incoming set);"merge"upserts only and never removes. An orphan is unlinked (itsfkset toNone) when the FK is nullable, else deleted — mirroringon_delete=SET_NULLvsCASCADE.field_map/exclude_fields— forwarded to the per-childcreate_from_input/update_from_inputcall, exactly as for the parent.m2m— optional callable(child_row) -> mappingderiving the child's many-to-many assignments from its incoming row (the per-child analogue of :func:~rest_framework_services.create_model'sm2m).children— a nested{relation_name: ChildSpec}map for grandchildren; recursion follows the declared tree, so depth is bounded by how deeply you nest specs.
The whole parent + children write runs inside the service's atomic block; field-level validation stays in the input serializer / dataclass — the helper owns persistence only.
ChildCollectionChange¶
ChildCollectionChange
dataclass
¶
What a nested write did to one reverse-FK child collection.
Carried in :attr:~rest_framework_services.ChangeResult.children, one
entry per children= relation. The tuples hold child primary keys:
created— children inserted.updated— existing children whose row was updated (matched by the :class:~rest_framework_services.ChildSpec'smatch_key).deleted— orphaned children removed because their FK is non-nullable.unlinked— orphaned children detached (FK set toNone) because their FK is nullable.
updated records every matched child the helper ran through
update_from_input, regardless of whether that child's own columns
actually changed.
UNSET¶
unset ¶
The UNSET sentinel and its type.
Used to distinguish "field omitted from input" from "field explicitly set to
None". Critical for partial updates where None must not stomp on an
existing value.
UNSET is the singleton value you compare against (value is UNSET).
UnsetType is its type, exported so callers can spell it in annotations —
e.g. bio: str | None | UnsetType.
UnsetType ¶
Singleton sentinel type. Always falsy; identity-equal to itself only.
Don't instantiate this directly — use the module-level UNSET singleton.
UnsetType() returns that same instance, but the sentinel is the value
you compare against and UnsetType is only useful as a type annotation.
NoInput¶
NoInput ¶
Sentinel type for the InputT slot when a service expects no body.
Pair with :class:DeleteService when the spec has no input_serializer::
@implements(DeleteService[NoInput, Author, None])
def delete_author(
*,
instance: Author,
**extras: Any,
) -> None: ...
The class itself is never instantiated — it exists purely to bind the
InputT type variable in a way that is searchable in IDEs and docs.
HttpExtras¶
HttpExtras ¶
Bases: TypedDict, Generic[UserT]
OfflineContext¶
OfflineContext
dataclass
¶
The HTTP-surrogate a spec needs when dispatched off the HTTP path.
Produced by :func:~rest_framework_services.dispatch.build_offline_context.
Its request / view feed the optional request= / view=
arguments of :func:~rest_framework_services.dispatch_spec, and the whole
value is consumed by :func:~rest_framework_services.enforce_permissions.
user— the acting principal (request.useris set to the same value);Anybecause the user model is project-defined.request— the synthetic DRF Request (.userset,.datacarrying the params), forwarded to spec callables that declarerequest.view— the :class:OfflineServiceViewforwarded to callables that declareviewand used as theviewargument of permission checks.
InputRequired¶
input_required ¶
The InputRequired schema marker and its type.
Marks a declared input as required in the generated schema without making it
required in the type system. Use it inside Annotated[...] on an extras
TypedDict key (or an ordinary parameter) of a service / selector::
class WidgetExtras(HttpExtras[MyUser], total=False):
project_pk: Annotated[int, InputRequired]
@implements(ListSelector[Widget])
def list_widgets(**extras: Unpack[WidgetExtras]) -> list[Widget]:
return Widget.objects.filter(project_id=extras["project_pk"])
Why a marker and not a required TypedDict key. Under PEP 692, a required
key in Unpack[<TypedDict>] makes the function reject callers that omit it,
which breaks assignability to :class:~rest_framework_services.ListSelector /
:class:~rest_framework_services.RetrieveSelector / the service Protocols — the
very reason :class:~rest_framework_services.HttpExtras mandates total=False.
So the two things a consumer wants — Protocol conformance and an honest schema —
are mutually exclusive through the TypedDict's own totality.
Annotated metadata carries no typing weight, so the key stays NotRequired
to the type checker while :func:spec_to_json_schema lists it in required.
The marker is advertisement plus enforcement, never delivery: it does not
change the kwargs pool, the view.kwargs-over-params precedence, or the
SPREAD_AUTHOR_WINS author-beats-client rule. It tells a schema-driven caller
(an MCP client, an LLM tool call) that the input is mandatory, and makes
dispatch_spec raise
:class:~rest_framework_services.ServiceValidationError when it is absent —
instead of the bare KeyError the callable would otherwise raise from deep
inside dispatch, which no transport maps to a useful error.
Its counterpart is :data:~rest_framework_services.NotClientInput, which hides a
key from the schema entirely. A key marked with both is a contradiction and
raises at schema-generation time.
InputRequired is the singleton you place in the annotation.
InputRequiredType is its type, exported only so the singleton can be spelled
in annotations; you never need to instantiate it.
InputRequiredType ¶
Singleton marker type. Identity-equal to itself only.
Don't instantiate this directly — use the module-level InputRequired
singleton. InputRequiredType() returns that same instance.
NotClientInput¶
not_client_input ¶
The NotClientInput schema marker and its type.
Marks a declared input as provider-owned: it is dropped from the generated
schema entirely, so a schema-driven caller never learns it exists and never
supplies it. Use it inside Annotated[...] on an extras TypedDict key (or
an ordinary parameter) of a service / selector::
class WidgetExtras(HttpExtras[MyUser], total=False):
project_pk: Annotated[int, InputRequired]
team_role: Annotated[str, NotClientInput] # resolved by spec.kwargs
Why. Reflecting Unpack[<TypedDict>] into the input schema makes every
declared key visible to an LLM / MCP client — including keys a spec.kwargs
provider supplies from request state, which the caller has no business setting.
Advertising those is merely a wart on the scoping keys, because the selector
default SPREAD_AUTHOR_WINS plus an always-resolving provider makes a
client-supplied value dead on arrival. NotClientInput removes the wart at the
source rather than relying on that invariant to absorb it.
The marker is advertisement-only, never delivery or enforcement: a marked key
is still spread into the kwargs pool exactly as before, and the provider still
resolves it. It also does not make the key safe on its own — the security
property is still the author-wins precedence documented in resolve_provider.
Marking a key hidden while opting into SPREAD_CALLER_WINS on a scoped spec
voids that guarantee just as it did before, and a provider owning a scoping key
must still never decline via UNSET.
A key marked NotClientInput is also excluded from
:func:declared_input_keys, so UnknownArguments.REJECT treats a caller that
supplies it as passing an unknown argument — which is exactly what it is.
Its counterpart is :data:~rest_framework_services.InputRequired, which marks a
key mandatory. A key marked with both is a contradiction and raises at
schema-generation time.
NotClientInput is the singleton you place in the annotation.
NotClientInputType is its type, exported only so the singleton can be spelled
in annotations; you never need to instantiate it.
NotClientInputType ¶
Singleton marker type. Identity-equal to itself only.
Don't instantiate this directly — use the module-level NotClientInput
singleton. NotClientInputType() returns that same instance.
UrlKwarg¶
UrlKwarg
dataclass
¶
A URL route capture exposed as a caller-supplied argument off-HTTP.
Over HTTP a nested route's captures (the project_pk of
/projects/{project_pk}/widgets/) reach a spec through view.kwargs —
directly, or through a spec.kwargs provider that scopes by them.
Off-HTTP there is no route, so the caller supplies the value as an ordinary
argument: the transport advertises it in the tool / operation schema, pops it
out of the arguments, and hands it to build_offline_context(kwargs=…),
from where :func:~rest_framework_services.dispatch_spec spreads it into the
selector / target pools — authoritative over the spec params, below a
spec.kwargs provider. It never reaches the spec as an ordinary input, so
the unknown-argument policy never flags it.
This type is declared here, not in each adapter, on purpose. It is the
same declaration whichever transport carries it, and two independent copies
had already drifted into validating the same declaration against different
reserved-name sets. Adapters import it and pair it with
:func:~rest_framework_services.validate_channel_names.
Reach for a UrlKwarg when the value is a URL-derived input a spec depends
on that is not already an ordinary argument — most commonly a scoping
spec.kwargs provider reading view.kwargs (off-HTTP that mapping is
otherwise empty, so the provider mis-scopes for every caller), or a
closed-surface spec whose route capture must be caller-suppliable.
A selector that reads the value from its own **extras: Unpack[TypedDict]
needs no UrlKwarg: drf-services reflects the key into the schema and
params delivers it. A key can be both reflected and registered — the
explicit UrlKwarg wins the adapter's schema merge, registration pops the
argument into kwargs=, and the authoritative spread still delivers it to
the selector pool, so both readers see it.
name— the argument / view-kwarg key. Must not collide with a reserved transport key; see :func:~rest_framework_services.validate_channel_names.type— the JSON-Schema type advertised to the caller ("string"by default;"integer"/"number"/"boolean"…).description— optional help text shown to the caller.default— optional value seeded when the caller omits the argument; also surfaced as the schemadefault.required— advertise the key in the schema'srequiredlist. Use it for a route capture the spec genuinely cannot run without, so a caller is told up front instead of failing mid-dispatch. Setting bothrequiredand adefaultis contradictory and raises in :func:~rest_framework_services.validate_channel_names.
required here is the registered-declaration counterpart of the
:data:~rest_framework_services.InputRequired marker, which does the same
job for a key the callable's own TypedDict declares. Both end up in the
schema's required; they differ only in where the key is declared.
json_schema ¶
The JSON-Schema property this kwarg contributes to an input schema.
QueryParam¶
QueryParam
dataclass
¶
A request-level query param exposed as a caller-supplied argument off-HTTP.
Generalizes the built-in page / limit / order list-selector
arguments to any read-shaping param a serializer reads off
request.query_params — django-restql field selection (?query= /
?fields=), or a custom serializer that branches on the query string. The
transport advertises it, pops it from the arguments, and hands it to
build_offline_context(query_params=…); it never reaches the spec as an
input, so the unknown-argument policy never flags it.
A :class:~rest_framework_services.SelectorSpec filter_set does not
need this — its fields are already generated into the schema and flow through
as ordinary params.
Declared here rather than in each adapter for the same reason as
:class:~rest_framework_services.UrlKwarg: it is the same declaration
whichever transport carries it. Pair it with
:func:~rest_framework_services.validate_channel_names.
name— the argument / query-string key. Must not collide with a reserved transport key; see :func:~rest_framework_services.validate_channel_names.type— the JSON-Schema type advertised to the caller ("string"by default;"integer"/"number"/"boolean"/"array"…).description— optional help text shown to the caller.default— optional value seeded when the caller omits the argument; also surfaced as the schemadefault.
No required flag, deliberately. A query param is read-shaping —
omitting one is legitimate by construction, and the spec runs correctly
without it. Requiredness belongs to inputs the spec cannot run without, which
is :class:~rest_framework_services.UrlKwarg and the
:data:~rest_framework_services.InputRequired marker.
json_schema ¶
The JSON-Schema property this param contributes to an input schema.
validate_channel_names¶
validate_channel_names ¶
validate_channel_names(
*,
label: str,
kind: str,
declarations: Sequence[_ChannelDeclaration],
reserved: frozenset[str] = frozenset(),
) -> None
Raise ImproperlyConfigured on a bad channel declaration set.
A :class:~rest_framework_services.UrlKwarg /
:class:~rest_framework_services.QueryParam is popped out of the caller's
arguments and routed to a side channel, so its name must not collide with a
key the transport controls, and must not be declared twice.
Three failure modes, all caught at registration time rather than on a call:
- Reserved-name collision. :data:
RESERVED_POOL_SEEDSis always included — those are the dispatcher's authoritative seeds, and letting a caller route a value onto one is the spoofing footgun the spread modes strip.reservedadds the transport's own keys on top; pass the pagination names the transport reserves (page/limitand whichever oforder/orderingit uses), since those genuinely differ per transport while the seed set does not. - Duplicate names within the set — the later declaration would silently shadow the earlier one.
requiredtogether with adefault— contradictory: a default means the argument can always be satisfied without the caller, so demanding it is either a no-op or a lie. Only checked on declarations that carry arequiredattribute (QueryParamdeliberately has none).
Adapters should call this once per tool / operation, with label
identifying the offending registration site and kind naming the
parameter the consumer passed ("url_kwargs", "query_params"), so the
message points at something the consumer can act on.
RESERVED_POOL_SEEDS¶
reserved_pool_seeds ¶
RESERVED_POOL_SEEDS — pool keys carrying transport-controlled seeds.