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,312 @@
1
+ """
2
+ Queue processor for handling messages.
3
+
4
+ This module provides the QueueProcessor class that polls messages from
5
+ the queue and dispatches them to appropriate handlers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import threading
12
+ import time
13
+ from collections.abc import Callable
14
+ from concurrent.futures import ThreadPoolExecutor
15
+ from dataclasses import dataclass
16
+ from datetime import timedelta
17
+ from typing import Any, Generic, TypeVar
18
+
19
+ from stabilize.queue.messages import Message, get_message_type_name
20
+ from stabilize.queue.queue import Queue
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ M = TypeVar("M", bound=Message)
25
+
26
+
27
+ class MessageHandler(Generic[M]):
28
+ """
29
+ Base class for message handlers.
30
+
31
+ Each handler processes a specific type of message.
32
+ """
33
+
34
+ @property
35
+ def message_type(self) -> type[M]:
36
+ """Return the type of message this handler processes."""
37
+ raise NotImplementedError
38
+
39
+ def handle(self, message: M) -> None:
40
+ """
41
+ Handle a message.
42
+
43
+ Args:
44
+ message: The message to handle
45
+ """
46
+ raise NotImplementedError
47
+
48
+
49
+ @dataclass
50
+ class QueueProcessorConfig:
51
+ """Configuration for the queue processor."""
52
+
53
+ # How often to poll the queue (milliseconds)
54
+ poll_frequency_ms: int = 50
55
+
56
+ # Maximum number of concurrent message handlers
57
+ max_workers: int = 10
58
+
59
+ # Delay before reprocessing a failed message
60
+ retry_delay: timedelta = timedelta(seconds=15)
61
+
62
+ # Whether to stop on unhandled exceptions
63
+ stop_on_error: bool = False
64
+
65
+
66
+ class QueueProcessor:
67
+ """
68
+ Processes messages from a queue using registered handlers.
69
+
70
+ The processor polls the queue at regular intervals and dispatches
71
+ messages to appropriate handlers. Handlers run in a thread pool
72
+ for concurrent processing.
73
+
74
+ Example:
75
+ queue = InMemoryQueue()
76
+ processor = QueueProcessor(queue)
77
+ processor.register_handler(StartWorkflowHandler(queue, repository))
78
+ processor.start()
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ queue: Queue,
84
+ config: QueueProcessorConfig | None = None,
85
+ ) -> None:
86
+ """
87
+ Initialize the queue processor.
88
+
89
+ Args:
90
+ queue: The queue to process
91
+ config: Optional configuration
92
+ """
93
+ self.queue = queue
94
+ self.config = config or QueueProcessorConfig()
95
+ self._handlers: dict[type[Message], MessageHandler[Any]] = {}
96
+ self._running = False
97
+ self._executor: ThreadPoolExecutor | None = None
98
+ self._poll_thread: threading.Thread | None = None
99
+ self._lock = threading.Lock()
100
+ self._active_count = 0
101
+
102
+ def register_handler(self, handler: MessageHandler[Any]) -> None:
103
+ """
104
+ Register a message handler.
105
+
106
+ Args:
107
+ handler: The handler to register
108
+ """
109
+ self._handlers[handler.message_type] = handler
110
+ logger.debug(f"Registered handler for {handler.message_type.__name__}")
111
+
112
+ def register_handler_func(
113
+ self,
114
+ message_type: type[M],
115
+ handler_func: Callable[[M], None],
116
+ ) -> None:
117
+ """
118
+ Register a handler function for a message type.
119
+
120
+ Args:
121
+ message_type: The type of message to handle
122
+ handler_func: Function to call with the message
123
+ """
124
+
125
+ class FuncHandler(MessageHandler[M]):
126
+ @property
127
+ def message_type(self) -> type[M]:
128
+ return message_type
129
+
130
+ def handle(self, message: M) -> None:
131
+ handler_func(message)
132
+
133
+ self.register_handler(FuncHandler())
134
+
135
+ def start(self) -> None:
136
+ """Start the queue processor."""
137
+ if self._running:
138
+ return
139
+
140
+ self._running = True
141
+ self._executor = ThreadPoolExecutor(max_workers=self.config.max_workers)
142
+ self._poll_thread = threading.Thread(target=self._poll_loop, daemon=True)
143
+ self._poll_thread.start()
144
+ logger.info("Queue processor started")
145
+
146
+ def stop(self, wait: bool = True) -> None:
147
+ """
148
+ Stop the queue processor.
149
+
150
+ Args:
151
+ wait: Whether to wait for pending messages to complete
152
+ """
153
+ self._running = False
154
+
155
+ if self._poll_thread:
156
+ self._poll_thread.join(timeout=5.0)
157
+
158
+ if self._executor:
159
+ self._executor.shutdown(wait=wait)
160
+
161
+ logger.info("Queue processor stopped")
162
+
163
+ def _poll_loop(self) -> None:
164
+ """Main polling loop."""
165
+ poll_interval = self.config.poll_frequency_ms / 1000.0
166
+
167
+ while self._running:
168
+ try:
169
+ # Check if we have capacity
170
+ with self._lock:
171
+ if self._active_count >= self.config.max_workers:
172
+ time.sleep(poll_interval)
173
+ continue
174
+
175
+ # Try to get a message
176
+ message = self.queue.poll_one()
177
+
178
+ if message:
179
+ self._submit_message(message)
180
+ else:
181
+ time.sleep(poll_interval)
182
+
183
+ except Exception as e:
184
+ logger.error(f"Error in poll loop: {e}", exc_info=True)
185
+ if self.config.stop_on_error:
186
+ self._running = False
187
+ break
188
+ time.sleep(poll_interval)
189
+
190
+ def _submit_message(self, message: Message) -> None:
191
+ """Submit a message to the thread pool for processing."""
192
+ with self._lock:
193
+ self._active_count += 1
194
+
195
+ def process_and_ack() -> None:
196
+ try:
197
+ self._handle_message(message)
198
+ self.queue.ack(message)
199
+ except Exception as e:
200
+ logger.error(
201
+ f"Error handling {get_message_type_name(message)}: {e}",
202
+ exc_info=True,
203
+ )
204
+ # Message will be reprocessed after lock expires or reschedule
205
+ self.queue.reschedule(message, self.config.retry_delay)
206
+ finally:
207
+ with self._lock:
208
+ self._active_count -= 1
209
+
210
+ if self._executor is not None:
211
+ self._executor.submit(process_and_ack)
212
+
213
+ def _handle_message(self, message: Message) -> None:
214
+ """Handle a message with the appropriate handler."""
215
+ message_type = type(message)
216
+ handler = self._handlers.get(message_type)
217
+
218
+ if handler is None:
219
+ logger.warning(f"No handler registered for {get_message_type_name(message)}")
220
+ return
221
+
222
+ logger.debug(f"Handling {get_message_type_name(message)} (execution={getattr(message, 'execution_id', 'N/A')})")
223
+
224
+ handler.handle(message)
225
+
226
+ def process_one(self) -> bool:
227
+ """
228
+ Process a single message synchronously.
229
+
230
+ Useful for testing and debugging.
231
+
232
+ Returns:
233
+ True if a message was processed, False otherwise
234
+ """
235
+ message = self.queue.poll_one()
236
+ if message:
237
+ try:
238
+ self._handle_message(message)
239
+ self.queue.ack(message)
240
+ return True
241
+ except Exception as e:
242
+ logger.error(f"Error handling message: {e}", exc_info=True)
243
+ self.queue.reschedule(message, self.config.retry_delay)
244
+ raise
245
+ return False
246
+
247
+ def process_all(self, timeout: float = 60.0) -> int:
248
+ """
249
+ Process all messages synchronously until queue is empty.
250
+
251
+ Useful for testing.
252
+
253
+ Args:
254
+ timeout: Maximum time to wait for processing
255
+
256
+ Returns:
257
+ Number of messages processed
258
+ """
259
+ count = 0
260
+ start = time.time()
261
+
262
+ while time.time() - start < timeout:
263
+ if self.queue.size() == 0:
264
+ break
265
+ if self.process_one():
266
+ count += 1
267
+ else:
268
+ # No ready messages, wait a bit
269
+ time.sleep(0.01)
270
+
271
+ return count
272
+
273
+ @property
274
+ def is_running(self) -> bool:
275
+ """Check if the processor is running."""
276
+ return self._running
277
+
278
+ @property
279
+ def active_count(self) -> int:
280
+ """Get the number of actively processing messages."""
281
+ with self._lock:
282
+ return self._active_count
283
+
284
+
285
+ class SynchronousQueueProcessor(QueueProcessor):
286
+ """
287
+ A synchronous queue processor that processes messages immediately.
288
+
289
+ Useful for testing where you want deterministic execution order.
290
+ """
291
+
292
+ def __init__(self, queue: Queue) -> None:
293
+ super().__init__(queue)
294
+ self._running = True
295
+
296
+ def start(self) -> None:
297
+ """No-op for synchronous processor."""
298
+ pass
299
+
300
+ def stop(self, wait: bool = True) -> None:
301
+ """No-op for synchronous processor."""
302
+ pass
303
+
304
+ def push_and_process(self, message: Message) -> None:
305
+ """
306
+ Push a message and process it immediately.
307
+
308
+ Args:
309
+ message: The message to push and process
310
+ """
311
+ self.queue.push(message)
312
+ self.process_all(timeout=5.0)