flowstash-runtime 0.7.2__tar.gz → 0.8.0__tar.gz

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 (25) hide show
  1. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/PKG-INFO +3 -3
  2. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/pyproject.toml +3 -3
  3. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/http_entrypoint.py +104 -44
  4. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/__init__.py +0 -0
  5. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/ingress/__init__.py +0 -0
  6. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/ingress/app.py +0 -0
  7. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/ingress/router.py +0 -0
  8. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/wiring/__init__.py +0 -0
  9. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/wiring/runtime.py +0 -0
  10. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/__init__.py +0 -0
  11. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/__init__.py +0 -0
  12. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/__init__.py +0 -0
  13. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/bootstrap.py +0 -0
  14. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/consumer_middleware.py +0 -0
  15. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/dramatiq_backend.py +0 -0
  16. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/dramatiq_consumer.py +0 -0
  17. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/entrypoint.py +0 -0
  18. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/README.md +0 -0
  19. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/__init__.py +0 -0
  20. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/drain.py +0 -0
  21. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/feed_consumer.py +0 -0
  22. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/main.py +0 -0
  23. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/managed_consumer.py +0 -0
  24. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/task_resolver.py +0 -0
  25. {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flowstash-runtime
3
- Version: 0.7.2
3
+ Version: 0.8.0
4
4
  Summary: Actual runtime engine and bootstrap layer for the flowstash platform.
5
5
  Author: juraj.bezdek@gmail.com
6
6
  Author-email: juraj.bezdek@gmail.com
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.13
12
12
  Requires-Dist: apscheduler (>=3.11.0)
13
13
  Requires-Dist: dramatiq (>=1.16.0)
14
14
  Requires-Dist: fastapi (>=0.110.0)
15
- Requires-Dist: flowstash-clients (>=0.7.2,<0.8.0)
16
- Requires-Dist: flowstash-lib (>=0.7.2,<0.8.0)
15
+ Requires-Dist: flowstash-clients (>=0.8.0,<0.9.0)
16
+ Requires-Dist: flowstash-lib (>=0.8.0,<0.9.0)
17
17
  Requires-Dist: pyyaml (>=6.0.1)
18
18
  Requires-Dist: uvicorn (>=0.29.0)
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "flowstash-runtime"
3
- version = "0.7.2"
3
+ version = "0.8.0"
4
4
  description = "Actual runtime engine and bootstrap layer for the flowstash platform."
5
5
  authors = [{name = "juraj.bezdek@gmail.com", email = "juraj.bezdek@gmail.com"}]
6
6
  requires-python = ">=3.11"
7
7
  dependencies = [
8
- "flowstash-clients>=0.7.2,<0.8.0",
9
- "flowstash-lib>=0.7.2,<0.8.0",
8
+ "flowstash-clients>=0.8.0,<0.9.0",
9
+ "flowstash-lib>=0.8.0,<0.9.0",
10
10
  "fastapi>=0.110.0",
11
11
  "uvicorn>=0.29.0",
12
12
  "dramatiq>=1.16.0",
@@ -10,7 +10,6 @@ Feed endpoints:
10
10
  POST /internal/feed/deliver/classic — single-record classic delivery (called by Cloud Tasks)
11
11
  """
12
12
 
13
- import inspect
14
13
  import logging
15
14
  import os
16
15
  from contextlib import asynccontextmanager
@@ -20,7 +19,7 @@ from typing import Any, Dict, List, Optional
20
19
  import httpx
21
20
  from fastapi import APIRouter, HTTPException, Request, status
22
21
  from fastapi.responses import JSONResponse
23
- from pydantic import BaseModel
22
+ from pydantic import BaseModel, Field
24
23
 
25
24
  from flowstash.context import integration_context
26
25
  from flowstash.observability.ingestion import (
@@ -31,7 +30,6 @@ from flowstash.observability.ingestion import (
31
30
 
32
31
  from .drain import ManagedTaskDrainController
33
32
  from .task_resolver import (
34
- register_task,
35
33
  resolve_function as _registry_resolve,
36
34
  _invoke_task_callable,
37
35
  )
@@ -52,28 +50,80 @@ class DelegationMetadataModel(BaseModel):
52
50
  target_task: Optional[str] = None
53
51
  accepted_id: Optional[str] = None
54
52
  schedule_time: Optional[str] = None
55
- attrs: Dict[str, Any] = {}
53
+ attrs: Dict[str, Any] = Field(default_factory=dict)
56
54
 
57
55
 
58
56
  class ManagedHandleTaskPayload(BaseModel):
59
- """Nested payload within the /handle_task protocol envelope."""
57
+ """Legacy nested payload accepted for backward compatibility."""
60
58
 
61
59
  func_ref: Optional[str] = None
62
- args: list = []
63
- kwargs: dict = {}
60
+ args: list = Field(default_factory=list)
61
+ kwargs: dict = Field(default_factory=dict)
62
+ integration: Optional[str] = None
63
+ pipeline: Optional[str] = None
64
+ triggered_by: Optional[str] = None
65
+ cron: Optional[str] = None
66
+ run_id: Optional[str] = None
64
67
  tags: Optional[Dict[str, Any]] = None
65
68
  delegation: Optional[DelegationMetadataModel] = None
66
69
 
67
70
 
68
- class ManagedHandleTaskEnvelope(BaseModel):
69
- """Protocol envelope delivered by the platform to /handle_task."""
71
+ class TaskPayload(BaseModel):
72
+ """Payload pushed by Managed Scheduler or Platform API."""
70
73
 
71
- task_id: str
74
+ task_id: Optional[str] = None
72
75
  task_name: Optional[str] = None
76
+ func_ref: Optional[str] = None
77
+ args: list = Field(default_factory=list)
78
+ kwargs: dict = Field(default_factory=dict)
73
79
  integration: Optional[str] = None
74
80
  pipeline: Optional[str] = None
75
81
  triggered_by: Optional[str] = None
76
- payload: ManagedHandleTaskPayload = ManagedHandleTaskPayload()
82
+ cron: Optional[str] = None
83
+ # run_id is kept for backward compatibility with older payloads that carry it,
84
+ # but it is NOT used as the execution run_id. The execution side always allocates
85
+ # a fresh run_id. Causal metadata is carried in the `delegation` field instead.
86
+ run_id: Optional[str] = None
87
+ tags: Optional[Dict[str, Any]] = None
88
+ delegation: Optional[DelegationMetadataModel] = None
89
+ payload: Optional[ManagedHandleTaskPayload] = None
90
+
91
+ def _payload_value(self, field_name: str) -> Any:
92
+ if self.payload is None:
93
+ return None
94
+ return getattr(self.payload, field_name)
95
+
96
+ def effective_func_ref(self) -> Optional[str]:
97
+ return self.func_ref or self._payload_value("func_ref")
98
+
99
+ def effective_args(self) -> list:
100
+ return self.args or self._payload_value("args") or []
101
+
102
+ def effective_kwargs(self) -> dict:
103
+ return self.kwargs or self._payload_value("kwargs") or {}
104
+
105
+ def effective_integration(self) -> Optional[str]:
106
+ return self.integration or self._payload_value("integration")
107
+
108
+ def effective_pipeline(self) -> Optional[str]:
109
+ return self.pipeline or self._payload_value("pipeline")
110
+
111
+ def effective_triggered_by(self) -> Optional[str]:
112
+ return self.triggered_by or self._payload_value("triggered_by")
113
+
114
+ def effective_cron(self) -> Optional[str]:
115
+ return self.cron or self._payload_value("cron")
116
+
117
+ def effective_tags(self) -> Dict[str, Any]:
118
+ tags = dict(self._payload_value("tags") or {})
119
+ tags.update(self.tags or {})
120
+ return tags
121
+
122
+ def effective_delegation(self) -> Optional[DelegationMetadataModel]:
123
+ return self.delegation or self._payload_value("delegation")
124
+
125
+ def execution_task_ref(self) -> Optional[str]:
126
+ return self.task_id or self.task_name or self.effective_func_ref()
77
127
 
78
128
 
79
129
  # ─── Task Resolution ─────────────────────────────────────────────────
@@ -131,23 +181,30 @@ async def _managed_request_scope(request: Request, kind: str, task_ref: str):
131
181
  await _flush_observability()
132
182
 
133
183
 
134
- async def _execute_managed_task(envelope: ManagedHandleTaskEnvelope) -> dict:
135
- payload = envelope.payload
136
- integration = envelope.integration or "unknown"
137
- pipeline = envelope.pipeline or "unknown"
184
+ async def _execute_managed_task(payload: TaskPayload) -> dict:
185
+ integration = payload.effective_integration() or "unknown"
186
+ pipeline = payload.effective_pipeline() or "unknown"
138
187
  parent_run_id: Optional[str] = None
139
188
  operation_id: Optional[str] = None
140
- if payload.delegation:
141
- parent_run_id = payload.delegation.parent_run_id
142
- operation_id = payload.delegation.operation_id
143
- tags = dict(payload.tags or {})
144
-
145
- if envelope.triggered_by:
146
- tags["triggered_by"] = envelope.triggered_by
147
-
148
- func_ref = payload.func_ref or envelope.task_id
149
- entry_point = envelope.task_name or func_ref
150
- raw_args = {"args": payload.args, "kwargs": payload.kwargs}
189
+ delegation = payload.effective_delegation()
190
+ if delegation:
191
+ parent_run_id = delegation.parent_run_id
192
+ operation_id = delegation.operation_id
193
+ tags = payload.effective_tags()
194
+
195
+ triggered_by = payload.effective_triggered_by()
196
+ if triggered_by:
197
+ tags["triggered_by"] = triggered_by
198
+
199
+ cron = payload.effective_cron()
200
+ if cron:
201
+ tags["cron"] = cron
202
+
203
+ func_ref = payload.effective_func_ref() or payload.task_id
204
+ entry_point = payload.task_name or func_ref or payload.task_id or "unknown"
205
+ args = payload.effective_args()
206
+ kwargs = payload.effective_kwargs()
207
+ raw_args = {"args": args, "kwargs": kwargs}
151
208
 
152
209
  with integration_context(
153
210
  integration=integration,
@@ -159,7 +216,9 @@ async def _execute_managed_task(envelope: ManagedHandleTaskEnvelope) -> dict:
159
216
  record_lifecycle=False,
160
217
  ) as ctx:
161
218
  try:
162
- func = _resolve_task_callable(envelope.task_name, payload.func_ref)
219
+ func = _resolve_task_callable(
220
+ payload.task_name or payload.task_id, func_ref
221
+ )
163
222
  except ValueError as e:
164
223
  logger.warning(f"Could not resolve task '{entry_point}': {e}")
165
224
  await record_run_started(
@@ -174,7 +233,7 @@ async def _execute_managed_task(envelope: ManagedHandleTaskEnvelope) -> dict:
174
233
  )
175
234
  raise
176
235
 
177
- normalized_args = normalize_arguments(func, payload.args, payload.kwargs)
236
+ normalized_args = normalize_arguments(func, args, kwargs)
178
237
  await record_run_started(
179
238
  correlation=ctx.corelation,
180
239
  entry_point=entry_point,
@@ -182,7 +241,7 @@ async def _execute_managed_task(envelope: ManagedHandleTaskEnvelope) -> dict:
182
241
  )
183
242
 
184
243
  try:
185
- await _invoke_task_callable(func, payload.args, payload.kwargs)
244
+ await _invoke_task_callable(func, args, kwargs)
186
245
  except Exception as e:
187
246
  import traceback
188
247
 
@@ -194,10 +253,6 @@ async def _execute_managed_task(envelope: ManagedHandleTaskEnvelope) -> dict:
194
253
  correlation=ctx.corelation,
195
254
  attrs={"error": error, "traceback": tb},
196
255
  )
197
- raise HTTPException(
198
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
199
- detail={"status": "FAILED", "error": error, "task": func_ref},
200
- )
201
256
 
202
257
  await record_run_ended(
203
258
  status="SUCCEEDED",
@@ -214,31 +269,36 @@ async def _execute_managed_task(envelope: ManagedHandleTaskEnvelope) -> dict:
214
269
 
215
270
 
216
271
  @router.post("/handle_task")
217
- async def handle_task(request: Request, envelope: ManagedHandleTaskEnvelope):
272
+ async def handle_task(request: Request, payload: TaskPayload):
218
273
  """
219
274
  Receive and execute a task delivered by the managed platform.
220
275
 
221
- Expects the three-tier protocol envelope with a nested `payload` object.
276
+ Accepts the original flat task payload. A nested `payload` object is still
277
+ accepted for backward compatibility with already-enqueued deliveries.
222
278
  Returns 404 + {"status": "TASK_NOT_FOUND"} when the task cannot be resolved
223
279
  (signals the platform to clean up orphaned schedules).
224
280
  Returns 500 on execution failure so Cloud Tasks retries the delivery.
225
281
  """
226
- task_id = envelope.task_id
227
- task_name = envelope.task_name
228
- async with _managed_request_scope(request, "task", task_id) as admitted:
282
+ task_ref = payload.execution_task_ref()
283
+ if not task_ref:
284
+ raise HTTPException(
285
+ status_code=status.HTTP_400_BAD_REQUEST,
286
+ detail="Missing 'task_id', 'task_name', or 'func_ref' in payload",
287
+ )
288
+
289
+ async with _managed_request_scope(request, "task", task_ref) as admitted:
229
290
  if not admitted:
230
291
  return JSONResponse(
231
292
  status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
232
- content={"status": "DRAINING", "task": task_id},
293
+ content={"status": "DRAINING", "task": task_ref},
233
294
  )
234
295
 
235
296
  try:
236
- return await _execute_managed_task(envelope)
237
- except Exception as e:
238
- print(f"Failed processing task '{task_name}': {e}")
297
+ return await _execute_managed_task(payload)
298
+ except ValueError as e:
239
299
  return JSONResponse(
240
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
241
- content={"status": "Processing failed!", "task_id": task_id},
300
+ status_code=status.HTTP_404_NOT_FOUND,
301
+ content={"status": "TASK_NOT_FOUND", "detail": str(e)},
242
302
  )
243
303
 
244
304