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,226 @@
1
+ """
2
+ Base message handler classes.
3
+
4
+ This module provides the base classes for all message handlers in the
5
+ pipeline execution engine.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import time
12
+ from abc import ABC, abstractmethod
13
+ from collections.abc import Callable
14
+ from datetime import timedelta
15
+ from typing import TYPE_CHECKING, Generic, TypeVar
16
+
17
+ from stabilize.models.stage import StageExecution
18
+ from stabilize.models.task import TaskExecution
19
+ from stabilize.models.workflow import Workflow
20
+ from stabilize.queue.messages import (
21
+ CompleteWorkflow,
22
+ ContinueParentStage,
23
+ InvalidStageId,
24
+ InvalidTaskId,
25
+ InvalidWorkflowId,
26
+ Message,
27
+ StageLevel,
28
+ StartStage,
29
+ TaskLevel,
30
+ WorkflowLevel,
31
+ )
32
+
33
+ if TYPE_CHECKING:
34
+ from stabilize.persistence.store import WorkflowStore
35
+ from stabilize.queue.queue import Queue
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ M = TypeVar("M", bound=Message)
40
+
41
+
42
+ class MessageHandler(ABC, Generic[M]):
43
+ """
44
+ Base class for message handlers.
45
+
46
+ Each handler processes a specific type of message.
47
+ """
48
+
49
+ @property
50
+ @abstractmethod
51
+ def message_type(self) -> type[M]:
52
+ """Return the type of message this handler processes."""
53
+ pass
54
+
55
+ @abstractmethod
56
+ def handle(self, message: M) -> None:
57
+ """Handle a message."""
58
+ pass
59
+
60
+
61
+ class StabilizeHandler(MessageHandler[M], ABC):
62
+ """
63
+ Base handler with common utilities.
64
+
65
+ Provides helper methods for retrieving executions, stages, and tasks,
66
+ as well as the startNext() implementation.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ queue: Queue,
72
+ repository: WorkflowStore,
73
+ retry_delay: timedelta = timedelta(seconds=15),
74
+ ) -> None:
75
+ self.queue = queue
76
+ self.repository = repository
77
+ self.retry_delay = retry_delay
78
+
79
+ # ========== Execution Retrieval ==========
80
+
81
+ def with_execution(
82
+ self,
83
+ message: WorkflowLevel,
84
+ block: Callable[[Workflow], None],
85
+ ) -> None:
86
+ """
87
+ Execute a block with the execution for a message.
88
+
89
+ Args:
90
+ message: Message containing execution ID
91
+ block: Function to call with the execution
92
+ """
93
+ try:
94
+ execution = self.repository.retrieve(message.execution_id)
95
+ block(execution)
96
+ except Exception as e:
97
+ logger.error("Failed to retrieve execution %s: %s", message.execution_id, e)
98
+ self.queue.push(
99
+ InvalidWorkflowId(
100
+ execution_type=message.execution_type,
101
+ execution_id=message.execution_id,
102
+ )
103
+ )
104
+
105
+ def with_stage(
106
+ self,
107
+ message: StageLevel,
108
+ block: Callable[[StageExecution], None],
109
+ ) -> None:
110
+ """
111
+ Execute a block with the stage for a message.
112
+
113
+ Args:
114
+ message: Message containing stage ID
115
+ block: Function to call with the stage
116
+ """
117
+
118
+ def on_execution(execution: Workflow) -> None:
119
+ try:
120
+ stage = execution.stage_by_id(message.stage_id)
121
+ block(stage)
122
+ except ValueError:
123
+ logger.error("Stage not found: %s", message.stage_id)
124
+ self.queue.push(
125
+ InvalidStageId(
126
+ execution_type=message.execution_type,
127
+ execution_id=message.execution_id,
128
+ stage_id=message.stage_id,
129
+ )
130
+ )
131
+
132
+ self.with_execution(message, on_execution)
133
+
134
+ def with_task(
135
+ self,
136
+ message: TaskLevel,
137
+ block: Callable[[StageExecution, TaskExecution], None],
138
+ ) -> None:
139
+ """
140
+ Execute a block with the stage and task for a message.
141
+
142
+ Args:
143
+ message: Message containing task ID
144
+ block: Function to call with (stage, task)
145
+ """
146
+
147
+ def on_stage(stage: StageExecution) -> None:
148
+ task = self._find_task(stage, message.task_id)
149
+ if task is None:
150
+ logger.error("Task not found: %s", message.task_id)
151
+ self.queue.push(
152
+ InvalidTaskId(
153
+ execution_type=message.execution_type,
154
+ execution_id=message.execution_id,
155
+ stage_id=message.stage_id,
156
+ task_id=message.task_id,
157
+ )
158
+ )
159
+ else:
160
+ block(stage, task)
161
+
162
+ self.with_stage(message, on_stage)
163
+
164
+ def _find_task(
165
+ self,
166
+ stage: StageExecution,
167
+ task_id: str,
168
+ ) -> TaskExecution | None:
169
+ """Find a task by ID in a stage."""
170
+ for task in stage.tasks:
171
+ if task.id == task_id:
172
+ return task
173
+ return None
174
+
175
+ # ========== Stage Navigation ==========
176
+
177
+ def start_next(self, stage: StageExecution) -> None:
178
+ """
179
+ Start the next stage(s) after a stage completes.
180
+
181
+ This is the critical method for DAG traversal:
182
+ 1. Find downstream stages (those that depend on this stage)
183
+ 2. Push StartStage for each downstream stage
184
+ 3. If this is a synthetic stage, notify parent
185
+ 4. If no downstream and not synthetic, complete execution
186
+ """
187
+ execution = stage.execution
188
+ downstream_stages = stage.downstream_stages()
189
+ phase = stage.synthetic_stage_owner
190
+
191
+ if downstream_stages:
192
+ # Start all downstream stages
193
+ for downstream in downstream_stages:
194
+ self.queue.push(
195
+ StartStage(
196
+ execution_type=execution.type.value,
197
+ execution_id=execution.id,
198
+ stage_id=downstream.id,
199
+ )
200
+ )
201
+ elif phase is not None:
202
+ # Synthetic stage - notify parent
203
+ parent = stage.parent()
204
+ self.queue.ensure(
205
+ ContinueParentStage(
206
+ execution_type=execution.type.value,
207
+ execution_id=execution.id,
208
+ stage_id=parent.id,
209
+ phase=phase,
210
+ ),
211
+ timedelta(seconds=0),
212
+ )
213
+ else:
214
+ # Top-level stage with no downstream - complete execution
215
+ self.queue.push(
216
+ CompleteWorkflow(
217
+ execution_type=execution.type.value,
218
+ execution_id=execution.id,
219
+ )
220
+ )
221
+
222
+ # ========== Utility Methods ==========
223
+
224
+ def current_time_millis(self) -> int:
225
+ """Get current time in milliseconds."""
226
+ return int(time.time() * 1000)
@@ -0,0 +1,209 @@
1
+ """
2
+ CompleteStageHandler - handles stage completion.
3
+
4
+ This is a critical handler that:
5
+ 1. Determines stage status from tasks and synthetic stages
6
+ 2. Plans and starts after stages
7
+ 3. Plans and starts on-failure stages
8
+ 4. Triggers downstream stages via startNext()
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from typing import TYPE_CHECKING
15
+
16
+ from stabilize.handlers.base import StabilizeHandler
17
+ from stabilize.models.status import WorkflowStatus
18
+ from stabilize.queue.messages import (
19
+ CancelStage,
20
+ CompleteStage,
21
+ CompleteWorkflow,
22
+ StartStage,
23
+ )
24
+
25
+ if TYPE_CHECKING:
26
+ from stabilize.models.stage import StageExecution
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class CompleteStageHandler(StabilizeHandler[CompleteStage]):
32
+ """
33
+ Handler for CompleteStage messages.
34
+
35
+ Execution flow:
36
+ 1. Check if stage already complete
37
+ 2. Determine status from synthetic stages and tasks
38
+ 3. If success: Plan and start after stages
39
+ 4. If failure: Plan and start on-failure stages
40
+ 5. Update stage status and end time
41
+ 6. If status allows continuation: startNext()
42
+ 7. Otherwise: CancelStage + CompleteWorkflow
43
+ """
44
+
45
+ @property
46
+ def message_type(self) -> type[CompleteStage]:
47
+ return CompleteStage
48
+
49
+ def handle(self, message: CompleteStage) -> None:
50
+ """Handle the CompleteStage message."""
51
+
52
+ def on_stage(stage: StageExecution) -> None:
53
+ # Check if already complete
54
+ if stage.status not in {WorkflowStatus.RUNNING, WorkflowStatus.NOT_STARTED}:
55
+ logger.debug(
56
+ "Stage %s already has status %s, ignoring CompleteStage",
57
+ stage.name,
58
+ stage.status,
59
+ )
60
+ return
61
+
62
+ try:
63
+ # Determine status from tasks and synthetic stages
64
+ status = stage.determine_status()
65
+
66
+ # Handle after stages
67
+ if status.is_complete and not status.is_halt:
68
+ after_stages = stage.first_after_stages()
69
+ if not after_stages:
70
+ self._plan_after_stages(stage)
71
+ after_stages = stage.first_after_stages()
72
+
73
+ not_started = [s for s in after_stages if s.status == WorkflowStatus.NOT_STARTED]
74
+ if not_started:
75
+ for s in not_started:
76
+ self.queue.push(
77
+ StartStage(
78
+ execution_type=message.execution_type,
79
+ execution_id=message.execution_id,
80
+ stage_id=s.id,
81
+ )
82
+ )
83
+ return
84
+
85
+ # If status is NOT_STARTED with no after stages, it's weird
86
+ if status == WorkflowStatus.NOT_STARTED:
87
+ logger.warning("Stage %s had no tasks or synthetic stages", stage.name)
88
+ status = WorkflowStatus.SKIPPED
89
+
90
+ # Handle failure - plan on-failure stages
91
+ elif status.is_failure:
92
+ has_on_failure = self._plan_on_failure_stages(stage)
93
+ if has_on_failure:
94
+ after_stages = stage.first_after_stages()
95
+ for s in after_stages:
96
+ self.queue.push(
97
+ StartStage(
98
+ execution_type=message.execution_type,
99
+ execution_id=message.execution_id,
100
+ stage_id=s.id,
101
+ )
102
+ )
103
+ return
104
+
105
+ # Update stage status
106
+ stage.status = status
107
+ stage.end_time = self.current_time_millis()
108
+ self.repository.store_stage(stage)
109
+
110
+ logger.info("Stage %s completed with status %s", stage.name, status)
111
+
112
+ # Handle FAILED_CONTINUE propagation to parent
113
+ if (
114
+ status == WorkflowStatus.FAILED_CONTINUE
115
+ and stage.synthetic_stage_owner is not None
116
+ and not stage.allow_sibling_stages_to_continue_on_failure
117
+ and stage.parent_stage_id is not None
118
+ ):
119
+ # Propagate failure to parent
120
+ self.queue.push(
121
+ CompleteStage(
122
+ execution_type=message.execution_type,
123
+ execution_id=message.execution_id,
124
+ stage_id=stage.parent_stage_id,
125
+ )
126
+ )
127
+ elif status in {
128
+ WorkflowStatus.SUCCEEDED,
129
+ WorkflowStatus.FAILED_CONTINUE,
130
+ WorkflowStatus.SKIPPED,
131
+ }:
132
+ # Continue to downstream stages
133
+ self.start_next(stage)
134
+ else:
135
+ # Failure - cancel and complete execution
136
+ self.queue.push(
137
+ CancelStage(
138
+ execution_type=message.execution_type,
139
+ execution_id=message.execution_id,
140
+ stage_id=message.stage_id,
141
+ )
142
+ )
143
+ if stage.synthetic_stage_owner is None or stage.parent_stage_id is None:
144
+ self.queue.push(
145
+ CompleteWorkflow(
146
+ execution_type=message.execution_type,
147
+ execution_id=message.execution_id,
148
+ )
149
+ )
150
+ else:
151
+ # Propagate to parent
152
+ self.queue.push(
153
+ CompleteStage(
154
+ execution_type=message.execution_type,
155
+ execution_id=message.execution_id,
156
+ stage_id=stage.parent_stage_id,
157
+ )
158
+ )
159
+
160
+ except Exception as e:
161
+ logger.error(
162
+ "Error completing stage %s: %s",
163
+ stage.name,
164
+ e,
165
+ exc_info=True,
166
+ )
167
+ stage.context["exception"] = {
168
+ "details": {"error": str(e)},
169
+ }
170
+ stage.status = WorkflowStatus.TERMINAL
171
+ stage.end_time = self.current_time_millis()
172
+ self.repository.store_stage(stage)
173
+
174
+ self.queue.push(
175
+ CancelStage(
176
+ execution_type=message.execution_type,
177
+ execution_id=message.execution_id,
178
+ stage_id=message.stage_id,
179
+ )
180
+ )
181
+ self.queue.push(
182
+ CompleteWorkflow(
183
+ execution_type=message.execution_type,
184
+ execution_id=message.execution_id,
185
+ )
186
+ )
187
+
188
+ self.with_stage(message, on_stage)
189
+
190
+ def _plan_after_stages(self, stage: StageExecution) -> None:
191
+ """
192
+ Plan after stages using the stage definition builder.
193
+
194
+ TODO: Implement StageDefinitionBuilder integration
195
+ """
196
+ # For now, do nothing - after stages should be pre-defined
197
+ pass
198
+
199
+ def _plan_on_failure_stages(self, stage: StageExecution) -> bool:
200
+ """
201
+ Plan on-failure stages using the stage definition builder.
202
+
203
+ TODO: Implement StageDefinitionBuilder integration
204
+
205
+ Returns:
206
+ True if on-failure stages were added
207
+ """
208
+ # For now, return False - no on-failure stages
209
+ return False
@@ -0,0 +1,75 @@
1
+ """
2
+ CompleteTaskHandler - handles task completion.
3
+
4
+ This handler updates task status and triggers either the next task
5
+ or stage completion.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import TYPE_CHECKING
12
+
13
+ from stabilize.handlers.base import StabilizeHandler
14
+ from stabilize.queue.messages import (
15
+ CompleteStage,
16
+ CompleteTask,
17
+ StartTask,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from stabilize.models.stage import StageExecution
22
+ from stabilize.models.task import TaskExecution
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class CompleteTaskHandler(StabilizeHandler[CompleteTask]):
28
+ """
29
+ Handler for CompleteTask messages.
30
+
31
+ Execution flow:
32
+ 1. Update task status and end time
33
+ 2. If there's a next task: Push StartTask
34
+ 3. Otherwise: Push CompleteStage
35
+ """
36
+
37
+ @property
38
+ def message_type(self) -> type[CompleteTask]:
39
+ return CompleteTask
40
+
41
+ def handle(self, message: CompleteTask) -> None:
42
+ """Handle the CompleteTask message."""
43
+
44
+ def on_task(stage: StageExecution, task: TaskExecution) -> None:
45
+ # Update task status
46
+ task.status = message.status
47
+ task.end_time = self.current_time_millis()
48
+
49
+ # Save the stage
50
+ self.repository.store_stage(stage)
51
+
52
+ logger.debug("Task %s completed with status %s", task.name, message.status)
53
+
54
+ # Check for next task
55
+ next_task = stage.next_task(task)
56
+ if next_task is not None:
57
+ self.queue.push(
58
+ StartTask(
59
+ execution_type=message.execution_type,
60
+ execution_id=message.execution_id,
61
+ stage_id=message.stage_id,
62
+ task_id=next_task.id,
63
+ )
64
+ )
65
+ else:
66
+ # No more tasks - complete stage
67
+ self.queue.push(
68
+ CompleteStage(
69
+ execution_type=message.execution_type,
70
+ execution_id=message.execution_id,
71
+ stage_id=message.stage_id,
72
+ )
73
+ )
74
+
75
+ self.with_task(message, on_task)
@@ -0,0 +1,150 @@
1
+ """
2
+ CompleteWorkflowHandler - handles execution completion.
3
+
4
+ This handler determines the final execution status based on all
5
+ top-level stages and marks the execution as complete.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import TYPE_CHECKING
12
+
13
+ from stabilize.handlers.base import StabilizeHandler
14
+ from stabilize.models.status import CONTINUABLE_STATUSES, WorkflowStatus
15
+ from stabilize.queue.messages import (
16
+ CancelStage,
17
+ CompleteWorkflow,
18
+ StartWaitingWorkflows,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from stabilize.models.stage import StageExecution
23
+ from stabilize.models.workflow import Workflow
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class CompleteWorkflowHandler(StabilizeHandler[CompleteWorkflow]):
29
+ """
30
+ Handler for CompleteWorkflow messages.
31
+
32
+ Execution flow:
33
+ 1. Check if execution already complete
34
+ 2. Determine final status from top-level stages
35
+ 3. Update execution status
36
+ 4. Cancel any running stages if failed
37
+ 5. Start waiting executions if queue is enabled
38
+ """
39
+
40
+ @property
41
+ def message_type(self) -> type[CompleteWorkflow]:
42
+ return CompleteWorkflow
43
+
44
+ def handle(self, message: CompleteWorkflow) -> None:
45
+ """Handle the CompleteWorkflow message."""
46
+
47
+ def on_execution(execution: Workflow) -> None:
48
+ # Check if already complete
49
+ if execution.status.is_complete:
50
+ logger.debug(
51
+ "Execution %s already complete with status %s",
52
+ execution.id,
53
+ execution.status,
54
+ )
55
+ return
56
+
57
+ # Determine final status
58
+ status = self._determine_final_status(execution, message)
59
+ if status is None:
60
+ # Not ready to complete - stages still running
61
+ return
62
+
63
+ # Update execution
64
+ execution.status = status
65
+ execution.end_time = self.current_time_millis()
66
+ self.repository.update_status(execution)
67
+
68
+ logger.info("Execution %s completed with status %s", execution.id, status)
69
+
70
+ # Cancel any running stages if not successful
71
+ if status != WorkflowStatus.SUCCEEDED:
72
+ running_stages = [s for s in execution.top_level_stages() if s.status == WorkflowStatus.RUNNING]
73
+ for stage in running_stages:
74
+ self.queue.push(
75
+ CancelStage(
76
+ execution_type=message.execution_type,
77
+ execution_id=message.execution_id,
78
+ stage_id=stage.id,
79
+ )
80
+ )
81
+
82
+ # Start waiting executions if configured
83
+ if execution.status != WorkflowStatus.RUNNING and execution.pipeline_config_id:
84
+ self.queue.push(
85
+ StartWaitingWorkflows(
86
+ pipeline_config_id=execution.pipeline_config_id,
87
+ purge_queue=not execution.keep_waiting_pipelines,
88
+ )
89
+ )
90
+
91
+ self.with_execution(message, on_execution)
92
+
93
+ def _determine_final_status(
94
+ self,
95
+ execution: Workflow,
96
+ message: CompleteWorkflow,
97
+ ) -> WorkflowStatus | None:
98
+ """
99
+ Determine the final execution status.
100
+
101
+ Returns None if execution is not ready to complete.
102
+ """
103
+ stages = execution.top_level_stages()
104
+ statuses = [s.status for s in stages]
105
+
106
+ # All succeeded/skipped/failed_continue -> SUCCEEDED
107
+ if all(s in CONTINUABLE_STATUSES for s in statuses):
108
+ return WorkflowStatus.SUCCEEDED
109
+
110
+ # Any TERMINAL -> TERMINAL
111
+ if WorkflowStatus.TERMINAL in statuses:
112
+ return WorkflowStatus.TERMINAL
113
+
114
+ # Any CANCELED -> CANCELED
115
+ if WorkflowStatus.CANCELED in statuses:
116
+ return WorkflowStatus.CANCELED
117
+
118
+ # Any STOPPED and no other branches incomplete
119
+ if WorkflowStatus.STOPPED in statuses:
120
+ if not self._other_branches_incomplete(stages):
121
+ # Check for override
122
+ if self._should_override_success(execution):
123
+ return WorkflowStatus.TERMINAL
124
+ return WorkflowStatus.SUCCEEDED
125
+
126
+ # Still running - re-queue
127
+ logger.debug(
128
+ "Re-queuing CompleteWorkflow for %s - stages not complete. Statuses: %s",
129
+ execution.id,
130
+ statuses,
131
+ )
132
+ self.queue.push(message, self.retry_delay)
133
+ return None
134
+
135
+ def _other_branches_incomplete(self, stages: list[StageExecution]) -> bool:
136
+ """Check if any other branches are incomplete."""
137
+ for stage in stages:
138
+ if stage.status == WorkflowStatus.RUNNING:
139
+ return True
140
+ if stage.status == WorkflowStatus.NOT_STARTED and stage.all_upstream_stages_complete():
141
+ return True
142
+ return False
143
+
144
+ def _should_override_success(self, execution: Workflow) -> bool:
145
+ """Check if success should be overridden to failure."""
146
+ for stage in execution.stages:
147
+ if stage.status == WorkflowStatus.STOPPED:
148
+ if stage.context.get("completeOtherBranchesThenFail"):
149
+ return True
150
+ return False