Audience projection¶
One serializer, more than one kind of reader. These helpers let a field say who it is written for — see the recipe for the task-shaped version — and let an agent transport apply that declaration to payloads and schemas from the same source, so the two cannot disagree.
Nothing here is read by the DRF view path. A serializer marked up for an agent renders byte-identically behind a viewset.
Nor is there any wording for a model here. The markings say what a field is;
what a reader should do about it depends on the reader, and the transport is
what knows — so annotate_output_schema takes the sentence rather than
supplying one.
The value types (FieldAudience, FieldMarking, AudienceProjection, MARKING) are
documented under Types; the render entry points
(render_for_audience / arender_for_audience) under
Dispatch.
audience_projection_for_spec¶
audience_projection_for_spec ¶
audience_projection_for_spec(
spec: ServiceSpec[Any, Any, Any] | SelectorSpec[Any, Any],
*,
overrides: Mapping[str, FieldMarking] | None = None,
name: str | None = None,
) -> AudienceProjection
Resolve the field markings on whatever serializer spec renders through.
A selector keeps it on output_serializer; a service keeps it one level
down, on output_selector_spec. A transport that registers its tools up
front calls this once per spec and hands the result to
render_for_audience,
rather than paying a serializer instantiation on every call — and rather than
each transport re-deriving where a spec keeps its output serializer.
overrides and name are
build_audience_projection's,
forwarded: a mount holding an
OfflineContract
passes its field_audiences straight through, and every agent transport
layers that one declaration by the same rule.
build_audience_projection¶
build_audience_projection ¶
build_audience_projection(
serializer_cls: type | None,
*,
overrides: Mapping[str, FieldMarking] | None = None,
name: str | None = None,
) -> AudienceProjection
Resolve one serializer class's agent presentation, recursing into children.
Reads
FieldMarking markings
out of each field's style bag and collects the ChoiceField labels DRF
already holds. Pure in the serializer class, so the result is built once and
reused — see
AudienceProjection.
A class that is not a DRF serializer (a plain @dataclass output, or
None) yields an empty projection rather than an error: not every spec
renders through a serializer, and nothing is marked up in those that don't.
overrides layers a caller's markings on top, for the one case the
serializer cannot express: a mount that needs what its sibling hides. They
reach a transport as
OfflineContract.field_audiences,
which is one declaration read by every agent transport — and the merge lives
here, next to the clash rule it extends, so that stays true. Two copies of it
is how one spec comes to project a different field set depending on which
transport served it. name identifies the mount in the error message and
defaults to the serializer's own class name.
Raises:
| Type | Description |
|---|---|
ImproperlyConfigured
|
If a field carries
something other than an |
project_payload¶
project_payload ¶
Drop plumbing and speak enum labels, at any depth.
Two changes, both driven by the same declaration that shapes the schema so the two cannot disagree:
- fields marked
HIDDENare removed. Removed rather than nested under some reserved subtree: a payload that is also emitted as text is read in full either way, so relocating a field costs its keys again and hides nothing. What an agent must never use should not be there. - a
ChoiceField's constant is replaced by its display value, so the enum does not have to be spelled out to a person asPENDING_REVIEW. Not on aHANDLE, which some other tool takes as input. AMultipleChoiceFieldrenders a collection of constants, so each member is substituted rather than the collection looked up whole. - a field carrying a
ValueFormatteris rendered through it — a date-time read as a local date-time rather than raw ISO-8601, an amount with its currency. Also not on aHANDLE, and for the same reason.
Apply this where a payload becomes the agent's answer. Not where it feeds the next step of a chain, which still needs the handles.
annotate_output_schema¶
annotate_output_schema ¶
annotate_output_schema(
schema: dict[str, Any] | None,
projection: AudienceProjection,
*,
handle_description: str | None = None,
) -> dict[str, Any] | None
Apply the same projection to a schema that
project_payload
applies to the payload.
Three changes, each the mirror of one the payload undergoes:
- hidden properties are removed, and dropped from
required; - a marked field's
descriptionreplaces thehelp_textone, so a handle says what it is for in the schema a model reads without that wording leaking into the browsable API; - a substituted choice field is re-declared in terms of its display
values, because that is what the projected payload now carries. The
constant is gone from the response by design — a field another tool takes
as input should be marked
HANDLE, which suppresses the substitution on both sides. - a formatted field is re-declared as the type its
ValueFormattersays it produces, plus whatever that declaration adds about the shape of the produced value. The framework writes thetypefromproducesrather than taking one from the fragment, so a renderer cannot contradict its own advertisement.
Generating both sides from one declaration is the point: a schema that advertises a field the payload no longer carries is worse than either behaviour on its own.
handle_description is the fallback wording for a HANDLE that
declares none of its own, and defaults to nothing. Telling a reader what
to do with an identifier is advice for one kind of reader, and this package
does not know which kind is reading — a CSV export has no use for it, and
"do not read this out" only means something to a consumer that reads things
out. The transport that knows its audience supplies the sentence.
Takes the item schema. Callers that wrap items in an envelope of their
own — an array, or a pagination object — annotate the item and wrap
afterwards; output_to_json_schema(projection=...) does exactly that.