Usage Guide¶
This guide provides a comprehensive overview of how to use graphable for managing and visualizing dependency graphs.
Detailed Examples¶
For complete, runnable scripts and their expected outputs, see the following pages:
Core Concepts¶
Orchestration & Edge Attributes¶
graphable supports rich metadata on both nodes and edges, making it ideal for task orchestration and workflow management.
Node Attributes
Nodes have built-in support for duration and status:
task = Graphable("Compile")
task.duration = 15.5 # seconds
task.status = "running"
Edge Attributes
Edges can store arbitrary key-value pairs, such as weights or labels:
# Add edge with attributes
graph.add_edge(node_a, node_b, weight=10, label="primary")
# Retrieve attributes
attrs = node_a.edge_attributes(node_b)
print(attrs["weight"])
# Modify attributes
node_a.set_edge_attribute(node_b, "weight", 20)
Dependency Algorithms¶
Beyond simple topological sorts, graphable provides native implementations of common graph algorithms.
Critical Path Method (CPM)
Identify the “critical” chain of tasks that determines the minimum project duration:
# Returns ES, EF, LS, LF, and slack for all nodes
analysis = graph.cpm_analysis()
# Returns a list of nodes on the critical path
cp = graph.critical_path()
Longest Path
Find the deepest chain of dependencies based on node durations:
lp = graph.longest_path()
Transitive Closure
Analyze the full reachability of your graph. The transitive closure contains an edge (u, v) if a path exists from u to v in the original graph:
closure = graph.transitive_closure()
All Paths
Find every possible route between two nodes.
Python API
# Returns a list of paths, each being a list of node objects
paths = graph.all_paths(source, target)
Command Line
Use the paths subcommand to see all connections between two nodes:
graphable paths topology.json Database React
Advanced Slicing¶
Extract focused subgraphs to analyze specific parts of your dependency tree.
Python API
# All nodes that 'target' depends on (recursively)
upstream = graph.upstream_of(target)
# All nodes that depend on 'source' (recursively)
downstream = graph.downstream_of(source)
# All nodes and edges on any path between 'start' and 'end'
between = graph.subgraph_between(start, end)
Command Line
The info, convert, and render commands support slicing via flags:
# Render only the ancestors of the 'React' node
graphable render topology.json react_upstream.png --upstream-of React
# Get information about the blast radius of the 'Database' node
graphable info topology.json --downstream-of Database
Custom Traversals (BFS & DFS)¶
For custom logic, you can perform breadth-first or depth-first searches using generators. This allows you to use standard Python loops to process nodes in a specific order.
from graphable.enums import Direction
# Breadth-First Search (level-by-level)
for node in graph.bfs(start_node):
print(f"Current depth level node: {node.reference}")
# Depth-First Search (deep chains)
for node in graph.dfs(start_node, direction=Direction.UP):
print(f"Ancestry chain node: {node.reference}")
The bfs() and dfs() methods both accept a direction parameter (Direction.UP or Direction.DOWN) and a limit_to_graph boolean (defaulting to True).
Graph Diffing¶
Compare two versions of a graph to identify what changed.
Structural Diff
Get a detailed dictionary of added, removed, and modified nodes/edges:
diff_data = g_old.diff(g_new)
print(diff_data["added_nodes"])
Visual Diff
Create a special “diff graph” where changes are tagged and colored (added=green, removed=red, modified=orange):
dg = g_old.diff_graph(g_new)
print(dg.render(create_topology_mermaid_mmd))
Cycle Resolution¶
If your graph contains cycles (which prevents it from being a DAG), graphable can suggest which edges to remove to restore its integrity.
# Returns a list of (source, target) tuples to remove
suggested_breaks = graph.suggest_cycle_breaks()
Cycle Detection¶
When building a graph, especially when relationships are defined dynamically or based on user input, it’s important to avoid circular dependencies. graphable provides a mechanism to check for cycles whenever you add a relationship.
You can enable cycle detection by passing check_cycles=True to any of the dependency management methods on a Graphable node:
from graphable.graphable import Graphable
from graphable.errors import GraphCycleError
a = Graphable("A")
b = Graphable("B")
c = Graphable("C")
a.add_dependency(b)
b.add_dependency(c)
try:
# This would create a cycle: C -> B -> A -> C
c.add_dependency(a, check_cycles=True)
except GraphCycleError as e:
print(f"Cycle detected! Path: {[n.reference for n in e.cycle]}")
This check is performed using a Breadth-First Search (BFS) to find if a path already exists in the direction that would complete a loop.
The following methods support the check_cycles parameter:
add_dependency(dependency, check_cycles=False, **attributes)add_dependencies(dependencies, check_cycles=False, **attributes)add_dependent(dependent, check_cycles=False, **attributes)add_dependents(dependents, check_cycles=False, **attributes)requires(dependency, check_cycles=False)provides_to(dependent, check_cycles=False)
In addition, the Graph.add_edge and Graph.add_node methods always perform cycle detection to ensure the integrity of the graph.
Transitive Reduction¶
For complex graphs, redundant edges can clutter the visualization. Transitive reduction removes these edges while preserving the reachability of the graph.
# Returns a new Graph instance with redundant edges removed
reduced_g = graph.transitive_reduction()
# Or render directly using the convenience method
from graphable.views.mermaid import create_topology_mermaid_mmd
print(graph.render(create_topology_mermaid_mmd, transitive_reduction=True))
Parsing Graphs¶
graphable can reconstruct graphs from all major export formats. This is useful for terminal-based tools or for persisting graph structures between sessions.
from graphable.graph import Graph
# Load from JSON file
g_json = Graph.from_json("topology.json")
# Load from YAML string
yaml_content = "nodes: [{id: A}, {id: B}], edges: [{source: A, target: B}]"
g_yaml = Graph.from_yaml(yaml_content)
# Load from CSV edge list
g_csv = Graph.from_csv("edges.csv")
The following static methods are available on the Graph class:
from_json(source, reference_type=str)from_yaml(source, reference_type=str)from_toml(source, reference_type=str)from_csv(source, reference_type=str)from_graphml(source, reference_type=str)
Equality Comparison¶
Graphs can be compared for equality using the standard == operator or the is_equal_to() method. Two graphs are considered equal if they have:
1. The same number of nodes.
2. Nodes with matching references, tags, durations, and statuses.
3. The same directed edges with matching attributes.
g1 = Graph.from_json("graph.json")
g2 = Graph.from_yaml("graph.yaml")
if g1 == g2:
print("The graphs are identical.")
Subgraph Semantics & Syncing¶
A Graph instance acts as a “view” of a specific set of nodes. Even if a node in the graph is connected to “external” nodes that are not members of the graph, operations like topological sorts and checksums will only respect and include nodes that are explicitly part of the Graph instance.
Filtering Behavior
Topological Order: Methods like
topological_order()andparallelized_topological_order()will filter out any nodes not present in the graph’s membership.Checksums: The
checksum()method only accounts for nodes in the graph and edges between those specific nodes.
Syncing with Discover
If you want to expand a Graph to include all reachable ancestors and descendants of its current nodes, you can use the discover() method. This effectively “syncs” the graph with the full connected structure of its members.
# G only contains node 'A' initially
g = Graph({a})
# A depends on B, B depends on C
# After discover(), G will contain A, B, and C
g.discover()
Node Ordering¶
Graphable nodes support rich comparison operators based on their reachability in the graph:
- a < b: a is a proper ancestor of b.
- a <= b: a is an ancestor of or identical to b.
- a > b: a is a proper descendant of b.
- a >= b: a is a descendant of or identical to b.
This provides a clean way to check for dependency relationships directly between node objects.
if node_a < node_b:
print("node_a must come before node_b")
Caching & Performance¶
The Graph class implements an efficient observer-based caching system for expensive operations:
* topological_order()
* parallelized_topological_order()
* checksum()
Calculations are performed once and cached. If any node in the graph is modified (tags changed, duration/status updated, dependencies added/removed), the graph is automatically notified and invalidates its cache. This ensures high performance for repeated access while maintaining absolute correctness.
Unified I/O¶
graphable provides high-level read() and write() methods that automatically detect the file format based on the extension. This is the simplest way to work with graph files.
# Reading
g = Graph.read("topology.json")
# Writing (supports all formats including graphical)
g.write("topology.svg")
g.write("topology.yaml")
# Supports transitive reduction during write
g.write("simple.mmd", transitive_reduction=True)
Integrity & Checksums¶
To ensure your graph structure and metadata (tags, duration, status, edge attributes) haven’t changed, you can use the deterministic BLAKE2b checksum feature.
# Calculate hex digest
digest = g.checksum()
# Validate later
if g.validate_checksum(digest):
print("Integrity verified!")
The checksum is stable across different Python sessions and is independent of node creation order.
Parallel Processing¶
For task orchestration, you often need to know which nodes can be processed simultaneously. The parallelized_topological_order() method groups nodes into independent “layers.”
for i, layer in enumerate(g.parallelized_topological_order()):
print(f"Layer {i} (can run in parallel): {[n.reference for n in layer]}")
Like the standard topological sort, this also supports _filtered and _tagged variants.
Command Line Interface¶
graphable includes a command-line tool for managing graph files without writing Python code. It is available as the graphable command after installation.
Installation
To get the full experience with formatted tables and panels, install the cli extra:
pipx install "graphable[cli]"
Subcommands
``info <file>``: Displays summary statistics about the graph.
``check <file>``: Performs validation (cycles and consistency).
``convert <input> <output>``: Converts between formats.
``reduce <input> <output>``: Computes transitive reduction.
``render <input> <output> [-e engine]``: Renders a graph as an image (SVG, PNG).
``paths <file> <source> <target>``: Finds all paths between two nodes.
``diff <file1> <file2> [-o output]``: Compares two graphs.
``serve <file> [-p port]``: Starts a live-reloading interactive visualization.
``checksum <file>``: Prints the graph checksum.
``write-checksum <file> <output>``: Writes the graph checksum to a file.
``verify <file> [–expected hash]``: Verifies integrity.
CI/CD and Automation
If you are using graphable in a script or CI/CD pipeline and want to ensure plain-text output regardless of installed dependencies, use the --bare flag before the subcommand:
graphable --bare info topology.json
Supported Extensions
Input:
.json,.yaml,.yml,.toml,.csv,.graphmlOutput: All input formats plus
.dot,.gv,.mmd,.d2,.puml,.html,.tex,.txt,.ascii,.svg,.png
Live Visualization¶
The serve command provides a “Live Preview” experience. It starts a local web server and automatically reloads the visualization in your browser whenever you save changes to the input graph file.
graphable serve architecture.json --port 8080
This is perfect for architecting and debugging complex dependency systems in real-time.
Enhanced Interactive UI¶
The interactive HTML export (and the serve command) includes several UI features:
Search Bar: Quickly find nodes by reference. Matching nodes are highlighted in the viewport.
Metadata Sidebar: Click on any node to view its detailed properties, including tags, duration, and execution status.
Panning & Zooming: Standard Cytoscape.js controls for navigating large graphs.
ASCII Flowchart¶
For a quick, boxed representation of the graph that handles multiple parents better than a standard tree, use the asciiflow view:
from graphable.views.asciiflow import create_topology_ascii_flow
print(create_topology_ascii_flow(g))
This is ideal for terminal-based tools or quick debugging where you need to see the full directed structure without leaving the command line.
Scientific Publishing (TikZ)¶
If you are writing a LaTeX paper or report, you can export your graph directly to TikZ code:
from graphable.views.tikz import create_topology_tikz, TikzStylingConfig
# Generates a \begin{tikzpicture} block
tikz_code = create_topology_tikz(g)
It supports the modern TikZ graphs library by default, ensuring high-quality vector output that matches your document’s font and style perfectly.
Data Export & Interoperability¶
graphable makes it easy to move your graph data into other tools for analysis or custom visualization.
JSON, YAML & TOML
Export to standard machine-readable formats:
from graphable.views.json import create_topology_json
from graphable.views.yaml import create_topology_yaml
from graphable.views.toml import create_topology_toml
json_data = create_topology_json(g)
yaml_data = create_topology_yaml(g)
toml_data = create_topology_toml(g)
Cytoscape
Generate a standalone, interactive HTML file or a machine-readable Cytoscape JSON file. The HTML version uses Cytoscape.js for rendering and supports zooming, panning, and dragging:
from graphable.views.html import export_topology_html
from graphable.views.cytoscape import export_topology_cytoscape
# Export as interactive HTML
export_topology_html(g, "interactive_graph.html")
# Export as machine-readable Cytoscape JSON
export_topology_cytoscape(g, "graph.cy.json")
You can view a live demonstration here: topology_interactive.html
GraphML
For professional graph analysis in tools like Gephi or yEd, export your graph to the GraphML XML standard:
from graphable.views.graphml import export_topology_graphml
export_topology_graphml(g, "graph_data.graphml")
CSV Edge List
For processing in Excel, Pandas, or other data tools:
from graphable.views.csv import create_topology_csv
# Generates "source,target" rows
csv_data = create_topology_csv(g)
NetworkX Integration¶
For users who need advanced graph analysis capabilities, graphable provides seamless integration with the NetworkX library.
If you have networkx installed, you can convert any graphable.Graph to a networkx.DiGraph using the to_networkx() method:
import networkx as nx
from graphable.graph import Graph
from graphable.graphable import Graphable
g = Graph()
# ... build your graph ...
# Convert to NetworkX
dg = g.to_networkx()
# Use any NetworkX algorithm
longest_path = nx.dag_longest_path(dg)
print(f"Longest path: {longest_path}")
The conversion preserves node references and tags as node attributes, allowing you to access your original data within NetworkX algorithms.