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.
Files changed (85) hide show
  1. stabilize/__init__.py +255 -0
  2. stabilize/assertions.py +465 -0
  3. stabilize/cli.py +1878 -0
  4. stabilize/conditions.py +351 -0
  5. stabilize/config_validation.py +568 -0
  6. stabilize/context/__init__.py +7 -0
  7. stabilize/context/stage_context.py +170 -0
  8. stabilize/dag/__init__.py +15 -0
  9. stabilize/dag/graph.py +215 -0
  10. stabilize/dag/topological.py +199 -0
  11. stabilize/errors.py +383 -0
  12. stabilize/examples/__init__.py +1 -0
  13. stabilize/examples/docker-example.py +480 -0
  14. stabilize/examples/golden-standard-expected-result.txt +1 -0
  15. stabilize/examples/golden-standard.py +490 -0
  16. stabilize/examples/highway-integration-example.py +412 -0
  17. stabilize/examples/http-example.py +499 -0
  18. stabilize/examples/llama-example.py +683 -0
  19. stabilize/examples/python-example.py +593 -0
  20. stabilize/examples/shell-example.py +347 -0
  21. stabilize/examples/ssh-example.py +468 -0
  22. stabilize/handlers/__init__.py +53 -0
  23. stabilize/handlers/base.py +226 -0
  24. stabilize/handlers/complete_stage.py +219 -0
  25. stabilize/handlers/complete_task.py +81 -0
  26. stabilize/handlers/complete_workflow.py +150 -0
  27. stabilize/handlers/run_task.py +518 -0
  28. stabilize/handlers/start_stage.py +320 -0
  29. stabilize/handlers/start_task.py +74 -0
  30. stabilize/handlers/start_workflow.py +136 -0
  31. stabilize/launcher.py +307 -0
  32. stabilize/logging.py +228 -0
  33. stabilize/migrations/01KDQ4N9QPJ6Q4MCV3V9GHWPV4_initial_schema.sql +97 -0
  34. stabilize/migrations/01KE39HEAN221S7AWNS4A0H750_add_performance_indexes.sql +40 -0
  35. stabilize/migrations/01KE4YJT2W8A6NBX5R7CQMJK84_add_dlq_table.sql +28 -0
  36. stabilize/migrations/01KE5ZQT3X9B7PCS8R8DRMLN85_add_processed_messages.sql +26 -0
  37. stabilize/migrations/__init__.py +1 -0
  38. stabilize/models/__init__.py +15 -0
  39. stabilize/models/stage.py +389 -0
  40. stabilize/models/status.py +266 -0
  41. stabilize/models/task.py +125 -0
  42. stabilize/models/workflow.py +317 -0
  43. stabilize/monitor/__init__.py +63 -0
  44. stabilize/monitor/data.py +482 -0
  45. stabilize/monitor/display.py +800 -0
  46. stabilize/orchestrator.py +113 -0
  47. stabilize/persistence/__init__.py +28 -0
  48. stabilize/persistence/connection.py +185 -0
  49. stabilize/persistence/factory.py +140 -0
  50. stabilize/persistence/memory.py +347 -0
  51. stabilize/persistence/postgres.py +862 -0
  52. stabilize/persistence/sqlite.py +1253 -0
  53. stabilize/persistence/store.py +498 -0
  54. stabilize/queue/__init__.py +59 -0
  55. stabilize/queue/messages.py +377 -0
  56. stabilize/queue/processor.py +379 -0
  57. stabilize/queue/queue.py +741 -0
  58. stabilize/queue/sqlite_queue.py +595 -0
  59. stabilize/recovery.py +353 -0
  60. stabilize/resilience/__init__.py +19 -0
  61. stabilize/resilience/bulkheads.py +148 -0
  62. stabilize/resilience/circuits.py +164 -0
  63. stabilize/resilience/config.py +84 -0
  64. stabilize/resilience/executor.py +168 -0
  65. stabilize/stages/__init__.py +11 -0
  66. stabilize/stages/builder.py +253 -0
  67. stabilize/tasks/__init__.py +19 -0
  68. stabilize/tasks/docker.py +463 -0
  69. stabilize/tasks/highway/__init__.py +32 -0
  70. stabilize/tasks/highway/config.py +87 -0
  71. stabilize/tasks/highway/task.py +465 -0
  72. stabilize/tasks/http.py +592 -0
  73. stabilize/tasks/interface.py +335 -0
  74. stabilize/tasks/python.py +351 -0
  75. stabilize/tasks/registry.py +255 -0
  76. stabilize/tasks/result.py +283 -0
  77. stabilize/tasks/shell.py +246 -0
  78. stabilize/tasks/ssh.py +171 -0
  79. stabilize/tracing.py +327 -0
  80. stabilize/verification.py +316 -0
  81. stabilize-0.12.2.dist-info/METADATA +287 -0
  82. stabilize-0.12.2.dist-info/RECORD +85 -0
  83. stabilize-0.12.2.dist-info/WHEEL +4 -0
  84. stabilize-0.12.2.dist-info/entry_points.txt +2 -0
  85. 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
+ ]
@@ -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