Skip to content

Service specs on the kernel

A ServiceSpec or SelectorSpec declared here is a source for django-service-specs: it answers as_kernel_spec(), so the kernel's kernel_spec(spec) compiles it once, and dispatch, authorize and a SpecRegistry take the drfs spec itself. Every drfs field lands on a kernel seat or is named below as the HTTP views' own.

A many spec's list is judged differently by the two routes. dispatch_spec judges the list on the rows sent: where the list as sent is within its length bounds, a row that is no object is refused alone, and the list's validate() is left to the rows as sent. The kernel's dispatch called directly on a drfs spec has no such body to hold the rows and judges the list on the rows that survived, in the kernel's own classes and words: [good, 1] under a minimum of two answers row 1's Expected an object. beside Ensure this field has at least 2 elements., and a list-level validate() runs over the rows left.

What a spec declares for a caller off HTTP

Over HTTP a URLconf captures a nested route's project_pk and a query string carries fields. Off HTTP somebody has to say they exist, so a spec declares them itself:

from rest_framework_services import QueryParam, SelectorKind, SelectorSpec, UrlKwarg

list_tasks = SelectorSpec(
    kind=SelectorKind.LIST,
    selector=tasks_for_project,
    reads=[
        UrlKwarg("project_pk", type="integer", required=True),
        QueryParam("fields", description="Fields to return."),
    ],
)
  • reads: each UrlKwarg and QueryParam becomes one kernel Parameter, so it is an argument a caller sends and the schema lists. A QueryParam no callable of the call takes by name compiles to the kernel spec's shapes rather than its reads, and reaches the presenter on PresentationContext.shapes (see QueryParam). An entry of the same name as a selector parameter replaces the reflected one and keeps what that parameter requires: a read naming a parameter with no default, of the selector, a target lookup, the service or a precondition, is required too, unless the read declares a default; the compiled parameters and the kernel's input schema say so, and a parameter marked InputRequired stays required under the read. A sequence is required, and a name may be declared once. A selector taking a bare **kwargs is not handed a route capture no callable declares: declare it here or as a keyword parameter. A UrlKwarg's type has to be the one the route's converter yields: UrlKwarg("pk", type="integer") for <int:pk>/, since the default string refuses an int capture as Expected a string..
  • field_audiences: per-field markings layered over the output serializer's own. On a ServiceSpec they layer over its output selector's. A spec declaring them with no output serializer is refused at compile, with a message naming both.
  • agent_contract(): the OfflineContract these two declare, which a registry entry answers for transports that read it. None when both are empty.

Where each field lands

drfs field Kernel seat
permission_classes one DRFPermissionCheck; [] is Unrestricted(), and None is no permissions, which a registry refuses
input_serializer validator (a SerializerValidator)
output_selector_spec.output_serializer, output_serializer presenter (a SerializerPresenter)
input_serializer_context the SerializerValidator's serializer_context
output_selector_spec.output_serializer_context, output_serializer_context the SerializerPresenter's serializer_context
partial partial, kept only with an input_serializer
many, many_argument the same; the list bounds come from the serializer's ListSerializer and apply off HTTP and on an OfflineServiceView, while a live view's bulk body is bounded by the ListSerializer alone
extend_queryset extend_queryset, called as extend(queryset, view, request)
affordances each service's compiled spec
everything else of the same name carried by name

A service with no input_serializer declares none of its own parameters, so kernel_spec(spec).parameters() lists only its target lookup's keys and its reads, and no body reaches the service's keywords. Its signature is declared only in the variant compiled for an explicit ArgumentBinding.SPREAD_AUTHOR_WINS (kernel_spec_variant(spec, spread=True)). A selector always takes its arguments from its signature, except for the names its filter_set owns. Parameters marked NotClientInput on any callable of the call are hidden. So are the names the pool fills: user, request, view, queryset, result and the rest.

parameters() is not available on every compiled spec. Where a target lookup's selector reads a name that is also a writable input field, such as renaming a row by the slug that finds it, kernel_spec(spec).parameters() and as_kernel_spec(spec).parameters() raise ImproperlyConfigured ("Parameters ['slug'] are declared by two sources"), because the kernel declares one source per name. Such a spec still registers and dispatches, and spec_to_json_schema describes it: the route's capture scopes the lookup, and the body's value is the field's.

Three reads declarations are refused with ImproperlyConfigured when the spec compiles (at registration, or whenever it is dispatched, over HTTP too), each naming the read:

  • a name the pool fills, which would never be the caller's;
  • a name the spec's service or selector, or one of its preconditions, marks NotClientInput. Dispatch hands a declared read to each of those callables by name, so the same callable would be given the client's value for a parameter it keeps the server's. The message names the callable and the mark; remove one of the two;
  • a name a writable field of the input serializer also takes, declared on the spec or on its target lookup. The body's value of that name is the field's, so the read would reach neither the service nor the lookup. The message names the read, the serializer and, where it reads the name too, the target lookup. A UrlKwarg the target lookup reads is accepted: that is the rename-by-key shape above, where the route's capture scopes the lookup. A many spec's fields are each row's, and a read-only field and a HiddenField take nothing from the body, so none is a clash: a UrlKwarg named like a HiddenField whose default reads view.kwargs is how a route capture scopes a write, and off HTTP the caller's value for it is written onto view.kwargs, where the default reads it.

A serializer whose fields cannot be read when the spec compiles - they read the request, or query a table migrate has not made yet, or a row not yet there - is taken to declare no name, so registering it at import never needs the database; the call that describes it is where such a read is refused. A many=True spec whose serializer cannot be built then compiles with no list bound of its own, and the list serializer that validates the call still holds the list to the bound it declares.

A name only a target lookup marks NotClientInput is accepted: the lookup is never handed a read it hides, so the read is the service's argument and the lookup keeps its own value.

A nested spec compiles to what the kernel runs:

  • its permission_classes are not carried;
  • an instance lookup's allow_none and output_serializer are dropped;
  • an output selector's kwargs are dropped;
  • an output selector with no selector moves its serializer onto the service spec's presenter;
  • preconditions and progress_reporter, which would never run there, are refused.

success_status, document_service_error and response_finalizer are the HTTP views' and are not compiled.

The two serializer context callables ride on the adapters and are asked when the serializer is built for a call, over DRF's own context and the view hooks dispatch_spec registers for the call; see Customise serializer context.

The serializer as Validator and Presenter

