stagegate 0.1.0__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.
@@ -0,0 +1,19 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ docs/spec
12
+ AGENTS.md
13
+ plans.md
14
+ uv.lock
15
+ .gitignore
16
+ .python-version
17
+
18
+ code.md
19
+ test.md
stagegate-0.1.0/API.md ADDED
@@ -0,0 +1,685 @@
1
+ # stagegate API Reference
2
+
3
+ ## Top-Level Namespace
4
+
5
+ ```python
6
+ import stagegate
7
+ ```
8
+
9
+ Public names:
10
+
11
+ - `Scheduler`
12
+ - `Pipeline`
13
+ - `TaskHandle`
14
+ - `PipelineHandle`
15
+ - `FIRST_COMPLETED`
16
+ - `FIRST_EXCEPTION`
17
+ - `ALL_COMPLETED`
18
+ - `CancelledError`
19
+ - `UnknownResourceError`
20
+ - `UnschedulableTaskError`
21
+
22
+ ## `Scheduler`
23
+
24
+ ```python
25
+ stagegate.Scheduler(
26
+ *,
27
+ resources: dict[str, int | float],
28
+ pipeline_parallelism: int = 1,
29
+ task_parallelism: int | None = None,
30
+ )
31
+ ```
32
+
33
+ Create a single-process scheduler.
34
+
35
+ ### Constructor arguments
36
+
37
+ - `resources: dict[str, int | float]`
38
+ Abstract resource capacities such as `{"cpu": 16, "mem": 64}`.
39
+ These are scheduler admission labels, not OS-enforced quotas.
40
+ - `pipeline_parallelism: int = 1`
41
+ Maximum number of pipeline coordinator threads that may run `Pipeline.run()` concurrently.
42
+ - `task_parallelism: int | None = None`
43
+ Maximum number of tasks that may be admitted concurrently.
44
+ If `None`, the effective worker count is `1`.
45
+
46
+ ### Context manager behavior
47
+
48
+ `Scheduler` can be used in a `with` block:
49
+
50
+ ```python
51
+ with stagegate.Scheduler(resources={"cpu": 8}) as scheduler:
52
+ ...
53
+ ```
54
+
55
+ Leaving the block calls `close()`.
56
+
57
+ ### `Scheduler.run_pipeline(pipeline)`
58
+
59
+ ```python
60
+ run_pipeline(pipeline: Pipeline) -> PipelineHandle
61
+ ```
62
+
63
+ Submit a pipeline instance for FIFO execution.
64
+
65
+ Arguments:
66
+
67
+ - `pipeline`
68
+ A `Pipeline` instance.
69
+
70
+ Returns:
71
+
72
+ - `PipelineHandle`
73
+ A handle for observing, waiting for, or cancelling that pipeline submission.
74
+
75
+ Raises:
76
+
77
+ - `TypeError`
78
+ If `pipeline` is not a `Pipeline` instance.
79
+ - `RuntimeError`
80
+ If shutdown has already started.
81
+ - `RuntimeError`
82
+ If the same pipeline instance has already been submitted once before.
83
+
84
+ Notes:
85
+
86
+ - the same pipeline instance is single-use
87
+ - queue order is FIFO
88
+ - pipeline execution begins later on a scheduler-owned coordinator thread
89
+
90
+ ### `Scheduler.wait_pipelines(handles, timeout=None, return_when=...)`
91
+
92
+ ```python
93
+ wait_pipelines(
94
+ handles,
95
+ timeout: float | None = None,
96
+ return_when: str = stagegate.ALL_COMPLETED,
97
+ ) -> tuple[set[PipelineHandle], set[PipelineHandle]]
98
+ ```
99
+
100
+ Wait on pipeline handles owned by the same scheduler.
101
+
102
+ Arguments:
103
+
104
+ - `handles`
105
+ An iterable of `PipelineHandle` objects created by this scheduler.
106
+ - `timeout`
107
+ Maximum wait time in seconds.
108
+ `None` means wait indefinitely.
109
+ `0` means immediate poll.
110
+ - `return_when`
111
+ One of `stagegate.FIRST_COMPLETED`, `stagegate.FIRST_EXCEPTION`, or `stagegate.ALL_COMPLETED`.
112
+
113
+ Returns:
114
+
115
+ - `tuple[set[PipelineHandle], set[PipelineHandle]]`
116
+ A pair `(done, pending)`.
117
+ - `done`
118
+ The subset of input handles that are terminal at the moment the call returns.
119
+ A pipeline handle is terminal when it is `SUCCEEDED`, `FAILED`, or `CANCELLED`.
120
+ - `pending`
121
+ The subset of input handles that are still non-terminal at the moment the call returns.
122
+
123
+ Important details:
124
+
125
+ - timeout does not raise; it returns the current `(done, pending)`
126
+ - duplicate handles are deduplicated before waiting
127
+ - already terminal handles are placed in `done` immediately
128
+ - this method does not raise the pipeline's stored execution exception
129
+
130
+ Raises:
131
+
132
+ - `ValueError`
133
+ If `handles` is empty.
134
+ - `TypeError`
135
+ If any element is not a `PipelineHandle`.
136
+ - `ValueError`
137
+ If any handle belongs to a different scheduler.
138
+ - `ValueError`
139
+ If `timeout < 0`.
140
+ - `ValueError`
141
+ If `return_when` is invalid.
142
+
143
+ ### `Scheduler.shutdown(cancel_pending_pipelines=False)`
144
+
145
+ ```python
146
+ shutdown(
147
+ cancel_pending_pipelines: bool = False,
148
+ ) -> None
149
+ ```
150
+
151
+ Start shutdown.
152
+
153
+ Arguments:
154
+
155
+ - `cancel_pending_pipelines`
156
+ If `True`, cancel pipelines that are still queued and not yet started.
157
+ Running pipelines are not cancelled.
158
+
159
+ Behavior:
160
+
161
+ - returns after shutdown initiation
162
+ - does not wait for in-flight work to finish
163
+ - does not stop or join internal coordinator, dispatcher, or worker threads
164
+ - after shutdown starts, new pipeline submission is rejected
165
+ - already running pipelines continue
166
+ - already running pipelines may still submit tasks on their coordinator thread
167
+ - existing handles remain usable
168
+ - repeated shutdown calls are allowed
169
+
170
+ Related predicates:
171
+
172
+ ```python
173
+ shutdown_started() -> bool
174
+ closed() -> bool
175
+ ```
176
+
177
+ Return values:
178
+
179
+ - `shutdown_started()`
180
+ `True` once shutdown has begun.
181
+ - `closed()`
182
+ `True` only after `close()` completes full shutdown.
183
+
184
+ ### `Scheduler.close(cancel_pending_pipelines=False)`
185
+
186
+ ```python
187
+ close(
188
+ cancel_pending_pipelines: bool = False,
189
+ ) -> None
190
+ ```
191
+
192
+ Fully close the scheduler.
193
+
194
+ Arguments:
195
+
196
+ - `cancel_pending_pipelines`
197
+ If `True`, cancel pipelines that are still queued and not yet started.
198
+ Running pipelines are not cancelled.
199
+
200
+ Behavior:
201
+
202
+ - starts shutdown if it has not started already
203
+ - waits until no live work remains
204
+ - joins the scheduler-owned runtime threads that were started
205
+ - returns only after the scheduler is fully closed
206
+ - already running pipelines continue
207
+ - already running pipelines may still submit tasks on their coordinator thread
208
+ - existing handles remain usable
209
+ - repeated close calls are allowed
210
+ - later calls made before the scheduler reaches `CLOSED` may strengthen the request by adding `cancel_pending_pipelines=True`
211
+
212
+ Raises:
213
+
214
+ - `RuntimeError`
215
+ If called from a scheduler-owned runtime thread such as a pipeline coordinator thread, worker thread, or dispatcher thread.
216
+
217
+ Related predicates:
218
+
219
+ ```python
220
+ shutdown_started() -> bool
221
+ closed() -> bool
222
+ ```
223
+
224
+ Return values:
225
+
226
+ - `shutdown_started()`
227
+ `True` once shutdown has begun.
228
+ - `closed()`
229
+ `True` once no live work remains and the scheduler-owned runtime threads have terminated.
230
+
231
+ ## `Pipeline`
232
+
233
+ Subclass `stagegate.Pipeline` and implement `run()`.
234
+
235
+ ```python
236
+ class MyPipeline(stagegate.Pipeline):
237
+ def run(self):
238
+ ...
239
+ ```
240
+
241
+ Pipeline control APIs are valid only while `run()` is actively executing on the scheduler-owned coordinator thread.
242
+
243
+ ### `Pipeline.run()`
244
+
245
+ ```python
246
+ run() -> Any
247
+ ```
248
+
249
+ User-defined pipeline body.
250
+
251
+ Return value:
252
+
253
+ - any Python object
254
+ If `run()` returns normally, that value becomes the success result of the `PipelineHandle`.
255
+
256
+ Failure behavior:
257
+
258
+ - if `run()` raises, the pipeline becomes `FAILED`
259
+ - the original exception is stored on the handle and re-raised by `PipelineHandle.result()`
260
+
261
+ ### `Pipeline.task(...)`
262
+
263
+ ```python
264
+ task(
265
+ fn,
266
+ *,
267
+ resources: dict[str, int | float],
268
+ args: tuple = (),
269
+ kwargs: dict | None = None,
270
+ name: str | None = None,
271
+ ) -> TaskBuilder
272
+ ```
273
+
274
+ Create a task builder.
275
+ The task is not submitted until `.run()` is called on the returned builder.
276
+
277
+ Arguments:
278
+
279
+ - `fn`
280
+ The callable to execute later on a worker thread.
281
+ - `resources`
282
+ Abstract resource requirements for admission control.
283
+ - `args`
284
+ Positional arguments passed to `fn`.
285
+ - `kwargs`
286
+ Keyword arguments passed to `fn`.
287
+ - `name`
288
+ Optional user-facing label for debugging or future diagnostics.
289
+
290
+ Returns:
291
+
292
+ - `TaskBuilder`
293
+ A builder/factory object.
294
+ Calling `.run()` on it submits the task and returns a `TaskHandle`.
295
+
296
+ Raises on builder `.run()`:
297
+
298
+ - `RuntimeError`
299
+ If the pipeline is not currently running.
300
+ - `RuntimeError`
301
+ If the call is not made from the pipeline's coordinator thread.
302
+ - `UnknownResourceError`
303
+ If any resource label is unknown.
304
+ - `UnschedulableTaskError`
305
+ If a single-task requirement exceeds configured capacity.
306
+ - `ValueError`
307
+ If a resource amount is non-numeric, non-finite, or negative.
308
+
309
+ ### `Pipeline.stage_forward()`
310
+
311
+ ```python
312
+ stage_forward() -> None
313
+ ```
314
+
315
+ Advance the internal stage by one.
316
+
317
+ Effects:
318
+
319
+ - later task submissions get the new stage snapshot
320
+ - already queued tasks keep the priority captured when they were submitted
321
+
322
+ Raises:
323
+
324
+ - `RuntimeError`
325
+ If called before pipeline execution starts.
326
+ - `RuntimeError`
327
+ If called after pipeline execution ends.
328
+ - `RuntimeError`
329
+ If called from any thread other than the pipeline's coordinator thread.
330
+
331
+ ### `Pipeline.wait(handles, timeout=None, return_when=...)`
332
+
333
+ ```python
334
+ wait(
335
+ handles,
336
+ timeout: float | None = None,
337
+ return_when: str = stagegate.ALL_COMPLETED,
338
+ ) -> tuple[set[TaskHandle], set[TaskHandle]]
339
+ ```
340
+
341
+ Wait on task handles created by that pipeline.
342
+
343
+ Arguments:
344
+
345
+ - `handles`
346
+ An iterable of `TaskHandle` objects created by this pipeline.
347
+ - `timeout`
348
+ Maximum wait time in seconds.
349
+ `None` means wait indefinitely.
350
+ `0` means immediate poll.
351
+ - `return_when`
352
+ One of `stagegate.FIRST_COMPLETED`, `stagegate.FIRST_EXCEPTION`, or `stagegate.ALL_COMPLETED`.
353
+
354
+ Returns:
355
+
356
+ - `tuple[set[TaskHandle], set[TaskHandle]]`
357
+ A pair `(done, pending)`.
358
+ - `done`
359
+ The subset of input handles that are terminal when the call returns.
360
+ - `pending`
361
+ The subset of input handles that are still non-terminal when the call returns.
362
+
363
+ Important details:
364
+
365
+ - timeout does not raise; it returns the current `(done, pending)`
366
+ - duplicate handles are deduplicated before waiting
367
+ - already terminal handles are placed in `done` immediately
368
+ - this method does not raise the task's stored execution exception
369
+
370
+ Raises:
371
+
372
+ - `RuntimeError`
373
+ If called outside the active pipeline coordinator-thread context.
374
+ - `ValueError`
375
+ If `handles` is empty.
376
+ - `TypeError`
377
+ If any element is not a `TaskHandle`.
378
+ - `ValueError`
379
+ If any handle belongs to a different pipeline.
380
+ - `ValueError`
381
+ If `timeout < 0`.
382
+ - `ValueError`
383
+ If `return_when` is invalid.
384
+
385
+ ## `TaskHandle`
386
+
387
+ Handle for one submitted task.
388
+
389
+ ### `TaskHandle.cancel()`
390
+
391
+ ```python
392
+ cancel() -> bool
393
+ ```
394
+
395
+ Return value:
396
+
397
+ - `True`
398
+ The task had not started yet and was transitioned to `CANCELLED`.
399
+ - `False`
400
+ The task was already running or already terminal.
401
+
402
+ Important detail:
403
+
404
+ - cancellation is non-preemptive
405
+ - running tasks are never force-killed
406
+
407
+ ### `TaskHandle.done()`
408
+
409
+ ```python
410
+ done() -> bool
411
+ ```
412
+
413
+ Return value:
414
+
415
+ - `True`
416
+ The task is terminal: `SUCCEEDED`, `FAILED`, or `CANCELLED`.
417
+ - `False`
418
+ The task is still queued, ready, or running.
419
+
420
+ ### `TaskHandle.running()`
421
+
422
+ ```python
423
+ running() -> bool
424
+ ```
425
+
426
+ Return value:
427
+
428
+ - `True`
429
+ The task callable is actively executing on a worker thread.
430
+ - `False`
431
+ Otherwise.
432
+
433
+ ### `TaskHandle.cancelled()`
434
+
435
+ ```python
436
+ cancelled() -> bool
437
+ ```
438
+
439
+ Return value:
440
+
441
+ - `True`
442
+ The task ended in the cancelled state.
443
+ - `False`
444
+ Otherwise.
445
+
446
+ ### `TaskHandle.result(timeout=None)`
447
+
448
+ ```python
449
+ result(timeout: float | None = None) -> Any
450
+ ```
451
+
452
+ Arguments:
453
+
454
+ - `timeout`
455
+ Maximum wait time in seconds.
456
+ `None` means wait indefinitely.
457
+ `0` means immediate check.
458
+
459
+ Return value:
460
+
461
+ - the task callable's normal return value
462
+
463
+ Raises:
464
+
465
+ - `TimeoutError`
466
+ If the task is not terminal before the timeout expires.
467
+ - `CancelledError`
468
+ If the task was cancelled.
469
+ - original task exception
470
+ If the task failed by raising.
471
+ - `ValueError`
472
+ If `timeout < 0`.
473
+
474
+ ### `TaskHandle.exception(timeout=None)`
475
+
476
+ ```python
477
+ exception(timeout: float | None = None) -> BaseException | None
478
+ ```
479
+
480
+ Arguments:
481
+
482
+ - `timeout`
483
+ Maximum wait time in seconds.
484
+ `None` means wait indefinitely.
485
+ `0` means immediate check.
486
+
487
+ Return value:
488
+
489
+ - `None`
490
+ If the task succeeded.
491
+ - the stored exception object
492
+ If the task failed.
493
+
494
+ Raises:
495
+
496
+ - `TimeoutError`
497
+ If the task is not terminal before the timeout expires.
498
+ - `CancelledError`
499
+ If the task was cancelled.
500
+ - `ValueError`
501
+ If `timeout < 0`.
502
+
503
+ ## `PipelineHandle`
504
+
505
+ Handle for one submitted pipeline.
506
+
507
+ ### `PipelineHandle.cancel()`
508
+
509
+ ```python
510
+ cancel() -> bool
511
+ ```
512
+
513
+ Return value:
514
+
515
+ - `True`
516
+ The pipeline had not started yet and was transitioned to `CANCELLED`.
517
+ - `False`
518
+ The pipeline was already running or already terminal.
519
+
520
+ Important detail:
521
+
522
+ - started pipelines are not force-stopped
523
+
524
+ ### `PipelineHandle.done()`
525
+
526
+ ```python
527
+ done() -> bool
528
+ ```
529
+
530
+ Return value:
531
+
532
+ - `True`
533
+ The pipeline is terminal: `SUCCEEDED`, `FAILED`, or `CANCELLED`.
534
+ - `False`
535
+ The pipeline is still queued or running.
536
+
537
+ ### `PipelineHandle.running()`
538
+
539
+ ```python
540
+ running() -> bool
541
+ ```
542
+
543
+ Return value:
544
+
545
+ - `True`
546
+ The pipeline `run()` method is actively executing.
547
+ - `False`
548
+ Otherwise.
549
+
550
+ ### `PipelineHandle.cancelled()`
551
+
552
+ ```python
553
+ cancelled() -> bool
554
+ ```
555
+
556
+ Return value:
557
+
558
+ - `True`
559
+ The pipeline ended in the cancelled state.
560
+ - `False`
561
+ Otherwise.
562
+
563
+ ### `PipelineHandle.result(timeout=None)`
564
+
565
+ ```python
566
+ result(timeout: float | None = None) -> Any
567
+ ```
568
+
569
+ Arguments:
570
+
571
+ - `timeout`
572
+ Maximum wait time in seconds.
573
+ `None` means wait indefinitely.
574
+ `0` means immediate check.
575
+
576
+ Return value:
577
+
578
+ - the normal return value of `Pipeline.run()`
579
+
580
+ Raises:
581
+
582
+ - `TimeoutError`
583
+ If the pipeline is not terminal before the timeout expires.
584
+ - `CancelledError`
585
+ If the pipeline was cancelled.
586
+ - original pipeline exception
587
+ If the pipeline failed by raising.
588
+ - `ValueError`
589
+ If `timeout < 0`.
590
+
591
+ ### `PipelineHandle.exception(timeout=None)`
592
+
593
+ ```python
594
+ exception(timeout: float | None = None) -> BaseException | None
595
+ ```
596
+
597
+ Arguments:
598
+
599
+ - `timeout`
600
+ Maximum wait time in seconds.
601
+ `None` means wait indefinitely.
602
+ `0` means immediate check.
603
+
604
+ Return value:
605
+
606
+ - `None`
607
+ If the pipeline succeeded.
608
+ - the stored exception object
609
+ If the pipeline failed.
610
+
611
+ Raises:
612
+
613
+ - `TimeoutError`
614
+ If the pipeline is not terminal before the timeout expires.
615
+ - `CancelledError`
616
+ If the pipeline was cancelled.
617
+ - `ValueError`
618
+ If `timeout < 0`.
619
+
620
+ ## Wait Constants
621
+
622
+ ```python
623
+ stagegate.FIRST_COMPLETED
624
+ stagegate.FIRST_EXCEPTION
625
+ stagegate.ALL_COMPLETED
626
+ ```
627
+
628
+ These constants are used by both:
629
+
630
+ - `Pipeline.wait(...)`
631
+ - `Scheduler.wait_pipelines(...)`
632
+
633
+ ### `FIRST_COMPLETED`
634
+
635
+ Return once at least one handle is terminal.
636
+
637
+ Terminal means:
638
+
639
+ - `SUCCEEDED`
640
+ - `FAILED`
641
+ - `CANCELLED`
642
+
643
+ ### `FIRST_EXCEPTION`
644
+
645
+ Return once at least one handle is `FAILED`.
646
+
647
+ Important detail:
648
+
649
+ - `CANCELLED` does not count as an exception trigger
650
+ - if no handle ever fails, this behaves like `ALL_COMPLETED`
651
+
652
+ ### `ALL_COMPLETED`
653
+
654
+ Return once every handle is terminal.
655
+
656
+ ## Exceptions
657
+
658
+ ### `CancelledError`
659
+
660
+ Raised by `result()` and `exception()` on cancelled task or pipeline handles.
661
+
662
+ ### `UnknownResourceError`
663
+
664
+ Raised when task submission requests a resource label unknown to the scheduler.
665
+
666
+ ### `UnschedulableTaskError`
667
+
668
+ Raised when a single task requires more of some resource than the scheduler can ever provide.
669
+
670
+ ## Minimal Example
671
+
672
+ ```python
673
+ import stagegate
674
+
675
+
676
+ class Demo(stagegate.Pipeline):
677
+ def run(self) -> str:
678
+ handle = self.task(lambda: "ok", resources={"cpu": 1}).run()
679
+ return handle.result()
680
+
681
+
682
+ with stagegate.Scheduler(resources={"cpu": 2}) as scheduler:
683
+ pipeline_handle = scheduler.run_pipeline(Demo())
684
+ print(pipeline_handle.result())
685
+ ```
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ttkkmg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.