Skip to content

Guide 04 of 9

Pipelines and concurrency

Join commands stage to stage, find the stage that failed, and run independent commands side by side.

Plate 13 Stages joined by unions, couplings, and a flange carry one stream in sequence, then split to run in parallel before rejoining.

Connect a pipeline

Join commands with |. The result is a PipelineResult with one CommandResult per stage. Only the final stage's stdout is captured; interior stdout feeds the next stage and appears as None on that stage's result.

pipeline.py Tested
import sys

from cuprum import Program, ProgramCatalogue, sh

catalogue = ProgramCatalogue.from_programs(sys.executable, name="pipeline")
python = sh.make(Program(sys.executable), catalogue=catalogue)
producer = python("-c", "print('hello')")
consumer = python("-c", "import sys; print(sys.stdin.read().upper(), end='')")
result = (producer | consumer).run_sync()
assert result.ok
assert result.stdout == "HELLO\n"
assert [stage.exit_code for stage in result.stages] == [0, 0]
Run 2026-10-19 against Cuprum 0.2.0b2 · Python 3.12 · Linux

Find the stage that failed

The final stage can succeed after an earlier one has failed, so check ok or failure_index, never only the last exit code. When a stage fails while others are still running, Cuprum terminates those peers. The first observed failure is the one recorded; when two stages finish at almost the same moment, stage order breaks the tie. The pipeline job sheet shows a three-stage run failing in the middle.

Run commands side by side

run_concurrent is async; run_concurrent_sync serves synchronous callers. Bound the number of simultaneous children with ConcurrentConfig.concurrency. Results come back in submission order.

concurrent.py Tested
import sys

from cuprum import Program, ConcurrentConfig, ProgramCatalogue, run_concurrent_sync, sh

catalogue = ProgramCatalogue.from_programs(sys.executable, name="concurrent")
python = sh.make(Program(sys.executable), catalogue=catalogue)
commands = [python("-c", f"print({number})") for number in (1, 2)]
result = run_concurrent_sync(*commands, config=ConcurrentConfig(concurrency=2))
assert result.ok
assert [item.stdout for item in result.results] == ["1\n", "2\n"]
assert result.submission_indices == (0, 1)
Run 2026-10-19 against Cuprum 0.2.0b2 · Python 3.12 · Linux

With fail_fast=True, the remaining commands are cancelled after the first non-zero exit, so some may be missing from the results and a command that had already finished can still appear. failures indexes the returned tuple; submission_indices and failure_submission_indices map each result back to the command that produced it. Use those rather than assuming a result's position is its submission index.