Spec registry¶
A named, taggable home for a project's spec set, so each transport reads one source instead of enumerating the same specs again. Both symbols are importable from the top-level package.
For the task-shaped walkthrough — where the instance lives, multiple registries, filtered views — see the recipe Declare specs once, project them to many transports.
SpecRegistry¶
SpecRegistry ¶
A name → spec map with tags, stable ordering, and filtered views.
A project that exposes the same operations over more than one transport otherwise writes the list once per transport, and the lists drift. A registry gives that set one declaration site: each transport reads the same source instead of enumerating specs again.
It holds only the invariant part of an operation — which spec, its canonical name, its tags. Per-transport configuration stays at the binding that already owns it, so this is a source for a transport's own registry, never a replacement for one. Nothing consults a registry per request: adapters read it at configuration time to build their binding tables.
registry = SpecRegistry()
registry.register("list_orders", orders_spec, tags=("read", "public"))
registry.register("refund_order", refund_spec, tags=("write", "admin"))
public = registry.by_tag("public") # a new registry — a snapshot
Adoption needs no adapter support: :meth:specs returns the
dict[str, spec] today's adapters already accept.
There is no global registry. A consumer holds its own instances — a
module attribute in its app, or built in a server's constructor — and may
hold as many as it likes, with no shared state between them. Two shapes
both work: independent registries (separate name namespaces, full
isolation, one per mount) and one registry with many projections
(:meth:by_tag / :meth:subset views feeding several toolsets). Both are
what a multi-instance deployment needs; a blessed global would nudge every
project toward a single surface instead.
Registries are mutable containers (:meth:register adds), but every
derivation — :meth:by_tag, :meth:subset, :meth:merge — returns a
new registry holding a snapshot of the selected entries. The spec
objects themselves are shared, not copied. So a derived view never mutates
its source, and a later register() on the source does not appear in
a view derived earlier.
register ¶
register(
name: str,
spec: ServiceSpec[Any, Any, Any] | SelectorSpec[Any, Any],
*,
tags: Iterable[str] = (),
) -> None
Add a spec under name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The canonical name. Must be unused in this registry — a duplicate raises rather than overwriting, so a copy-paste declaration fails at import time instead of silently shadowing an operation. |
required |
spec
|
ServiceSpec[Any, Any, Any] | SelectorSpec[Any, Any]
|
A |
required |
tags
|
Iterable[str]
|
Free-form labels, deduplicated into a frozen set. |
()
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
TypeError
|
|
all ¶
Every entry, in registration order.
Order is stable so a transport's tool listing is stable — a listing that reshuffles between processes is noise for clients that diff it.
mutations ¶
The ServiceSpec entries, in registration order.
by_tag ¶
A new registry holding the entries carrying any of tags.
Union rather than intersection, because intersection composes from
this (reg.by_tag("read").by_tag("public")) while union cannot be
composed from an intersection. Passing no tags matches nothing and
returns an empty registry.
subset ¶
A new registry holding the named entries, in the order given.
Raises:
| Type | Description |
|---|---|
KeyError
|
A name is not registered here. Naming entries explicitly means a typo is a configuration error, not a quietly smaller surface. |
merge ¶
A new registry combining this one with others, in order.
Names are unique per registry, so independent registries may reuse one; merging is the single place that reuse becomes a conflict.
Raises:
| Type | Description |
|---|---|
ValueError
|
The same name appears in more than one input. |
specs ¶
A fresh name → spec dict — the shape adapters already accept.
This is what makes a registry usable before any adapter grows
registry-aware sugar: pass registry.specs() wherever a
dict[str, spec] goes today. Mutating the returned dict does not
affect the registry.
RegisteredSpec¶
RegisteredSpec
dataclass
¶
A spec under its canonical name, with free-form tags.
The value type held by
:class:~rest_framework_services.registry.spec_registry.SpecRegistry. It
carries only the part of a "tool" that is invariant across transports —
which spec, what it is called, and how it is grouped. Per-transport knobs
(MCP annotations and output schemas, Pydantic-AI pagination or query-param
registrations, …) stay at the binding that configures them.
Fields:
name— the canonical name for this operation. Unique within the registry that holds it; two independent registries are separate namespaces and may reuse a name.spec— a :class:~rest_framework_services.types.service_spec.ServiceSpec(a mutation) or a :class:~rest_framework_services.types.selector_spec.SelectorSpec(a read). The kind is deliberately not stored: it is derived byisinstancewherever it is needed, so a stored discriminator can never drift from the object it describes.tags— a frozen set of free-form labels used to derive filtered views (registry.by_tag("public")). Tags carry boolean-ish facts —"read","admin","destructive"— that every transport can interpret in its own vocabulary. Structured, transport-specific payloads do not belong in a tag string; they belong at the binding.