fastprocesses 0.22.0__tar.gz → 0.22.2__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.
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/PKG-INFO +2 -2
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/README.md +1 -1
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/pyproject.toml +1 -1
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/base_process.py +10 -3
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/worker/celery_app.py +3 -1
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/worker/chord_tasks.py +13 -3
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/worker/job_status.py +82 -1
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/worker/pipeline.py +19 -4
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/AUTHORS.md +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/__init__.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/api/__init__.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/api/manager.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/api/router.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/api/server.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/common.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/__init__.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/cache.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/config.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/exceptions.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/logging.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/models.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/output_protocol.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/output_schema_resolver.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/outputs_handler.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/redis_connection.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/types.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/processes/__init__.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/processes/process_registry.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/py.typed +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/static/style.css +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/templates/landing.html +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/worker/__init__.py +0 -0
- {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/worker/executors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastprocesses
|
|
3
|
-
Version: 0.22.
|
|
3
|
+
Version: 0.22.2
|
|
4
4
|
Summary: A library to create a FastAPI-based OGC API Processes wrapper around existing projects.
|
|
5
5
|
License-File: AUTHORS.md
|
|
6
6
|
Author: Stefan Schuhart
|
|
@@ -39,7 +39,7 @@ A library to create a FastAPI-based OGC API Processes wrapper around existing pr
|
|
|
39
39
|
|
|
40
40
|
AI helped to create this code.
|
|
41
41
|
|
|
42
|
-
## Version: 0.22.
|
|
42
|
+
## Version: 0.22.2
|
|
43
43
|
|
|
44
44
|
### Description
|
|
45
45
|
|
|
@@ -79,7 +79,7 @@ exclude = [
|
|
|
79
79
|
|
|
80
80
|
[tool.poetry]
|
|
81
81
|
name = "fastprocesses"
|
|
82
|
-
version = "0.22.
|
|
82
|
+
version = "0.22.2"
|
|
83
83
|
description = "A library to create a FastAPI-based OGC API Processes wrapper around existing projects."
|
|
84
84
|
authors = ["Stefan Schuhart <stefan.schuhart@gv.hamburg.de>"]
|
|
85
85
|
readme = "README.md"
|
|
@@ -104,7 +104,10 @@ class BaseProcess(ABC):
|
|
|
104
104
|
else:
|
|
105
105
|
return result
|
|
106
106
|
|
|
107
|
-
def resolve_remote_inputs(
|
|
107
|
+
def resolve_remote_inputs(
|
|
108
|
+
self, exec_body: Dict[str, Any],
|
|
109
|
+
job_progress_callback: JobProgressCallback | None = None
|
|
110
|
+
) -> Dict[str, Any]:
|
|
108
111
|
"""
|
|
109
112
|
Substitutes URI-valued inputs with the downloaded data they reference.
|
|
110
113
|
|
|
@@ -238,7 +241,9 @@ class BaseProcess(ABC):
|
|
|
238
241
|
|
|
239
242
|
return True
|
|
240
243
|
|
|
241
|
-
def validate_inputs(
|
|
244
|
+
def validate_inputs(
|
|
245
|
+
self, inputs: Dict[str, Any],
|
|
246
|
+
) -> bool:
|
|
242
247
|
"""
|
|
243
248
|
Validates the input data against the process description.
|
|
244
249
|
|
|
@@ -464,7 +469,8 @@ class BaseParallelProcess(BaseProcess, ABC):
|
|
|
464
469
|
|
|
465
470
|
@abstractmethod
|
|
466
471
|
def merge_results(
|
|
467
|
-
self, results: List[Dict[str, Any]]
|
|
472
|
+
self, results: List[Dict[str, Any]],
|
|
473
|
+
job_progress_callback: JobProgressCallback | None = None,
|
|
468
474
|
) -> BaseProcessResult:
|
|
469
475
|
"""
|
|
470
476
|
Combine N partial results into the final output.
|
|
@@ -610,6 +616,7 @@ class BaseScatterProcess(BaseProcess, ABC):
|
|
|
610
616
|
self,
|
|
611
617
|
results: Dict[str, Any],
|
|
612
618
|
exec_body: Dict[str, Any],
|
|
619
|
+
job_progress_callback: JobProgressCallback | None = None,
|
|
613
620
|
) -> BaseProcessResult:
|
|
614
621
|
"""
|
|
615
622
|
Combine the results of all parallel steps into the final output.
|
|
@@ -116,7 +116,9 @@ def execute_process(self, process_id: str, serialized_data: str | bytes):
|
|
|
116
116
|
logger.debug("Cache lookup failed (non-fatal), proceeding: {}", e)
|
|
117
117
|
|
|
118
118
|
process = _load_process(process_id, job_id)
|
|
119
|
-
data = _run_pipeline(
|
|
119
|
+
data = _run_pipeline(
|
|
120
|
+
process, process_id, data, job_id, job_progress_callback
|
|
121
|
+
)
|
|
120
122
|
|
|
121
123
|
try:
|
|
122
124
|
update_job_status(
|
|
@@ -17,7 +17,10 @@ from fastprocesses.core.models import CalculationTask, JobStatusCode
|
|
|
17
17
|
from fastprocesses.processes.process_registry import get_process_registry
|
|
18
18
|
from fastprocesses.worker.job_status import (
|
|
19
19
|
_cleanup_progress_counter,
|
|
20
|
+
_cleanup_scatter_roster,
|
|
20
21
|
_increment_and_report_progress,
|
|
22
|
+
_init_scatter_roster,
|
|
23
|
+
_record_scatter_step_done,
|
|
21
24
|
update_job_status,
|
|
22
25
|
)
|
|
23
26
|
|
|
@@ -270,7 +273,7 @@ def execute_scatter_step(
|
|
|
270
273
|
result = jsonable_encoder(partial)
|
|
271
274
|
rkey = _result_key(job_id, step_name)
|
|
272
275
|
temp_result_cache.put(key=rkey, value=result)
|
|
273
|
-
|
|
276
|
+
_record_scatter_step_done(job_id, step_name, total)
|
|
274
277
|
return {"__claim_check__": rkey}
|
|
275
278
|
except Exception as exc:
|
|
276
279
|
update_job_status(
|
|
@@ -310,6 +313,7 @@ def _run_scatter(
|
|
|
310
313
|
f"{total} step(s): {step_names}."
|
|
311
314
|
)
|
|
312
315
|
|
|
316
|
+
_init_scatter_roster(job_id, step_names)
|
|
313
317
|
pkey = _payload_key(job_id)
|
|
314
318
|
temp_result_cache.put(
|
|
315
319
|
key=pkey,
|
|
@@ -357,7 +361,12 @@ def finalize_scatter(
|
|
|
357
361
|
|
|
358
362
|
payload = temp_result_cache.get(payload_key)
|
|
359
363
|
temp_result_cache.delete(payload_key)
|
|
360
|
-
|
|
364
|
+
# step_input is the resolved data (after resolve_remote_inputs + pipeline).
|
|
365
|
+
# original_input is the wire-format exec_body (may contain URL strings)
|
|
366
|
+
# and is used only for deriving the stable cache key — never for business
|
|
367
|
+
# logic, because the resolved content may change between cache fills.
|
|
368
|
+
exec_body: dict = payload["step_input"] if payload else {}
|
|
369
|
+
original_input: dict = payload.get("original_input", exec_body) if payload else {}
|
|
361
370
|
|
|
362
371
|
update_job_status(
|
|
363
372
|
job_id, 95, "Merging scatter results.", JobStatusCode.RUNNING
|
|
@@ -366,7 +375,7 @@ def finalize_scatter(
|
|
|
366
375
|
merged = merge_result.model_dump(mode="json")
|
|
367
376
|
|
|
368
377
|
try:
|
|
369
|
-
calculation_task = CalculationTask(**
|
|
378
|
+
calculation_task = CalculationTask(**original_input)
|
|
370
379
|
temp_result_cache.put(key=calculation_task.celery_key, value=merged)
|
|
371
380
|
# Also store under job_id so get_job_result can retrieve it.
|
|
372
381
|
temp_result_cache.put(key=job_id, value=merged)
|
|
@@ -382,6 +391,7 @@ def finalize_scatter(
|
|
|
382
391
|
job_id, 100, "Process completed.", JobStatusCode.SUCCESSFUL
|
|
383
392
|
)
|
|
384
393
|
_cleanup_progress_counter(job_id)
|
|
394
|
+
_cleanup_scatter_roster(job_id)
|
|
385
395
|
logger.info(
|
|
386
396
|
f"Scatter process {process_id} (job {job_id}) completed successfully."
|
|
387
397
|
)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from datetime import datetime, timezone
|
|
2
|
-
|
|
2
|
+
import json
|
|
3
3
|
from fastprocesses.common import job_status_cache
|
|
4
4
|
from fastprocesses.core.logging import logger
|
|
5
5
|
from fastprocesses.core.models import JobStatusCode, JobStatusInfo, Link
|
|
6
6
|
|
|
7
7
|
_SUBTASK_PROGRESS_KEY = "fp:subtask_progress"
|
|
8
|
+
_SCATTER_ALL_KEY = "fp:scatter_all"
|
|
9
|
+
_SCATTER_DONE_KEY = "fp:scatter_done"
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
def update_job_status(
|
|
@@ -144,3 +146,82 @@ def _cleanup_progress_counter(job_id: str) -> None:
|
|
|
144
146
|
logger.warning(
|
|
145
147
|
"Could not clean up progress counter for job {}: {!r}", job_id, exc
|
|
146
148
|
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _init_scatter_roster(job_id: str, step_names: list[str]) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Stores the full list of scatter step names for *job_id* so that any
|
|
154
|
+
subtask can later read it back to build the step-status message.
|
|
155
|
+
Called once by ``_run_scatter`` before the chord is dispatched.
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
redis = job_status_cache.redis_connection
|
|
159
|
+
all_key = f"{_SCATTER_ALL_KEY}:{job_id}"
|
|
160
|
+
|
|
161
|
+
redis._execute_redis_command("set", all_key, json.dumps(step_names))
|
|
162
|
+
redis._execute_redis_command("expire", all_key, 86400)
|
|
163
|
+
except Exception as exc: # pragma: no cover
|
|
164
|
+
logger.warning(
|
|
165
|
+
"Could not initialise scatter roster for job {}: {!r}", job_id, exc
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _record_scatter_step_done(job_id: str, step_name: str, total: int) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Marks *step_name* as completed for *job_id* and updates the job status
|
|
172
|
+
message with a human-readable roster:
|
|
173
|
+
|
|
174
|
+
Steps: vulnerability_pipeline ✓, exposure_pipeline ✓,
|
|
175
|
+
hazard_wb_pipeline …, hazard_ma_pipeline … (2/4 done)
|
|
176
|
+
|
|
177
|
+
Uses an atomic INCR for the progress percentage (same 0-90 % band as
|
|
178
|
+
``_increment_and_report_progress``) and a Redis set for the done-step
|
|
179
|
+
roster so concurrent workers never race.
|
|
180
|
+
|
|
181
|
+
All exceptions are swallowed so a Redis hiccup never causes a subtask
|
|
182
|
+
to fail.
|
|
183
|
+
"""
|
|
184
|
+
import json as json
|
|
185
|
+
counter_key = f"{_SUBTASK_PROGRESS_KEY}:{job_id}"
|
|
186
|
+
done_key = f"{_SCATTER_DONE_KEY}:{job_id}"
|
|
187
|
+
all_key = f"{_SCATTER_ALL_KEY}:{job_id}"
|
|
188
|
+
try:
|
|
189
|
+
redis = job_status_cache.redis_connection
|
|
190
|
+
completed = redis._execute_redis_command("incr", counter_key)
|
|
191
|
+
if completed == 1:
|
|
192
|
+
redis._execute_redis_command("expire", counter_key, 86400)
|
|
193
|
+
redis._execute_redis_command("sadd", done_key, step_name)
|
|
194
|
+
redis._execute_redis_command("expire", done_key, 86400)
|
|
195
|
+
|
|
196
|
+
# Read back all_steps and done_steps to build the message.
|
|
197
|
+
raw_all = redis._execute_redis_command("get", all_key)
|
|
198
|
+
all_steps: list[str] = json.loads(raw_all) if raw_all else []
|
|
199
|
+
done_raw = redis._execute_redis_command("smembers", done_key)
|
|
200
|
+
done_steps: set[str] = {
|
|
201
|
+
m.decode() if isinstance(m, bytes) else m for m in (done_raw or [])
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
parts = [
|
|
205
|
+
f"{s} \u2713" if s in done_steps else f"{s} \u2026"
|
|
206
|
+
for s in (all_steps or [step_name])
|
|
207
|
+
]
|
|
208
|
+
message = ", ".join(parts) + f" ({len(done_steps)}/{total} done)"
|
|
209
|
+
|
|
210
|
+
progress = min(int(completed / total * 90), 90)
|
|
211
|
+
update_job_status(job_id, progress, message, JobStatusCode.RUNNING)
|
|
212
|
+
except Exception as exc: # pragma: no cover
|
|
213
|
+
logger.warning(
|
|
214
|
+
"Could not record scatter step progress for job {}: {!r}", job_id, exc
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _cleanup_scatter_roster(job_id: str) -> None:
|
|
219
|
+
"""Deletes scatter roster keys for *job_id* after the job completes."""
|
|
220
|
+
try:
|
|
221
|
+
redis = job_status_cache.redis_connection
|
|
222
|
+
redis._execute_redis_command("delete", f"{_SCATTER_ALL_KEY}:{job_id}")
|
|
223
|
+
redis._execute_redis_command("delete", f"{_SCATTER_DONE_KEY}:{job_id}")
|
|
224
|
+
except Exception as exc: # pragma: no cover
|
|
225
|
+
logger.warning(
|
|
226
|
+
"Could not clean up scatter roster for job {}: {!r}", job_id, exc
|
|
227
|
+
)
|
|
@@ -8,6 +8,7 @@ from fastprocesses.core.exceptions import (
|
|
|
8
8
|
)
|
|
9
9
|
from fastprocesses.core.logging import logger
|
|
10
10
|
from fastprocesses.core.models import JobStatusCode
|
|
11
|
+
from fastprocesses.core.types import JobProgressCallback
|
|
11
12
|
from fastprocesses.processes.process_registry import get_process_registry
|
|
12
13
|
from fastprocesses.worker.job_status import update_job_status
|
|
13
14
|
|
|
@@ -38,7 +39,8 @@ def _load_process(process_id: str, job_id: str) -> BaseProcess:
|
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
def _run_pipeline(
|
|
41
|
-
process: BaseProcess, process_id: str, data: dict, job_id: str
|
|
42
|
+
process: BaseProcess, process_id: str, data: dict, job_id: str,
|
|
43
|
+
job_progress_callback: JobProgressCallback | None = None,
|
|
42
44
|
) -> dict:
|
|
43
45
|
"""
|
|
44
46
|
Runs the pre-execution pipeline: validate → resolve remote inputs → late_validate.
|
|
@@ -47,7 +49,11 @@ def _run_pipeline(
|
|
|
47
49
|
Returns the (possibly modified) data dict after remote input resolution.
|
|
48
50
|
"""
|
|
49
51
|
# Step 1: validate wire-format inputs against the process description schema
|
|
50
|
-
update_job_status(
|
|
52
|
+
update_job_status(
|
|
53
|
+
job_id, 0,
|
|
54
|
+
"Pre-process step 1: Validating inputs against process description.",
|
|
55
|
+
JobStatusCode.RUNNING
|
|
56
|
+
)
|
|
51
57
|
try:
|
|
52
58
|
process.validate_inputs(data["inputs"])
|
|
53
59
|
except ValueError as e:
|
|
@@ -56,9 +62,13 @@ def _run_pipeline(
|
|
|
56
62
|
raise InputValidationError(process_id, repr(e))
|
|
57
63
|
|
|
58
64
|
# Step 2: resolve remote inputs (URI strings → downloaded data)
|
|
59
|
-
update_job_status(
|
|
65
|
+
update_job_status(
|
|
66
|
+
job_id, 0,
|
|
67
|
+
"Pre-process step 2: Resolving remote inputs.",
|
|
68
|
+
JobStatusCode.RUNNING
|
|
69
|
+
)
|
|
60
70
|
try:
|
|
61
|
-
data = process.resolve_remote_inputs(data)
|
|
71
|
+
data = process.resolve_remote_inputs(data, job_progress_callback)
|
|
62
72
|
except (SSRFBlockedError, ValueError) as e:
|
|
63
73
|
logger.error(
|
|
64
74
|
"Remote input resolution failed for process {}: {}", process_id, e
|
|
@@ -67,6 +77,11 @@ def _run_pipeline(
|
|
|
67
77
|
raise InputValidationError(process_id, repr(e))
|
|
68
78
|
|
|
69
79
|
# Step 3: late validation of resolved inputs (no-op by default)
|
|
80
|
+
update_job_status(
|
|
81
|
+
job_id, 0,
|
|
82
|
+
"Pre-process step 3: Validating remotely resolved inputs.",
|
|
83
|
+
JobStatusCode.RUNNING
|
|
84
|
+
)
|
|
70
85
|
try:
|
|
71
86
|
process.late_validate(data["inputs"])
|
|
72
87
|
except ValueError as e:
|
|
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
|
{fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/output_schema_resolver.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/processes/process_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|