Library Overview¶
hexa lets you describe a hierarchical pipeline as a tree of typed contracts, then get a fully wired instance back from plain class annotations. This page builds the mental model: the three layers, the two interchangeable representations, and the container that reads annotations at runtime.
🏗️ Architecture¶
Text Only
┌───────────────────────────────────────────────────────────────┐
│ Representation layer (optional, interchangeable) │
│ │
│ YAML spec (.yaml) ◄── parse_yaml ──┐ │
│ │ │ PortNode tree │
│ │ generate_abc │ (the single model) │
│ ▼ ▼ │
│ ABC module (_abc.py) ◄─ generate_yaml ─┐ │
│ (contracts only: ABC + annotations) │ │
└──────────────────────────────────────────┼────────────────────┘
│
┌──────────────────────────────────────────┼────────────────────┐
│ Runtime layer (annotations are decision)▼ │
│ │
│ root class ── build(cls, instances=, config=) ──► wired │
│ (e.g. ImplParentClass) instance │
│ │ __annotations__ merged across MRO │
│ ▼ │
│ recursively: instantiate pinned concrete port, setattr on │
│ parent, inject runtime instances / config values by name │
└───────────────────────────────────────────────────────────────┘
Two layers, deliberately decoupled:
- Representation — the same contract can be written as a YAML spec or as a Python ABC module. hexa's converters translate between them through one neutral
PortNodetree. This layer is optional: you may hand-write only the ABC file, only the YAML, or both. - Runtime —
build(cls)reads the merged annotations of a root class. Every port slot annotated with a concrete type is the decision; the container instantiates the tree recursively andsetattrs each child onto its parent.
🔄 The Three Layers (the mental model)¶
Whatever you author, the pattern is built from three fixed layers:
| Layer | Example file | Role |
|---|---|---|
| 1. ABC contracts | samples/minimal/abc.py |
Declaration of need — attributes, port slots, @abstractmethod bodies. Never runs. |
2. Generic Impl* / concrete defaults |
samples/minimal/impl.py |
Shared behavior — subclasses the ABC, implements every @abstractmethod, re-pins slots to concrete types. |
| 3. Specialization (optional) | samples/extraction_pipeline/banks/{axis,icici}/pdf.py |
Variation — overrides only the members that differ from Impl*. |
The root class you hand to
buildis always a concrete type (layer 2 or 3). The ABC (layer 1) is only a contract — it is never instantiated.
🔄 Lifecycle Rules¶
| Step | Call | Why |
|---|---|---|
| 1 | Author the contract | As YAML (.yaml) or ABC (_abc.py) — or both, and let check_matches prove they agree. |
| 2 | Generate one from the other (optional) | generate_abc(spec.yaml) when you want ABCs from YAML; generate_yaml(abc.py) for the reverse. |
| 3 | Write concrete implementations (Impl*) |
Subclass the ABC, implement abstracts, pin each slot to a concrete port type. |
| 4 | Wire | build(ImplRoot, instances={"repo": repo}, config={"retries": 3}). |
| 5 | Verify (optional) | check_matches(spec.yaml, abc.py) returns True or a list of difference strings. |
Steps 1–3 choose what the pipeline needs; step 4 is automatic. Variation between pipelines happens purely by which root class you build.
🧭 Which Entry Point Should You Use?¶
| Entry point | Use when | Adds |
|---|---|---|
parse_yaml(path) |
You author specs in YAML | YAML → PortNode tree (structural validation only) |
generate_abc(node) |
You want ABC source from a spec | YAML model → Python ABC module source |
parse_abc(mod) |
You author contracts in Python | ABC module → PortNode tree (reflection) |
generate_yaml(node) |
You want a spec for your ABCs | Python ABC module → YAML source |
check_matches(a, b) |
You keep both representations | Verifies the two trees agree; prints diffs |
build(cls, ...) |
You want a wired runtime instance | Recursively instantiates the port tree from annotations |
➡️ Read Next¶
- Core Components — the public API surface in detail.
- Use case 01 – YAML first — the minimal spec-to-ABC workflow.
- Philosophy — the why behind type-declared dependencies.