Getting Started¶
This guide walks you through building your first yieldgraph pipeline step by step. By the end you will understand the core data-flow model and be ready to tackle more advanced patterns.
Installation¶
To unlock richer structured logging, install the optional loguru dependency:
Anatomy of a pipeline¶
Every yieldgraph pipeline is made up of three kinds of building blocks:
graph LR
S([Source\nnode]) -->|Edge| T([Transform\nnode]) -->|Edge| L([Terminal\nnode])
| Role | Description |
|---|---|
| Source | The entry point. Generates or loads raw data. Receives the Graph instance as its first argument. |
| Transform | Processes items from the previous node and yields results onward. Can yield zero, one, or many items per input. |
| Terminal | The last node in a chain. Its outputs are collected in graph.output after the run. |
Any node can play any role — the distinction is purely positional.
Your first pipeline¶
from yieldgraph import Graph
def source(graph): # (1)!
for value in [10, 20, 30]:
yield value
def double(x): # (2)!
yield x * 2
g = Graph()
g.add_chain(source, double) # (3)!
g.run() # (4)!
print(g.output)
# [(20,), (40,), (60,)] # (5)!
- The first function in a chain always receives the
Graphinstance as its first positional argument. - Each subsequent function receives whatever the previous step
yields — unpacked as individual arguments. add_chainregisters the functions and wires up the internalEdgequeues automatically.g.run()executes all nodes in order. No data moves until you callrun.g.outputisList[Tuple[Any, ...]]. Every yielded value is normalised to a tuple, so a plainintbecomes a one-element tuple(int,).
Source functions¶
The source function kicks off the pipeline. It always receives the Graph instance as its first argument — useful for inspecting state or previous outputs.
Static data source¶
def source(graph):
for record in [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
]:
yield record
File-based source¶
import csv
def read_csv(graph):
with open("data.csv", newline="") as fh:
reader = csv.DictReader(fh)
for row in reader:
yield row
API or database source¶
import sqlite3
def from_database(graph):
conn = sqlite3.connect("app.db")
cursor = conn.execute("SELECT id, value FROM measurements")
for row in cursor:
yield row # yields (id, value) tuple
conn.close()
Source with graph.output
When chaining multiple pipelines you can feed the output of a previous run into a new source:
def re_process(graph):
for row in graph.output: # (1)!
yield row
g2 = Graph()
g2.add_chain(re_process, transform)
g2.run()
graph.outputfrom any previous run is available here.graphis the new Graph instance, so you would pass the previous graph's output differently — see Patterns & Recipes.
Transform functions¶
Transform functions sit in the middle of a chain. They receive whatever the upstream node yields (unpacked as *args) and yield results downstream.
One-to-one transform¶
One-to-many transform (expand)¶
Many-to-one (filter / aggregate)¶
Stateful transform¶
def running_total(x):
# Use a mutable default or a closure for state
running_total.acc = getattr(running_total, 'acc', 0) + x
yield running_total.acc
Yielding nothing is fine
If a transform yields nothing for a given input, the pipeline simply skips that item. Use this as a clean filter pattern.
Regular (non-generator) functions¶
You do not need to use yield. A plain function that returns a value is automatically wrapped into a one-shot generator:
def add_tax(price):
return price * 1.19 # return, not yield
def format_price(price):
return f"€ {price:.2f}"
g = Graph()
g.add_chain(source, add_tax, format_price)
g.run()
# g.output → [('€ 11.90',), ('€ 23.80',), ...]
The rule is simple: return → one result; yield → zero or more results.
Multi-argument passing with tuples¶
When a function yields a tuple, the downstream function receives the tuple's elements as separate positional arguments:
def source(graph):
# Yield pairs of (name, score)
yield ("Alice", 95)
yield ("Bob", 80)
yield ("Carol", 88)
def grade(name, score): # receives two arguments
label = "pass" if score >= 85 else "fail"
yield f"{name}: {label}"
g = Graph()
g.add_chain(source, grade)
g.run()
for row in g.output:
print(row[0])
# Alice: pass
# Bob: fail
# Carol: pass
Single values vs tuples
- Yield a plain value (
yield x) → downstream receivesxas a single argument. - Yield a tuple (
yield (a, b)) → downstream receivesaandbas separate arguments.
This mirrors Python's normal *args unpacking.
Multi-step chains¶
Chain as many functions as you need — there is no limit:
from yieldgraph import Graph
import json
def fetch_raw(graph):
"""Simulate fetching JSON records."""
raw = [
'{"id": 1, "temp": 23.5}',
'{"id": 2, "temp": -999}',
'{"id": 3, "temp": 21.0}',
]
for line in raw:
yield line
def parse_json(line):
yield json.loads(line)
def filter_outliers(record):
if -50 < record["temp"] < 50:
yield record
def extract_temp(record):
yield record["temp"]
def celsius_to_fahrenheit(c):
yield round(c * 9 / 5 + 32, 1)
g = Graph()
g.add_chain(fetch_raw, parse_json, filter_outliers, extract_temp, celsius_to_fahrenheit)
g.run()
print(g.output)
# [(74.3,), (69.8,)] ← the -999 outlier was filtered out
Reading pipeline output¶
g.output returns a List[Tuple[Any, ...]] collected from the terminal nodes (the last node of each chain).
g.run()
# Iterate over rows
for row in g.output:
print(row)
# Unpack single-value tuples
values = [row[0] for row in g.output]
# Check whether the run produced anything
if g.has_output:
print(f"Pipeline produced {len(g.output)} rows")
# Check for success
if g.succeeded:
print("Pipeline completed without errors")
else:
print(f"Pipeline error: {g.error}")
Calling the graph as a callable¶
Graph supports __call__, so you can invoke it like a function:
Next steps¶
- Patterns & Recipes — fan-out branches, error handling, cancellation, threaded mode
- Configuration — environment variables, logging levels
- API Reference — complete class documentation