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.
- stabilize/__init__.py +29 -0
- stabilize/cli.py +1193 -0
- stabilize/context/__init__.py +7 -0
- stabilize/context/stage_context.py +170 -0
- stabilize/dag/__init__.py +15 -0
- stabilize/dag/graph.py +215 -0
- stabilize/dag/topological.py +199 -0
- stabilize/examples/__init__.py +1 -0
- stabilize/examples/docker-example.py +759 -0
- stabilize/examples/golden-standard-expected-result.txt +1 -0
- stabilize/examples/golden-standard.py +488 -0
- stabilize/examples/http-example.py +606 -0
- stabilize/examples/llama-example.py +662 -0
- stabilize/examples/python-example.py +731 -0
- stabilize/examples/shell-example.py +399 -0
- stabilize/examples/ssh-example.py +603 -0
- stabilize/handlers/__init__.py +53 -0
- stabilize/handlers/base.py +226 -0
- stabilize/handlers/complete_stage.py +209 -0
- stabilize/handlers/complete_task.py +75 -0
- stabilize/handlers/complete_workflow.py +150 -0
- stabilize/handlers/run_task.py +369 -0
- stabilize/handlers/start_stage.py +262 -0
- stabilize/handlers/start_task.py +74 -0
- stabilize/handlers/start_workflow.py +136 -0
- stabilize/launcher.py +307 -0
- stabilize/migrations/01KDQ4N9QPJ6Q4MCV3V9GHWPV4_initial_schema.sql +97 -0
- stabilize/migrations/01KDRK3TXW4R2GERC1WBCQYJGG_rag_embeddings.sql +25 -0
- stabilize/migrations/__init__.py +1 -0
- stabilize/models/__init__.py +15 -0
- stabilize/models/stage.py +389 -0
- stabilize/models/status.py +146 -0
- stabilize/models/task.py +125 -0
- stabilize/models/workflow.py +317 -0
- stabilize/orchestrator.py +113 -0
- stabilize/persistence/__init__.py +28 -0
- stabilize/persistence/connection.py +185 -0
- stabilize/persistence/factory.py +136 -0
- stabilize/persistence/memory.py +214 -0
- stabilize/persistence/postgres.py +655 -0
- stabilize/persistence/sqlite.py +674 -0
- stabilize/persistence/store.py +235 -0
- stabilize/queue/__init__.py +59 -0
- stabilize/queue/messages.py +377 -0
- stabilize/queue/processor.py +312 -0
- stabilize/queue/queue.py +526 -0
- stabilize/queue/sqlite_queue.py +354 -0
- stabilize/rag/__init__.py +19 -0
- stabilize/rag/assistant.py +459 -0
- stabilize/rag/cache.py +294 -0
- stabilize/stages/__init__.py +11 -0
- stabilize/stages/builder.py +253 -0
- stabilize/tasks/__init__.py +19 -0
- stabilize/tasks/interface.py +335 -0
- stabilize/tasks/registry.py +255 -0
- stabilize/tasks/result.py +283 -0
- stabilize-0.9.2.dist-info/METADATA +301 -0
- stabilize-0.9.2.dist-info/RECORD +61 -0
- stabilize-0.9.2.dist-info/WHEEL +4 -0
- stabilize-0.9.2.dist-info/entry_points.txt +2 -0
- 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())
|