Skip to content

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

as_kernel_spec() -> NoReturn

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 DRF Request.
  • 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; None on 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 spec.kwargs.

input_data Mapping[str, Any] | None

Merges onto the client payload before validation, beneath spec.input_data; server-provided keys win over the client's.

input_serializer_context Mapping[str, Any] | None

Layers onto the baseline serializer context (base_serializer_context), beneath the Validator's own serializer_context.

output_serializer_context Callable[[Any], Mapping[str, Any]] | None

Lazy — a callable taking the final post-selector result and returning the context — because the output context provider is documented to see the exact instance being serialized, which does not exist until after the service and output selector have run.

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.user is set to the same value); Any because the user model is project-defined.
  • request — the synthetic DRF Request (.user set, .data carrying the params), forwarded to spec callables that declare request.
  • view — the OfflineServiceView forwarded to callables that declare view and used as the view argument 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_uri returns 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 first ALLOWED_HOSTS entry, 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

offline_host: str | None = None

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

build_absolute_uri(location: str | None = None) -> str

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

get_host() -> str

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 DRF Request built by build_offline_context.
  • action — an optional label identifying the operation; None when 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

REST_TRANSPORT: Final = 'rest'

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

MARKING: Final = 'drf_marking'

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 Field subclasses, matched by isinstance when a SerializerValidator or SerializerPresenter walks its serializer.

extend

extend(*, fields: Sequence[_Rule] = ()) -> JsonSchemaRegistry

Return a new registry with the given rules prepended (they win first).

DEFAULT_JSON_SCHEMA_REGISTRY module-attribute

DEFAULT_JSON_SCHEMA_REGISTRY: JsonSchemaRegistry = JsonSchemaRegistry()