Graph Model¶
The graph model is the foundation of Honey Badgeria's backend. It consists of four core data structures: Vertex, Edge, Graph, and GraphTopology.
Vertex¶
A vertex represents a single operation in the DAG. It maps to a Python handler function with declared inputs, outputs, and metadata.
from honey_badgeria.back.graph import Vertex
v = Vertex(
name="fetch_user",
handler="vertices.users.fetch", # dotted import path or callable
inputs={"user_id": "int"}, # type declarations or data bindings
outputs={"user": "dict"}, # output type declarations
version="1", # version tracking
effect="pure", # "pure" or "side_effect"
)
Fields¶
| Field | Type | Description |
|---|---|---|
name |
str |
Unique identifier for the vertex (e.g., "fetch_user") |
handler |
Callable \| str |
Python function or dotted import path |
inputs |
dict[str, str] |
Input declarations: type names or data bindings |
outputs |
dict[str, str] |
Output type declarations |
version |
str \| None |
Version string for rewrite-over-patch tracking |
effect |
str |
"pure" (cacheable, no side effects) or "side_effect" |
Effect Types¶
pure— The handler always returns the same output for the same input. Pure vertices can be cached, retried, and run in parallel safely.side_effect— The handler has observable effects (database writes, API calls, file I/O). Side-effect vertices skip caching and require special handling in atomic groups.
Identity¶
The id property is an alias for name, provided for backward compatibility:
Edge¶
An edge represents a directed connection between two vertices:
Edges are compared by their (source, target) pair:
Graph¶
The graph is the container for vertices, edges, flows, and atomic groups:
from honey_badgeria.back.graph import Graph, Vertex, Edge
graph = Graph()
graph.add_vertex(Vertex(name="a", handler=fn_a))
graph.add_vertex(Vertex(name="b", handler=fn_b))
graph.add_edge(Edge("a", "b"))
Key Methods¶
| Method | Returns | Description |
|---|---|---|
add_vertex(v) |
— | Add a vertex (raises VertexAlreadyExistsError on duplicate) |
get_vertex(name) |
Vertex |
Retrieve by name (raises VertexNotFoundError) |
has_vertex(name) |
bool |
Check if vertex exists |
add_edge(e) |
— | Add an edge |
get_sources() |
list[Vertex] |
Vertices with no incoming edges (entry points) |
get_sinks() |
list[Vertex] |
Vertices with no outgoing edges (terminal nodes) |
entry_vertex(flow_name) |
Vertex |
Get the entry point of a named flow |
adjacency_list() |
dict[str, list[str]] |
Forward adjacency mapping |
reverse_adjacency_list() |
dict[str, list[str]] |
Reverse adjacency mapping |
atomic_group_for_vertex(name) |
AtomicGroup \| None |
Get the atomic group a vertex belongs to |
Flows¶
Flows are named entry points into the graph:
graph.flows = {
"create_user": {"entry": "normalize"},
"delete_user": {"entry": "check_perms"},
}
entry = graph.entry_vertex("create_user")
# Returns the Vertex named "normalize"
Atomic Groups¶
Groups are stored on the graph and can be queried per-vertex:
group = graph.atomic_group_for_vertex("charge")
if group:
print(f"Vertex 'charge' belongs to atomic group '{group.name}'")
GraphTopology¶
GraphTopology computes the execution structure of a graph. It is the algorithm layer that determines ordering and parallelism.
Topological Sort¶
Returns vertices in dependency order using Kahn's algorithm:
If the graph has cycles, this raises GraphCycleError.
Execution Stages¶
The most important computation. Returns groups of vertices where all vertices in a stage can execute in parallel:
For a diamond DAG A → (B, C) → D:
- Stage 1:
[A]— must execute first - Stage 2:
[B, C]— can execute in parallel (no mutual dependencies) - Stage 3:
[D]— depends on both B and C
This is how HBIA determines what can run concurrently — you don't write parallel code, you declare dependencies, and the topology computation reveals parallelism.
Sources and Sinks¶
sources = topo.sources() # Vertices with no incoming edges
sinks = topo.sinks() # Vertices with no outgoing edges
Dependency Queries¶
deps = topo.dependencies("validate") # What "validate" depends on
dependents = topo.dependents("fetch") # What depends on "fetch"
AtomicGroup¶
Declares a set of vertices that must execute as an all-or-nothing unit:
from honey_badgeria.back.atomicity import AtomicGroup, FailurePolicy
group = AtomicGroup(
name="payment_processing",
vertices=["reserve", "charge", "confirm"],
failure_policy=FailurePolicy.ROLLBACK,
no_cache=True, # bypass cache inside group
no_parallel=True, # force serial execution
)
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
— | Group identifier |
vertices |
list[str] |
— | Vertex names in this group |
failure_policy |
FailurePolicy |
— | ROLLBACK, COMPENSATE, or ABORT |
no_cache |
bool |
True |
Bypass cache for vertices in this group |
no_parallel |
bool |
True |
Force serial execution within the group |
For full atomicity documentation, see Atomicity.
Building Graphs Programmatically¶
From Dictionaries¶
from honey_badgeria.back.loader import GraphBuilder
graph = GraphBuilder.from_dict({
"flow": {
"my_flow": {
"vertex_a": {
"handler": "mod.fn_a",
"effect": "pure",
"outputs": {"x": "int"},
"next": ["vertex_b"],
},
"vertex_b": {
"handler": "mod.fn_b",
"inputs": {"x": "vertex_a.x"},
},
}
}
})
From YAML¶
from honey_badgeria.back.dsl import load_graph_from_yaml
graph = load_graph_from_yaml("flows/my_flow.yaml")
Merging Graphs¶
Combine multiple graphs into one:
from honey_badgeria.utils.graph_tools import merge_graphs
combined = merge_graphs(graph1, graph2, graph3)
Extracting Subgraphs¶
from honey_badgeria.utils.graph_tools import subgraph
sub = subgraph(graph, ["vertex_a", "vertex_b"])
Validation¶
Graphs can be validated for structural integrity:
from honey_badgeria.back.graph.validator import validate_graph
errors = validate_graph(graph)
# Returns list of error strings, or empty list if valid
Checks performed:
- Cycle detection (DAGs cannot have cycles)
- Edge endpoint existence (both source and target must exist)
- Atomic group connectivity (members must form a connected subgraph)
See Validation for full details.