Seams¶
Structure cache¶
structure_cache ¶
The structure-response cache seam.
The generic structure route can serve a previously-rendered response straight
from a consumer-owned cache, skipping materialise + serialise + splice. The
library does not know which responses are worth caching (that is a
catalog-shaped speedup decision — e.g. a consumer may cache its largest
codelists and hierarchies) nor where the bytes live; it
only knows to ask the injected StructureCache before doing work and to
offer the finished response back for storage.
A consumer implements StructureCache; the default
NullStructureCache caches nothing, so a bare catalog just always does
the work. The cache is carried on the CatalogRuntime.
StructureQuery ¶
The resolved structure request a cache keys on.
version is post-latest-resolution and token is the runtime's
effective_token: one edition, with the annotator's
fingerprint folded in, so a new edition or an annotation change invalidates
every slot.
StructureCache ¶
Bases: Protocol
Consumer-owned cache for rendered structure responses.
lookup returns a ready response to short-circuit with, or None to
proceed with the live render. store is offered the finished response
(only 2xx, body-bearing responses are passed) for the consumer to cache if
its own gating says the query is worth it.
Availability values¶
availability_values ¶
The precomputed-availability seam, which supplies value lists for the library to render.
/availability answers in two steps — compute the per-component distinct
value sets and the observation count, then render them. This seam sits between
the two. A consumer that already has the values, because its producer baked them
into the edition or because it computed them once and kept them, hands them back
and the scan never runs. The library renders the response either way, through
~sdmxlib_rest.responses.availability.render_availability.
The seam is consulted only for unfiltered requests, which keeps its key small.
Precompute can only ever cover the unfiltered answer: the filter space is
unbounded, so nobody can bake every combination of c[] values, partial keys
and period bounds. Restricting the seam to that case means its key is just the
build, the flow and the component — nothing to normalise, so no way for two
spellings of one filter to land in different slots, or two different filters to
share one. A filtered request always scans.
"Unfiltered" is judged after the ~sdmxlib_rest.filter_shaper.ComponentFilterShaper
runs. A shaper may add a constraint the request never named, and the answer is
then filtered whatever the URL said — so a precomputed unfiltered answer would be
wrong for it. A shaper may only narrow requests that were already filtered, but
the library cannot assume every shaper does.
The key omits mode, format, language and references. With no constraints,
mode=available relaxes nothing and returns exactly what mode=exact does.
Accept, Accept-Language and references= change only the
rendering. So one entry per (build, flow, component) serves every client
spelling — where the response-level cache this replaced needed a slot per
accept variant x query suffix x language, and still missed ?lang=fr.
Repeat-caching a filtered answer is memoisation and belongs above the route.
A consumer can install an HTTP response cache through
create_app(outer_middleware=...). It sits outside compression there, so it
must key on Accept-Encoding as well as Accept and
Accept-Language.
AvailabilityKey ¶
Identifies one precomputable availability answer.
Hashable and canonical — SdmxUrn compares on agency, id and version — so a
consumer can key a plain dict on it, or rebuild one from a baked
filename with Dataflow.urn_for(...) and get a matching key.
Attributes:
| Name | Type | Description |
|---|---|---|
token |
str
|
Pins the entry to one edition, so a new edition invalidates every slot without any bookkeeping. |
flow_urn |
SdmxUrn[Dataflow]
|
The resolved dataflow. Keying on the flow lets the 1.5 and 2.2.2 surfaces share an entry, and keeps a forwarder's path shape from colliding two flows. |
component_id |
str
|
|
ResolvedAvailability ¶
The two things a scan produces, and everything rendering needs from it.
Attributes:
| Name | Type | Description |
|---|---|---|
key_values |
tuple[KeyValue, ...]
|
One entry per enumerated component, in the order the route would scan them — the cube region preserves it. |
obs_count |
int
|
Rows in the flow. With no constraints this is the total, and it is the figure .Stat shows in its "(N observations)" badge. Zero renders as a 204. |
AvailabilityValues ¶
Bases: Protocol
Consumer-owned source of precomputed availability values.
lookup is asked before scanning and returns the values, or None to
fall through to the scan, so a partial bake is a normal state. store is
offered the scanned values afterwards, which lets a consumer with no
producer-side bake still pay the scan once per build instead of once per
request. Both are called only for unfiltered requests.
Metadata store¶
metadata_store ¶
The reference-metadata store seam.
The /metadata/* routes need to answer "which MetadataSets are attached to
this structural artefact / metadataflow?" — a lookup over the edition's
referential-metadata linkage. The consumer decides how that linkage is
stored (a consumer's edition keeps it in its own DuckDB tables); the library
needs only the set URNs back, which it then materialises through the catalog like any
other artefact.
A consumer implements MetadataStore; the default
NullMetadataStore returns nothing, so a catalogue without a metadata
linkage simply reports no attached sets. The store is carried on the
CatalogRuntime.
MetadataStore ¶
Bases: Protocol
Consumer-owned lookup from structural artefacts to attached MetadataSet URNs.
The runtime owns the DuckDB connection (the metadata linkage lives in the same meta store as everything else), so it is passed in; the store owns only the queries.
sets_for_structure ¶
sets_for_structure(
conn: DuckDBPyConnection,
*,
structure_urn: str,
provider_id: str | None,
limit: int,
) -> list[str]
MetadataSet URNs whose declared structure is structure_urn.
provider_id (or None for all providers) narrows to sets from one
data provider; limit caps the result. URNs come back sorted.
sets_for_target ¶
sets_for_target(
conn: DuckDBPyConnection,
*,
target_urn: str,
item_pattern: str | None,
limit: int,
) -> list[str]
MetadataSet URNs attached to target_urn (or item URNs matching item_pattern).
item_pattern is a URN LIKE-pattern for item-level targets under the
artefact (e.g. Concepts of a ConceptScheme); None matches the exact
target only. URNs come back distinct + sorted.
NullMetadataStore ¶
Default store — no metadata linkage, so every artefact reports no attached sets.
sets_for_structure ¶
sets_for_structure(
conn: DuckDBPyConnection,
*,
structure_urn: str,
provider_id: str | None,
limit: int,
) -> list[str]
Always empty.
sets_for_target ¶
sets_for_target(
conn: DuckDBPyConnection,
*,
target_urn: str,
item_pattern: str | None,
limit: int,
) -> list[str]
Always empty.
Component filter shaper¶
filter_shaper ¶
The component-filter shaper seam.
Between "parse the request into a ComponentFilter" and "turn
that filter into WHERE clauses" there is a gap where catalogs legitimately want
to intervene, for two different reasons:
Semantics. A vendor query parameter can widen the filter — a
within[GEO]=<parent> descends a geographic hierarchy and rewrites the
dimension's value list to every code beneath that parent. The client asked one
question; the catalog answers a broader one because it knows the hierarchy.
Physics. A catalog that knows its own partition layout can add a redundant
predicate that leaves the result unchanged and narrows the scan — e.g. a lake
partitioned by geographic level can inject a GEO_LEVEL equality when
REF_AREA is a short concrete list, which is what lets DuckDB prune
partitions.
Neither belongs in a generic library: the first is a vendor extension, the
second is knowledge of a specific lake layout. But both have to happen at the
same point in every route that builds a filter, so the library defines the hook
and the catalog fills it. The default NullComponentFilterShaper returns
the filter untouched, so a catalog that models neither pays nothing.
The shaper receives the raw query items because
the library does not know which parameters a given extension reads — parsing
within[...] is the shaper's own business.
FilterContext ¶
Everything a shaper needs to rewrite a filter, resolved once per request.
query_items is the full multi-valued query string as (name, value)
pairs — repeated parameters are preserved, which bracketed vendor params
like within[DIM] rely on.
ComponentFilterShaper ¶
Bases: Protocol
Consumer hook to rewrite a parsed component filter before SQL generation.
Implementations must be pure with respect to the request: return a new
filter and leave the input unchanged, and raise ValueError /
KeyError for a malformed vendor parameter so the route can turn it into
a 400 alongside the standard key-parse errors.
shape ¶
Return the filter to generate SQL from — possibly the input unchanged.
Message annotator¶
message_annotator ¶
The pre-serialise structure-message annotation seam.
Some consumers need to project extra annotations onto a StructureMessage
before it is written — e.g. the .Stat Suite Data Explorer discovers a
DSD's reference-metadata (MSD) by reading a type="METADATA" annotation on
the DataStructure (its getMSDIdentifiers selector). Because this seam runs
on the model, ahead of to_json / to_xml, the annotation reaches
every wire format (SDMX-JSON 2.0, SDMX-ML 2.1 / 3.0) — unlike a post-serialise
JSON splice, which only ever touches the JSON body.
The library does not know which annotations a given catalogue wants to emit, so
a consumer implements MessageAnnotator; the default
NullMessageAnnotator returns the message untouched. The annotator is
carried on the CatalogRuntime and applied by the
structure route right after it builds the message and before it serialises.
The runtime owns the DuckDB connection (structural + metadata linkage live in the same meta store), so it is passed in; the annotator owns only the projection.
An annotator whose output can change while the edition stays the same — one
applying an annotation overlay that is edited on a running deployment — also
implements SupportsFingerprint (GH-2). The runtime folds the fingerprint into
CatalogRuntime.effective_token, which the structure cache and the ETag key
on, so an annotation change is never served from a stale cache entry or
answered with a 304.
MessageAnnotator ¶
Bases: Protocol
Consumer-owned projection applied to a structure message before serialisation.
annotate ¶
annotate(
message: StructureMessage,
*,
registry: EditionRegistry,
conn: DuckDBPyConnection,
token: str,
) -> StructureMessage
Return message (or a copy) with the consumer's annotations applied.
registry + conn let the annotator derive whatever it needs from
the edition; token changes with every edition, so
the annotator can memoise derivations per edition.
SupportsFingerprint ¶
Bases: Protocol
An annotator whose output can change while the edition's token does not.
fingerprint ¶
A value that changes whenever annotate would produce different output.
Called on every request, by the ETag middleware and the structure
route, so it must be cheap: a version counter or a digest computed when
the annotator's inputs change, never I/O. "" means the output
depends on the edition alone, which is the same as not implementing this.
CompositeMessageAnnotator ¶
Several annotators applied in order, as one.
Its fingerprint joins its members': "" while none has one, so a
composite of fingerprint-free annotators keys exactly as they would alone.
NullMessageAnnotator ¶
Default annotator — leaves every message untouched.
annotate ¶
annotate(
message: StructureMessage,
*,
registry: EditionRegistry,
conn: DuckDBPyConnection,
token: str,
) -> StructureMessage
Return the message unchanged.
annotator_fingerprint ¶
annotator's fingerprint, or "" when it does not implement one.
Overlays¶
overlays ¶
A MessageAnnotator that applies sdmxlib annotation overlays at serve time (GH-10).
An overlay (sdmxlib.edition.Overlay) is a versioned document of entries that
set, add or remove annotations of one type on the artefacts and items a
selector picks: annotations only, never ids, codes, structure or data. It lets
a deployment change display hints without rebuilding its edition.
OverlayAnnotator applies its overlays to every structure message before it
is serialised, so the change reaches every wire format. Its fingerprint is the
hooks line and the overlays' digests as applied to its audience, so
CatalogRuntime.effective_token comes out equal to sdmxlib's
effective_token(manifest, overlays, merge=..., audience=...): the
structure cache, the splices and the ETag move whenever an overlay is added,
removed, reordered or edited, or a hook or the audience changes, and
AvailabilityKey does not, since annotations cannot change availability.
replace swaps the overlays on a running deployment, for example from an
admin endpoint or a file watcher the consumer owns. A request in flight when
they are swapped may pair the new body with the old ETag; the next request
carries the new ETag, so a client never holds a stale body for longer than one
revalidation.
Applying overlays copies every artefact an entry selects, on every request the
structure cache does not answer. A * or class-name selector that reaches a
very large codelist's codes pays that on each such request; keep selectors
specific, or settle a change into the edition with sdmxlib.edition.fold_overlays.
With an audience, an entry applies when its scope is that audience or
null; without one, every entry applies. An overlay that changes a name or
description (texts) is refused when it is set, since sdmxlib applies those
only in fold_overlays, into a new edition.
OverlayAnnotator ¶
Applies annotation overlays, in order, to every structure message.
Attributes:
| Name | Type | Description |
|---|---|---|
merge |
Mapping[str, MergeHook] | None
|
Per annotation type, the |
audience |
str | None
|
The audience whose scoped entries apply, with the unscoped
ones; |
An overlay that changes a name or description raises OverlayError at
construction.
from_files
classmethod
¶
from_files(
paths: Iterable[str | Path],
*,
merge: Mapping[str, MergeHook] | None = None,
audience: str | None = None,
) -> Self
Build an annotator applying the overlay documents at paths, in order.
A file that is not an overlay document this sdmxlib can read raises
sdmxlib.edition.OverlayError from read_overlay.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paths
|
Iterable[str | Path]
|
Overlay documents, applied in this order. |
required |
merge
|
Mapping[str, MergeHook] | None
|
Per annotation type, the |
None
|
audience
|
str | None
|
The audience whose scoped entries apply; |
None
|
Returns:
| Type | Description |
|---|---|
Self
|
The annotator. |
replace ¶
Apply overlays from the next request on, in place of the current ones.
An overlay that changes a name or description raises OverlayError,
and the current overlays stay.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
overlays
|
Iterable[Overlay]
|
The overlays to apply, in order. |
required |
annotate ¶
annotate(
message: StructureMessage,
*,
registry: EditionRegistry,
conn: DuckDBPyConnection,
token: str,
) -> StructureMessage
message with the overlays applied: changed copies, never in place.
fingerprint ¶
The hooks line and the overlays' digests, computed when they were set; "" for none.
Splices¶
splice ¶
Response-splice registry — the post-serialise extension seam.
sdmxlib's JSON writer emits a spec-clean structure body. Consumers routinely need to splice extra fields into it after serialisation — client-specific annotations, detail-aware codelist/concept hydration, .Stat compatibility trims. The library must not know about any of those; it only knows how to run an ordered list of splices in one parse/encode pass.
A splice is (parsed_json: dict, ctx: SpliceContext) -> bool (sync or
async) that mutates parsed_json in place and returns whether it changed
anything. Consumers register their splices in a SpliceRegistry; the
generic route calls run_structure_splices, which runs them in order.
Instead of a hardcoded import-and-call list, the order is a registry the consumer owns,
and the single orjson.loads → mutate-each → orjson.dumps cycle lives here.
SpliceContext ¶
Everything a structure splice may need about the request being served.
The generic route builds this and hands it to every splice; a splice reads
only the fields it cares about. extra carries surface-specific flags
(e.g. inline_contentconstraint, strip_hierarchy_managed) so
consumer sub-apps can vary splice behaviour without new context fields.
SpliceRegistry ¶
Ordered splices a consumer installs for a given app surface.
structure runs on structure-message JSON bodies. Each app instance
(canonical, /v1, /de) carries its own registry, so the same generic
route serves different post-processing per surface without the library
knowing which splices exist.
run_structure_splices
async
¶
run_structure_splices(
response: Response,
registry: SpliceRegistry,
ctx: SpliceContext,
) -> Response
Run every registered structure splice in one parse/encode pass.
Skips non-JSON responses untouched. Splices may be sync or async; each is awaited if it returns an awaitable. Rebuilds the response only when something actually changed (dropping the stale ETag / Content-Length so the outer middleware recomputes them).