SerializerValidator is the kernel's Validator over an input_serializer (a DRF serializer or a dataclass): its parameters are the serializer's fields, and it validates a call with the target as instance, the call's partial, and a serializer context built for the principal, so CurrentUserDefault reads it. A HiddenField or a read-only field is not a parameter, and neither is a field named like a seed (user, result, a registered seed's name): the serializer reads it from the body, so its value lands in data and the seed keeps its own. For a request a live DRF view dispatches, the edge compiles a lenient variant (kernel_spec_variant(spec, lenient=True)) whose parameters carry their names alone, so DRF's serializer is the only validator and answers in its own words; off HTTP the parameters carry their types and the kernel checks them first.

A required field named like a seed has no parameter, so no call through the kernel's dispatch can send it, although HTTP and dispatch_spec read it from the body. SerializerValidator.unsendable names those fields, a frozenset of field names computed beside parameters(), so a transport can refuse such a spec before a call reaches it: getattr(compiled.validator, "unsendable", frozenset()). It names a required user, result or serializer, a many spec's row fields included, and the names the edge's variant excludes. It names no optional, defaulted, hidden or read-only field, no field the Validator declares a parameter for (a field named like a registered seed is one on the compiled spec), and says nothing of partial, which is the compiled spec's. Nothing in drfs reads it; HTTP is unchanged. SerializerPresenter is the kernel's Presenter over an output serializer, with the spec's field_audiences layered over the serializer's own markings.

SerializerValidator

Bases: Validator

A Validator whose declaration is a DRF serializer, or a dataclass read through one.

It takes what a drfs spec's input_serializer takes: a Serializer subclass, ModelSerializer and DataclassSerializer included, or a dataclass, which is validated through a DataclassSerializer exactly as drfs' dispatch validates it. Anything else is refused with ImproperlyConfigured when the Validator is built.

parameters() walks the serializer's fields, in order, as the field walk in service_specs/utils.py reads them: a nested serializer is an object with Parameters of its own, many=True and a ListField an array, a file field a file, choices with their labels; a HiddenField and a read-only field are no parameter. It is read on each call rather than when the Validator is built, because specs are built at import time, when a ModelSerializer cannot yet read its model, and so that help is rendered in the language active when it is asked for. It never queries.

validate() builds the serializer as a DRF view would:

  • over the body the call was sent, where a DRF edge call names this Validator (validator_input): the caller's own data, a QueryDict kept one, with what the edge put over it, as a DRF view's serializer was always handed request.data. Anywhere else, the kernel's checked arguments.

  • with the target as instance, so an update's uniqueness checks exclude the row being updated. The queryset a collection selector resolves is no row, so a call over one binds as a create, as drfs' dispatch binds it; the target is handed over as it is, since a serializer, unlike a ModelForm, writes nothing onto its instance while it validates.

  • partial as the call is, the context's partial, so a partial call may leave out a required field and returns only what was sent.
  • under the serializer context DRF builds on HTTP, request, format and view. The request and view the call registered as pool seeds are the serializer's own, read from ValidationContext.ambient as the kernel resolved them, and where a seed is absent or None, the entry of that name in ValidationContext.call_context. A call that carries neither gets an offline request whose user is the principal and whose data is the arguments, built by build_offline_context, so a CurrentUserDefault reads the principal and a field or a validate() reading self.context["request"] finds a request rather than a KeyError. What a registered request's user is, is the transport's to make the principal; the Validator never writes to a call's own objects. Over that, the input half of the view hooks a DRF edge carries for the call (input_view_layers), then serializer_context, the spec's own input_serializer_context, each winning on a key it shares with what it layers over (see serializer_context in service_specs/utils.py). The callable declares what it reads from the call's pool: request, view, user, call_context, a registered seed, or an argument. It is asked once per call, a many call's included, except where the call builds the serializer twice, for a body the server puts a value into or a row a target lookup resolves, and what the callable is handed differs between the two.

A refusal is one InvalidArguments, the kernel's refusal that is DRF's ValidationError as well, carrying the serializer's errors unchanged: DRF's ErrorDetail leaves, each with its code, cross-field messages under non_field_errors, and a nested list's errors in whichever shape the installed DRF writes them.

validated() hands the service what drfs' dispatch hands it: data is the serializer's validated_data - the validated mapping, or the dataclass instance a DataclassSerializer builds - and the bound serializer is bound for the call as serializer. values follow what validated_data is, as drfs' dispatch reads it: a mapping's entries, keyed as DRF keys them, by each field's source; a dataclass instance's __init__ fields, whether a dataclass was declared or a DataclassSerializer subclass was; and nothing for any other object a serializer's validate() returns, which reaches the service only as data. A key named like a seed of the call is a value only in data: DRF's HiddenField(default=CurrentUserDefault()) named user, or a key a validate() adds named result, stays in validated_data as the serializer built it, and the service's user is the call's principal, as drfs always handed them (see _unseeded). So is a key named in server_owned, the names a callable of the call marks NotClientInput, which the compiled spec hands it: the client's value is the service's in data alone, unless the edge put the server's value into the field it comes from (see _unowned). Where the edge did, that field's validation of the value the edge put is the key's value, validated on its own once the serializer has accepted the body, never read back off the body (see _server_values), whatever the field reads from the body, and whatever a later field of the same source, a source="*" nested serializer, a validate() or a to_internal_value override did to the key: it is the parameter's, by the field's source or, where that is no single key, by the field's name, and by the key an author named it under (see _handed), and it is laid back over the key in a mapping data (see _lay_back). A dataclass instance is data as built, and a many call's rows are neither validated apart nor laid back.

A partial call over a dataclass is refused, a DataclassSerializer subclass included: one builds its instance from every field, and a field a partial call left out fails as a TypeError from inside the library. supports_partial is therefore answered per instance, False for either declaration and True for any other serializer, so a spec declaring partial=True over a dataclass is refused when it is built. A partial call made on the Validator directly is refused with ImproperlyConfigured as well.

validated_many() validates a many spec's rows as DRF's own list serializer does, once: the serializer's child class under many=True (so a Meta.list_serializer_class and its list-level validate() run), with the call's context and partial flag. The one list serializer is bound for the call as serializer, data is the list of each row's validated_data and the values are the rows' values under many_argument. A refusal is an InvalidArguments whose detail keys each failing row by the index the caller's list gave it, whichever of DRF's two shapes the installed version reports item errors in, and whose list-level message sits under non_field_errors. A live view's list body (EdgeCallState.body_rows) is bound whole, every row as sent, and what the list serializer refused is also left on the edge's state as DRF reported it, for the edge to answer with as a view always did. Where the kernel refused a row the edge sent, the rows it passed are read but the list is judged as sent: its length bounds count every row sent, and its list-level validate() does not run, as DRF never runs it past a refused row.

unsendable property

unsendable: frozenset[str]

The required input fields no call on the kernel route can send.

