stagepipe 0.0.2__tar.gz → 0.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stagepipe
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Staged pipelines for blocking Python code: per-stage thread workers, no stage barrier, ordered stages, bounded in-flight. Standard library only.
5
5
  Author: Rafael Borja
6
6
  License-Expression: Apache-2.0
@@ -14,6 +14,11 @@ Classifier: Intended Audience :: Developers
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
17
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
23
  Classifier: Typing :: Typed
19
24
  Requires-Python: >=3.10
@@ -23,6 +28,12 @@ Dynamic: license-file
23
28
 
24
29
  # stagepipe
25
30
 
31
+ [![PyPI](https://img.shields.io/pypi/v/stagepipe)](https://pypi.org/project/stagepipe/)
32
+ [![CI](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml/badge.svg)](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
33
+ [![Python](https://img.shields.io/badge/python-3.10%20to%203.14-blue)](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
34
+ [![License](https://img.shields.io/pypi/l/stagepipe)](https://github.com/rafaelborja/stagepipe/blob/main/LICENSE)
35
+ [![Downloads](https://img.shields.io/pypi/dm/stagepipe)](https://pypistats.org/packages/stagepipe)
36
+
26
37
  **Run your slow, blocking steps in parallel, and let every item move on the moment the next step is free.**
27
38
 
28
39
  A tiny pipeline for plain Python: no event loop, no server, no dependencies, about 130 lines of
@@ -88,6 +99,11 @@ aiostream is excellent and well maintained, and most of that difference is simpl
88
99
  being loaded. If you already run an event loop it costs you nothing extra. If you do not, and
89
100
  memory is tight, a thread-and-queue design is the lighter tool. Reproduce with `bench/memory.py`.
90
101
 
102
+ Continuous integration repeats the footprint measurement on Linux, Windows and macOS for every
103
+ Python from 3.10 to 3.14 (see `bench/footprint.py` and the run summaries). Across those 15
104
+ combinations, importing stagepipe cost between 1.6 and 2.6 MB of resident memory and the
105
+ machinery cost between 2 and 17 microseconds per item per stage.
106
+
91
107
  **Seconds, not nanoseconds.** stagepipe is for pipelines whose steps take milliseconds to minutes
92
108
  (an OCR page, an API call, a transcode), where running in parallel saves *seconds*. Ten
93
109
  microseconds of bookkeeping per item disappears next to that. So whenever speed and footprint
@@ -171,12 +187,39 @@ docs = run(urls, [
171
187
  | `ordered=True` | The stage sees items in index order (needs `workers=1`). Use it when a stage carries state across items. Earlier stages still run out of order. |
172
188
  | `max_in_flight=M` | At most M items are being processed at any moment. Bounds memory and keeps stages balanced. |
173
189
  | `cancelled=callable` | Checked before every call; when it returns true the run stops and raises `Cancelled`. |
174
- | `on_done=callable` | Called with `(index, value)` as each item leaves the last stage. Good for progress bars. **It runs in a worker thread**, and concurrently if the last stage has several workers, so it must be thread-safe (or make the last stage `ordered`, which has exactly one worker). |
190
+ | `on_done=callable` | Called with `(index, value)` as each item leaves the last stage. Good for progress bars. **It runs in a worker thread**, and concurrently if the last stage has several workers, so it must be thread-safe (or make the last stage `ordered`, which has exactly one worker). If it raises, the run is aborted and the exception is re-raised from `run()`, like an error in a stage. |
175
191
 
176
192
  If any stage raises, new work stops, workers drain, and the first exception is re-raised from `run()`.
177
193
 
178
194
  ---
179
195
 
196
+ ## Examples
197
+
198
+ Two runnable scripts in [`examples/`](https://github.com/rafaelborja/stagepipe/tree/main/examples).
199
+ Both run straight from a clone, with no install and no extra packages.
200
+
201
+ **A pizza kitchen** (`examples/pizza_kitchen.py`), the playful one. Eight orders go through dough,
202
+ toppings, two ovens and a boxing station. It prints when each pizza leaves, then a timeline of
203
+ what every station was doing, and compares with a kitchen where each station finishes the whole
204
+ batch first:
205
+
206
+ ```
207
+ dough 222223333355555...........788888....................................
208
+ toppings .....22222233333335555555......7888888..............................
209
+ oven ...........111111111111113333333333333555555555555588888888888888...
210
+ box .........................12...........34...........56...........788.
211
+
212
+ Kitchen 1 finished in 2.02s, kitchen 2 in 3.25s: 1.6x faster, and the first pizza left
213
+ after 0.78s instead of 3.25s.
214
+ ```
215
+
216
+ **Compress a folder** (`examples/compress_folder.py`), the useful one. Read, gzip and write every
217
+ file in a folder as separate stages, with `max_in_flight=6` so only six files are ever in memory,
218
+ however big the folder is. On 24 files it ran about **4x faster than one file at a time**.
219
+ Point it at your own folders with `python examples/compress_folder.py SRC OUT`.
220
+
221
+ ---
222
+
180
223
  ## Where it fits
181
224
 
182
225
  stagepipe is for **blocking work that spends its time waiting or in native code**, which is
@@ -225,7 +268,31 @@ These come from running stagepipe on a real five-stage document pipeline.
225
268
 
226
269
  - Not a job queue or scheduler: no broker, no workers on other machines, no persistence (yet, see the roadmap).
227
270
  - Not a workflow engine with branching graphs (yet): stages form a straight line.
228
- - Not async. If your code is already `async`, use [aiostream](https://github.com/vxgmichel/aiostream).
271
+ - Not async: there is no event loop. If your code is already `async`, see the next section.
272
+
273
+ ---
274
+
275
+ ## Which one should I use?
276
+
277
+ **If your code is already async, use [aiostream](https://github.com/vxgmichel/aiostream).**
278
+ "Already async" means your steps are `async def` functions that you `await`: an async HTTP client,
279
+ an async database driver, `asyncio` tasks. Your program already runs an event loop, and
280
+ aiostream chains those steps on it directly. Pushing async code through threads would be a step
281
+ backwards, and stagepipe would add nothing.
282
+
283
+ **If your steps are ordinary blocking functions, use stagepipe.** OCR engines, onnxruntime,
284
+ PyTorch, `requests`, PDF and image libraries, subprocesses: plain `def` functions you can call
285
+ and test on their own. There is no event loop to start and nothing to wrap in `async`.
286
+
287
+ Two more reasons to pick stagepipe, both about running small:
288
+
289
+ - **Small memory footprint.** No event loop and no heavy imports. Loading `asyncio` alone costs
290
+ about 6 MB of resident memory on CPython 3.12; stagepipe is about 2 MB today and the roadmap
291
+ takes it to a few hundred KB. That matters in serverless functions and on small devices.
292
+ - **Surviving a crash without a database** (planned for 0.2, not in 0.0.x yet). The design is
293
+ plain files: one small file per finished item and an append-only journal, with memory use that
294
+ does not grow with the number of items. aiostream has no persistence of any kind, so with it
295
+ you would build that yourself.
229
296
 
230
297
  ---
231
298
 
@@ -244,8 +311,8 @@ Checked on Python 3.12 in a clean virtual environment, with blocking functions.
244
311
 
245
312
  ## Status and known limits
246
313
 
247
- **Alpha, version 0.0.2.** It was extracted from a working document-processing pipeline where it
248
- replaces a hand-rolled look-ahead loop, and it passes its stress suite on CPython 3.12 and 3.15.
314
+ **Alpha, version 0.0.3.** It was extracted from a working document-processing pipeline where it
315
+ replaces a hand-rolled look-ahead loop, and it passes its stress suite in CI on CPython 3.10 to 3.14 on Linux, Windows and macOS (and locally on the 3.15 release candidate).
249
316
  In its first real use, five stages with different worker counts and at most 8 items in flight
250
317
  gave output identical to the sequential loop, and cancelling then resuming worked.
251
318
 
@@ -287,12 +354,16 @@ do not cover and that the older thread-based libraries have left behind.
287
354
  are handed over as they finish;
288
355
  - workers let go of the last item they handled as soon as they are idle;
289
356
  - optional smaller thread stacks for very small containers.
290
- - [ ] Failure policy per run: `collect` failed items and finish the rest, `skip`, or `retry(n, backoff)`.
357
+ - [ ] Failure policy: `on_error` per stage or per run (`collect` failed items and finish the rest, `skip`,
358
+ `raise`, or `retry(n, backoff)`), so one bad item never kills a long job. *Asked for by both
359
+ production users, who each wrapped every stage function in try/except.*
360
+ - [ ] Partial results on cancel: return what finished, with a marker for unfinished items.
291
361
  - [ ] Per-worker setup: `Stage(init=...)`, run once in each worker thread, e.g. one model
292
362
  session per worker. *Asked for by the first real user, who had to build it by hand with
293
363
  `threading.local()`.*
294
- - [ ] A bottleneck report after every run: per stage, busy time against waiting time, items
295
- done and queue depth, so the stage to scale is obvious. *Also asked for by the first real
364
+ - [ ] A bottleneck report after every run: per stage and per item, **queue wait time** (item ready, no
365
+ free worker) against run time, busy and idle totals, items done and queue depth, so the
366
+ stage to scale is obvious. Wait time cannot be measured from inside a stage function. *Also asked for by the first real
296
367
  user, who timed inside their stage functions.*
297
368
 
298
369
  ### Then: make it safe to rerun and easy to watch (0.2)
@@ -302,7 +373,9 @@ do not cover and that the older thread-based libraries have left behind.
302
373
  - [ ] **Events for dashboards**: a stable, versioned event stream (enter/exit per stage, worker,
303
374
  duration, queue depth) written as JSON lines or sent to a callback. A dashboard can be a
304
375
  separate project that just reads it.
305
- - [ ] Shared limits across stages, e.g. a `gpu=1` slot that three stages compete for.
376
+ - [ ] Shared limits across stages, e.g. a `gpu=1` slot that three stages compete for, and
377
+ `Stage(limiter=<Semaphore>)` so one semaphore caps a resource across several runs in the same
378
+ process (three runs of 3 workers must not put 9 calls on a service that accepts 4).
306
379
  - [ ] A memory budget in bytes, not only an item count.
307
380
 
308
381
  ### Later: bigger shapes (0.3 and beyond)
@@ -315,8 +388,10 @@ do not cover and that the older thread-based libraries have left behind.
315
388
  - [ ] Verified on free-threaded Python builds.
316
389
 
317
390
  ### Python versions
318
- Developed against CPython 3.12 and tested on 3.15 (release candidate). Python 3.15's lazy
319
- imports (PEP 810) are on the list: the optional features above will load only when used, so
391
+ Tested in CI on CPython 3.10, 3.11, 3.12, 3.13 and 3.14, on Linux, Windows and macOS, and the
392
+ release workflow only publishes if the whole matrix passes. Python 3.15 works locally (release
393
+ candidate) and joins the CI matrix and the release workflow as soon as GitHub's runners offer it.
394
+ Python 3.15's lazy imports (PEP 810) are on the list: the optional features above will load only when used, so
320
395
  `import stagepipe` stays near-instant.
321
396
 
322
397
  Want something on this list sooner, or something that is missing? Open an
@@ -333,6 +408,8 @@ pip install -e .
333
408
  python tests/stress.py # plain script, no pytest needed
334
409
  python bench/bench.py # speed numbers above
335
410
  python bench/memory.py # memory numbers above (needs psutil; aiostream optional)
411
+ python bench/footprint.py # memory and time on your Python and OS
412
+ python examples/pizza_kitchen.py
336
413
  ```
337
414
 
338
415
  ## Changelog
@@ -1,5 +1,11 @@
1
1
  # stagepipe
2
2
 
3
+ [![PyPI](https://img.shields.io/pypi/v/stagepipe)](https://pypi.org/project/stagepipe/)
4
+ [![CI](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml/badge.svg)](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
5
+ [![Python](https://img.shields.io/badge/python-3.10%20to%203.14-blue)](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
6
+ [![License](https://img.shields.io/pypi/l/stagepipe)](https://github.com/rafaelborja/stagepipe/blob/main/LICENSE)
7
+ [![Downloads](https://img.shields.io/pypi/dm/stagepipe)](https://pypistats.org/packages/stagepipe)
8
+
3
9
  **Run your slow, blocking steps in parallel, and let every item move on the moment the next step is free.**
4
10
 
5
11
  A tiny pipeline for plain Python: no event loop, no server, no dependencies, about 130 lines of
@@ -65,6 +71,11 @@ aiostream is excellent and well maintained, and most of that difference is simpl
65
71
  being loaded. If you already run an event loop it costs you nothing extra. If you do not, and
66
72
  memory is tight, a thread-and-queue design is the lighter tool. Reproduce with `bench/memory.py`.
67
73
 
74
+ Continuous integration repeats the footprint measurement on Linux, Windows and macOS for every
75
+ Python from 3.10 to 3.14 (see `bench/footprint.py` and the run summaries). Across those 15
76
+ combinations, importing stagepipe cost between 1.6 and 2.6 MB of resident memory and the
77
+ machinery cost between 2 and 17 microseconds per item per stage.
78
+
68
79
  **Seconds, not nanoseconds.** stagepipe is for pipelines whose steps take milliseconds to minutes
69
80
  (an OCR page, an API call, a transcode), where running in parallel saves *seconds*. Ten
70
81
  microseconds of bookkeeping per item disappears next to that. So whenever speed and footprint
@@ -148,12 +159,39 @@ docs = run(urls, [
148
159
  | `ordered=True` | The stage sees items in index order (needs `workers=1`). Use it when a stage carries state across items. Earlier stages still run out of order. |
149
160
  | `max_in_flight=M` | At most M items are being processed at any moment. Bounds memory and keeps stages balanced. |
150
161
  | `cancelled=callable` | Checked before every call; when it returns true the run stops and raises `Cancelled`. |
151
- | `on_done=callable` | Called with `(index, value)` as each item leaves the last stage. Good for progress bars. **It runs in a worker thread**, and concurrently if the last stage has several workers, so it must be thread-safe (or make the last stage `ordered`, which has exactly one worker). |
162
+ | `on_done=callable` | Called with `(index, value)` as each item leaves the last stage. Good for progress bars. **It runs in a worker thread**, and concurrently if the last stage has several workers, so it must be thread-safe (or make the last stage `ordered`, which has exactly one worker). If it raises, the run is aborted and the exception is re-raised from `run()`, like an error in a stage. |
152
163
 
153
164
  If any stage raises, new work stops, workers drain, and the first exception is re-raised from `run()`.
154
165
 
155
166
  ---
156
167
 
168
+ ## Examples
169
+
170
+ Two runnable scripts in [`examples/`](https://github.com/rafaelborja/stagepipe/tree/main/examples).
171
+ Both run straight from a clone, with no install and no extra packages.
172
+
173
+ **A pizza kitchen** (`examples/pizza_kitchen.py`), the playful one. Eight orders go through dough,
174
+ toppings, two ovens and a boxing station. It prints when each pizza leaves, then a timeline of
175
+ what every station was doing, and compares with a kitchen where each station finishes the whole
176
+ batch first:
177
+
178
+ ```
179
+ dough 222223333355555...........788888....................................
180
+ toppings .....22222233333335555555......7888888..............................
181
+ oven ...........111111111111113333333333333555555555555588888888888888...
182
+ box .........................12...........34...........56...........788.
183
+
184
+ Kitchen 1 finished in 2.02s, kitchen 2 in 3.25s: 1.6x faster, and the first pizza left
185
+ after 0.78s instead of 3.25s.
186
+ ```
187
+
188
+ **Compress a folder** (`examples/compress_folder.py`), the useful one. Read, gzip and write every
189
+ file in a folder as separate stages, with `max_in_flight=6` so only six files are ever in memory,
190
+ however big the folder is. On 24 files it ran about **4x faster than one file at a time**.
191
+ Point it at your own folders with `python examples/compress_folder.py SRC OUT`.
192
+
193
+ ---
194
+
157
195
  ## Where it fits
158
196
 
159
197
  stagepipe is for **blocking work that spends its time waiting or in native code**, which is
@@ -202,7 +240,31 @@ These come from running stagepipe on a real five-stage document pipeline.
202
240
 
203
241
  - Not a job queue or scheduler: no broker, no workers on other machines, no persistence (yet, see the roadmap).
204
242
  - Not a workflow engine with branching graphs (yet): stages form a straight line.
205
- - Not async. If your code is already `async`, use [aiostream](https://github.com/vxgmichel/aiostream).
243
+ - Not async: there is no event loop. If your code is already `async`, see the next section.
244
+
245
+ ---
246
+
247
+ ## Which one should I use?
248
+
249
+ **If your code is already async, use [aiostream](https://github.com/vxgmichel/aiostream).**
250
+ "Already async" means your steps are `async def` functions that you `await`: an async HTTP client,
251
+ an async database driver, `asyncio` tasks. Your program already runs an event loop, and
252
+ aiostream chains those steps on it directly. Pushing async code through threads would be a step
253
+ backwards, and stagepipe would add nothing.
254
+
255
+ **If your steps are ordinary blocking functions, use stagepipe.** OCR engines, onnxruntime,
256
+ PyTorch, `requests`, PDF and image libraries, subprocesses: plain `def` functions you can call
257
+ and test on their own. There is no event loop to start and nothing to wrap in `async`.
258
+
259
+ Two more reasons to pick stagepipe, both about running small:
260
+
261
+ - **Small memory footprint.** No event loop and no heavy imports. Loading `asyncio` alone costs
262
+ about 6 MB of resident memory on CPython 3.12; stagepipe is about 2 MB today and the roadmap
263
+ takes it to a few hundred KB. That matters in serverless functions and on small devices.
264
+ - **Surviving a crash without a database** (planned for 0.2, not in 0.0.x yet). The design is
265
+ plain files: one small file per finished item and an append-only journal, with memory use that
266
+ does not grow with the number of items. aiostream has no persistence of any kind, so with it
267
+ you would build that yourself.
206
268
 
207
269
  ---
208
270
 
@@ -221,8 +283,8 @@ Checked on Python 3.12 in a clean virtual environment, with blocking functions.
221
283
 
222
284
  ## Status and known limits
223
285
 
224
- **Alpha, version 0.0.2.** It was extracted from a working document-processing pipeline where it
225
- replaces a hand-rolled look-ahead loop, and it passes its stress suite on CPython 3.12 and 3.15.
286
+ **Alpha, version 0.0.3.** It was extracted from a working document-processing pipeline where it
287
+ replaces a hand-rolled look-ahead loop, and it passes its stress suite in CI on CPython 3.10 to 3.14 on Linux, Windows and macOS (and locally on the 3.15 release candidate).
226
288
  In its first real use, five stages with different worker counts and at most 8 items in flight
227
289
  gave output identical to the sequential loop, and cancelling then resuming worked.
228
290
 
@@ -264,12 +326,16 @@ do not cover and that the older thread-based libraries have left behind.
264
326
  are handed over as they finish;
265
327
  - workers let go of the last item they handled as soon as they are idle;
266
328
  - optional smaller thread stacks for very small containers.
267
- - [ ] Failure policy per run: `collect` failed items and finish the rest, `skip`, or `retry(n, backoff)`.
329
+ - [ ] Failure policy: `on_error` per stage or per run (`collect` failed items and finish the rest, `skip`,
330
+ `raise`, or `retry(n, backoff)`), so one bad item never kills a long job. *Asked for by both
331
+ production users, who each wrapped every stage function in try/except.*
332
+ - [ ] Partial results on cancel: return what finished, with a marker for unfinished items.
268
333
  - [ ] Per-worker setup: `Stage(init=...)`, run once in each worker thread, e.g. one model
269
334
  session per worker. *Asked for by the first real user, who had to build it by hand with
270
335
  `threading.local()`.*
271
- - [ ] A bottleneck report after every run: per stage, busy time against waiting time, items
272
- done and queue depth, so the stage to scale is obvious. *Also asked for by the first real
336
+ - [ ] A bottleneck report after every run: per stage and per item, **queue wait time** (item ready, no
337
+ free worker) against run time, busy and idle totals, items done and queue depth, so the
338
+ stage to scale is obvious. Wait time cannot be measured from inside a stage function. *Also asked for by the first real
273
339
  user, who timed inside their stage functions.*
274
340
 
275
341
  ### Then: make it safe to rerun and easy to watch (0.2)
@@ -279,7 +345,9 @@ do not cover and that the older thread-based libraries have left behind.
279
345
  - [ ] **Events for dashboards**: a stable, versioned event stream (enter/exit per stage, worker,
280
346
  duration, queue depth) written as JSON lines or sent to a callback. A dashboard can be a
281
347
  separate project that just reads it.
282
- - [ ] Shared limits across stages, e.g. a `gpu=1` slot that three stages compete for.
348
+ - [ ] Shared limits across stages, e.g. a `gpu=1` slot that three stages compete for, and
349
+ `Stage(limiter=<Semaphore>)` so one semaphore caps a resource across several runs in the same
350
+ process (three runs of 3 workers must not put 9 calls on a service that accepts 4).
283
351
  - [ ] A memory budget in bytes, not only an item count.
284
352
 
285
353
  ### Later: bigger shapes (0.3 and beyond)
@@ -292,8 +360,10 @@ do not cover and that the older thread-based libraries have left behind.
292
360
  - [ ] Verified on free-threaded Python builds.
293
361
 
294
362
  ### Python versions
295
- Developed against CPython 3.12 and tested on 3.15 (release candidate). Python 3.15's lazy
296
- imports (PEP 810) are on the list: the optional features above will load only when used, so
363
+ Tested in CI on CPython 3.10, 3.11, 3.12, 3.13 and 3.14, on Linux, Windows and macOS, and the
364
+ release workflow only publishes if the whole matrix passes. Python 3.15 works locally (release
365
+ candidate) and joins the CI matrix and the release workflow as soon as GitHub's runners offer it.
366
+ Python 3.15's lazy imports (PEP 810) are on the list: the optional features above will load only when used, so
297
367
  `import stagepipe` stays near-instant.
298
368
 
299
369
  Want something on this list sooner, or something that is missing? Open an
@@ -310,6 +380,8 @@ pip install -e .
310
380
  python tests/stress.py # plain script, no pytest needed
311
381
  python bench/bench.py # speed numbers above
312
382
  python bench/memory.py # memory numbers above (needs psutil; aiostream optional)
383
+ python bench/footprint.py # memory and time on your Python and OS
384
+ python examples/pizza_kitchen.py
313
385
  ```
314
386
 
315
387
  ## Changelog
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stagepipe"
7
- version = "0.0.2"
7
+ version = "0.0.3"
8
8
  description = "Staged pipelines for blocking Python code: per-stage thread workers, no stage barrier, ordered stages, bounded in-flight. Standard library only."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -18,6 +18,11 @@ classifiers = [
18
18
  "Operating System :: OS Independent",
19
19
  "Programming Language :: Python :: 3",
20
20
  "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
21
26
  "Topic :: Software Development :: Libraries :: Python Modules",
22
27
  "Typing :: Typed",
23
28
  ]
@@ -2,4 +2,4 @@
2
2
  from .pipeline import Cancelled, Stage, run
3
3
 
4
4
  __all__ = ["Cancelled", "Stage", "run"]
5
- __version__ = "0.0.2"
5
+ __version__ = "0.0.3"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stagepipe
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Staged pipelines for blocking Python code: per-stage thread workers, no stage barrier, ordered stages, bounded in-flight. Standard library only.
5
5
  Author: Rafael Borja
6
6
  License-Expression: Apache-2.0
@@ -14,6 +14,11 @@ Classifier: Intended Audience :: Developers
14
14
  Classifier: Operating System :: OS Independent
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
17
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
23
  Classifier: Typing :: Typed
19
24
  Requires-Python: >=3.10
@@ -23,6 +28,12 @@ Dynamic: license-file
23
28
 
24
29
  # stagepipe
25
30
 
31
+ [![PyPI](https://img.shields.io/pypi/v/stagepipe)](https://pypi.org/project/stagepipe/)
32
+ [![CI](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml/badge.svg)](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
33
+ [![Python](https://img.shields.io/badge/python-3.10%20to%203.14-blue)](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
34
+ [![License](https://img.shields.io/pypi/l/stagepipe)](https://github.com/rafaelborja/stagepipe/blob/main/LICENSE)
35
+ [![Downloads](https://img.shields.io/pypi/dm/stagepipe)](https://pypistats.org/packages/stagepipe)
36
+
26
37
  **Run your slow, blocking steps in parallel, and let every item move on the moment the next step is free.**
27
38
 
28
39
  A tiny pipeline for plain Python: no event loop, no server, no dependencies, about 130 lines of
@@ -88,6 +99,11 @@ aiostream is excellent and well maintained, and most of that difference is simpl
88
99
  being loaded. If you already run an event loop it costs you nothing extra. If you do not, and
89
100
  memory is tight, a thread-and-queue design is the lighter tool. Reproduce with `bench/memory.py`.
90
101
 
102
+ Continuous integration repeats the footprint measurement on Linux, Windows and macOS for every
103
+ Python from 3.10 to 3.14 (see `bench/footprint.py` and the run summaries). Across those 15
104
+ combinations, importing stagepipe cost between 1.6 and 2.6 MB of resident memory and the
105
+ machinery cost between 2 and 17 microseconds per item per stage.
106
+
91
107
  **Seconds, not nanoseconds.** stagepipe is for pipelines whose steps take milliseconds to minutes
92
108
  (an OCR page, an API call, a transcode), where running in parallel saves *seconds*. Ten
93
109
  microseconds of bookkeeping per item disappears next to that. So whenever speed and footprint
@@ -171,12 +187,39 @@ docs = run(urls, [
171
187
  | `ordered=True` | The stage sees items in index order (needs `workers=1`). Use it when a stage carries state across items. Earlier stages still run out of order. |
172
188
  | `max_in_flight=M` | At most M items are being processed at any moment. Bounds memory and keeps stages balanced. |
173
189
  | `cancelled=callable` | Checked before every call; when it returns true the run stops and raises `Cancelled`. |
174
- | `on_done=callable` | Called with `(index, value)` as each item leaves the last stage. Good for progress bars. **It runs in a worker thread**, and concurrently if the last stage has several workers, so it must be thread-safe (or make the last stage `ordered`, which has exactly one worker). |
190
+ | `on_done=callable` | Called with `(index, value)` as each item leaves the last stage. Good for progress bars. **It runs in a worker thread**, and concurrently if the last stage has several workers, so it must be thread-safe (or make the last stage `ordered`, which has exactly one worker). If it raises, the run is aborted and the exception is re-raised from `run()`, like an error in a stage. |
175
191
 
176
192
  If any stage raises, new work stops, workers drain, and the first exception is re-raised from `run()`.
177
193
 
178
194
  ---
179
195
 
196
+ ## Examples
197
+
198
+ Two runnable scripts in [`examples/`](https://github.com/rafaelborja/stagepipe/tree/main/examples).
199
+ Both run straight from a clone, with no install and no extra packages.
200
+
201
+ **A pizza kitchen** (`examples/pizza_kitchen.py`), the playful one. Eight orders go through dough,
202
+ toppings, two ovens and a boxing station. It prints when each pizza leaves, then a timeline of
203
+ what every station was doing, and compares with a kitchen where each station finishes the whole
204
+ batch first:
205
+
206
+ ```
207
+ dough 222223333355555...........788888....................................
208
+ toppings .....22222233333335555555......7888888..............................
209
+ oven ...........111111111111113333333333333555555555555588888888888888...
210
+ box .........................12...........34...........56...........788.
211
+
212
+ Kitchen 1 finished in 2.02s, kitchen 2 in 3.25s: 1.6x faster, and the first pizza left
213
+ after 0.78s instead of 3.25s.
214
+ ```
215
+
216
+ **Compress a folder** (`examples/compress_folder.py`), the useful one. Read, gzip and write every
217
+ file in a folder as separate stages, with `max_in_flight=6` so only six files are ever in memory,
218
+ however big the folder is. On 24 files it ran about **4x faster than one file at a time**.
219
+ Point it at your own folders with `python examples/compress_folder.py SRC OUT`.
220
+
221
+ ---
222
+
180
223
  ## Where it fits
181
224
 
182
225
  stagepipe is for **blocking work that spends its time waiting or in native code**, which is
@@ -225,7 +268,31 @@ These come from running stagepipe on a real five-stage document pipeline.
225
268
 
226
269
  - Not a job queue or scheduler: no broker, no workers on other machines, no persistence (yet, see the roadmap).
227
270
  - Not a workflow engine with branching graphs (yet): stages form a straight line.
228
- - Not async. If your code is already `async`, use [aiostream](https://github.com/vxgmichel/aiostream).
271
+ - Not async: there is no event loop. If your code is already `async`, see the next section.
272
+
273
+ ---
274
+
275
+ ## Which one should I use?
276
+
277
+ **If your code is already async, use [aiostream](https://github.com/vxgmichel/aiostream).**
278
+ "Already async" means your steps are `async def` functions that you `await`: an async HTTP client,
279
+ an async database driver, `asyncio` tasks. Your program already runs an event loop, and
280
+ aiostream chains those steps on it directly. Pushing async code through threads would be a step
281
+ backwards, and stagepipe would add nothing.
282
+
283
+ **If your steps are ordinary blocking functions, use stagepipe.** OCR engines, onnxruntime,
284
+ PyTorch, `requests`, PDF and image libraries, subprocesses: plain `def` functions you can call
285
+ and test on their own. There is no event loop to start and nothing to wrap in `async`.
286
+
287
+ Two more reasons to pick stagepipe, both about running small:
288
+
289
+ - **Small memory footprint.** No event loop and no heavy imports. Loading `asyncio` alone costs
290
+ about 6 MB of resident memory on CPython 3.12; stagepipe is about 2 MB today and the roadmap
291
+ takes it to a few hundred KB. That matters in serverless functions and on small devices.
292
+ - **Surviving a crash without a database** (planned for 0.2, not in 0.0.x yet). The design is
293
+ plain files: one small file per finished item and an append-only journal, with memory use that
294
+ does not grow with the number of items. aiostream has no persistence of any kind, so with it
295
+ you would build that yourself.
229
296
 
230
297
  ---
231
298
 
@@ -244,8 +311,8 @@ Checked on Python 3.12 in a clean virtual environment, with blocking functions.
244
311
 
245
312
  ## Status and known limits
246
313
 
247
- **Alpha, version 0.0.2.** It was extracted from a working document-processing pipeline where it
248
- replaces a hand-rolled look-ahead loop, and it passes its stress suite on CPython 3.12 and 3.15.
314
+ **Alpha, version 0.0.3.** It was extracted from a working document-processing pipeline where it
315
+ replaces a hand-rolled look-ahead loop, and it passes its stress suite in CI on CPython 3.10 to 3.14 on Linux, Windows and macOS (and locally on the 3.15 release candidate).
249
316
  In its first real use, five stages with different worker counts and at most 8 items in flight
250
317
  gave output identical to the sequential loop, and cancelling then resuming worked.
251
318
 
@@ -287,12 +354,16 @@ do not cover and that the older thread-based libraries have left behind.
287
354
  are handed over as they finish;
288
355
  - workers let go of the last item they handled as soon as they are idle;
289
356
  - optional smaller thread stacks for very small containers.
290
- - [ ] Failure policy per run: `collect` failed items and finish the rest, `skip`, or `retry(n, backoff)`.
357
+ - [ ] Failure policy: `on_error` per stage or per run (`collect` failed items and finish the rest, `skip`,
358
+ `raise`, or `retry(n, backoff)`), so one bad item never kills a long job. *Asked for by both
359
+ production users, who each wrapped every stage function in try/except.*
360
+ - [ ] Partial results on cancel: return what finished, with a marker for unfinished items.
291
361
  - [ ] Per-worker setup: `Stage(init=...)`, run once in each worker thread, e.g. one model
292
362
  session per worker. *Asked for by the first real user, who had to build it by hand with
293
363
  `threading.local()`.*
294
- - [ ] A bottleneck report after every run: per stage, busy time against waiting time, items
295
- done and queue depth, so the stage to scale is obvious. *Also asked for by the first real
364
+ - [ ] A bottleneck report after every run: per stage and per item, **queue wait time** (item ready, no
365
+ free worker) against run time, busy and idle totals, items done and queue depth, so the
366
+ stage to scale is obvious. Wait time cannot be measured from inside a stage function. *Also asked for by the first real
296
367
  user, who timed inside their stage functions.*
297
368
 
298
369
  ### Then: make it safe to rerun and easy to watch (0.2)
@@ -302,7 +373,9 @@ do not cover and that the older thread-based libraries have left behind.
302
373
  - [ ] **Events for dashboards**: a stable, versioned event stream (enter/exit per stage, worker,
303
374
  duration, queue depth) written as JSON lines or sent to a callback. A dashboard can be a
304
375
  separate project that just reads it.
305
- - [ ] Shared limits across stages, e.g. a `gpu=1` slot that three stages compete for.
376
+ - [ ] Shared limits across stages, e.g. a `gpu=1` slot that three stages compete for, and
377
+ `Stage(limiter=<Semaphore>)` so one semaphore caps a resource across several runs in the same
378
+ process (three runs of 3 workers must not put 9 calls on a service that accepts 4).
306
379
  - [ ] A memory budget in bytes, not only an item count.
307
380
 
308
381
  ### Later: bigger shapes (0.3 and beyond)
@@ -315,8 +388,10 @@ do not cover and that the older thread-based libraries have left behind.
315
388
  - [ ] Verified on free-threaded Python builds.
316
389
 
317
390
  ### Python versions
318
- Developed against CPython 3.12 and tested on 3.15 (release candidate). Python 3.15's lazy
319
- imports (PEP 810) are on the list: the optional features above will load only when used, so
391
+ Tested in CI on CPython 3.10, 3.11, 3.12, 3.13 and 3.14, on Linux, Windows and macOS, and the
392
+ release workflow only publishes if the whole matrix passes. Python 3.15 works locally (release
393
+ candidate) and joins the CI matrix and the release workflow as soon as GitHub's runners offer it.
394
+ Python 3.15's lazy imports (PEP 810) are on the list: the optional features above will load only when used, so
320
395
  `import stagepipe` stays near-instant.
321
396
 
322
397
  Want something on this list sooner, or something that is missing? Open an
@@ -333,6 +408,8 @@ pip install -e .
333
408
  python tests/stress.py # plain script, no pytest needed
334
409
  python bench/bench.py # speed numbers above
335
410
  python bench/memory.py # memory numbers above (needs psutil; aiostream optional)
411
+ python bench/footprint.py # memory and time on your Python and OS
412
+ python examples/pizza_kitchen.py
336
413
  ```
337
414
 
338
415
  ## Changelog
File without changes
File without changes