Skip to content

Core Components

A validated reference to the public API of hexa (package root exports from hexa/__init__.py). Signatures match the code exactly. For runnable recipes see the Use Cases. Exact parameter contracts live in the API reference under docs/lib/ — this page is usage-level.


1. The Shared Model — PortNode and MethodSpec

Every parser and generator serializes through one neutral tree. It is not tied to any sample — any port tree fits.

1.1 PortNode

A node in a hexa port tree. Three kinds:

Kind Meaning Example
parent The root of the tree ExtractionPipeline
port Has nested port slots (children) TransactionParserPort
leaf No children NumberPort

Fields:

Field Type Meaning
name str Slot name, e.g. "parser", "amount_balance"
port_cls str ABC class name that gives this port its shape
kind str "leaf" | "port" | "parent"
attributes dict[str, ConfigValue] name → type_expr for scalars, or a nested dict of sub-attributes for grouped values
children list[PortNode] Nested port slots
methods dict[str, MethodSpec] Abstract methods keyed by method name

Tree helpers (used internally and handy for inspection):

  • node.find(name) -> PortNode | None — direct child by slot name.
  • node.walk() -> list[PortNode] — all nodes, depth-first, self first.
  • node.diff(other, path="") -> list[str] — human-readable difference strings; empty list means equal.

1.2 MethodSpec

Specification of a single abstract method: name, positional args: list[str], and keyword-only kwargs: dict[str, str] (name → type expression). Supports equality (==/!=).

1.3 ConfigValue

str | NestedConfig, where NestedConfig = dict[str, ConfigValue]. An attribute is either a scalar type expression (e.g. "str") or a nested structure of sub-attributes. Nesting is recursive.


2. Converters — YAML ⇄ ABC

All converters are pure (they operate on the model) and optional: you never have to touch them if you hand-write a single representation.

2.1 parse_yaml(path) -> PortNode

Parses a hexa YAML spec into a PortNode tree. The grammar uses three member kinds:

  • attributename: type scalar (e.g. source_type: str)
  • nested structname: mapping of sub-attributes (e.g. source:)
  • portname: mapping whose single key is a class whose value is a body (e.g. ingest: IngestPort: ...)

Disambiguation rule: a mapping with exactly one key whose value is a non-scalar body is a port; any other mapping is a nested struct of attributes. Raises ValueError on structural errors (see Error Handling).

2.2 generate_abc(node, out_path=None) -> str

Generates _abc.py source from a PortNode tree. Emits class X(ABC) per node, synthesized @dataclasses for nested structs, port-slot annotations, and @abstractmethod stubs. Deterministic (walk order) so output is idempotent. When out_path is given the source is also written there; the source string is always returned.

2.3 parse_abc(module_or_path) -> PortNode

Reflects an ABC Python module into a PortNode tree. Accepts a .py file path, a module import string, or an already-loaded module. ABC classes become nodes; a @dataclass annotation on a port expands into a nested attributes entry. The root is auto-detected (the class not referenced as any other slot, preferring names containing Pipeline/Root/Parent).

2.4 generate_yaml(node, out_path=None) -> str

Generates a YAML spec (nested mapping form) from a PortNode tree — the reverse of generate_abc. A deferred/empty slot renders as slot: Class: ....


3. Verification — check_matches(a, b)

check_matches(path_a, path_b) -> bool | list[str] parses two representations (one .yaml/.yml and one .py, either direction) and compares the trees with PortNode.diff. Returns True when they describe the same tree, otherwise a list of human-readable difference strings. Inequality checks cover port_cls, kind, attributes (including nested structs), methods (args/kwargs), and children.


4. Container — build

build(cls, *, instances=None, config=None) -> T recursively instantiates the port tree implied by a root class's annotations.

Algorithm:

  1. Merge __annotations__ across the MRO (reverse order) so inherited slot annotations are visible.
  2. Instantiate the parent class (cls(); if that raises TypeError, retry passing None for every __init__ parameter after self).
  3. For each annotation:
  4. A name in instances wins — the provided object is setattred verbatim, never reconstructed.
  5. Builtin / typing / PEP 604-union types are config fields — applied from config by name.
  6. Abstract classes are skipped (but take a config value when provided).
  7. Any other concrete type is a port slotbuild recurses and the child is setattred onto the parent.
  8. Config defaults: any annotated attribute still unset is defaulted to its config value (or None) so methods can safely reference it.

Runtime values (repos, handlers, clients) go in instances=; plain config values go in config=. Both propagate by name across the whole tree.


5. CLI

Command Behavior
hexa parse-yaml PATH Print the parsed PortNode tree.
hexa generate-abc PATH [--out FILE] Emit ABC source from a YAML spec.
hexa parse-abc PATH Print the tree reflected from an ABC module.
hexa generate-yaml PATH [--out FILE] Emit a YAML spec from an ABC module.
hexa check PATH_A PATH_B Print Matches./Differences found: and exit 0/1.

Errors print Error: <message> to stderr and exit 1.