A field named like a seed (RESERVED_POOL_SEEDS, BOUND or excluded) is no parameter (see parameters()), so the kernel has nowhere to take its value from, while HTTP and dispatch_spec read it from the body. Where the serializer requires such a field, a call through the kernel's dispatch is refused for it however it is made. This names those fields, by the name parameters() would have declared them under, so a transport can refuse such a spec before a call reaches it::

getattr(compiled.validator, "unsendable", frozenset())
  • Only what parameters() omits. A field the Validator declares a parameter for is never named, even one a seed, a server argument or a call seed then fills: a field named like a registered seed is one, since the compiled spec leaves excluded empty and only the DRF edge's variant fills it. A required field called serializer or user is named.
  • Required only. An optional field, one with a default, a read_only field and a HiddenField are no input to require (test_an_optional_field_is_not_unsendable, test_a_read_only_field_is_not_unsendable, test_a_hidden_field_is_not_unsendable), and a many spec's row fields are read through the row serializer this Validator holds, so a row's required user is named and a row's tenant is not (test_a_many_specs_row_fields_are_read_through_the_row_serializer).
  • Not partial. Whether a call may leave a required field out is the compiled spec's partial, not this attribute's question; it names the field whether or not the spec is partial (test_a_partial_spec_names_the_same_fields).
  • Not lenient. The declaration relaxes what each parameter accepts and not which names are parameters (test_a_lenient_validator_names_the_same_fields).

Computed on each read, from the serializer parameters() describes, for the reason parameters() is: a ModelSerializer cannot read its model when specs are built. HTTP is unchanged: nothing in drfs reads it.

parameters

parameters() -> Parameters

The serializer's input fields, as Parameters.

Read off describe_serializer: inside an edge call, the serializer the call builds once and validates with, so a serializer that queries as it is built queries once in the call (test_its_queries_run_once_off_http); outside one, a serializer built afresh for each read, as a schema or a registration reads it.

A field named like a seed of the call is no parameter: one of the dispatcher's own names (RESERVED_POOL_SEEDS, test_a_writable_field_named_user_validates_off_http) and the serializer this Validator binds (BOUND, test_an_input_field_named_serializer_validates_into_data) always, and each of excluded, which the DRF edge compiles with the names the call's registered seeds take (test_a_writable_field_named_like_a_registered_seed_validates). The kernel refuses a parameter a seed occupies, where a serializer was always free to declare such a field: the client sent its value, the serializer validated it into data, and the seed kept its own. So the field is the serializer's alone, read from the body the edge hands it (validator_input), and never meets the seed in the call's pool.

lenient declares each field by name alone - any value, optional, nullable - so the kernel's shape check passes every value through and the serializer is the one to answer it, as a live DRF view's request always was answered (kernel_spec_variant(lenient=True)).

validate

validate(arguments: Mapping[str, Any], context: ValidationContext) -> dict[str, Any]

The validated values, keyed by source, or InvalidArguments carrying DRF's detail.

validated

validated(arguments: Mapping[str, Any], context: ValidationContext) -> Validated

The values, validated_data as the call's data, and the bound serializer.

validated_many

validated_many(
    rows: Mapping[int, Mapping[str, Any]],
    context: ValidationContext,
    *,
    many_argument: str,
) -> Validated

The rows validated once as DRF's list serializer, the one serializer bound for the call.

SerializerPresenter

Bases: Presenter

An operation's output declared as a DRF serializer, rendered through it.

``output()`` reads the serializer's fields into the kernel's
[`Output`][django_service_specs.output.output.Output], in declaration order,
the way drfs' own output schema and audience walk read them:

- A ``write_only`` field is never rendered, so it is not declared.
- **Type**: JSON's own, with ``decimal``, ``date-time`` and ``date`` as
  formats. A nested serializer is an object carrying its own ``Output``; a
  ``many=True`` serializer, and a ``ListField`` whose child is one, is an
  array of that ``Output``; a ``MultipleChoiceField`` is an array of its
  choices. A field DRF gives no type, a
  ``SerializerMethodField`` or a ``JSONField`` say, is declared ``any``,
  so a schema describes it with no ``type`` rather than as a string it may
  not be.
  • Label: the author's, when one was written. DRF derives Vat id from vat_id when none was, and that is None here, as it is no title in drfs' schema.

    • Choices: every (value, display) pair a ChoiceField holds, typed by the values when they share a type.
    • Presence: always_present is true where DRF is bound to emit the key: a required field, one with a default, and one allowing null. A read-only field with neither is dropped when DRF cannot read it. Under shaped=True, no field at any depth is: the spec compiles a shape, an argument such as a fields selector that the serializer reads off the request, and a key is always present only if it is present under every value the caller may send. A drfs QueryParam declares no choices, so the values are any string, and which keys each one drops is the serializer's to decide when it renders, so the keys present under all of them are none that can be named. The default, False, is the presence above.
    • Marking: the FieldMarking found in the field's style, under MARKING or any other key, and a value under MARKING that is not one refused, as drfs' walk reads it. Nested outputs carry their own.

    field_audiences is the spec's own output declaration, a field name to a FieldMarking, layered over the serializer's markings at the top level only, as drfs' overrides= always has been. The layered marking is the compiled OutputField.marking, which is the seat audience_projection_for_spec reads, so every engine projects the one declaration with no overrides=, which stays a mount's. A name that is no output field is refused when the output is read: a hidden marking that silently applies to nothing is a field shown to an agent.

    presented(value, context) is what the kernel's present asks for: the serializer's .data for one value, under the serializer context derived from the call's PresentationContext. The request and view the call registered as pool seeds, read from ambient, are the serializer's own, and where a seed is absent or None, the entry of that name in call_context; a call that carries neither gets an offline request whose user is the principal, so a SerializerMethodField reading self.context["request"].user renders for the principal the call ran for, and whose query string is the call's shapes, so a serializer reading a fields selector off request.query_params finds the one the caller sent. A request the call carries is left as it is. A list result is presented row by row by the kernel's present, each row with the same context, which is what a many=True render is. Over that, the output half of the view hooks a DRF edge adds to the result's context (output_view_layers), resolved with the value, then serializer_context, the spec's own output_serializer_context, each winning on a key it shares with what it layers over (see serializer_context in service_specs/utils.py). The callable declares what it reads from the call's pool, request, view, user or a registered seed, and the value being presented under value_keyword: result for a service's output, instance for a retrieve. It is asked once per value presented, so for a list or a many service's output once per row, and no keyword names the row then (value_keyword is None), nor is the view's hook handed it: the kernel presents a set row by row, and the whole set a callable may declare, a list's page or a many output's result, is never in hand here.

    present(value) is the context-free form, and delegates to presented(value, None): a None context is the serializer context drfs describes a serializer with (request, view and format present, each None), so a get_fields reading self.context["request"] neither raises there nor sees a principal. It is what a DispatchResult a transport built by hand is presented with, and with no call to resolve it against, serializer_context is not asked (test_present_with_no_call_runs_no_callable).

    A dataclass type is read and rendered through the DataclassSerializer drfs renders it with, and a BaseSerializer that is not a Serializer declares no fields, so its output is empty while it still presents.

