Specs & serialization¶
Every component, and everything that configures one (normalizers, runners, schemas), is
serializable: an instance is "a class plus its configuration", and a Spec is its wire
form. Specs are how a DAG travels to another process, how a run is described in a YAML file,
and how the platform stores and rebuilds components.
The spec envelope¶
spec = source.to_spec()
spec.path # "my_package.sources.Shop" the class, as an import path
spec.key # "" (or a catalog key instead of a path)
spec.id # the instance id
spec.init # the constructor payload, nested specs included
A component is referenced by exactly one of path (a fully qualified import path, what
to_spec() emits) or key (a catalog key, what hand-written specs may use).
init holds the field values; fields set to None are omitted; nested serializables
(destinations, resources, a normalizer) are nested specs.
Round-trip:
rebuilt = il.Source.from_spec(spec) # must reconstruct to a Source
rebuilt = il.Component.from_spec(spec.model_dump())
Calling from_spec on a subclass enforces the kind: a spec reconstructing to something else
raises TypeError. Nested key references resolve through the catalog passed in, or the
settings-configured catalog built lazily.
Sources and their assets¶
A source is the unit of reconstruction. Its spec carries the assets as an override map
keyed by asset key rather than as individual specs, which keeps the document compact and lets
per-asset state (destinations, materializable, dependency wiring) survive:
Shop(account_id="act_1").to_spec().init
# {"account_id": "act_1", "assets": {"orders": {"id": "...", "materializable": True, ...}, ...}}
Reconstruction builds each asset class with its overrides; assets absent from the map are built bare.
Spec files¶
Spec.from_file(path) loads a YAML document, interpolating ${VAR} placeholders from the
environment in every string value. Unresolved variables are a hard error, so credentials never
need to live in the file:
# shop.yaml
key: shop
init:
account_id: act_1
destinations:
- key: bigquery_destination
resources:
connection:
key: shop_connection
init:
api_key: ${SHOP_API_KEY}
source = il.Source.from_spec_file("shop.yaml")
dag = il.DAG.from_spec_file("shop.yaml") # any runnable component
Invalid YAML, a missing file, undefined variables and malformed documents raise SpecError; a
component kind that is not a workload raises DAGError from DAG.from_spec_file. The CLI's
interloper run -f is this call.
DAG specs¶
A DAGSpec is a flat list of component specs: sources with their asset override maps, plus
standalone assets. It is what MultiProcessRunner ships to its workers and what
interloper run --format inline accepts:
from interloper.dag import DAGSpec
dag_spec = dag.to_spec()
payload = dag_spec.model_dump(mode="json")
dag = il.DAG.from_spec(DAGSpec(**payload))
The override map is built from the DAG's actual asset instances, so a mini-DAG's read-only parents stay read-only after the round-trip.
What makes something serializable¶
Serializable is a pydantic model with a few additions:
- a class-level
key, snake_cased from the class name unless declared; - strict construction: unknown keyword arguments raise
TypeErrorinstead of being dropped; to_spec(),from_spec(),from_spec_file(),classpath(),resolve_path();config_schema(), the JSON Schema of the user-facing fields.
Component extends it with identity (kind, id), resource slots and relations. Runners,
normalizers and schemas are serializable without being components. The full picture is in the
component model.