Skip to content

Tutorial

Five minutes from install to a flow that runs on threads, on processes and on two machines without changing a line of it.

1. Install and run

pip install tolquane
import tolquane as tq

@tq.source
def lines():
    with open("access.log") as f:
        yield from f

@tq.node
def status(line: str) -> int:
    return int(line.split()[8])

@tq.sink
def count(code: int) -> None:
    print(code)

tq.run(lines >> status >> count)

Three kinds of node: a source yields items, a node turns one item into another, a sink consumes. >> connects them with bounded channels; tq.run runs each on its own thread and returns a report.

2. Filter, fan out, keep state

@tq.node
def errors(code: int) -> int:
    return code if code >= 500 else tq.SKIP        # SKIP sends nothing; None would be sent

@tq.node
def words(line: str):
    yield from line.split()                        # a generator yields zero or many

class Tally:                                        # a class holds state, one per worker
    def __init__(self):
        self.counts = {}
    def __call__(self, word: str):
        self.counts[word] = self.counts.get(word, 0) + 1
        return tq.SKIP
    def on_end(self, ctx):                          # flush when the stream ends
        for pair in self.counts.items():
            ctx.send(pair)

3. Farms

tq.farm(status, workers=8)                          # 8 copies, round robin, first come
tq.farm(status, 8, ordered=True)                    # results in input order
tq.farm(status, 8, emit="on_demand")                # one item at a time per worker
tq.farm(Tally, 8, key=lambda w: w)                  # same key, same worker
tq.farm(chunk_sum, 8, emit="scatter")               # split a sequence, gather the parts

Put the farm where the time goes: lines >> tq.farm(status, 8) >> count.

4. Look before running, and read the errors

tq.check(graph)      # validates the wiring; a GraphError says the fix
tq.explain(graph)    # kinds, policies and the rule that wired each edge
tq.draw(graph)       # Mermaid diagram

A node that raises stops the run with a NodeError naming it. A graph that cannot make progress raises DeadlockError naming the nodes and what each waits for. Nothing hangs.

5. Pick a runtime

tq.run(graph)                                       # threads
tq.run(graph, runtime="processes")                  # farm workers in child processes
tq.run(graph, runtime="sync")                       # one thread, deterministic, for tests
tq.run(graph, deploy="deploy.toml", group="G1")     # this host's share of the graph
tq.run(tq.optimize(graph))                          # same graph, fewer threads

Same graph every time. See Runtimes for which one to pick, and print(tq.run(graph)) for the busy time of every stage: the stage that is busy while its neighbours wait is the one to farm.

6. Test a flow

out = tq.to_list()
tq.run(tq.from_iterable(["... 200 ...", "... 503 ..."]) >> status >> out, runtime="sync")
assert out.items == [200, 503]

Decorated functions stay callable, so status("... 200 ...") still works in a unit test.