output

output() -> Output

The serializer's fields as an Output, with field_audiences layered on. Never queries.

present

present(value: Any) -> Any

One value as the serializer's .data, with no call to render for.

presented

presented(value: Any, context: PresentationContext | None) -> Any

One value as the serializer's .data, under the context of the call it belongs to.

None is presented as None: it is JSON's null, and a serializer handed none renders a row of blank fields indistinguishable from a real one.

A serializer refusing to render, DRF's ValidationError, leaves as the dual-base InvalidArguments, raised from it, so a route over the kernel's present reads the kernel's refusal of the arguments, a read-shaping fields naming no field among them. A mapping detail is kept as it is, and a list, which is all DRF makes of a message, goes under the kernel's NON_FIELD_ERRORS; every message keeps its code. One that is already the kernel's InvalidArguments leaves as raised. No route of this package's own presents through here, so a live view and render_spec_output still answer DRF's class (tests/service_specs/test_a_presenter_s_refusal.py).

DRF permission classes off HTTP

DRFPermissionCheck asks each class in order against the offline request and view built for the principal. The first refusal raises NotPermitted with that class's message and code.

Before the class-level check, view.kwargs is filled from the call's arguments with what a route would have captured: every declared UrlKwarg, taking its default when the argument is absent or null, and the arguments the spec's row lookup reads. A declared QueryParam goes to request.query_params. The object check asks only about model instances. A create or a collection is never asked about an object, as on HTTP.

A class that raises its own refusal rather than answering False is asked inside translate_drf_refusals: its PermissionDenied leaves as NotPermitted with its message and code, and its NotAuthenticated as DRFNotAuthenticated, which DRF still answers 401.

DRFPermissionCheck

Bases: PermissionCheck

DRF permission classes asked the kernel's three questions, off HTTP.

The kernel asks a principal and a spec; a DRF permission class asks a request and a view. So each question asks every class in order, a fresh instance each time, as a DRF view's get_permissions does, against the request and view the call carries, else the offline pair build_offline_context builds for the principal. The first refusal raises the dual-base NotPermitted with that class's own message and code, so a client is told what a DRF view would have told it; returning False instead would leave the kernel to raise its own class with the generic wording. A class that raises its own refusal is asked inside translate_drf_refusals, so DRF's PermissionDenied leaves as that NotPermitted with the class's message and code, and NotAuthenticated as DRFNotAuthenticated, which DRF still answers 401 (tests/service_specs/test_drf_refusals_on_the_kernel_route.py).

