Skip to content

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:

  1. 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 PortNode tree. This layer is optional: you may hand-write only the ABC file, only the YAML, or both.
  2. Runtimebuild(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 and setattrs 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 build is 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