Skip to content

hexa

hexa

Summary

Type-declared dependency injection for Python.

Hexa is a pattern and utility library for structuring complex hierarchical pipelines. It rests on one core idea: a concrete class's annotated slot type is the dependency decision. Variation is expressed entirely as class annotations — never as imperative wiring, never as __init__ parameters, never as a composition root that assembles objects by hand.

All utilities serialize through a single shared model (PortNode):

  • parse_yaml / generate_yaml — between YAML specs and the model
  • parse_abc / generate_abc — between ABC Python modules and the model
  • check_matches — verify two representations describe the same tree
  • build — recursively instantiate a wired pipeline from annotations

Installation

Install using pip:

pip install hexa

Quick start

Define contracts, pin concrete types on the slots, and let build wire the tree from annotations:

from hexa import build

class NumberPort(ABC):
    @abstractmethod
    def extract(self, line: str) -> float: ...

class ParserPort(ABC):
    number: NumberPort

    @abstractmethod
    def parse(self, text: str) -> dict: ...

class RegexNumberPort(NumberPort):
    def extract(self, line: str) -> float:
        ...

class StandardParserPort(ParserPort):
    number: RegexNumberPort  # <-- the dependency decision

    def parse(self, text: str) -> dict:
        return {"value": self.number.extract(text)}

pipeline = build(StandardParserPort)
print(pipeline.parse("Value: 42"))

CLI usage

Convert between YAML and ABC representations:

1
2
3
4
5
hexa parse-yaml spec.yaml
hexa generate-abc spec.yaml --out spec_abc.py
hexa parse-abc spec_abc.py
hexa generate-yaml spec_abc.py --out spec.yaml
hexa check spec.yaml spec_abc.py

Core concepts

Shared model

PortNode — the neutral tree every parser and generator serializes through.

Converters

Parsers and generators that translate between YAML specs and ABC Python modules via the shared model.

Verification

check_matches — machine-check that two representations agree.

Container

build — recursively instantiate a full pipeline from class annotations.


Notes

  • All utilities share a single model; any port tree fits.
  • Generators emit source text only; hexa performs no runtime code generation.

Classes

MethodSpec dataclass

1
2
3
4
5
MethodSpec(
    name: str,
    args: list[str] = list(),
    kwargs: dict[str, str] = dict(),
)

Specification of a single abstract method.

Attributes:

Name Type Description
name str

Name of the method.

args list[str]

Positional parameter names.

kwargs dict[str, str]

Keyword-only parameter names mapped to their type expressions.

PortNode dataclass

1
2
3
4
5
6
7
8
PortNode(
    name: str,
    port_cls: str,
    kind: str = "leaf",
    attributes: dict[str, ConfigValue] = dict(),
    children: list[PortNode] = list(),
    methods: dict[str, MethodSpec] = dict(),
)

A node in the hexa port tree.

Each node is a leaf (no children), a port (has nested port slots), or the parent root of the tree. Every node carries a concrete typed shape derived from an ABC class name.

Attributes:

Name Type Description
name str

Slot name, e.g. "parser", "amount_balance".

port_cls str

ABC class name that gives this port its shape, e.g. "TransactionParserPort". Every port has a concrete typed shape.

kind str

One of "leaf", "port", or "parent". - "leaf": no children (e.g. NumberPort) - "port": has children (e.g. TransactionParserPort) - "parent": the root of the tree (e.g. ExtractionPipeline)

attributes dict[str, ConfigValue]

Annotated attributes: name -> type_expr for scalars, or a nested dict of sub-attributes (a struct) for grouped values.

children list[PortNode]

Nested port slots.

methods dict[str, MethodSpec]

Abstract methods keyed by method name.

Functions
diff
diff(other: PortNode, path: str = '') -> list[str]

Return a list of human-readable difference strings.

An empty list means the trees are equal.

Parameters:

Name Type Description Default
other PortNode

Tree to compare against.

required
path str

Path prefix used when recursing into nested nodes; defaults to the root path.

''

Returns:

Type Description
list[str]

list[str]: Human-readable difference strings, one per discrepancy.

find
find(name: str) -> PortNode | None

Find a direct child by slot name.

Parameters:

Name Type Description Default
name str

Slot name of the child to look up.

required

Returns:

Type Description
PortNode | None

PortNode | None: The matching child node, or None if no child has that name.

walk
walk() -> list[PortNode]

Return all nodes in depth-first order (self first).

Returns:

Type Description
list[PortNode]

list[PortNode]: All nodes of the tree, self first, depth-first.

Functions

build

1
2
3
4
5
6
build(
    cls: type[T],
    *,
    instances: dict[str, Any] | None = None,
    config: dict[str, Any] | None = None
) -> T

Recursively instantiate the port tree from annotations.

Parameters:

Name Type Description Default
cls type[T]

The root class to build (e.g. AxisExtractionPipeline).

required
instances dict[str, Any] | None

{slot_name: object} injected verbatim into every node whose annotations contain that name (runtime values — repos, handlers, clients — that must not be re-constructed). Applied to the whole tree by name.

None
config dict[str, Any] | None

{field_name: value} propagated by name to every node's annotated config fields (non-port attributes), replacing the default None.

None

Returns:

Name Type Description
T T

A fully wired instance of cls and all its nested ports.