The view's kwargs are filled from the call's arguments before the class-level check. A permission scoping by a route capture reads view.kwargs["project_pk"] in has_permission, and off HTTP there is no URLconf to put it there; asked against an empty mapping it would refuse every caller it means to admit. Each method opts in to the kernel's arguments channel by declaring the keyword, and the names that go onto view.kwargs are the ones a route would have captured: every declared UrlKwarg, and route_reads, the arguments the spec's row lookup reads (pk of an update's instance lookup, or of a retrieve), which as_kernel_spec passes in. Each declared QueryParam goes onto request.query_params instead, where a query string would have put it. A UrlKwarg or QueryParam the call left out, or sent as null, takes its declared default, as its docstring says a transport seeds it, and is otherwise absent. Where a transport asks with no arguments at all (None), nothing is filled, and the classes decide what an empty route means, as they would for a URL that captured nothing.

The request and view the call carries come first. The class-level and object-level methods declare request and view, so a call registering either as a seed hands it over, and call_context, so a call that puts either in its context does: a transport running the kernel's dispatch carries the request a run arrived on, or a role view built for the operation, that way. Each is the seed, else the context's entry, and None in either place is no value. A carried view, or DRF Request, is asked as it is, with nothing filled onto it. Where the call carries one and not the other, the missing half is the offline pair's, built as below, so no class is ever handed None for a request or a view; where it carries neither, the whole offline pair is asked. The offline view's action is the call's context's action, where that is a non-empty string, so a class branching on view.action reads the operation's name; a carried view keeps its own. A carried Django request that DRF has not wrapped, the request a run arrived on as the kernel's call context holds it, is asked as a DRF Request around a copy of it, built as the offline pair is: its user the principal and its query string the declared QueryParam values the call sent, replacing its own, which is the endpoint's the run arrived on. Asked with no arguments at all, nothing is routed and its own query string stays. On drfs' own edge the kernel asks no check, since dispatch_spec runs under the Grant its view's permission classes produced.

has_object_permission asks only about a model instance, as enforce_permissions always has off HTTP; any other target is allowed here, because a DRF class written for a row reads attributes off it.

has_scope_permission allows. The kernel asks it of a bulk call's set, a collection's rows and a create's None, and DRF has no such question: on HTTP, a create and a collection action run the class-level check and no object check, and a DRF has_object_permission handed a QuerySet or None would raise AttributeError or decide on the wrong object. The class-level check has already run, with the view's kwargs filled, by the time the kernel asks this one.

No state is kept between questions: the compiled spec holding this check is shared by every call and every thread, so each question builds its own request and view.

has_permission

has_permission(
    principal: Any,
    spec: Any,
    *,
    arguments: Mapping[str, Any] | None = None,
    call_context: dict[str, Any] | None = None,
    request: Any = None,
    view: Any = None,
) -> bool

Whether every class's has_permission admits principal, raising on the first that does not.

has_object_permission

has_object_permission(
    principal: Any,
    spec: Any,
    target: Any,
    *,
    arguments: Mapping[str, Any] | None = None,
    call_context: dict[str, Any] | None = None,
    request: Any = None,
    view: Any = None,
) -> bool

Whether every class's has_object_permission admits principal to a model target.

has_scope_permission

has_scope_permission(
    principal: Any, spec: Any, scope: Any, *, arguments: Mapping[str, Any] | None = None
) -> bool

Allows: DRF has no question for a set or a create, as the class docstring says.

NotPermitted

Bases: NotPermitted, PermissionDenied

A permission check refused the principal, as both the kernel's NotPermitted and DRF's PermissionDenied.

Every published consumer of this package catches DRF's PermissionDenied by name and ends the attempt on it, and DRF's exception handler answers it 403. None of them catches the kernel's class, so a denial dispatch makes leaves as this, and an except written against either side catches it.

Deliberately not a service error, which the kernel's class is not either: an agent reads a service refusal as something to route around and keeps trying, where a denial should end the attempt.

message is the kernel's, and detail is the same words as DRF's ErrorDetail, coded "permission_denied" unless code is given.

Both __init__s run by name, so neither depends on the order of the bases, which is a choice (the kernel's first, as the class is named) and no test can tell from the other. The kernel's sets message: test_the_default_is_the_kernels_message fails without it. DRF's sets detail with the code given, which the kernel's super() chain never passes: test_a_code_given_is_kept fails without it.

build_offline_context

build_offline_context(
    user: Any,
    params: Mapping[str, Any] | list[Any] | None = None,
    *,
    http_request: HttpRequest | None = None,
    action: str | None = None,
    kwargs: Mapping[str, Any] | None = None,
    query_params: Mapping[str, Any] | None = None,
    host: str | None = None,
) -> OfflineContext

Build the OfflineContext for dispatching a spec outside an HTTP request.

dispatch_spec forwards request / view to spec callables (kwargs providers, extend_queryset, context providers) that declare them, and enforce_permissions needs a request + view to evaluate permission_classes. This synthesizes both so a spec written for the HTTP transport keeps working when driven from a Pydantic-AI toolset, the MCP server, a management command, or a task runner.

Parameters:

Name Type Description Default
user Any

Set as request.user, so callables and permissions that read it behave as on HTTP.

required
params Mapping[str, Any] | list[Any] | None

Seeds request.data, straight into DRF's parsed-data cache (a synthetic request has no stream to parse). It is not validated here — that is dispatch_spec's job, which takes params directly and never touches request.data. Pass both the same value.

None
http_request HttpRequest | None

An ambient Django request to wrap, so its headers / META are available (the MCP server passes its real one); otherwise an OfflineHttpRequest is created. Either way the method is forced to POST, because mutation callables often branch on it. The caller keeps ownership: a request passed here is never written to. What gets wrapped is a shallow copy, so the method / GET / user this function sets land on the copy alone, while META, the session, the upload handlers and any body already read stay shared — headers and session writes behave as they do on HTTP, and the live request keeps its own method, query string and user for the rest of its cycle. Pass the request you are serving; dispatching several specs from one request is safe, and neither leaks into the next.

None
action str | None

The view action name, exposed on the OfflineServiceView.

None
kwargs Mapping[str, Any] | None

The channel for route-derived values — the off-HTTP counterpart of a view's URL captures. dispatch_spec spreads it into the selector / target pools exactly where the HTTP path spreads extra_url_kwargs=view.kwargs: authoritative over params on a key conflict, below a spec.kwargs provider, which also sees it as view.kwargs. Pass every route capture a spec depends on.

None
query_params Mapping[str, Any] | None

Seeds the request's GET QueryDict, the source request.query_params reads — how read-shaping params that are not spec inputs reach the serializer offline: a serializer branching on query_params, and a request-scoped FilterSet reading self.request.query_params itself. It is not what a filter_set is bound to: dispatch_spec binds a SelectorSpec's own, and a target lookup's, to filter_data, else to params, and a service's output_selector_spec.filter_set to filter_data, else to an empty mapping (always to an empty mapping under output_filter_from_call=False). To filter from this query string, pass filter_data=context.request.query_params. Values are stringified as on HTTP; a list / tuple becomes a multi-valued param. Replaces a wrapped http_request's GET — on the copy that is wrapped, so the caller's own GET still reads its real query string afterwards.

None
host str | None

The origin the synthesized request reports, so build_absolute_uri — which DRF's FileField / HyperlinkedIdentityField call whenever a request is in the serializer context — returns real absolute URLs off HTTP. Accepts "example.com", "example.com:8000", or a full origin whose scheme decides whether links are https. There is no default: unset, absolute-URI building degrades to the relative URL rather than raising (see OfflineHttpRequest). Ignored when http_request is supplied, whose own headers are authoritative, so a caller can pass both unconditionally.

None

The

Type Description
OfflineContext
OfflineContext

to hand to dispatch_spec.

The kernel's refusals as DRF's

The kernel raises its own InvalidArguments, NotPermitted and PrincipalUnavailable, which neither DRF's exception handler nor a transport catching DRF's ValidationError or PermissionDenied knows. The edge runs the kernel inside translate_dispatch_errors(), so what dispatch_spec and adispatch_spec raise for a kernel refusal is one of the three classes below, each both the kernel's refusal and DRF's: an except written against either package catches it, detail carries ErrorDetails with their codes, and DRF answers it 400 or 403. The one refusal that is DRF's alone is a many spec's rows sent as a bare list, answered as DRF's list serializer answers them, with a plain rest_framework.exceptions.ValidationError: a body that is no list, a refused row, a row's undeclared key, or more rows than the bound. So catch DRF's ValidationError for every refused argument.

A caller running the kernel's dispatch, resolve_target or shape_queryset itself, or calling apply_queryset_shaping, gets the kernel's classes and wraps the call in the same block where it wants DRF's.

A deactivated principal is PrincipalUnavailable, a 403.

translate_dispatch_errors

translate_dispatch_errors

translate_dispatch_errors() -> Generator[None]

Re-raise the kernel's DispatchErrors inside the block as this package's dual-base classes.

Every published consumer of this package catches DRF's ValidationError and PermissionDenied by name, and DRF's exception handler answers only those; the kernel's own refusals reach none of them, and a view serves one as a 500. So the DRF edge runs the kernel's dispatch inside this block:

  • the kernel's InvalidArguments leaves as InvalidArguments, with the same tree and every message's code;
  • its NotPermitted as NotPermitted, and its PrincipalUnavailable as PrincipalUnavailable, each with the same message;
  • a subclass of any of the three leaves as that class's twin, with the subclass's message.

The new exception is raised from the kernel's, so the traceback keeps where dispatch refused. Each dual-base class is still the kernel's, so an except DispatchError around the block catches what leaves it as well.

Left exactly as raised:

  • anything that is not a DispatchError, a service's own refusals among them;
  • a DispatchError that is already a DRF exception, such as a dual-base class a validator raised itself, which both ladders already catch. An author callable's DRF PermissionDenied and NotAuthenticated reach here as such a class, translated by translate_drf_refusals as the compile hands the kernel the callable, and the DRF edge restores the author's exception itself (its ValidationError reaches here as a service's refusal, not a DispatchError);
  • a DispatchError with no twin here, which keeps the kernel's class rather than be given a DRF answer nobody declared.

Use it as a with block, around await too. Not as a decorator on an async def: that would wrap the creation of the coroutine, not its run.

InvalidArguments

InvalidArguments

Bases: InvalidArguments, ValidationError

The arguments were refused, as both the kernel's InvalidArguments and DRF's ValidationError.

Every published consumer of this package catches DRF's ValidationError by name - an MCP server answering "invalid arguments", an agent tool asking the model to retry - and DRF's exception handler answers it 400. None of them catches the kernel's class, so what dispatch refuses leaves as this, and an except written against either side catches it.

Deliberately not a service validation error, which the kernel's class is not either: a business rule stated by a service about well-shaped input is a different refusal, and an MCP server answers the two differently.

detail is the kernel's tree - fields, nested objects, rows under their int index - with every message an ErrorDetail. A message that carries a code (the kernel's CodedMessage does) keeps it, and one that carries none takes code, "invalid" unless given.

DRF's __init__ runs on purpose. By the order of the bases the kernel's runs, and it leaves detail as plain strings: DRF's handler still answers 400 with a byte-identical body, but get_codes() raises, so a test asserting the body passes on the broken class. test_get_codes_keeps_the_kernels_code_and_defaults_the_rest and test_every_message_is_an_error_detail are what fail without it.

PrincipalUnavailable

PrincipalUnavailable

Bases: PrincipalUnavailable, PermissionDenied

The principal cannot act, as both the kernel's PrincipalUnavailable and DRF's PermissionDenied.

Raised for an identifier naming nobody or a deactivated account, before any permission check. It reaches every published consumer's ladder as the denial DRF names, answered 403 and ending the attempt, and an except written against the kernel's class catches it too.

Deliberately not a service error, which the kernel's class is not either: a refused principal is terminal, never something to route around.

message is the kernel's, and detail is the same words as DRF's ErrorDetail, coded "permission_denied" unless code is given. The default is the kernel's wording, not DRF's, since there is nobody to refuse a permission to.

Both __init__s run by name, as NotPermitted's do, so the order of the bases is a choice no test can tell from the other. The kernel's sets message: test_the_default_is_the_kernels_message_not_drfs fails without it. DRF's sets detail with the code given, which the kernel's super() chain never passes: test_a_code_given_is_kept fails without it.

register_drf_row_errors

A nested write's row service written for DRF refuses its row with DRF's ValidationError. Registered, which dispatch_spec's module does when it is imported, that refusal lands at its row, {"books": {1: {...}}}, with its codes, and is still DRF's ValidationError.

register_drf_row_errors

register_drf_row_errors() -> None

Register DRF's ValidationError as a refusal the kernel re-roots under the row that raised it.

A nested write runs a row service per related row, and one written for DRF refuses its row with DRF's ValidationError, which the kernel cannot name, so it would otherwise escape the write naming no row. Registered, it is re-addressed as the kernel's own refusals are - a collection's row under its int index, a message about the row itself under non_field_errors - and rebuilt as DRF's ValidationError, with every message's code kept, so DRF still answers it 400.

Process-wide and for the life of the process, as the kernel's registry is. Running it again does nothing, because the reader is one module-level function; a second registration of the class with any other reader is refused by the kernel.

Not a home for this package's dual-base InvalidArguments: the kernel reads its own InvalidArguments before any registered class, so that one leaves a nested row as the kernel's class, and reaches DRF's ladders through translate_dispatch_errors.

An author's DRF refusals as the kernel's

The reverse direction. An author writes a service, a selector, a precondition, a kwargs or input_data provider, an extend_queryset or a permission class against DRF, and refuses with DRF's exceptions, which a transport over the kernel's dispatch reads as nothing it knows. So each of those runs inside translate_drf_refusals(), and on the kernel route (dispatch(kernel_spec(spec))) a refusal leaves as the kernel's class, still DRF's:

Raised by the author Leaves the kernel route as DRF answers it
ValidationError DRFServiceValidationError, a kernel ServiceValidationError, same detail and codes 400
PermissionDenied NotPermitted, the kernel's denial, with its message and code 403
NotAuthenticated DRFNotAuthenticated, the kernel's denial 401

A project's subclass of any of them keeps its detail and status_code. A refusal that is already the kernel's leaves as raised, and so does any other exception, DRF's NotFound and Throttled among them. A progress_reporter is not run inside it, since it reports and refuses nothing. A DRF refusal raised from any of these still reaches the kernel route as DRF's class, because the kernel or a Presenter runs them itself:

  • an input_serializer_context provider, which runs inside the Validator, and an output_serializer_context provider, which runs inside the Presenter;
  • an affordance's when callable;
  • the methods of a selector spec's filter_set, which the kernel binds. A service's output selector is the exception while the call narrows its re-read (output_filter_from_call, the default): that filter runs inside the compiled extend_queryset, so its refusals are translated;
  • a refusal a generator service raises while it is iterated, since the service returned the generator and the iteration is the caller's.

The compiled callables are wrappers around the author's. Identity is not kept (inspect.unwrap reaches the author's own), and a function's or method's name, qualname, module, docstring, attributes, annotations and signature are, with whether it is async. A functools.partial or a callable object gets neither its docstring nor its attributes copied, so a reader that wants the author's own docstring for one of those reads it off inspect.unwrap(...).

Over HTTP and through dispatch_spec, nothing changes. The edge re-raises the exception the author raised before it reads the refusal, with the context it was raised in: a view answers the status and body it answered before, and a dispatch_spec caller catches DRF's own class, keyed by the names the request used. A refusal drfs raised itself, such as a permission class answering False, is not one the edge restores.

The Presenter answers a serializer refusing to render the same way, as the dual InvalidArguments: a mapping detail as it is, a list under non_field_errors. No route of this package presents through it, so only the kernel's present sees that.

translate_drf_refusals

translate_drf_refusals

translate_drf_refusals() -> Generator[None]

Re-raise the DRF refusals raised inside the block as the kernel's classes, still DRF's.

The reverse of translate_dispatch_errors: an author writes a service, a selector, a precondition or a permission class against DRF and refuses with DRF's exceptions, which a transport over the kernel's dispatch reads as nothing it knows. So the compile runs each author callable it hands the kernel inside this block, and DRFPermissionCheck asks each permission class inside it:

  • ValidationError leaves as DRFServiceValidationError, with the same detail and codes;
  • PermissionDenied as NotPermitted, with its message and code;
  • NotAuthenticated as DRFNotAuthenticated, which DRF still answers 401;
  • a subclass of any of the three as that class's twin, with the subclass's detail and status_code.

Each is still DRF's class, so an except written against DRF's still catches it, and DRF's handler answers it with the status the author's exception had. It is raised from the author's, which it carries under TRANSLATED_FROM too, so the DRF edge re-raises the exception itself and every route that answered DRF's exception answers it as before.

Left exactly as raised:

  • a refusal that is already the kernel's, drfs' dual-base classes and a twin translated by an inner block among them, which a kernel route already reads;
  • any other exception, DRF's NotFound, AuthenticationFailed and Throttled included, which have no kernel class to leave as.

Use it as a with block, around await too, for the reason translate_dispatch_errors gives.

translating_drf_refusals

translating_drf_refusals

translating_drf_refusals(fn: None) -> None
translating_drf_refusals(fn: Callable[..., Any]) -> Callable[..., Any]
translating_drf_refusals(fn: Callable[..., Any] | None) -> Callable[..., Any] | None

fn, its DRF refusals leaving as the kernel's classes; None for no callable.

What the compile hands the kernel in place of each author callable that runs within a call: the service or selector, the preconditions, the kwargs and input_data providers and extend_queryset. fn is called as it was, inside translate_drf_refusals.

It keeps what a reader of the compiled spec takes from the callable: the name, qualname, module, annotations and signature, and, of a function or a method, the docstring and the attributes (a tool description is read off the docstring). An async def stays one, since the kernel awaits it by its kind (test_an_async_service_s_refusal_is_translated_too); the signature is fn's, which the kernel binds the call by; the annotations are fn's and __wrapped__ is fn, so a provider's return TypedDict is resolved in its own module and still names the keys it fills (tests/service_specs/test_translating_drf_refusals.py).

Identity is not kept: the compiled callable is a wrapper, so inspect.unwrap is what reaches the author's own. A functools.partial or a callable object gets neither docstring nor attributes, as its __doc__ is its class's and a reader that tells a function from an object by inspect.isfunction would otherwise be handed that class's words. fn itself is never stamped, as it is the author's own function.

DRFServiceValidationError

DRFServiceValidationError

Bases: ServiceValidationError, ValidationError

A service refused its input, as both the kernel's ServiceValidationError and DRF's ValidationError.

What an author callable's DRF ValidationError leaves the kernel route as, through translate_drf_refusals: a service, a selector, a precondition, a provider or an extend_queryset refusing well-shaped input by a rule of its own. A transport reading the kernel's classes answers it as the service's refusal, and an except written against DRF's still catches it.

Named apart from the package root's ServiceValidationError, which is the kernel's own class re-exported, so neither is mistaken for the other. Deliberately not an InvalidArguments: the kernel's refusal of the arguments' shape is a different refusal, which an MCP server answers differently.

detail is DRF's, any shape, with every message an ErrorDetail keeping its code; message is the kernel's, the detail where that is a string and the kernel's default otherwise. DRF makes a list of a single message, so a refusal translated from DRF's carries the default (test_a_translated_message_is_the_kernel_s_default).

Both __init__s run by name, as NotPermitted's do. The kernel's sets message: test_a_string_detail_is_the_message fails without it. DRF's makes the detail ErrorDetails with the code given, which the kernel's super() chain never passes: test_a_code_given_is_kept fails without it.

DRFNotAuthenticated

DRFNotAuthenticated

Bases: NotPermitted, NotAuthenticated

No principal was authenticated, as both the kernel's NotPermitted and DRF's NotAuthenticated.

What an author callable's or a permission class's DRF NotAuthenticated leaves the kernel route as, through translate_drf_refusals. A transport reading the kernel's classes ends the attempt on it as a denial, and DRF's exception handler still answers it 401, with the authenticator's challenge, rather than the 403 the dual NotPermitted would be.

message is the kernel's, and detail is the same words as DRF's ErrorDetail, coded "not_authenticated" unless code is given. The default is DRF's wording, not the kernel's: the refusal is that nobody signed in, which the kernel's generic denial does not say (test_the_default_is_drf_s_message).

Both __init__s run by name, as NotPermitted's do. The kernel's sets message: test_a_message_given_is_the_kernel_s fails without it. DRF's sets detail with the code given, which the kernel's super() chain never passes: test_a_code_given_is_kept fails without it.

Compiling

as_kernel_spec

as_kernel_spec(
    spec: Any,
    *,
    partial: bool | None = None,
    spread: bool = False,
    seeded: frozenset[str] = frozenset(),
) -> KernelServiceSpec | KernelSelectorSpec

Compile spec, a drfs ServiceSpec or SelectorSpec, to the kernel's.

What each drfs spec's own as_kernel_spec() answers, so the kernel's entry points - dispatch, authorize, the registry and the schemas - take a drfs spec as it is; they read it through kernel_spec, which compiles it once per object. This function compiles afresh on every call.

Each field lands on one seat:

  • input_serializer becomes the SerializerValidator, and the output serializer the SerializerPresenter, with the spec's field_audiences layered over the serializer's markings. A ServiceSpec's output serializer is its output_selector_spec's; one whose selector is None re-reads nothing, so it compiles to no output selector, and its serializer is the compiled spec's own presenter. Where that output is a LIST and the spec is not many, the service returns the rows, which compiles to returns_many=True: the kernel presents the return row by row and describes it as an array of the row.
  • permission_classes becomes one DRFPermissionCheck; an empty sequence, which drfs reads as "no permissions, explicitly", becomes [Unrestricted()], the kernel's spelling of the same; None compiles to no permissions, which the kernel's registry and dispatch refuse, so a drfs spec reaches a transport without a view only guarded.
  • reads is what the selector and the target lookup take, and under spread what a service with no input_serializer takes, read off their signatures as spec_to_json_schema reads them: named parameters and the keys of an Unpack-ed TypedDict, with InputRequired read as required and a NotClientInput key on any callable of the call left out. A named parameter with no default is required too, as the callable raises TypeError without it, unless a kwargs provider that says nothing of its keys may fill it or the callable is a wrapper that fills its own (_reflected). A row handed in as dispatch(target=...) does not excuse its lookup's key on this compile: the kernel checks the arguments before it reads the target, so a caller handing one still sends the key. The DRF edge's per-request variants (kernel_spec_variant) leave optional exactly the reads this rule requires (required_for_having_no_default), so a view handing the row it resolved, and each refusal stage the edge documents, answer as they always did. A misplaced marker is refused here, so at registration. Each declared UrlKwarg and QueryParam in the spec's own reads is a Parameter too, winning over a reflected one of its name, required where the read says so or the parameter it replaces is (a read declaring a default is not: _merge), and a read unless it is a shape (below); one the target lookup reads joins the lookup's reads. Every read taken off a signature is marked reflected and a declared one is not, so the kernel fills a registered pool seed's name in the former on every dispatch, kernel_spec(spec)'s included, and still refuses the latter as an argument meeting a seed (test_the_kernel_fills_a_selector_s_reflected_read_from_the_seed, test_a_declared_read_named_like_the_seed_is_still_refused_by_the_kernel).
  • shapes is each declared QueryParam of the top-level spec that no callable of the call takes by name: not the service or selector, a precondition, a target lookup, a kwargs or input_data provider, a bare **kwargs or a filter_set field. It shapes what is rendered, as a serializer reading ?fields= does, so the kernel hands it to the presenter alone, on PresentationContext.shapes, rather than to a callable. One a callable names stays in reads, and so does a nested spec's own. A name the filter_set reads is the filter's, as the filter field always won the schema. The pool's own names (user, request, view, serializer and the kernel's reserved seeds) are never arguments, and a declared one is refused. So is a declared read the service or selector, or one of its preconditions, marks NotClientInput: dispatch would hand that callable the client's value under the name it keeps the server's. A name only a target lookup hides is not refused; the lookup is never handed it, and the read is the service's argument. A route capture no signature names and no UrlKwarg declares is no argument at all, so it no longer reaches a spec.
  • The rest carries by name: atomic, idempotent, many, many_argument, allow_none, affordances, kwargs, input_data, preconditions, progress_reporter, metadata, select_related, prefetch_related, annotations, filter_set. many takes the bounds of the list serializer DRF builds for the input_serializer. extend_queryset, which drfs calls as (queryset, view, request), is wrapped so the kernel, which binds it by name, calls it the same way; view and request are then the pool's registered seeds. A SelectorSpec's affordances map to the compiled form of each ServiceSpec.

