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,136 @@
1
+ """
2
+ StartWorkflowHandler - handles pipeline execution startup.
3
+
4
+ This handler is triggered when a new pipeline execution is started.
5
+ It finds initial stages (those with no dependencies) and queues them
6
+ for execution.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import TYPE_CHECKING
13
+
14
+ from stabilize.handlers.base import StabilizeHandler
15
+ from stabilize.models.status import WorkflowStatus
16
+ from stabilize.queue.messages import (
17
+ CancelWorkflow,
18
+ StartStage,
19
+ StartWorkflow,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from stabilize.models.workflow import Workflow
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class StartWorkflowHandler(StabilizeHandler[StartWorkflow]):
29
+ """
30
+ Handler for StartWorkflow messages.
31
+
32
+ When a pipeline execution starts:
33
+ 1. Check if execution should be queued (concurrent limits)
34
+ 2. Find initial stages (no dependencies)
35
+ 3. Push StartStage for each initial stage
36
+ 4. Mark execution as RUNNING
37
+ """
38
+
39
+ @property
40
+ def message_type(self) -> type[StartWorkflow]:
41
+ return StartWorkflow
42
+
43
+ def handle(self, message: StartWorkflow) -> None:
44
+ """Handle the StartWorkflow message."""
45
+
46
+ def on_execution(execution: Workflow) -> None:
47
+ # Check if already started or canceled
48
+ if execution.status != WorkflowStatus.NOT_STARTED:
49
+ logger.warning(
50
+ "Execution %s already has status %s, ignoring StartWorkflow",
51
+ execution.id,
52
+ execution.status,
53
+ )
54
+ return
55
+
56
+ if execution.is_canceled:
57
+ logger.info("Execution %s was canceled before start", execution.id)
58
+ self._terminate(execution)
59
+ return
60
+
61
+ # Check if start time has expired
62
+ if self._is_after_start_time_expiry(execution):
63
+ logger.warning("Execution %s start time expired, canceling", execution.id)
64
+ self.queue.push(
65
+ CancelWorkflow(
66
+ execution_type=message.execution_type,
67
+ execution_id=message.execution_id,
68
+ user="system",
69
+ reason="Could not begin execution before start time expiry",
70
+ )
71
+ )
72
+ return
73
+
74
+ # TODO: Check if should queue (concurrent execution limits)
75
+ # if execution.should_queue():
76
+ # self.pending_execution_service.enqueue(execution.pipeline_config_id, message)
77
+ # return
78
+
79
+ self._start(execution, message)
80
+
81
+ self.with_execution(message, on_execution)
82
+
83
+ def _start(
84
+ self,
85
+ execution: Workflow,
86
+ message: StartWorkflow,
87
+ ) -> None:
88
+ """Start the execution."""
89
+ initial_stages = execution.initial_stages()
90
+
91
+ if not initial_stages:
92
+ logger.warning("No initial stages found for execution %s", execution.id)
93
+ execution.status = WorkflowStatus.TERMINAL
94
+ self.repository.update_status(execution)
95
+ # Publish ExecutionComplete event
96
+ return
97
+
98
+ # Mark as running
99
+ execution.status = WorkflowStatus.RUNNING
100
+ execution.start_time = self.current_time_millis()
101
+ self.repository.update_status(execution)
102
+
103
+ # Queue all initial stages
104
+ for stage in initial_stages:
105
+ logger.debug(
106
+ "Queuing initial stage %s (%s) for execution %s",
107
+ stage.name,
108
+ stage.id,
109
+ execution.id,
110
+ )
111
+ self.queue.push(
112
+ StartStage(
113
+ execution_type=message.execution_type,
114
+ execution_id=message.execution_id,
115
+ stage_id=stage.id,
116
+ )
117
+ )
118
+
119
+ logger.info(
120
+ "Started execution %s with %d initial stage(s)",
121
+ execution.id,
122
+ len(initial_stages),
123
+ )
124
+
125
+ def _terminate(self, execution: Workflow) -> None:
126
+ """Terminate a canceled execution."""
127
+ # Publish ExecutionComplete event
128
+ if execution.pipeline_config_id:
129
+ # Queue start waiting executions
130
+ pass
131
+
132
+ def _is_after_start_time_expiry(self, execution: Workflow) -> bool:
133
+ """Check if current time is past start time expiry."""
134
+ if execution.start_time_expiry is None:
135
+ return False
136
+ return self.current_time_millis() > execution.start_time_expiry
stabilize/launcher.py ADDED
@@ -0,0 +1,307 @@
1
+ """
2
+ WorkflowLauncher - creates and starts pipeline executions.
3
+
4
+ This module provides the high-level interface for launching pipelines.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from stabilize.models.stage import StageExecution
13
+ from stabilize.models.workflow import (
14
+ Trigger,
15
+ Workflow,
16
+ )
17
+ from stabilize.stages.builder import StageDefinitionBuilderFactory
18
+
19
+ if TYPE_CHECKING:
20
+ from stabilize.orchestrator import Orchestrator
21
+ from stabilize.persistence.store import WorkflowStore
22
+ from stabilize.tasks.registry import TaskRegistry
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class WorkflowLauncher:
28
+ """
29
+ Launcher for pipeline executions.
30
+
31
+ Creates executions from configuration and starts them via the runner.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ repository: WorkflowStore,
37
+ runner: Orchestrator,
38
+ stage_builder_factory: StageDefinitionBuilderFactory | None = None,
39
+ task_registry: TaskRegistry | None = None,
40
+ ) -> None:
41
+ """
42
+ Initialize the launcher.
43
+
44
+ Args:
45
+ repository: The execution repository
46
+ runner: The execution runner
47
+ stage_builder_factory: Factory for stage builders
48
+ task_registry: Registry for task implementations
49
+ """
50
+ self.repository = repository
51
+ self.runner = runner
52
+ self.stage_builder_factory = stage_builder_factory or StageDefinitionBuilderFactory()
53
+ self.task_registry = task_registry
54
+
55
+ def start(
56
+ self,
57
+ pipeline_config: dict[str, Any],
58
+ trigger: dict[str, Any] | None = None,
59
+ ) -> Workflow:
60
+ """
61
+ Start a pipeline execution from configuration.
62
+
63
+ Args:
64
+ pipeline_config: Pipeline configuration dictionary
65
+ trigger: Optional trigger information
66
+
67
+ Returns:
68
+ The created execution
69
+ """
70
+ # Parse the execution
71
+ execution = self.parse_execution(pipeline_config, trigger)
72
+
73
+ # Store it
74
+ self.repository.store(execution)
75
+
76
+ # Start it
77
+ self.runner.start(execution)
78
+
79
+ logger.info(f"Launched execution {execution.id} for pipeline {execution.name}")
80
+
81
+ return execution
82
+
83
+ def parse_execution(
84
+ self,
85
+ config: dict[str, Any],
86
+ trigger_config: dict[str, Any] | None = None,
87
+ ) -> Workflow:
88
+ """
89
+ Parse a pipeline configuration into an execution.
90
+
91
+ Args:
92
+ config: Pipeline configuration
93
+ trigger_config: Optional trigger configuration
94
+
95
+ Returns:
96
+ A Workflow ready to run
97
+ """
98
+ # Parse trigger
99
+ trigger = Trigger()
100
+ if trigger_config:
101
+ trigger = Trigger(
102
+ type=trigger_config.get("type", "manual"),
103
+ user=trigger_config.get("user", "anonymous"),
104
+ parameters=trigger_config.get("parameters", {}),
105
+ artifacts=trigger_config.get("artifacts", []),
106
+ payload=trigger_config.get("payload", {}),
107
+ )
108
+
109
+ # Parse stages
110
+ stages = self._parse_stages(config.get("stages", []))
111
+
112
+ # Create execution
113
+ execution = Workflow.create(
114
+ application=config.get("application", "unknown"),
115
+ name=config.get("name", "Unnamed Pipeline"),
116
+ stages=stages,
117
+ trigger=trigger,
118
+ pipeline_config_id=config.get("id"),
119
+ )
120
+
121
+ # Set additional properties
122
+ execution.is_limit_concurrent = config.get("limitConcurrent", False)
123
+ execution.max_concurrent_executions = config.get("maxConcurrentExecutions", 0)
124
+ execution.keep_waiting_pipelines = config.get("keepWaitingPipelines", False)
125
+
126
+ return execution
127
+
128
+ def _parse_stages(
129
+ self,
130
+ stage_configs: list[dict[str, Any]],
131
+ ) -> list[StageExecution]:
132
+ """
133
+ Parse stage configurations into StageExecution objects.
134
+
135
+ Args:
136
+ stage_configs: List of stage configuration dictionaries
137
+
138
+ Returns:
139
+ List of StageExecution objects
140
+ """
141
+ stages = []
142
+
143
+ for config in stage_configs:
144
+ stage = self._parse_stage(config)
145
+ stages.append(stage)
146
+
147
+ return stages
148
+
149
+ def _parse_stage(self, config: dict[str, Any]) -> StageExecution:
150
+ """
151
+ Parse a single stage configuration.
152
+
153
+ Args:
154
+ config: Stage configuration dictionary
155
+
156
+ Returns:
157
+ A StageExecution object
158
+ """
159
+ # Get requisite stages
160
+ requisite_ids: set[str] = set()
161
+ if "requisiteStageRefIds" in config:
162
+ requisite_ids = set(config["requisiteStageRefIds"])
163
+
164
+ # Create context from config (excluding metadata fields)
165
+ context = {
166
+ k: v
167
+ for k, v in config.items()
168
+ if k
169
+ not in {
170
+ "id",
171
+ "refId",
172
+ "type",
173
+ "name",
174
+ "requisiteStageRefIds",
175
+ "parentStageId",
176
+ "syntheticStageOwner",
177
+ }
178
+ }
179
+
180
+ stage = StageExecution.create(
181
+ type=config.get("type", "unknown"),
182
+ name=config.get("name", config.get("type", "Unknown")),
183
+ ref_id=config.get("refId", config.get("id", "")),
184
+ context=context,
185
+ requisite_stage_ref_ids=requisite_ids,
186
+ )
187
+
188
+ # Build tasks using stage definition builder
189
+ builder = self.stage_builder_factory.get(stage.type)
190
+ stage.tasks = builder.build_tasks(stage)
191
+
192
+ # Mark first/last tasks
193
+ if stage.tasks:
194
+ stage.tasks[0].stage_start = True
195
+ stage.tasks[-1].stage_end = True
196
+
197
+ return stage
198
+
199
+ def create_orchestration(
200
+ self,
201
+ application: str,
202
+ name: str,
203
+ stages: list[dict[str, Any]],
204
+ ) -> Workflow:
205
+ """
206
+ Create an ad-hoc orchestration (single execution not from a pipeline).
207
+
208
+ Args:
209
+ application: Application name
210
+ name: Orchestration name
211
+ stages: Stage configurations
212
+
213
+ Returns:
214
+ The created execution
215
+ """
216
+ parsed_stages = self._parse_stages(stages)
217
+
218
+ execution = Workflow.create_orchestration(
219
+ application=application,
220
+ name=name,
221
+ stages=parsed_stages,
222
+ )
223
+
224
+ self.repository.store(execution)
225
+ self.runner.start(execution)
226
+
227
+ return execution
228
+
229
+
230
+ def create_simple_pipeline(
231
+ name: str,
232
+ application: str,
233
+ stages: list[dict[str, Any]],
234
+ ) -> dict[str, Any]:
235
+ """
236
+ Create a simple pipeline configuration.
237
+
238
+ Helper function for building pipeline configs programmatically.
239
+
240
+ Args:
241
+ name: Pipeline name
242
+ application: Application name
243
+ stages: List of stage configurations
244
+
245
+ Returns:
246
+ A pipeline configuration dictionary
247
+
248
+ Example:
249
+ config = create_simple_pipeline(
250
+ name="Deploy to Prod",
251
+ application="myapp",
252
+ stages=[
253
+ {"refId": "1", "type": "wait", "name": "Wait", "waitTime": 30},
254
+ {"refId": "2", "type": "deploy", "name": "Deploy",
255
+ "requisiteStageRefIds": ["1"]},
256
+ ],
257
+ )
258
+ """
259
+ return {
260
+ "name": name,
261
+ "application": application,
262
+ "stages": stages,
263
+ }
264
+
265
+
266
+ def create_stage_config(
267
+ ref_id: str,
268
+ stage_type: str,
269
+ name: str,
270
+ requisites: list[str] | None = None,
271
+ **kwargs: Any,
272
+ ) -> dict[str, Any]:
273
+ """
274
+ Create a stage configuration.
275
+
276
+ Helper function for building stage configs.
277
+
278
+ Args:
279
+ ref_id: Stage reference ID
280
+ stage_type: Stage type
281
+ name: Stage name
282
+ requisites: Prerequisite stage ref IDs
283
+ **kwargs: Additional stage context
284
+
285
+ Returns:
286
+ A stage configuration dictionary
287
+
288
+ Example:
289
+ stage = create_stage_config(
290
+ ref_id="1",
291
+ stage_type="wait",
292
+ name="Wait for approval",
293
+ waitTime=3600,
294
+ )
295
+ """
296
+ config: dict[str, Any] = {
297
+ "refId": ref_id,
298
+ "type": stage_type,
299
+ "name": name,
300
+ }
301
+
302
+ if requisites:
303
+ config["requisiteStageRefIds"] = requisites
304
+
305
+ config.update(kwargs)
306
+
307
+ return config
@@ -0,0 +1,97 @@
1
+ -- migration: initial_schema
2
+ -- id: 01KDQ4N9QPJ6Q4MCV3V9GHWPV4
3
+
4
+ -- migrate: up
5
+
6
+ CREATE TABLE pipeline_executions (
7
+ id VARCHAR(26) PRIMARY KEY,
8
+ type VARCHAR(50) NOT NULL,
9
+ application VARCHAR(255) NOT NULL,
10
+ name VARCHAR(255),
11
+ status VARCHAR(50) NOT NULL,
12
+ start_time BIGINT,
13
+ end_time BIGINT,
14
+ start_time_expiry BIGINT,
15
+ trigger JSONB,
16
+ context JSONB,
17
+ is_canceled BOOLEAN DEFAULT FALSE,
18
+ canceled_by VARCHAR(255),
19
+ cancellation_reason TEXT,
20
+ paused JSONB,
21
+ pipeline_config_id VARCHAR(255),
22
+ is_limit_concurrent BOOLEAN DEFAULT FALSE,
23
+ max_concurrent_executions INT DEFAULT 0,
24
+ keep_waiting_pipelines BOOLEAN DEFAULT FALSE,
25
+ origin VARCHAR(50) DEFAULT 'unknown',
26
+ created_at TIMESTAMP DEFAULT NOW()
27
+ );
28
+
29
+ CREATE TABLE stage_executions (
30
+ id VARCHAR(26) PRIMARY KEY,
31
+ execution_id VARCHAR(26) NOT NULL REFERENCES pipeline_executions(id) ON DELETE CASCADE,
32
+ ref_id VARCHAR(50) NOT NULL,
33
+ type VARCHAR(100) NOT NULL,
34
+ name VARCHAR(255),
35
+ status VARCHAR(50) NOT NULL,
36
+ context JSONB,
37
+ outputs JSONB,
38
+ requisite_stage_ref_ids TEXT[],
39
+ parent_stage_id VARCHAR(26),
40
+ synthetic_stage_owner VARCHAR(20),
41
+ start_time BIGINT,
42
+ end_time BIGINT,
43
+ start_time_expiry BIGINT,
44
+ scheduled_time BIGINT,
45
+ UNIQUE(execution_id, ref_id)
46
+ );
47
+
48
+ CREATE TABLE task_executions (
49
+ id VARCHAR(26) PRIMARY KEY,
50
+ stage_id VARCHAR(26) NOT NULL REFERENCES stage_executions(id) ON DELETE CASCADE,
51
+ name VARCHAR(255) NOT NULL,
52
+ implementing_class VARCHAR(255) NOT NULL,
53
+ status VARCHAR(50) NOT NULL,
54
+ start_time BIGINT,
55
+ end_time BIGINT,
56
+ stage_start BOOLEAN DEFAULT FALSE,
57
+ stage_end BOOLEAN DEFAULT FALSE,
58
+ loop_start BOOLEAN DEFAULT FALSE,
59
+ loop_end BOOLEAN DEFAULT FALSE,
60
+ task_exception_details JSONB
61
+ );
62
+
63
+ CREATE TABLE queue_messages (
64
+ id SERIAL PRIMARY KEY,
65
+ message_id VARCHAR(36) NOT NULL UNIQUE,
66
+ message_type VARCHAR(100) NOT NULL,
67
+ payload JSONB NOT NULL,
68
+ deliver_at TIMESTAMP NOT NULL DEFAULT NOW(),
69
+ attempts INT DEFAULT 0,
70
+ max_attempts INT DEFAULT 10,
71
+ locked_until TIMESTAMP,
72
+ created_at TIMESTAMP DEFAULT NOW()
73
+ );
74
+
75
+ CREATE INDEX idx_executions_application ON pipeline_executions(application);
76
+ CREATE INDEX idx_executions_config_id ON pipeline_executions(pipeline_config_id);
77
+ CREATE INDEX idx_executions_status ON pipeline_executions(status);
78
+ CREATE INDEX idx_stage_execution ON stage_executions(execution_id);
79
+ CREATE INDEX idx_stage_parent ON stage_executions(parent_stage_id);
80
+ CREATE INDEX idx_task_stage ON task_executions(stage_id);
81
+ CREATE INDEX idx_queue_deliver ON queue_messages(deliver_at) WHERE attempts < 10;
82
+ CREATE INDEX idx_queue_locked ON queue_messages(locked_until);
83
+
84
+ -- migrate: down
85
+
86
+ DROP INDEX IF EXISTS idx_queue_locked;
87
+ DROP INDEX IF EXISTS idx_queue_deliver;
88
+ DROP INDEX IF EXISTS idx_task_stage;
89
+ DROP INDEX IF EXISTS idx_stage_parent;
90
+ DROP INDEX IF EXISTS idx_stage_execution;
91
+ DROP INDEX IF EXISTS idx_executions_status;
92
+ DROP INDEX IF EXISTS idx_executions_config_id;
93
+ DROP INDEX IF EXISTS idx_executions_application;
94
+ DROP TABLE IF EXISTS queue_messages;
95
+ DROP TABLE IF EXISTS task_executions;
96
+ DROP TABLE IF EXISTS stage_executions;
97
+ DROP TABLE IF EXISTS pipeline_executions;
@@ -0,0 +1,25 @@
1
+ -- migration: rag_embeddings
2
+ -- id: 01KDRK3TXW4R2GERC1WBCQYJGG
3
+
4
+ -- migrate: up
5
+
6
+ CREATE TABLE rag_embeddings (
7
+ id SERIAL PRIMARY KEY,
8
+ doc_id VARCHAR(255) NOT NULL,
9
+ content TEXT NOT NULL,
10
+ embedding JSONB NOT NULL,
11
+ embedding_model VARCHAR(100) NOT NULL,
12
+ chunk_index INT DEFAULT 0,
13
+ created_at TIMESTAMP DEFAULT NOW(),
14
+ updated_at TIMESTAMP DEFAULT NOW(),
15
+ UNIQUE(doc_id, chunk_index, embedding_model)
16
+ );
17
+
18
+ CREATE INDEX idx_rag_embeddings_doc ON rag_embeddings(doc_id);
19
+ CREATE INDEX idx_rag_embeddings_model ON rag_embeddings(embedding_model);
20
+
21
+ -- migrate: down
22
+
23
+ DROP INDEX IF EXISTS idx_rag_embeddings_model;
24
+ DROP INDEX IF EXISTS idx_rag_embeddings_doc;
25
+ DROP TABLE IF EXISTS rag_embeddings;
@@ -0,0 +1 @@
1
+ """Stabilize database migrations for PostgreSQL."""
@@ -0,0 +1,15 @@
1
+ """Core data models for pipeline execution."""
2
+
3
+ from stabilize.models.stage import StageExecution, SyntheticStageOwner
4
+ from stabilize.models.status import WorkflowStatus
5
+ from stabilize.models.task import TaskExecution
6
+ from stabilize.models.workflow import Workflow, WorkflowType
7
+
8
+ __all__ = [
9
+ "WorkflowStatus",
10
+ "TaskExecution",
11
+ "StageExecution",
12
+ "SyntheticStageOwner",
13
+ "Workflow",
14
+ "WorkflowType",
15
+ ]