stabilize 0.9.2__py3-none-any.whl

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.
Files changed (61) hide show
  1. stabilize/__init__.py +29 -0
  2. stabilize/cli.py +1193 -0
  3. stabilize/context/__init__.py +7 -0
  4. stabilize/context/stage_context.py +170 -0
  5. stabilize/dag/__init__.py +15 -0
  6. stabilize/dag/graph.py +215 -0
  7. stabilize/dag/topological.py +199 -0
  8. stabilize/examples/__init__.py +1 -0
  9. stabilize/examples/docker-example.py +759 -0
  10. stabilize/examples/golden-standard-expected-result.txt +1 -0
  11. stabilize/examples/golden-standard.py +488 -0
  12. stabilize/examples/http-example.py +606 -0
  13. stabilize/examples/llama-example.py +662 -0
  14. stabilize/examples/python-example.py +731 -0
  15. stabilize/examples/shell-example.py +399 -0
  16. stabilize/examples/ssh-example.py +603 -0
  17. stabilize/handlers/__init__.py +53 -0
  18. stabilize/handlers/base.py +226 -0
  19. stabilize/handlers/complete_stage.py +209 -0
  20. stabilize/handlers/complete_task.py +75 -0
  21. stabilize/handlers/complete_workflow.py +150 -0
  22. stabilize/handlers/run_task.py +369 -0
  23. stabilize/handlers/start_stage.py +262 -0
  24. stabilize/handlers/start_task.py +74 -0
  25. stabilize/handlers/start_workflow.py +136 -0
  26. stabilize/launcher.py +307 -0
  27. stabilize/migrations/01KDQ4N9QPJ6Q4MCV3V9GHWPV4_initial_schema.sql +97 -0
  28. stabilize/migrations/01KDRK3TXW4R2GERC1WBCQYJGG_rag_embeddings.sql +25 -0
  29. stabilize/migrations/__init__.py +1 -0
  30. stabilize/models/__init__.py +15 -0
  31. stabilize/models/stage.py +389 -0
  32. stabilize/models/status.py +146 -0
  33. stabilize/models/task.py +125 -0
  34. stabilize/models/workflow.py +317 -0
  35. stabilize/orchestrator.py +113 -0
  36. stabilize/persistence/__init__.py +28 -0
  37. stabilize/persistence/connection.py +185 -0
  38. stabilize/persistence/factory.py +136 -0
  39. stabilize/persistence/memory.py +214 -0
  40. stabilize/persistence/postgres.py +655 -0
  41. stabilize/persistence/sqlite.py +674 -0
  42. stabilize/persistence/store.py +235 -0
  43. stabilize/queue/__init__.py +59 -0
  44. stabilize/queue/messages.py +377 -0
  45. stabilize/queue/processor.py +312 -0
  46. stabilize/queue/queue.py +526 -0
  47. stabilize/queue/sqlite_queue.py +354 -0
  48. stabilize/rag/__init__.py +19 -0
  49. stabilize/rag/assistant.py +459 -0
  50. stabilize/rag/cache.py +294 -0
  51. stabilize/stages/__init__.py +11 -0
  52. stabilize/stages/builder.py +253 -0
  53. stabilize/tasks/__init__.py +19 -0
  54. stabilize/tasks/interface.py +335 -0
  55. stabilize/tasks/registry.py +255 -0
  56. stabilize/tasks/result.py +283 -0
  57. stabilize-0.9.2.dist-info/METADATA +301 -0
  58. stabilize-0.9.2.dist-info/RECORD +61 -0
  59. stabilize-0.9.2.dist-info/WHEEL +4 -0
  60. stabilize-0.9.2.dist-info/entry_points.txt +2 -0
  61. stabilize-0.9.2.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1 @@
