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.
- {stagepipe-0.0.2/src/stagepipe.egg-info → stagepipe-0.0.3}/PKG-INFO +88 -11
- {stagepipe-0.0.2 → stagepipe-0.0.3}/README.md +82 -10
- {stagepipe-0.0.2 → stagepipe-0.0.3}/pyproject.toml +6 -1
- {stagepipe-0.0.2 → stagepipe-0.0.3}/src/stagepipe/__init__.py +1 -1
- {stagepipe-0.0.2 → stagepipe-0.0.3/src/stagepipe.egg-info}/PKG-INFO +88 -11
- {stagepipe-0.0.2 → stagepipe-0.0.3}/LICENSE +0 -0
- {stagepipe-0.0.2 → stagepipe-0.0.3}/setup.cfg +0 -0
- {stagepipe-0.0.2 → stagepipe-0.0.3}/src/stagepipe/pipeline.py +0 -0
- {stagepipe-0.0.2 → stagepipe-0.0.3}/src/stagepipe/py.typed +0 -0
- {stagepipe-0.0.2 → stagepipe-0.0.3}/src/stagepipe.egg-info/SOURCES.txt +0 -0
- {stagepipe-0.0.2 → stagepipe-0.0.3}/src/stagepipe.egg-info/dependency_links.txt +0 -0
- {stagepipe-0.0.2 → stagepipe-0.0.3}/src/stagepipe.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stagepipe
|
|
3
|
-
Version: 0.0.
|
|
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
|
+
[](https://pypi.org/project/stagepipe/)
|
|
32
|
+
[](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
|
|
33
|
+
[](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
|
|
34
|
+
[](https://github.com/rafaelborja/stagepipe/blob/main/LICENSE)
|
|
35
|
+
[](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`,
|
|
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.
|
|
248
|
-
replaces a hand-rolled look-ahead loop, and it passes its stress suite on CPython 3.
|
|
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
|
|
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,
|
|
295
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
+
[](https://pypi.org/project/stagepipe/)
|
|
4
|
+
[](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/rafaelborja/stagepipe/blob/main/LICENSE)
|
|
7
|
+
[](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`,
|
|
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.
|
|
225
|
-
replaces a hand-rolled look-ahead loop, and it passes its stress suite on CPython 3.
|
|
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
|
|
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,
|
|
272
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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.
|
|
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
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stagepipe
|
|
3
|
-
Version: 0.0.
|
|
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
|
+
[](https://pypi.org/project/stagepipe/)
|
|
32
|
+
[](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
|
|
33
|
+
[](https://github.com/rafaelborja/stagepipe/actions/workflows/ci.yml)
|
|
34
|
+
[](https://github.com/rafaelborja/stagepipe/blob/main/LICENSE)
|
|
35
|
+
[](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`,
|
|
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.
|
|
248
|
-
replaces a hand-rolled look-ahead loop, and it passes its stress suite on CPython 3.
|
|
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
|
|
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,
|
|
295
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|