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.
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/PKG-INFO +3 -3
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/pyproject.toml +3 -3
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/http_entrypoint.py +104 -44
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/__init__.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/ingress/__init__.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/ingress/app.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/ingress/router.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/wiring/__init__.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/wiring/runtime.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/__init__.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/__init__.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/__init__.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/bootstrap.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/consumer_middleware.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/dramatiq_backend.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/dramatiq_consumer.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/dramatiq/entrypoint.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/README.md +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/__init__.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/drain.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/feed_consumer.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/main.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/managed_consumer.py +0 -0
- {flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/backends/managed/task_resolver.py +0 -0
- {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.
|
|
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.
|
|
16
|
-
Requires-Dist: flowstash-lib (>=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.
|
|
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.
|
|
9
|
-
"flowstash-lib>=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
|
-
"""
|
|
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
|
|
69
|
-
"""
|
|
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
|
-
|
|
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(
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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":
|
|
293
|
+
content={"status": "DRAINING", "task": task_ref},
|
|
233
294
|
)
|
|
234
295
|
|
|
235
296
|
try:
|
|
236
|
-
return await _execute_managed_task(
|
|
237
|
-
except
|
|
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.
|
|
241
|
-
content={"status": "
|
|
300
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
301
|
+
content={"status": "TASK_NOT_FOUND", "detail": str(e)},
|
|
242
302
|
)
|
|
243
303
|
|
|
244
304
|
|
|
File without changes
|
{flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/ingress/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/wiring/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{flowstash_runtime-0.7.2 → flowstash_runtime-0.8.0}/src/flowstash/runtime/worker/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|