Types¶
The shapes this package owns: the view protocol and its hook chain, the DRF-typed authoring helpers, the offline request and view a caller off HTTP hands a serializer, and the serializer-field marking and schema registry the seats read.
Discriminated actions¶
DiscriminatedAction serves one write action at one URL through one of several
kernel specs, chosen by the request. One spec per route stays the default; this is
the opt-in for an endpoint that must keep one URL. It is this package's alone, never a
kernel spec: agents and MCP register each variant on its own.
DiscriminatedAction
dataclass
¶
One write action, one URL, and the request choosing which kernel spec serves it.
One spec per route stays the default; this is the opt-in for an endpoint
that must keep one URL. It attaches as an action_specs entry under
create, update, partial_update or destroy, or as
@service_action(action), and as_view() refuses it anywhere else.
It is this transport's alone and never a kernel spec: agents and MCP
register each variant on its own, so as_kernel_spec() raises.
discriminator picks the key: a sync callable declaring any of
request, data (the raw request.data), user, view and
call_context, or **kwargs, which is handed all but the last.
specs maps each key it may answer to the kernel ServiceSpec serving
it, and each is asked what a plain entry of that action is asked.
A caller every variant refuses is refused before the body is read and
before the discriminator runs. Otherwise the discriminator chooses and the
chosen variant runs as a plain entry would, its own checks deciding. A
callable success_status or response_finalizer on such an action may
declare variant, the chosen key.
These are checked by as_view() or the decorator, which know the action
and the view, rather than here.
as_kernel_spec ¶
Refuse to compile: the kernel asks this of anything handed to it as a spec.
Views and hooks¶
ServiceView ¶
Bases: Protocol
Minimal structural shape of a view as exposed to a kwargs provider.
Per-spec kwargs providers (ServiceSpec.kwargs / SelectorSpec.kwargs) receive
the calling view typed as
ServiceView. The
Protocol pins only the attributes a provider can rely on across both the standalone
Service*View classes and the viewset mixins:
request— the current DRFRequest.kwargs— the URL kwargs dict resolved by the URLconf (e.g.{"pk": 7}). Empty dict on routes without captured groups.action— the viewset action name ("create","list", etc.) on viewsets;Noneon standalone single-purpose views.
Keep the surface narrow on purpose: providers should translate view state into typed kwargs for the service, not reach for view internals.
ViewHooks
dataclass
¶
The HTTP view's hook-chain contributions, resolved and passed down.
The kernel's dispatch is the single execution core, but the view layer owns a
configuration surface the core knows nothing about: the get_service_kwargs /
get_<action>_service_kwargs / get_input_data / get_<action>_input_data /
get_*_serializer_context chains declared on
MutationFlowMixin
and its viewset mixins. Those are methods on a DRF view, so the core cannot resolve
them; this carrier is how a caller that has resolved them hands them over.
These are the view layers only — never the spec's own providers.
Each chain resolves view.get_<x> → view.get_<action>_<x> →
spec.<x>, spec winning on overlap. The kernel owns that last layer, so
a caller must leave the spec's provider out when resolving these; hand over
the fully resolved chain instead and the core runs the spec provider
twice, which a spec.kwargs doing a
tenant lookup will not survive.
Every field defaults to None (contributes nothing), so a transport with
no view — MCP, an agent toolset, a management command — simply omits the
argument and the core behaves exactly as it did before this existed.
Attributes:
| Name | Type | Description |
|---|---|---|
extra_kwargs |
Mapping[str, Any] | None
|
Merges into the dispatched callable's pool, beneath
|
input_data |
Mapping[str, Any] | None
|
Merges onto the client payload before validation, beneath
|
input_serializer_context |
Mapping[str, Any] | None
|
Layers onto the baseline serializer context
( |
output_serializer_context |
Callable[[Any], Mapping[str, Any]] | None
|
Lazy — a callable taking the final
post-selector |
progress |
Any | None
|
The view's own progress sink, resolved for both chains — a selector can be long too (a large export is a selector). Reach for it only when a buffered request genuinely needs it: if a request runs long enough to want progress, a task plus polling is usually the right shape, and this seam is for the cases where that does not apply (a streaming response, or a websocket sidecar the host already runs). |
HttpExtras ¶
Bases: TypedDict, Generic[UserT]
Off HTTP¶
OfflineContext
dataclass
¶
The HTTP-surrogate a spec needs when dispatched off the HTTP path.
Produced by
build_offline_context.
Its request / view feed the optional request= / view= arguments of
dispatch, and
the whole value is consumed by
the kernel's permission check.
user— the acting principal (request.useris set to the same value);Anybecause the user model is project-defined.request— the synthetic DRF Request (.userset,.datacarrying the params), forwarded to spec callables that declarerequest.view— theOfflineServiceViewforwarded to callables that declareviewand used as theviewargument of permission checks.
OfflineHttpRequest ¶
Bases: HttpRequest
An HttpRequest with no ambient host, made safe to use.
Built by
build_offline_context
when the caller has no real request to wrap (a Pydantic-AI toolset, a management
command, a task runner). A bare HttpRequest has an empty META, and
Django resolves the host from HTTP_HOST / SERVER_NAME — so get_host()
raises KeyError: 'SERVER_NAME' and takes build_absolute_uri() down with it.
That reaches serializers through the most ordinary field there is: DRF's
FileField.to_representation calls request.build_absolute_uri(value.url)
whenever a request is in the context.
Two behaviours, decided by whether the caller configured a host
(build_offline_context(host=…), which seeds META — a configured
request is an ordinary one and none of this applies):
- Host configured — nothing here intervenes. Absolute URIs are built by Django exactly as on HTTP.
- No host —
build_absolute_urireturns the location unchanged, i.e. the relative URL. There is no honest absolute URL to return: the process has no idea what origin serves it, and a guess (the firstALLOWED_HOSTSentry, say) would emit confidently-wrong links that look valid. A relative URL is the same thing DRF's own file / hyperlinked fields fall back to when there is no request in the context at all, so this is the shape those serializers already handle.
get_host() still raises without a host — but with a message naming the
fix, rather than a bare KeyError from Django's internals.
offline_host
class-attribute
instance-attribute
¶
The configured host ("example.com" / "example.com:8000"), or
None when the caller didn't configure one. Set by
build_offline_context;
when it is set, META carries the matching keys and Django's own
machinery is in charge, so nothing on this class changes behaviour.
build_absolute_uri ¶
Absolute URI when a host is configured; the location itself when not.
Degrading beats raising: the caller is a serializer field rendering a link, and a relative URL is a usable answer that its consumer can resolve against whatever origin it reached the data through.
get_host ¶
The configured host verbatim; a pointed error when there is none.
Deliberately not validated against ALLOWED_HOSTS. That setting
rejects spoofed Host headers from untrusted clients; this value came
from the project's own code, and there is no client. Requiring it to
appear in ALLOWED_HOSTS would break the ordinary case of a worker or
management command that renders links for a site it does not itself
serve — and would add nothing, since a caller who can pass a host can
equally pass one that is on the list.
OfflineServiceView
dataclass
¶
A concrete
ServiceView for
dispatching a spec outside an HTTP request.
Spec kwargs / extend_queryset / context providers are typed against
ServiceView — the
structural request / kwargs / action surface. When a spec is
dispatched off the HTTP path (a Pydantic-AI toolset, the MCP server, a
management command) there is no DRF view, so this frozen value stands in:
request— the synthetic DRFRequestbuilt bybuild_offline_context.action— an optional label identifying the operation;Nonewhen the caller has no meaningful action name.kwargs— the URL-kwargs equivalent (e.g. parent ids), empty by default.
The transport¶
REST_TRANSPORT
module-attribute
¶
What call_context["transport"] reads on a call a view of this package serves.
The key is the kernel's (TRANSPORT_KEY) and the value this package's, a plain
string, so call_context["transport"] == "rest" holds with nothing imported. A view
stamps it on the request's one dict (view_call_context). A serializer of this
package running under another transport's dispatch leaves that transport's stamp
alone, and a context handed in by hand reads as whatever its caller wrote.
Serializer fields¶
MARKING
module-attribute
¶
Key under which an :class:FieldMarking is declared in a DRF field's style.
Namespaced rather than bare ("agent") because style is a shared bag any
library may write to. The namespacing is courtesy, not correctness: readers
match on the value being an FieldMarking, so a marking under another key
still takes effect and another library's data under MARKING is refused loudly
rather than silently misread.
JsonSchemaRegistry
dataclass
¶
Consumer-extensible type → JSON Schema fragment rules for the DRF seats.
Rules are tried in order and the first match wins, before the built-in mappings — so a rule both adds support for a custom type and can override a built-in. The matched fragment is copied per use, so callers may freely mutate the returned schema.
Immutable: extend returns a new registry rather than mutating, so
there is no shared global state to leak across callers or tests. Start from
DEFAULT_JSON_SCHEMA_REGISTRY,
the empty base, layer rules on, and hand the result to a seat as its
json_schema_registry=:
registry = DEFAULT_JSON_SCHEMA_REGISTRY.extend(
fields=[(MoneyField, {"type": "string", "format": "money"})],
)
validator = SerializerValidator(MySerializer, json_schema_registry=registry)
That rule describes every MoneyField of the serializer as the kernel
spec it serves compiles, where only what the kernel can state is kept: the
field is a string there and format: "money", which is none of the
kernel's four formats, is dropped
(test_registry_through_the_compiled_spec's
test_the_kernel_s_schema_states_the_rule_but_for_the_dropped_format). A rule
extend prepends wins over the registry's own
(test_a_rule_an_extension_prepends_wins_over_the_registrys_own).
Attributes:
| Name | Type | Description |
|---|---|---|
fields |
tuple[_Rule, ...]
|
DRF |
extend ¶
Return a new registry with the given rules prepended (they win first).