1
+ PHASE1_SETUP::PHASE2A_RETRY_SUCCESS::PHASE2B_TIMEOUT_COMPENSATED::PHASE3_EVENT_RECEIVED::PHASE4_SYNC_GATE_PASSED::PHASE5_LOOP_ELSE_1::PHASE5_LOOP_IF_2::PHASE5_LOOP_ELSE_3::PHASE6_END
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Golden Standard Pipeline Test
4
+
5
+ A comprehensive deterministic workflow to stress-test all pipeline-runner features:
6
+ - Sequential execution (Phase 1 → Phase 2 → Phase 4 → Phase 5 → Phase 6)
7
+ - Parallel branches (Branch A, B, C run in parallel)
8
+ - Retry simulation (Branch A fails once, then succeeds)
9
+ - Timeout + Compensation simulation (Branch B times out, compensation runs)
10
+ - Event coordination (Branch D waits for Branch C)
11
+ - Join gate (Phase 4 waits for all branches)
12
+ - Loop expansion (Phase 5 with 3 iterations)
13
+ - Conditional logic (different output based on counter value)
14
+
15
+ Expected output:
16
+ PHASE1_SETUP::PHASE2A_RETRY_SUCCESS::PHASE2B_TIMEOUT_COMPENSATED::PHASE3_EVENT_RECEIVED::PHASE4_SYNC_GATE_PASSED::PHASE5_LOOP_ELSE_1::PHASE5_LOOP_IF_2::PHASE5_LOOP_ELSE_3::PHASE6_END
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import sys
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ # Add src to path for imports
26
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
27
+
28
+ from stabilize.context.stage_context import StageContext
29
+ from stabilize.handlers.complete_stage import CompleteStageHandler
30
+ from stabilize.handlers.complete_task import CompleteTaskHandler
31
+ from stabilize.handlers.complete_workflow import CompleteWorkflowHandler
32
+ from stabilize.handlers.run_task import RunTaskHandler
33
+ from stabilize.handlers.start_stage import StartStageHandler
34
+ from stabilize.handlers.start_task import StartTaskHandler
35
+ from stabilize.handlers.start_workflow import StartWorkflowHandler
36
+ from stabilize.models.stage import StageExecution
37
+ from stabilize.models.task import TaskExecution
38
+ from stabilize.models.workflow import Workflow
39
+ from stabilize.orchestrator import Orchestrator
40
+ from stabilize.persistence.sqlite import SqliteWorkflowStore
41
+ from stabilize.queue.processor import QueueProcessor
42
+ from stabilize.queue.sqlite_queue import SqliteQueue
43
+ from stabilize.tasks.interface import Task
44
+ from stabilize.tasks.registry import TaskRegistry
45
+ from stabilize.tasks.result import TaskResult
46
+
47
+ # =============================================================================
48
+ # Custom Task Implementations
49
+ # =============================================================================
50
+
51
+
52
+ class SetupTask(Task):
53
+ """Phase 1: Setup task that outputs PHASE1_SETUP."""
54
+
55
+ def execute(self, stage: StageExecution) -> TaskResult:
56
+ return TaskResult.success(outputs={"phase1_token": "PHASE1_SETUP"})
57
+
58
+
59
+ class RetryTask(Task):
60
+ """
61
+ Branch A: Simulates a task that fails once and succeeds on retry.
62
+
63
+ Uses RUNNING status to simulate polling behavior - first call returns
64
+ RUNNING (re-queued), second call returns SUCCESS.
65
+ """
66
+
67
+ # Class-level state to track attempts
68
+ _attempts: dict[str, int] = {}
69
+
70
+ def execute(self, stage: StageExecution) -> TaskResult:
71
+ stage_id = stage.id
72
+ attempts = RetryTask._attempts.get(stage_id, 0)
73
+ RetryTask._attempts[stage_id] = attempts + 1
74
+
75
+ if attempts < 1:
76
+ # First attempt: return RUNNING to simulate "fail and retry"
77
+ return TaskResult.running(context={"retry_attempt": attempts + 1})
78
+
79
+ # Second attempt: success
80
+ return TaskResult.success(outputs={"phase2a_token": "PHASE2A_RETRY_SUCCESS"})
81
+
82
+
83
+ class TimeoutTask(Task):
84
+ """
85
+ Branch B: Simulates a task that times out.
86
+
87
+ Returns FAILED_CONTINUE to allow pipeline to continue while
88
+ indicating this task failed. The compensation stage will run.
89
+ """
90
+
91
+ def execute(self, stage: StageExecution) -> TaskResult:
92
+ # Simulate timeout by returning FAILED_CONTINUE
93
+ # This allows the compensation stage to run
94
+ return TaskResult.failed_continue(
95
+ error="Task timed out",
96
+ outputs={"phase2b_timed_out": True},
97
+ )
98
+
99
+
100
+ class CompensationTask(Task):
101
+ """
102
+ Branch B Compensation: Runs after TimeoutTask fails.
103
+
104
+ Outputs the compensation token.
105
+ """
106
+
107
+ def execute(self, stage: StageExecution) -> TaskResult:
108
+ return TaskResult.success(outputs={"phase2b_token": "PHASE2B_TIMEOUT_COMPENSATED"})
109
+
110
+
111
+ class EventEmitterTask(Task):
112
+ """
113
+ Branch C: Simulates an event emitter.
114
+
115
+ Just completes successfully - downstream stages depend on this.
116
+ """
117
+
118
+ def execute(self, stage: StageExecution) -> TaskResult:
119
+ return TaskResult.success(outputs={"event_emitted": True})
120
+
121
+
122
+ class EventReceiverTask(Task):
123
+ """
124
+ Branch D: Receives event from Branch C.
125
+
126
+ This stage depends on Branch C (event emitter), so it only runs
127
+ after the event is "emitted" (i.e., Branch C completes).
128
+ """
129
+
130
+ def execute(self, stage: StageExecution) -> TaskResult:
131
+ return TaskResult.success(outputs={"phase3_token": "PHASE3_EVENT_RECEIVED"})
132
+
133
+
134
+ class JoinGateTask(Task):
135
+ """
136
+ Phase 4: Join gate that waits for all branches.
137
+
138
+ This stage depends on all parallel branches completing.
139
+ """
140
+
141
+ def execute(self, stage: StageExecution) -> TaskResult:
142
+ return TaskResult.success(outputs={"phase4_token": "PHASE4_SYNC_GATE_PASSED"})
143
+
144
+
145
+ class LoopIterationTask(Task):
146
+ """
147
+ Phase 5: Loop iteration with conditional logic.
148
+
149
+ Reads counter from stage context and outputs based on value:
150
+ - counter == 2: PHASE5_LOOP_IF_2
151
+ - counter != 2: PHASE5_LOOP_ELSE_{counter}
152
+ """
153
+
154
+ def execute(self, stage: StageExecution) -> TaskResult:
155
+ counter = stage.context.get("counter", 1)
156
+
157
+ if counter == 2:
158
+ token = "PHASE5_LOOP_IF_2"
159
+ else:
160
+ token = f"PHASE5_LOOP_ELSE_{counter}"
161
+
162
+ output_key = f"loop{counter}_token"
163
+ return TaskResult.success(outputs={output_key: token})
164
+
165
+
166
+ class FinalizeTask(Task):
167
+ """
168
+ Phase 6: Finalize - collect all tokens and assemble result.
169
+
170
+ Reads all tokens from ancestor outputs in the correct order
171
+ and produces the final result string.
172
+ """
173
+
174
+ def execute(self, stage: StageExecution) -> TaskResult:
175
+ context = StageContext(stage, stage.context)
176
+
177
+ # Collect tokens in expected order
178
+ tokens = [
179
+ context.get("phase1_token", "MISSING_PHASE1"),
180
+ context.get("phase2a_token", "MISSING_PHASE2A"),
181
+ context.get("phase2b_token", "MISSING_PHASE2B"),
182
+ context.get("phase3_token", "MISSING_PHASE3"),
183
+ context.get("phase4_token", "MISSING_PHASE4"),
184
+ context.get("loop1_token", "MISSING_LOOP1"),
185
+ context.get("loop2_token", "MISSING_LOOP2"),
186
+ context.get("loop3_token", "MISSING_LOOP3"),
187
+ "PHASE6_END",
188
+ ]
189
+
190
+ result = "::".join(tokens)
191
+ return TaskResult.success(outputs={"final_result": result})
192
+
193
+
194
+ # =============================================================================
195
+ # Pipeline Definition
196
+ # =============================================================================
197
+
198
+
199
+ def create_pipeline() -> Workflow:
200
+ """Create the golden standard pipeline."""
201
+ return Workflow.create(
202
+ application="golden-standard-test",
203
+ name="Golden Standard Pipeline",
204
+ stages=[
205
+ # Phase 1: Setup
206
+ StageExecution(
207
+ ref_id="1",
208
+ type="setup",
209
+ name="Phase 1: Setup",
210
+ tasks=[
211
+ TaskExecution.create(
212
+ name="Setup Task",
213
+ implementing_class="setup",
214
+ stage_start=True,
215
+ stage_end=True,
216
+ ),
217
+ ],
218
+ ),
219
+ # Branch A: Retry
220
+ StageExecution(
221
+ ref_id="2a",
222
+ type="retry",
223
+ name="Branch A: Retry",
224
+ requisite_stage_ref_ids={"1"},
225
+ tasks=[
226
+ TaskExecution.create(
227
+ name="Retry Task",
228
+ implementing_class="retry",
229
+ stage_start=True,
230
+ stage_end=True,
231
+ ),
232
+ ],
233
+ ),
234
+ # Branch B: Timeout (fails with FAILED_CONTINUE)
235
+ StageExecution(
236
+ ref_id="2b",
237
+ type="timeout",
238
+ name="Branch B: Timeout",
239
+ requisite_stage_ref_ids={"1"},
240
+ context={"continuePipelineOnFailure": True},
241
+ tasks=[
242
+ TaskExecution.create(
243
+ name="Timeout Task",
244
+ implementing_class="timeout",
245
+ stage_start=True,
246
+ stage_end=True,
247
+ ),
248
+ ],
249
+ ),
250
+ # Branch B Compensation: Runs after timeout
251
+ StageExecution(
252
+ ref_id="2b_comp",
253
+ type="compensation",
254
+ name="Branch B: Compensation",
255
+ requisite_stage_ref_ids={"2b"},
256
+ tasks=[
257
+ TaskExecution.create(
258
+ name="Compensation Task",
259
+ implementing_class="compensation",
260
+ stage_start=True,
261
+ stage_end=True,
262
+ ),
263
+ ],
264
+ ),
265
+ # Branch C: Event Emitter
266
+ StageExecution(
267
+ ref_id="2c",
268
+ type="event_emitter",
269
+ name="Branch C: Event Emitter",
270
+ requisite_stage_ref_ids={"1"},
271
+ tasks=[
272
+ TaskExecution.create(
273
+ name="Event Emitter Task",
274
+ implementing_class="event_emitter",
275
+ stage_start=True,
276
+ stage_end=True,
277
+ ),
278
+ ],
279
+ ),
280
+ # Branch D: Event Receiver (waits for C)
281
+ StageExecution(
282
+ ref_id="2d",
283
+ type="event_receiver",
284
+ name="Branch D: Event Receiver",
285
+ requisite_stage_ref_ids={"2c"},
286
+ tasks=[
287
+ TaskExecution.create(
288
+ name="Event Receiver Task",
289
+ implementing_class="event_receiver",
290
+ stage_start=True,
291
+ stage_end=True,
292
+ ),
293
+ ],
294
+ ),
295
+ # Phase 4: Join Gate (waits for all branches)
296
+ StageExecution(
297
+ ref_id="4",
298
+ type="join",
299
+ name="Phase 4: Join Gate",
300
+ requisite_stage_ref_ids={"2a", "2b_comp", "2d"},
301
+ tasks=[
302
+ TaskExecution.create(
303
+ name="Join Gate Task",
304
+ implementing_class="join_gate",
305
+ stage_start=True,
306
+ stage_end=True,
307
+ ),
308
+ ],
309
+ ),
310
+ # Phase 5: Loop Iteration 1 (counter=1)
311
+ StageExecution(
312
+ ref_id="5a",
313
+ type="loop",
314
+ name="Phase 5: Loop Iteration 1",
315
+ requisite_stage_ref_ids={"4"},
316
+ context={"counter": 1},
317
+ tasks=[
318
+ TaskExecution.create(
319
+ name="Loop Iteration Task",
320
+ implementing_class="loop_iteration",
321
+ stage_start=True,
322
+ stage_end=True,
323
+ ),
324
+ ],
325
+ ),
326
+ # Phase 5: Loop Iteration 2 (counter=2)
327
+ StageExecution(
328
+ ref_id="5b",
329
+ type="loop",
330
+ name="Phase 5: Loop Iteration 2",
331
+ requisite_stage_ref_ids={"5a"},
332
+ context={"counter": 2},
333
+ tasks=[
334
+ TaskExecution.create(
335
+ name="Loop Iteration Task",
336
+ implementing_class="loop_iteration",
337
+ stage_start=True,
338
+ stage_end=True,
339
+ ),
340
+ ],
341
+ ),
342
+ # Phase 5: Loop Iteration 3 (counter=3)
343
+ StageExecution(
344
+ ref_id="5c",
345
+ type="loop",
346
+ name="Phase 5: Loop Iteration 3",
347
+ requisite_stage_ref_ids={"5b"},
348
+ context={"counter": 3},
349
+ tasks=[
350
+ TaskExecution.create(
351
+ name="Loop Iteration Task",
352
+ implementing_class="loop_iteration",
353
+ stage_start=True,
354
+ stage_end=True,
355
+ ),
356
+ ],
357
+ ),
358
+ # Phase 6: Finalize
359
+ StageExecution(
360
+ ref_id="6",
361
+ type="finalize",
362
+ name="Phase 6: Finalize",
363
+ requisite_stage_ref_ids={"5c"},
364
+ tasks=[
365
+ TaskExecution.create(
366
+ name="Finalize Task",
367
+ implementing_class="finalize",
368
+ stage_start=True,
369
+ stage_end=True,
370
+ ),
371
+ ],
372
+ ),
373
+ ],
374
+ )
375
+
376
+
377
+ # =============================================================================
378
+ # Execution Setup
379
+ # =============================================================================
380
+
381
+
382
+ def setup_runner() -> tuple[SqliteWorkflowStore, SqliteQueue, QueueProcessor, Orchestrator]:
383
+ """Set up the pipeline runner with all components."""
384
+ # Create in-memory SQLite repository and queue
385
+ repository = SqliteWorkflowStore(
386
+ connection_string="sqlite:///:memory:",
387
+ create_tables=True,
388
+ )
389
+
390
+ queue = SqliteQueue(
391
+ connection_string="sqlite:///:memory:",
392
+ table_name="queue_messages",
393
+ )
394
+ queue._create_table()
395
+
396
+ # Register tasks
397
+ task_registry = TaskRegistry()
398
+ task_registry.register("setup", SetupTask)
399
+ task_registry.register("retry", RetryTask)
400
+ task_registry.register("timeout", TimeoutTask)
401
+ task_registry.register("compensation", CompensationTask)
402
+ task_registry.register("event_emitter", EventEmitterTask)
403
+ task_registry.register("event_receiver", EventReceiverTask)
404
+ task_registry.register("join_gate", JoinGateTask)
405
+ task_registry.register("loop_iteration", LoopIterationTask)
406
+ task_registry.register("finalize", FinalizeTask)
407
+
408
+ # Create processor and register handlers
409
+ processor = QueueProcessor(queue)
410
+
411
+ handlers: list[Any] = [
412
+ StartWorkflowHandler(queue, repository),
413
+ StartStageHandler(queue, repository),
414
+ StartTaskHandler(queue, repository),
415
+ RunTaskHandler(queue, repository, task_registry),
416
+ CompleteTaskHandler(queue, repository),
417
+ CompleteStageHandler(queue, repository),
418
+ CompleteWorkflowHandler(queue, repository),
419
+ ]
420
+
421
+ for handler in handlers:
422
+ processor.register_handler(handler)
423
+
424
+ runner = Orchestrator(queue)
425
+ return repository, queue, processor, runner
426
+
427
+
428
+ # =============================================================================
429
+ # Main Execution
430
+ # =============================================================================
431
+
432
+
433
+ def main() -> int:
434
+ """Run the golden standard pipeline and verify output."""
435
+ # Reset retry task state
436
+ RetryTask._attempts = {}
437
+
438
+ # Setup
439
+ repository, queue, processor, runner = setup_runner()
440
+
441
+ # Create and store pipeline
442
+ execution = create_pipeline()
443
+ repository.store(execution)
444
+
445
+ # Run pipeline
446
+ runner.start(execution)
447
+ processor.process_all(timeout=30.0)
448
+
449
+ # Retrieve result
450
+ result = repository.retrieve(execution.id)
451
+
452
+ # Find the finalize stage
453
+ finalize_stage = None
454
+ for stage in result.stages:
455
+ if stage.ref_id == "6":
456
+ finalize_stage = stage
457
+ break
458
+
459
+ if finalize_stage is None:
460
+ print("ERROR: Finalize stage not found!")
461
+ return 1
462
+
463
+ # Get the final result
464
+ final_result = finalize_stage.outputs.get("final_result", "")
465
+
466
+ # Print result
467
+ print(final_result)
468
+
469
+ # Load expected result
470
+ expected_path = Path(__file__).parent / "golden-standard-expected-result.txt"
471
+ if expected_path.exists():
472
+ expected = expected_path.read_text().strip()
473
+ if final_result == expected:
474
+ print("\n[PASS] Output matches expected result!")
475
+ return 0
476
+ else:
477
+ print("\n[FAIL] Output does not match!")
478
+ print(f"Expected: {expected}")
479
+ print(f"Got: {final_result}")
480
+ return 1
481
+ else:
482
+ print(f"\nWarning: Expected result file not found at {expected_path}")
483
+ print("Execution status:", result.status.name)
484
+ return 0
485
+
486
+
487
+ if __name__ == "__main__":
488
+ sys.exit(main())