partial overrides the spec's own for the variant the DRF edge builds per request (PATCH); None reads the spec's, where only True is partial. A spec with no input_serializer has nothing to relax, so it compiles unpartial.

spread is ArgumentBinding.SPREAD_AUTHOR_WINS: a service with no input_serializer then takes the caller's values for its own parameters. Without it, as AUTO and BUNDLE dispatch a service, a caller fills only what the Validator, the target lookup and the declared reads read, so def destroy(*, instance, hard=False) is never handed a hard the request sent (see _takes_its_signature).

seeded names seeds a dispatch registers beyond the pool's own, and leaves a signature's parameter of such a name - the selector's, a target lookup's, a spread service's - out of the compiled reads altogether (test_seeded_compiles_a_name_out_of_a_selector_s_reads_a_marked_read_stays, test_seeded_compiles_a_name_out_of_a_target_lookup_s_reads, test_seeded_compiles_a_name_out_of_a_spread_service_s_reads). Without it the parameter is a reflected read, which the kernel fills from a registered seed of its name on every dispatch but which stays among the compiled reads. The DRF edge no longer passes seeded. A declared read of the name stays one, which the kernel refuses as an argument meeting a seed.

What the kernel refuses at construction is stripped or refused here, naming the drfs field. Stripped, because drfs never read them: an output selector's kwargs, an instance lookup's allow_none (the kernel would resolve a missing row to None, an upsert, where drfs answers 404), a nested spec's permission_classes and a target lookup's output_serializer. Refused, as drfs refuses them at as_view(): a nested selector's preconditions and progress_reporter, which would never run. Also refused: a selector of None anywhere but an output selector or an instance lookup, which compiles to no lookup, and field_audiences with no output serializer to layer them over.

