stabilize 0.12.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 +255 -0
- stabilize/assertions.py +465 -0
- stabilize/cli.py +1878 -0
- stabilize/conditions.py +351 -0
- stabilize/config_validation.py +568 -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/errors.py +383 -0
- stabilize/examples/__init__.py +1 -0
- stabilize/examples/docker-example.py +480 -0
- stabilize/examples/golden-standard-expected-result.txt +1 -0
- stabilize/examples/golden-standard.py +490 -0
- stabilize/examples/highway-integration-example.py +412 -0
- stabilize/examples/http-example.py +499 -0
- stabilize/examples/llama-example.py +683 -0
- stabilize/examples/python-example.py +593 -0
- stabilize/examples/shell-example.py +347 -0
- stabilize/examples/ssh-example.py +468 -0
- stabilize/handlers/__init__.py +53 -0
- stabilize/handlers/base.py +226 -0
- stabilize/handlers/complete_stage.py +219 -0
- stabilize/handlers/complete_task.py +81 -0
- stabilize/handlers/complete_workflow.py +150 -0
- stabilize/handlers/run_task.py +518 -0
- stabilize/handlers/start_stage.py +320 -0
- stabilize/handlers/start_task.py +74 -0
- stabilize/handlers/start_workflow.py +136 -0
- stabilize/launcher.py +307 -0
- stabilize/logging.py +228 -0
- stabilize/migrations/01KDQ4N9QPJ6Q4MCV3V9GHWPV4_initial_schema.sql +97 -0
- stabilize/migrations/01KE39HEAN221S7AWNS4A0H750_add_performance_indexes.sql +40 -0
- stabilize/migrations/01KE4YJT2W8A6NBX5R7CQMJK84_add_dlq_table.sql +28 -0
- stabilize/migrations/01KE5ZQT3X9B7PCS8R8DRMLN85_add_processed_messages.sql +26 -0
- stabilize/migrations/__init__.py +1 -0
- stabilize/models/__init__.py +15 -0
- stabilize/models/stage.py +389 -0
- stabilize/models/status.py +266 -0
- stabilize/models/task.py +125 -0
- stabilize/models/workflow.py +317 -0
- stabilize/monitor/__init__.py +63 -0
- stabilize/monitor/data.py +482 -0
- stabilize/monitor/display.py +800 -0
- stabilize/orchestrator.py +113 -0
- stabilize/persistence/__init__.py +28 -0
- stabilize/persistence/connection.py +185 -0
- stabilize/persistence/factory.py +140 -0
- stabilize/persistence/memory.py +347 -0
- stabilize/persistence/postgres.py +862 -0
- stabilize/persistence/sqlite.py +1253 -0
- stabilize/persistence/store.py +498 -0
- stabilize/queue/__init__.py +59 -0
- stabilize/queue/messages.py +377 -0
- stabilize/queue/processor.py +379 -0
- stabilize/queue/queue.py +741 -0
- stabilize/queue/sqlite_queue.py +595 -0
- stabilize/recovery.py +353 -0
- stabilize/resilience/__init__.py +19 -0
- stabilize/resilience/bulkheads.py +148 -0
- stabilize/resilience/circuits.py +164 -0
- stabilize/resilience/config.py +84 -0
- stabilize/resilience/executor.py +168 -0
- stabilize/stages/__init__.py +11 -0
- stabilize/stages/builder.py +253 -0
- stabilize/tasks/__init__.py +19 -0
- stabilize/tasks/docker.py +463 -0
- stabilize/tasks/highway/__init__.py +32 -0
- stabilize/tasks/highway/config.py +87 -0
- stabilize/tasks/highway/task.py +465 -0
- stabilize/tasks/http.py +592 -0
- stabilize/tasks/interface.py +335 -0
- stabilize/tasks/python.py +351 -0
- stabilize/tasks/registry.py +255 -0
- stabilize/tasks/result.py +283 -0
- stabilize/tasks/shell.py +246 -0
- stabilize/tasks/ssh.py +171 -0
- stabilize/tracing.py +327 -0
- stabilize/verification.py +316 -0
- stabilize-0.12.2.dist-info/METADATA +287 -0
- stabilize-0.12.2.dist-info/RECORD +85 -0
- stabilize-0.12.2.dist-info/WHEEL +4 -0
- stabilize-0.12.2.dist-info/entry_points.txt +2 -0
- stabilize-0.12.2.dist-info/licenses/LICENSE +201 -0
stabilize/__init__.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stabilize - Highway Workflow Engine execution layer.
|
|
3
|
+
|
|
4
|
+
This package provides a message-driven DAG execution engine for running
|
|
5
|
+
workflows with full support for:
|
|
6
|
+
- Parallel and sequential stage execution
|
|
7
|
+
- Synthetic stages (before/after/onFailure)
|
|
8
|
+
- PostgreSQL and SQLite persistence
|
|
9
|
+
- Pluggable task system
|
|
10
|
+
- Verification phase for validating outputs
|
|
11
|
+
- Structured status conditions
|
|
12
|
+
- Assertion helpers for clean error handling
|
|
13
|
+
- Configuration validation with JSON Schema
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "0.12.2"
|
|
17
|
+
|
|
18
|
+
# Assertion helpers
|
|
19
|
+
from stabilize.assertions import (
|
|
20
|
+
ConfigError,
|
|
21
|
+
ContextError,
|
|
22
|
+
OutputError,
|
|
23
|
+
PreconditionError,
|
|
24
|
+
StabilizeError,
|
|
25
|
+
StabilizeExpectedError,
|
|
26
|
+
StabilizeFatalError,
|
|
27
|
+
StageNotReadyError,
|
|
28
|
+
VerificationError,
|
|
29
|
+
assert_config,
|
|
30
|
+
assert_context,
|
|
31
|
+
assert_context_in,
|
|
32
|
+
assert_context_type,
|
|
33
|
+
assert_no_upstream_failures,
|
|
34
|
+
assert_non_empty,
|
|
35
|
+
assert_not_none,
|
|
36
|
+
assert_output,
|
|
37
|
+
assert_output_type,
|
|
38
|
+
assert_stage_ready,
|
|
39
|
+
assert_true,
|
|
40
|
+
assert_verified,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Structured conditions
|
|
44
|
+
from stabilize.conditions import (
|
|
45
|
+
Condition,
|
|
46
|
+
ConditionReason,
|
|
47
|
+
ConditionSet,
|
|
48
|
+
ConditionType,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Configuration validation
|
|
52
|
+
from stabilize.config_validation import (
|
|
53
|
+
SchemaValidator,
|
|
54
|
+
ValidationError,
|
|
55
|
+
is_valid,
|
|
56
|
+
validate_context,
|
|
57
|
+
validate_outputs,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Context helpers
|
|
61
|
+
from stabilize.context.stage_context import StageContext
|
|
62
|
+
|
|
63
|
+
# Error hierarchy
|
|
64
|
+
from stabilize.errors import (
|
|
65
|
+
PermanentError,
|
|
66
|
+
RecoveryError,
|
|
67
|
+
TaskError,
|
|
68
|
+
TransientError,
|
|
69
|
+
is_permanent,
|
|
70
|
+
is_transient,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Handlers
|
|
74
|
+
from stabilize.handlers import (
|
|
75
|
+
CompleteStageHandler,
|
|
76
|
+
CompleteTaskHandler,
|
|
77
|
+
CompleteWorkflowHandler,
|
|
78
|
+
RunTaskHandler,
|
|
79
|
+
StabilizeHandler,
|
|
80
|
+
StartStageHandler,
|
|
81
|
+
StartTaskHandler,
|
|
82
|
+
StartWorkflowHandler,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Observability (optional - graceful degradation if not installed)
|
|
86
|
+
from stabilize.logging import (
|
|
87
|
+
bind_context,
|
|
88
|
+
clear_context,
|
|
89
|
+
configure_logging,
|
|
90
|
+
get_logger,
|
|
91
|
+
stage_logger,
|
|
92
|
+
task_logger,
|
|
93
|
+
unbind_context,
|
|
94
|
+
workflow_logger,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Core models
|
|
98
|
+
from stabilize.models.stage import StageExecution
|
|
99
|
+
from stabilize.models.status import WorkflowStatus
|
|
100
|
+
from stabilize.models.task import TaskExecution
|
|
101
|
+
from stabilize.models.workflow import Workflow
|
|
102
|
+
|
|
103
|
+
# Infrastructure
|
|
104
|
+
from stabilize.orchestrator import Orchestrator
|
|
105
|
+
from stabilize.persistence.postgres import PostgresWorkflowStore
|
|
106
|
+
from stabilize.persistence.sqlite import SqliteWorkflowStore
|
|
107
|
+
from stabilize.persistence.store import WorkflowStore
|
|
108
|
+
from stabilize.queue.processor import QueueProcessor
|
|
109
|
+
from stabilize.queue.queue import PostgresQueue, Queue
|
|
110
|
+
from stabilize.queue.sqlite_queue import SqliteQueue
|
|
111
|
+
|
|
112
|
+
# Recovery
|
|
113
|
+
from stabilize.recovery import (
|
|
114
|
+
RecoveryResult,
|
|
115
|
+
WorkflowRecovery,
|
|
116
|
+
recover_on_startup,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Tasks
|
|
120
|
+
from stabilize.tasks.docker import DockerTask
|
|
121
|
+
from stabilize.tasks.highway import HighwayTask
|
|
122
|
+
from stabilize.tasks.http import HTTPTask
|
|
123
|
+
from stabilize.tasks.interface import RetryableTask, Task
|
|
124
|
+
from stabilize.tasks.python import PythonTask
|
|
125
|
+
from stabilize.tasks.registry import TaskRegistry
|
|
126
|
+
from stabilize.tasks.result import TaskResult
|
|
127
|
+
from stabilize.tasks.shell import ShellTask
|
|
128
|
+
from stabilize.tasks.ssh import SSHTask
|
|
129
|
+
from stabilize.tracing import (
|
|
130
|
+
add_event,
|
|
131
|
+
configure_tracing,
|
|
132
|
+
get_tracer,
|
|
133
|
+
set_attribute,
|
|
134
|
+
trace_message_processing,
|
|
135
|
+
trace_operation,
|
|
136
|
+
trace_stage,
|
|
137
|
+
trace_task,
|
|
138
|
+
trace_workflow,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Verification system
|
|
142
|
+
from stabilize.verification import (
|
|
143
|
+
CallableVerifier,
|
|
144
|
+
OutputVerifier,
|
|
145
|
+
Verifier,
|
|
146
|
+
VerifyResult,
|
|
147
|
+
VerifyStatus,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
__all__ = [
|
|
151
|
+
# Core models
|
|
152
|
+
"WorkflowStatus",
|
|
153
|
+
"Workflow",
|
|
154
|
+
"StageExecution",
|
|
155
|
+
"TaskExecution",
|
|
156
|
+
# Infrastructure
|
|
157
|
+
"Orchestrator",
|
|
158
|
+
"QueueProcessor",
|
|
159
|
+
"Queue",
|
|
160
|
+
"PostgresQueue",
|
|
161
|
+
"SqliteQueue",
|
|
162
|
+
"WorkflowStore",
|
|
163
|
+
"PostgresWorkflowStore",
|
|
164
|
+
"SqliteWorkflowStore",
|
|
165
|
+
# Handlers
|
|
166
|
+
"StabilizeHandler",
|
|
167
|
+
"StartWorkflowHandler",
|
|
168
|
+
"StartStageHandler",
|
|
169
|
+
"StartTaskHandler",
|
|
170
|
+
"RunTaskHandler",
|
|
171
|
+
"CompleteTaskHandler",
|
|
172
|
+
"CompleteStageHandler",
|
|
173
|
+
"CompleteWorkflowHandler",
|
|
174
|
+
# Tasks
|
|
175
|
+
"Task",
|
|
176
|
+
"RetryableTask",
|
|
177
|
+
"TaskResult",
|
|
178
|
+
"TaskRegistry",
|
|
179
|
+
"ShellTask",
|
|
180
|
+
"HTTPTask",
|
|
181
|
+
"DockerTask",
|
|
182
|
+
"SSHTask",
|
|
183
|
+
"HighwayTask",
|
|
184
|
+
"PythonTask",
|
|
185
|
+
# Verification
|
|
186
|
+
"Verifier",
|
|
187
|
+
"VerifyResult",
|
|
188
|
+
"VerifyStatus",
|
|
189
|
+
"OutputVerifier",
|
|
190
|
+
"CallableVerifier",
|
|
191
|
+
# Conditions
|
|
192
|
+
"Condition",
|
|
193
|
+
"ConditionSet",
|
|
194
|
+
"ConditionType",
|
|
195
|
+
"ConditionReason",
|
|
196
|
+
# Context helpers
|
|
197
|
+
"StageContext",
|
|
198
|
+
# Assertions
|
|
199
|
+
"StabilizeError",
|
|
200
|
+
"StabilizeFatalError",
|
|
201
|
+
"StabilizeExpectedError",
|
|
202
|
+
"PreconditionError",
|
|
203
|
+
"ContextError",
|
|
204
|
+
"OutputError",
|
|
205
|
+
"ConfigError",
|
|
206
|
+
"VerificationError",
|
|
207
|
+
"StageNotReadyError",
|
|
208
|
+
"assert_true",
|
|
209
|
+
"assert_context",
|
|
210
|
+
"assert_context_type",
|
|
211
|
+
"assert_context_in",
|
|
212
|
+
"assert_output",
|
|
213
|
+
"assert_output_type",
|
|
214
|
+
"assert_stage_ready",
|
|
215
|
+
"assert_no_upstream_failures",
|
|
216
|
+
"assert_config",
|
|
217
|
+
"assert_verified",
|
|
218
|
+
"assert_not_none",
|
|
219
|
+
"assert_non_empty",
|
|
220
|
+
# Configuration validation
|
|
221
|
+
"ValidationError",
|
|
222
|
+
"SchemaValidator",
|
|
223
|
+
"validate_context",
|
|
224
|
+
"validate_outputs",
|
|
225
|
+
"is_valid",
|
|
226
|
+
# Observability
|
|
227
|
+
"configure_logging",
|
|
228
|
+
"get_logger",
|
|
229
|
+
"bind_context",
|
|
230
|
+
"unbind_context",
|
|
231
|
+
"clear_context",
|
|
232
|
+
"workflow_logger",
|
|
233
|
+
"stage_logger",
|
|
234
|
+
"task_logger",
|
|
235
|
+
"configure_tracing",
|
|
236
|
+
"get_tracer",
|
|
237
|
+
"trace_operation",
|
|
238
|
+
"trace_workflow",
|
|
239
|
+
"trace_stage",
|
|
240
|
+
"trace_task",
|
|
241
|
+
"trace_message_processing",
|
|
242
|
+
"add_event",
|
|
243
|
+
"set_attribute",
|
|
244
|
+
# Error hierarchy
|
|
245
|
+
"TransientError",
|
|
246
|
+
"PermanentError",
|
|
247
|
+
"TaskError",
|
|
248
|
+
"RecoveryError",
|
|
249
|
+
"is_transient",
|
|
250
|
+
"is_permanent",
|
|
251
|
+
# Recovery
|
|
252
|
+
"WorkflowRecovery",
|
|
253
|
+
"RecoveryResult",
|
|
254
|
+
"recover_on_startup",
|
|
255
|
+
]
|
stabilize/assertions.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Assertion helpers for validating preconditions and results.
|
|
3
|
+
|
|
4
|
+
This module provides assertion utilities that raise descriptive exceptions
|
|
5
|
+
when conditions are not met. These are useful for:
|
|
6
|
+
- Validating task inputs before processing
|
|
7
|
+
- Checking configuration values
|
|
8
|
+
- Ensuring stage prerequisites are met
|
|
9
|
+
- Verifying outputs after task execution
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
from stabilize.assertions import assert_context, assert_output
|
|
13
|
+
|
|
14
|
+
class MyTask(Task):
|
|
15
|
+
def execute(self, stage: StageExecution) -> TaskResult:
|
|
16
|
+
# Validate required inputs
|
|
17
|
+
assert_context(stage, "api_key", "API key is required")
|
|
18
|
+
assert_context_type(stage, "timeout", int, "Timeout must be an integer")
|
|
19
|
+
|
|
20
|
+
# Do work...
|
|
21
|
+
|
|
22
|
+
return TaskResult.success(outputs={"result": "done"})
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from stabilize.models.stage import StageExecution
|
|
31
|
+
|
|
32
|
+
T = TypeVar("T")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# Exception Classes
|
|
37
|
+
# =============================================================================
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StabilizeError(Exception):
|
|
41
|
+
"""Base exception for Stabilize errors."""
|
|
42
|
+
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class StabilizeFatalError(StabilizeError):
|
|
47
|
+
"""
|
|
48
|
+
Fatal error that should terminate the pipeline.
|
|
49
|
+
|
|
50
|
+
Fatal errors indicate unrecoverable situations like invalid configuration
|
|
51
|
+
or programming errors.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class StabilizeExpectedError(StabilizeError):
|
|
58
|
+
"""
|
|
59
|
+
Expected error that may allow retry or continuation.
|
|
60
|
+
|
|
61
|
+
Expected errors occur during normal operation and may be recoverable.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PreconditionError(StabilizeExpectedError):
|
|
68
|
+
"""
|
|
69
|
+
Error raised when a precondition is not met.
|
|
70
|
+
|
|
71
|
+
Precondition errors typically indicate that the stage should be
|
|
72
|
+
retried or that an upstream dependency hasn't completed.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, message: str, key: str | None = None) -> None:
|
|
76
|
+
super().__init__(message)
|
|
77
|
+
self.key = key
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ContextError(StabilizeFatalError):
|
|
81
|
+
"""
|
|
82
|
+
Error raised when required context is missing or invalid.
|
|
83
|
+
|
|
84
|
+
Context errors are fatal because they indicate misconfiguration.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self, message: str, key: str | None = None) -> None:
|
|
88
|
+
super().__init__(message)
|
|
89
|
+
self.key = key
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class OutputError(StabilizeExpectedError):
|
|
93
|
+
"""
|
|
94
|
+
Error raised when expected output is missing or invalid.
|
|
95
|
+
|
|
96
|
+
Output errors may be recoverable if the task can be retried.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, message: str, key: str | None = None) -> None:
|
|
100
|
+
super().__init__(message)
|
|
101
|
+
self.key = key
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ConfigError(StabilizeFatalError):
|
|
105
|
+
"""
|
|
106
|
+
Error raised when configuration is invalid.
|
|
107
|
+
|
|
108
|
+
Config errors are fatal because they indicate invalid setup.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, message: str, field: str | None = None) -> None:
|
|
112
|
+
super().__init__(message)
|
|
113
|
+
self.field = field
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class VerificationError(StabilizeExpectedError):
|
|
117
|
+
"""
|
|
118
|
+
Error raised when verification fails.
|
|
119
|
+
|
|
120
|
+
Verification errors may allow retry depending on the verifier.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:
|
|
124
|
+
super().__init__(message)
|
|
125
|
+
self.details = details or {}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class StageNotReadyError(StabilizeExpectedError):
|
|
129
|
+
"""
|
|
130
|
+
Error raised when a stage is not ready for execution.
|
|
131
|
+
|
|
132
|
+
This typically means upstream dependencies haven't completed.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, message: str, stage_ref_id: str | None = None) -> None:
|
|
136
|
+
super().__init__(message)
|
|
137
|
+
self.stage_ref_id = stage_ref_id
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# =============================================================================
|
|
141
|
+
# Assertion Functions
|
|
142
|
+
# =============================================================================
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def assert_true(condition: bool, message: str) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Assert that a condition is true.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
condition: The condition to check
|
|
151
|
+
message: Error message if condition is false
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
PreconditionError: If condition is false
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
assert_true(stage.status == WorkflowStatus.RUNNING, "Stage must be running")
|
|
158
|
+
"""
|
|
159
|
+
if not condition:
|
|
160
|
+
raise PreconditionError(message)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def assert_context(
|
|
164
|
+
stage: StageExecution,
|
|
165
|
+
key: str,
|
|
166
|
+
message: str | None = None,
|
|
167
|
+
) -> Any:
|
|
168
|
+
"""
|
|
169
|
+
Assert that a context key exists and return its value.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
stage: The stage execution
|
|
173
|
+
key: The context key to check
|
|
174
|
+
message: Optional custom error message
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The value from context
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
ContextError: If key is not in context
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
api_key = assert_context(stage, "api_key", "API key is required")
|
|
184
|
+
"""
|
|
185
|
+
if key not in stage.context:
|
|
186
|
+
raise ContextError(
|
|
187
|
+
message or f"Required context key '{key}' is missing",
|
|
188
|
+
key=key,
|
|
189
|
+
)
|
|
190
|
+
return stage.context[key]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def assert_context_type(
|
|
194
|
+
stage: StageExecution,
|
|
195
|
+
key: str,
|
|
196
|
+
expected_type: type[T],
|
|
197
|
+
message: str | None = None,
|
|
198
|
+
) -> T:
|
|
199
|
+
"""
|
|
200
|
+
Assert that a context key exists and has the expected type.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
stage: The stage execution
|
|
204
|
+
key: The context key to check
|
|
205
|
+
expected_type: The expected type
|
|
206
|
+
message: Optional custom error message
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
The typed value from context
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
ContextError: If key is missing or has wrong type
|
|
213
|
+
|
|
214
|
+
Example:
|
|
215
|
+
timeout = assert_context_type(stage, "timeout", int, "Timeout must be an integer")
|
|
216
|
+
"""
|
|
217
|
+
value = assert_context(stage, key, message)
|
|
218
|
+
if not isinstance(value, expected_type):
|
|
219
|
+
raise ContextError(
|
|
220
|
+
message or f"Context key '{key}' must be {expected_type.__name__}, got {type(value).__name__}",
|
|
221
|
+
key=key,
|
|
222
|
+
)
|
|
223
|
+
return value
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def assert_context_in(
|
|
227
|
+
stage: StageExecution,
|
|
228
|
+
key: str,
|
|
229
|
+
allowed_values: list[Any],
|
|
230
|
+
message: str | None = None,
|
|
231
|
+
) -> Any:
|
|
232
|
+
"""
|
|
233
|
+
Assert that a context value is in a list of allowed values.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
stage: The stage execution
|
|
237
|
+
key: The context key to check
|
|
238
|
+
allowed_values: List of valid values
|
|
239
|
+
message: Optional custom error message
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
The value from context
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
ContextError: If key is missing or value not allowed
|
|
246
|
+
|
|
247
|
+
Example:
|
|
248
|
+
env = assert_context_in(stage, "env", ["dev", "staging", "prod"])
|
|
249
|
+
"""
|
|
250
|
+
value = assert_context(stage, key, message)
|
|
251
|
+
if value not in allowed_values:
|
|
252
|
+
raise ContextError(
|
|
253
|
+
message or f"Context key '{key}' must be one of {allowed_values}, got '{value}'",
|
|
254
|
+
key=key,
|
|
255
|
+
)
|
|
256
|
+
return value
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def assert_output(
|
|
260
|
+
stage: StageExecution,
|
|
261
|
+
key: str,
|
|
262
|
+
message: str | None = None,
|
|
263
|
+
) -> Any:
|
|
264
|
+
"""
|
|
265
|
+
Assert that an output key exists and return its value.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
stage: The stage execution
|
|
269
|
+
key: The output key to check
|
|
270
|
+
message: Optional custom error message
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
The value from outputs
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
OutputError: If key is not in outputs
|
|
277
|
+
|
|
278
|
+
Example:
|
|
279
|
+
result = assert_output(stage, "deployment_id", "Deployment ID not found")
|
|
280
|
+
"""
|
|
281
|
+
if key not in stage.outputs:
|
|
282
|
+
raise OutputError(
|
|
283
|
+
message or f"Required output key '{key}' is missing",
|
|
284
|
+
key=key,
|
|
285
|
+
)
|
|
286
|
+
return stage.outputs[key]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def assert_output_type(
|
|
290
|
+
stage: StageExecution,
|
|
291
|
+
key: str,
|
|
292
|
+
expected_type: type[T],
|
|
293
|
+
message: str | None = None,
|
|
294
|
+
) -> T:
|
|
295
|
+
"""
|
|
296
|
+
Assert that an output key exists and has the expected type.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
stage: The stage execution
|
|
300
|
+
key: The output key to check
|
|
301
|
+
expected_type: The expected type
|
|
302
|
+
message: Optional custom error message
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
The typed value from outputs
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
OutputError: If key is missing or has wrong type
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
count = assert_output_type(stage, "item_count", int)
|
|
312
|
+
"""
|
|
313
|
+
value = assert_output(stage, key, message)
|
|
314
|
+
if not isinstance(value, expected_type):
|
|
315
|
+
raise OutputError(
|
|
316
|
+
message or f"Output key '{key}' must be {expected_type.__name__}, got {type(value).__name__}",
|
|
317
|
+
key=key,
|
|
318
|
+
)
|
|
319
|
+
return value
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def assert_stage_ready(
|
|
323
|
+
stage: StageExecution,
|
|
324
|
+
message: str | None = None,
|
|
325
|
+
) -> None:
|
|
326
|
+
"""
|
|
327
|
+
Assert that all upstream stages have completed successfully.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
stage: The stage execution to check
|
|
331
|
+
message: Optional custom error message
|
|
332
|
+
|
|
333
|
+
Raises:
|
|
334
|
+
StageNotReadyError: If any upstream stage hasn't completed
|
|
335
|
+
|
|
336
|
+
Example:
|
|
337
|
+
assert_stage_ready(stage, "Cannot start: upstream stages incomplete")
|
|
338
|
+
"""
|
|
339
|
+
if not stage.all_upstream_stages_complete():
|
|
340
|
+
raise StageNotReadyError(
|
|
341
|
+
message or "Upstream stages have not completed",
|
|
342
|
+
stage_ref_id=stage.ref_id,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def assert_no_upstream_failures(
|
|
347
|
+
stage: StageExecution,
|
|
348
|
+
message: str | None = None,
|
|
349
|
+
) -> None:
|
|
350
|
+
"""
|
|
351
|
+
Assert that no upstream stages have failed.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
stage: The stage execution to check
|
|
355
|
+
message: Optional custom error message
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
StageNotReadyError: If any upstream stage has failed
|
|
359
|
+
|
|
360
|
+
Example:
|
|
361
|
+
assert_no_upstream_failures(stage)
|
|
362
|
+
"""
|
|
363
|
+
if stage.any_upstream_stages_failed():
|
|
364
|
+
raise StageNotReadyError(
|
|
365
|
+
message or "One or more upstream stages have failed",
|
|
366
|
+
stage_ref_id=stage.ref_id,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def assert_config(
|
|
371
|
+
condition: bool,
|
|
372
|
+
message: str,
|
|
373
|
+
field: str | None = None,
|
|
374
|
+
) -> None:
|
|
375
|
+
"""
|
|
376
|
+
Assert a configuration condition.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
condition: The condition to check
|
|
380
|
+
message: Error message if condition is false
|
|
381
|
+
field: Optional field name for context
|
|
382
|
+
|
|
383
|
+
Raises:
|
|
384
|
+
ConfigError: If condition is false
|
|
385
|
+
|
|
386
|
+
Example:
|
|
387
|
+
assert_config(timeout > 0, "Timeout must be positive", field="timeout")
|
|
388
|
+
"""
|
|
389
|
+
if not condition:
|
|
390
|
+
raise ConfigError(message, field=field)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def assert_verified(
|
|
394
|
+
condition: bool,
|
|
395
|
+
message: str,
|
|
396
|
+
details: dict[str, Any] | None = None,
|
|
397
|
+
) -> None:
|
|
398
|
+
"""
|
|
399
|
+
Assert a verification condition.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
condition: The condition to check
|
|
403
|
+
message: Error message if condition is false
|
|
404
|
+
details: Optional details about the failure
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
VerificationError: If condition is false
|
|
408
|
+
|
|
409
|
+
Example:
|
|
410
|
+
assert_verified(response.ok, "API returned error", {"status": response.status_code})
|
|
411
|
+
"""
|
|
412
|
+
if not condition:
|
|
413
|
+
raise VerificationError(message, details=details)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def assert_not_none(
|
|
417
|
+
value: T | None,
|
|
418
|
+
message: str,
|
|
419
|
+
) -> T:
|
|
420
|
+
"""
|
|
421
|
+
Assert that a value is not None and return it.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
value: The value to check
|
|
425
|
+
message: Error message if value is None
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
The non-None value
|
|
429
|
+
|
|
430
|
+
Raises:
|
|
431
|
+
PreconditionError: If value is None
|
|
432
|
+
|
|
433
|
+
Example:
|
|
434
|
+
user = assert_not_none(get_user(id), f"User {id} not found")
|
|
435
|
+
"""
|
|
436
|
+
if value is None:
|
|
437
|
+
raise PreconditionError(message)
|
|
438
|
+
return value
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def assert_non_empty(
|
|
442
|
+
value: str | list[Any] | dict[str, Any],
|
|
443
|
+
message: str,
|
|
444
|
+
) -> str | list[Any] | dict[str, Any]:
|
|
445
|
+
"""
|
|
446
|
+
Assert that a value is not empty.
|
|
447
|
+
|
|
448
|
+
Works with strings, lists, and dicts.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
value: The value to check
|
|
452
|
+
message: Error message if value is empty
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
The non-empty value
|
|
456
|
+
|
|
457
|
+
Raises:
|
|
458
|
+
PreconditionError: If value is empty
|
|
459
|
+
|
|
460
|
+
Example:
|
|
461
|
+
items = assert_non_empty(stage.context.get("items", []), "Items list is empty")
|
|
462
|
+
"""
|
|
463
|
+
if not value:
|
|
464
|
+
raise PreconditionError(message)
|
|
465
|
+
return value
|