DRF seats¶
A kernel spec has three seats DRF can fill: its validator, its presenter
and its permissions. This page is the adapters that fill them, the DRF
refusal classes the kernel's refusals become, and the helpers a caller off
HTTP uses to give a serializer the context it reads.
None of these is exported from the package root; import each from its leaf module:
from django_spec_rest.service_specs.drf_permission_check import DRFPermissionCheck
from django_spec_rest.service_specs.serializer_presenter import SerializerPresenter
from django_spec_rest.service_specs.serializer_validator import SerializerValidator
The seats¶
SerializerValidator ¶
Bases: Validator
A Validator whose declaration is a DRF serializer, or a dataclass read through one.
It takes a Serializer subclass, ModelSerializer and
DataclassSerializer included, or a dataclass, which is validated
through a DataclassSerializer built for 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, aQueryDictkept one, with what the edge put over it, as a DRF view's serializer was always handedrequest.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; the target is handed over as it is, since a serializer, unlike aModelForm, 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,formatandview. Therequestandviewthe call registered as pool seeds are the serializer's own, read fromValidationContext.ambientas the kernel resolved them, and where a seed is absent orNone, the entry of that name inValidationContext.call_context. A call that carries neither gets an offline request whoseuseris the principal and whosedatais the arguments, built bybuild_offline_context, so aCurrentUserDefaultreads the principal and a field or avalidate()readingself.context["request"]finds a request rather than aKeyError. What a registered request'suseris, 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 this Validator's ownserializer_context, each winning on a key it shares with what it layers over (seeserializer_contextinservice_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, amanycall'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
DRFInvalidArguments,
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 data, 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, its save() the DRF one. values follow what validated_data is: 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 (see _unseeded).
So is a key named in server_owned, the names the caller declares
the server's: 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 validated None for a file field is "", what the model field
stores for no file, in data and the values, where the input is no form
(see _cleared). save(), create and update take it as it is and
the kernel's update_from_input reads it as a clear, so a null a
field's allow_null lets through clears the file however a service
writes it. The same holds for a field DRF fills with its None default on
a whole call (test_a_put_leaving_out_a_file_defaulting_to_none_clears_it).
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 DRFInvalidArguments 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. Over HTTP as off it,
the list bound is the rows the kernel passed: where it refused a row, the
list's own checks judge the rows left, and their refusal sits beside the
refused row's (test_bulk_rows_over_http's
test_a_lists_own_checks_judge_the_rows_the_kernel_passed).
unsendable
property
¶
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 an HTTP body carries it
by name. 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::
compiled.validator.unsendable
- 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 unless the caller names it inexcluded. A required field calledserializeroruseris named. - Required only. An optional field, one with a default, a
read_onlyfield and aHiddenFieldare 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 amanyspec's row fields are read through the row serializer this Validator holds, so a row's requireduseris named and a row'stenantis 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 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.
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); outside one, a serializer built
afresh for each read, as a schema or a registration reads it.
call_context is the dict of the call asking, which the kernel hands
a Validator declaring the keyword on every ask inside a call and none
outside one; only the edge's own call is described as the edge's
(describe_serializer), so the same declaration dispatched by a
service for another principal is described unbound
(test_a_kernel_call_of_the_view_s_own_spec_for_another_principal_is_never_described_under_the_view_s_request).
A field named like a seed of the call is no parameter: one of the
dispatcher's own names (RESERVED_POOL_SEEDS, the user case of
test_a_writable_field_named_like_a_seed_validates_into_data) and the
serializer this Validator binds (BOUND,
test_an_input_field_named_serializer_validates_into_data) always, and each of
excluded, the names a caller's registered seeds take
(test_a_field_the_validator_excludes_is_unsendable_when_required). 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. The edge dispatches a lenient copy of a spec's Validator
(recording_spec) for a form body, and for a JSON body where the view's
lenient_json_body says so.
validate ¶
The validated values, keyed by source, or DRFInvalidArguments carrying DRF's detail.
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.
The rows are the ones the kernel's shape check passed, as it hands
them: the edge holds no body for a many call (validator_input),
so a row the kernel refused is not bound twice, and the list's own
checks judge the rows left.
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, in declaration order:
- A
write_onlyfield is never rendered, so it is not declared. - Type: JSON's own, with
decimal,date-timeanddateas formats. A nested serializer is an object carrying its ownOutput; amany=Trueserializer, and aListFieldwhose child is one, is an array of thatOutput; aMultipleChoiceFieldis an array of its choices. A field DRF gives no type, aSerializerMethodFieldor aJSONFieldsay, is declaredany, so a schema describes it with notyperather than as a string it may not be. - Label: the author's, when one was written. DRF derives
Vat idfromvat_idwhen none was, and that isNonehere: a derived label says nothing the name does not. - Choices: every
(value, display)pair aChoiceFieldholds, typed by the values when they share a type. - Presence:
always_presentis true where DRF is bound to emit the key: a required field, one with a default, and one allowingnull. A read-only field with neither is dropped when DRF cannot read it. Undershaped=True, no field at any depth is: the spec compiles a shape, an argument such as afieldsselector 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. Such an argument 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
FieldMarkingfound in the field'sstyle, underMARKINGor any other key, and a value underMARKINGthat is not one refused. 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. 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.
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
this Presenter's own 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, nothing where it is None.
presented_rows(values, context) is what the kernel's present asks
for a list: a page's rows over HTTP, a many service's return. The
callable is asked once for them all, offered the rows under page,
the name drfs gave a list's, and under value_keyword too where it
names one, so a many service's result is its whole return, as the
view's output hook is handed it. A lookup it batches over the rows runs
once a page, not once a row. The rows are a list, already evaluated:
not a queryset to filter further, nor DRF's Page. They render as one
many=True serializer, so a Meta.list_serializer_class and its
to_representation run as behind a DRF view, and its answer must keep
one row per value, in order, which the kernel checks: it adds affordances
to each row by its position. A None row stays None in its place,
as presented answers it, and the serializer renders the others.
present(value) is the context-free form, and delegates to
presented(value, None): a None context is the serializer context
a serializer is described with outside any call (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 a DataclassSerializer
built for it, and a BaseSerializer that is not a Serializer
declares no fields, so its output is empty while it still presents.
output ¶
The serializer's fields as an Output, with field_audiences layered on. Never queries.
present ¶
One value as the serializer's .data, with no call to render for.
presented ¶
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
DRFInvalidArguments,
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. A view
presents through the kernel's present, so the refusal reaches HTTP
as a 400 whose body is the mapping
(tests/service_specs/test_a_presenter_s_refusal.py).
presented_rows ¶
A list's rows as one many=True render, its context asked for once.
A None row is presented as None in its place, and the
serializer renders the rest. A list_serializer_class answering
another count for them is refused here when a None is to be
placed among its answer; otherwise the kernel refuses it, comparing
the answer with the rows it handed over
(test_a_list_serializer_answering_another_count_beside_a_none_is_refused).
A serializer's refusal leaves as presented's does
(test_a_refusal_on_a_page_leaves_as_the_kernel_s).
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 what DRF's view raises for it,
permission_denied's
answer for the request asked: the dual-base
DRFNotAuthenticated
for a caller the request's authenticators did not recognise, else
DRFNotPermitted
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 DRFNotPermitted 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: route_reads,
the arguments the spec's row lookup reads (pk of an update's instance
lookup, or of a retrieve). 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 empty, 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.
has_object_permission asks only about a model instance; 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.
Refusals that are DRF's as well¶
Each class is the kernel's refusal and DRF's exception at once, so a caller of
the kernel's dispatch catches it by the kernel's class and DRF's exception
handler answers it with DRF's status.
DRFInvalidArguments ¶
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.
from_kernel
classmethod
¶
The kernel's exc as this class: the same tree, every message's code and message.
The kernel's class takes no message, so one a subclass declares is
carried across by hand
(test_a_kernel_refusal_keeps_a_subclass_s_message). Shared by the
kernel route's refusal map and translate_dispatch_errors, so the
two cannot build the twin differently.
DRFNotPermitted ¶
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.
from_kernel
classmethod
¶
The kernel's denial exc as this class, with its message.
Shared by the kernel route's refusal map and translate_dispatch_errors.
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,
and what a refusal of a caller nobody authenticated is, by
permission_denied.
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
DRFNotPermitted
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.
DRFPrincipalUnavailable ¶
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_a_permission_denial 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.
from_kernel
classmethod
¶
The kernel's refusal exc as this class, with its message.
Shared by the kernel route's refusal map and translate_dispatch_errors.
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.
Translating refusals¶
A permission check the spec declares denying a caller nobody authenticated,
an adapted DRF class or a kernel PermissionCheck answering False, is
answered as DRF's view answers it, permission_denied below: 401 with the
first authenticator's challenge, or 403 where it sends none. A service's own
NotPermitted, an inner dispatch its check refused, and a check that raises
NotPermitted itself, stay 403 with their message and no challenge.
permission_denied ¶
permission_denied(
request: Any, message: str | None = None, code: str | None = None
) -> DRFNotAuthenticated | DRFNotPermitted
The refusal of a caller request carries, decided as DRF's view decides it.
A request whose authenticators recognised nobody is
DRFNotAuthenticated,
with DRF's wording, which the view answers 401 with the first
authenticator's WWW-Authenticate, or 403 where that authenticator gives
no header. Any other, a request with no authenticator at all among them, is
DRFNotPermitted
with message and code, 403. So a permission's message reaches
only a caller who is signed in or could not have been, as in DRF.
The rule every permission check the spec declares is answered by:
DRFPermissionCheck
raises it for a class answering False, and the kernel route answers
with it a kernel PermissionCheck's denial, which the kernel marks as
PermissionCheckDenied, where the denied spec is the one the route
dispatched. A service's own NotPermitted, and a refused inner
dispatch's, are not decided by it: they stay 403 with their message
(tests/views/test_an_anonymous_caller_refused.py). An offline request
has no authenticator, so off HTTP a refusal is always the denial.
A request that is not DRF's has no authenticators and is read as having
none, so a carried stand-in is refused as an offline request is.
translate_dispatch_errors ¶
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
InvalidArgumentsleaves asDRFInvalidArguments, with the same tree and every message's code; - its
DRFNotPermittedasDRFNotPermitted, and itsDRFPrincipalUnavailableasDRFPrincipalUnavailable, 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
DispatchErrorthat is already a DRF exception, such as a dual-base class a validator raised itself, which both ladders already catch. An author callable's DRFPermissionDeniedandNotAuthenticatedreach here as such a class, translated bytranslate_drf_refusalsas the compile hands the kernel the callable, and the DRF edge restores the author's exception itself (itsValidationErrorreaches here as a service's refusal, not aDispatchError); - a
DispatchErrorwith 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.
translate_drf_refusals ¶
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:
ValidationErrorleaves asDRFServiceValidationError, with the same detail and codes;PermissionDeniedasDRFNotPermitted, with its message and code;NotAuthenticatedasDRFNotAuthenticated, 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, the dual-base
DRFclasses and a twin translated by an inner block among them, which a kernel route already reads; - any other exception, DRF's
NotFound,AuthenticationFailedandThrottledincluded, 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 ¶
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.
register_drf_row_errors ¶
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.
Serializer context¶
base_serializer_context ¶
Build the baseline serializer context a spec-driven render starts from.
Over HTTP every serializer DRF builds carries
get_serializer_context —
{"request", "format", "view"} — so serializers routinely read
self.context["request"] unguarded (build_absolute_uri,
request.user, a permission check in a SerializerMethodField). What a
call layers on - the serializer_context a SerializerValidator or
SerializerPresenter declares, and a view's
get_input_serializer_context / get_output_serializer_context - is
additive, not a replacement for that baseline, so off HTTP — where there is no DRF view to ask — the baseline has to be
synthesized rather than skipped. Without it the same serializer that renders
over HTTP raises KeyError: 'request' when the spec is dispatched from an
MCP tool call, a Pydantic-AI toolset, or a management command.
Two sources, in order:
view.get_serializer_context()when the view has it — a real DRF view (the HTTP bulk path renders through the same helper). It is the view's own documented extension point and may already be overridden, so it wins.- Otherwise DRF's shape, synthesized from the
view/requestthe caller passed: the synthetic pair frombuild_offline_contextoff HTTP, orNonewhen the caller supplied neither.formatis alwaysNone— content negotiation is an HTTP-only concern.
The keys are always present, mirroring HTTP: a serializer reading
self.context["request"] off HTTP sees the synthetic request (and
None only when the caller passed no request at all), never a
KeyError. Absolute-URI fields additionally need real headers — pass the
ambient http_request to build_offline_context when the transport has
one, as the MCP server does.
A seat's own serializer_context is merged over this by
serializer_context in service_specs/utils.py, so a provider keeps
the final say on every key — including these three.
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.
A spec written for the HTTP transport may read a DRF request or view: a
kwargs provider, a request-scoped FilterSet, a serializer reading its
context, and the DRF permission check evaluating permission_classes. Off HTTP
there is neither, so this synthesizes both; hand them to
dispatch as
call_context={"request": context.request, "view": context.view}, and the
spec keeps working when driven from an agent toolset, a management command or a
task runner.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user
|
Any
|
Set as |
required |
params
|
Mapping[str, Any] | list[Any] | None
|
Seeds |
None
|
http_request
|
HttpRequest | None
|
An ambient Django request to wrap, so its headers / |
None
|
action
|
str | None
|
The view action name, exposed on the
|
None
|
kwargs
|
Mapping[str, Any] | None
|
The view's |
None
|
query_params
|
Mapping[str, Any] | None
|
Seeds the request's |
None
|
host
|
str | None
|
The origin the synthesized request reports, so |
None
|
The
| Type | Description |
|---|---|
OfflineContext
|
|
OfflineContext
|
to hand to |
renderable_serializer_class ¶
The class a serializer or dataclass declaration renders through.
A bare dataclass type is wrapped in a DataclassSerializer — the payload
the kernel's schema already describes for it, field by field — and anything else comes back
exactly as declared, None included. Instantiating the declaration directly
works for a serializer and fails for a dataclass, whose own __init__ is
handed many= / context=; resolving it here first is what lets both
shapes render.
Every render site in this package resolves through it: a spec's
SerializerPresenter, which a view, viewset or action presents through and
whose output()
audience_projection_for_spec
reads, so an agent is shaped by the markings of the class it is actually
rendered with; the OpenAPI schema; and the browsable API's form serializer.
A transport that instantiates a declaration itself calls this and
instantiates what it returns:
serializer_class = renderable_serializer_class(declared)
payload = serializer_class(
value, many=many, context=base_serializer_context(view=view, request=request)
).data
A dataclass gets one class for the life of the process, so repeated calls return the same object.
The pass-through is deliberate rather than a check. A BaseSerializer
subclass is the ordinary case, and a declaration that is neither is refused
where its schema is derived; refusing it here as well would break a caller
that renders through a serializer-shaped factory without ever deriving one.