success_status, document_service_error and response_finalizer are the HTTP views', and none of them is carried. input_serializer_context rides on the SerializerValidator and output_serializer_context on the SerializerPresenter, which hands it the value it presents as result for a service's output and as instance for a retrieve.

kernel_spec_variant

kernel_spec_variant(
    spec: Any,
    *,
    partial: bool | None = None,
    unguarded: bool = False,
    target: bool = False,
    server_input: bool = True,
    spread: bool = False,
    lenient: bool = False,
    excluded: frozenset[str] = frozenset(),
) -> KernelServiceSpec | KernelSelectorSpec

spec compiled as one request of the DRF edge dispatches it, once per variant.

With every keyword at its default this is kernel_spec(spec), the one compiled spec every kernel entry point reads. Each other combination is compiled once per source object and kept while it lives, so a request never recompiles and a Grant minted for a variant covers it on the next call too.

Each other combination is the edge's, and it leaves optional every read kernel_spec(spec) requires only for having no default: the edge fills such a parameter from a row a view hands in, and refuses a missing one at the stage it documents, the target lookup's or the service's own (test_the_edge_s_variant_relaxes_exactly_the_reads_the_rule_asks_for). InputRequired, a required TypedDict key and a declared read saying required=True stay required; a declared read that does not is relaxed as the parameter it names would be.

  • partial: the per-request flag a PATCH sets, compiled by as_kernel_spec; None reads the spec's own.
  • unguarded: no permissions. An HTTP view has already run DRF's check_permissions, so the edge hands dispatch a Grant and the compiled checks would only be asked again against a synthesized request.
  • target: a service spec that declares no target lookup gets a stand-in instance lookup, so the edge can hand dispatch(target=...) the row the view's get_object() resolved; the kernel refuses target= on a spec with no target selector. The stand-in reads no arguments and is never called: a dispatch reaching it raises ImproperlyConfigured. A spec with a target lookup keeps its own.
  • server_input: False compiles the spec with no kwargs provider of its own and, on a service spec, no input_data: the DRF edge evaluates both itself and merges what they answer into the arguments, and a service spec's kwargs seat hands back the author values the edge call carries (author_values) instead. Leaving either on would call the provider a second time (test_a_provider_runs_once_per_call, test_the_input_data_provider_runs_once_per_call).
  • spread: a service spec with no input_serializer takes the caller's values for its own parameters, as an explicit ArgumentBinding.SPREAD_AUTHOR_WINS dispatches it. Off by default, as AUTO and BUNDLE dispatch a service, so kernel_spec(spec) declares none of them.
  • lenient: a service spec's Validator declares its fields by name alone (SerializerValidator(lenient=True)), so the kernel's shape check passes each value through to the serializer. The edge compiles this for a request a live DRF view dispatches, which DRF's serializer has always answered alone: a form's "on" for a boolean, 123 for a string, every field's refusal at once, in DRF's words. A caller off HTTP keeps the kernel's typed check. A many spec's list is unbounded there too, so its ListSerializer alone bounds a live view's body, as DRF's view always did: a body past the kernel's SERVICE_SPECS_MAX_ARRAY_LENGTH is taken where the list serializer declares no max_length, and one past a declared bound is refused in DRF's words.
  • excluded: names a service spec's Validator declares no parameter for (SerializerValidator(excluded=...)): the edge compiles the names a seed of the call takes, so a serializer field of that name is the serializer's alone rather than a parameter the kernel refuses.