fastprocesses 0.18.2__tar.gz → 0.18.3__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.18.2 → fastprocesses-0.18.3}/PKG-INFO +2 -2
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/README.md +1 -1
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/pyproject.toml +1 -1
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/processes/process_registry.py +1 -1
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/worker/celery_app.py +149 -76
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/AUTHORS.md +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/__init__.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/api/__init__.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/api/manager.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/api/router.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/api/server.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/common.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/core/__init__.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/core/base_process.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/core/cache.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/core/config.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/core/exceptions.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/core/logging.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/core/models.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/core/redis_connection.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/core/types.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/processes/__init__.py +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/py.typed +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/static/style.css +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/templates/landing.html +0 -0
- {fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/worker/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fastprocesses
|
|
3
|
-
Version: 0.18.
|
|
3
|
+
Version: 0.18.3
|
|
4
4
|
Summary: A library to create a FastAPI-based OGC API Processes wrapper around existing projects.
|
|
5
5
|
Author: Stefan Schuhart
|
|
6
6
|
Author-email: stefan.schuhart@gv.hamburg.de
|
|
@@ -38,7 +38,7 @@ A library to create a FastAPI-based OGC API Processes wrapper around existing pr
|
|
|
38
38
|
|
|
39
39
|
AI helped to create this code.
|
|
40
40
|
|
|
41
|
-
## Version: 0.18.
|
|
41
|
+
## Version: 0.18.3
|
|
42
42
|
|
|
43
43
|
### Description
|
|
44
44
|
|
|
@@ -79,7 +79,7 @@ exclude = [
|
|
|
79
79
|
|
|
80
80
|
[tool.poetry]
|
|
81
81
|
name = "fastprocesses"
|
|
82
|
-
version = "0.18.
|
|
82
|
+
version = "0.18.3"
|
|
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"
|
{fastprocesses-0.18.2 → fastprocesses-0.18.3}/src/fastprocesses/processes/process_registry.py
RENAMED
|
@@ -115,7 +115,7 @@ class ProcessRegistry:
|
|
|
115
115
|
|
|
116
116
|
process_info = json.loads(process_data) # type: ignore
|
|
117
117
|
logger.debug(
|
|
118
|
-
f"Process data retrieved from Redis
|
|
118
|
+
f"Process data retrieved from Redis for {process_id}."
|
|
119
119
|
)
|
|
120
120
|
|
|
121
121
|
process_class = cast(Type[BaseProcess], locate(process_info["class_path"]))
|
|
@@ -5,9 +5,9 @@ import json
|
|
|
5
5
|
import signal
|
|
6
6
|
import traceback
|
|
7
7
|
from datetime import datetime, timezone
|
|
8
|
-
from typing import Any, Dict
|
|
8
|
+
from typing import Any, Dict, cast
|
|
9
9
|
|
|
10
|
-
from celery import Task, chord
|
|
10
|
+
from celery import Task, chord
|
|
11
11
|
from celery.exceptions import SoftTimeLimitExceeded
|
|
12
12
|
from fastapi.encoders import jsonable_encoder
|
|
13
13
|
from pydantic import BaseModel, ValidationError
|
|
@@ -80,6 +80,7 @@ def update_job_status(
|
|
|
80
80
|
message: str | None = None,
|
|
81
81
|
status: str | None = None,
|
|
82
82
|
started: datetime | None = None,
|
|
83
|
+
process_id: str | None = None,
|
|
83
84
|
) -> None:
|
|
84
85
|
"""
|
|
85
86
|
Updates the progress of a job.
|
|
@@ -96,9 +97,50 @@ def update_job_status(
|
|
|
96
97
|
if not raw_job_info:
|
|
97
98
|
logger.warning(
|
|
98
99
|
"Job status for job_id={} not found in cache when updating; "
|
|
99
|
-
"
|
|
100
|
+
"initializing a fallback job status entry.",
|
|
100
101
|
job_id,
|
|
101
102
|
)
|
|
103
|
+
fallback_status = status or JobStatusCode.RUNNING
|
|
104
|
+
now = datetime.now(timezone.utc)
|
|
105
|
+
job_info = JobStatusInfo.model_validate(
|
|
106
|
+
{
|
|
107
|
+
"jobID": job_id,
|
|
108
|
+
"status": fallback_status,
|
|
109
|
+
"type": "process",
|
|
110
|
+
"processID": process_id,
|
|
111
|
+
"created": now,
|
|
112
|
+
"updated": now,
|
|
113
|
+
"progress": progress,
|
|
114
|
+
"message": message,
|
|
115
|
+
"links": [
|
|
116
|
+
Link.model_validate(
|
|
117
|
+
{
|
|
118
|
+
"href": f"/jobs/{job_id}",
|
|
119
|
+
"rel": "self",
|
|
120
|
+
"type": "application/json",
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
if fallback_status == JobStatusCode.SUCCESSFUL:
|
|
127
|
+
job_info.finished = now
|
|
128
|
+
job_info.links.append(
|
|
129
|
+
Link.model_validate(
|
|
130
|
+
{
|
|
131
|
+
"href": f"/jobs/{job_id}/results",
|
|
132
|
+
"rel": "results",
|
|
133
|
+
"type": "application/json",
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
job_status_cache.put(job_key, job_info)
|
|
138
|
+
logger.debug(
|
|
139
|
+
"Initialized fallback job status for job {}: {} at {}%",
|
|
140
|
+
job_id,
|
|
141
|
+
fallback_status,
|
|
142
|
+
progress,
|
|
143
|
+
)
|
|
102
144
|
return
|
|
103
145
|
|
|
104
146
|
try:
|
|
@@ -128,6 +170,8 @@ def update_job_status(
|
|
|
128
170
|
|
|
129
171
|
if message:
|
|
130
172
|
job_info.message = message
|
|
173
|
+
if process_id and not job_info.processID:
|
|
174
|
+
job_info.processID = process_id
|
|
131
175
|
|
|
132
176
|
job_status_cache.put(job_key, job_info)
|
|
133
177
|
logger.debug(f"Updated progress for job {job_id}: {progress}%, {message}")
|
|
@@ -170,7 +214,7 @@ def _increment_and_report_progress(job_id: str, total: int) -> None:
|
|
|
170
214
|
)
|
|
171
215
|
except Exception as exc: # pragma: no cover
|
|
172
216
|
logger.warning(
|
|
173
|
-
"Could not update subtask progress for job
|
|
217
|
+
"Could not update subtask progress for job {}: {!r}", job_id, exc
|
|
174
218
|
)
|
|
175
219
|
|
|
176
220
|
|
|
@@ -181,7 +225,7 @@ def _cleanup_progress_counter(job_id: str) -> None:
|
|
|
181
225
|
job_status_cache.redis_connection._execute_redis_command("delete", counter_key)
|
|
182
226
|
except Exception as exc: # pragma: no cover
|
|
183
227
|
logger.warning(
|
|
184
|
-
"Could not clean up progress counter for job
|
|
228
|
+
"Could not clean up progress counter for job {}: {!r}", job_id, exc
|
|
185
229
|
)
|
|
186
230
|
|
|
187
231
|
|
|
@@ -201,21 +245,36 @@ def execute_parallel_item(
|
|
|
201
245
|
counter shared by all sibling tasks and updates the parent job's progress
|
|
202
246
|
to ``(n_done / total) * 90 %``.
|
|
203
247
|
"""
|
|
204
|
-
|
|
205
|
-
|
|
248
|
+
try:
|
|
249
|
+
item: dict = json.loads(serialized_item)
|
|
250
|
+
service = cast(
|
|
251
|
+
BaseParallelProcess,
|
|
252
|
+
get_process_registry().get_process(process_id),
|
|
253
|
+
)
|
|
206
254
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
async def _await(p=partial):
|
|
213
|
-
return await p
|
|
214
|
-
partial = asyncio.run(_await())
|
|
255
|
+
partial = service.execute_single(item)
|
|
256
|
+
if inspect.isawaitable(partial):
|
|
257
|
+
if asyncio.iscoroutine(partial):
|
|
258
|
+
partial = asyncio.run(partial)
|
|
259
|
+
else:
|
|
215
260
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
261
|
+
async def _await(p=partial):
|
|
262
|
+
return await p
|
|
263
|
+
|
|
264
|
+
partial = asyncio.run(_await())
|
|
265
|
+
|
|
266
|
+
result = jsonable_encoder(partial)
|
|
267
|
+
_increment_and_report_progress(job_id, total)
|
|
268
|
+
return result
|
|
269
|
+
except Exception as exc:
|
|
270
|
+
update_job_status(
|
|
271
|
+
job_id,
|
|
272
|
+
0,
|
|
273
|
+
f"Parallel subtask failed: {exc}",
|
|
274
|
+
JobStatusCode.FAILED,
|
|
275
|
+
process_id=process_id,
|
|
276
|
+
)
|
|
277
|
+
raise
|
|
219
278
|
|
|
220
279
|
|
|
221
280
|
def _run_parallel(
|
|
@@ -252,9 +311,19 @@ def _run_parallel(
|
|
|
252
311
|
f"{total} subtask(s)."
|
|
253
312
|
)
|
|
254
313
|
|
|
314
|
+
execute_parallel_item_task = cast(Any, execute_parallel_item)
|
|
315
|
+
finalize_parallel_task = cast(Any, finalize_parallel)
|
|
255
316
|
chord(
|
|
256
|
-
[
|
|
257
|
-
|
|
317
|
+
[
|
|
318
|
+
execute_parallel_item_task.s(
|
|
319
|
+
process_id,
|
|
320
|
+
job_id,
|
|
321
|
+
total,
|
|
322
|
+
json.dumps(item),
|
|
323
|
+
)
|
|
324
|
+
for item in items
|
|
325
|
+
]
|
|
326
|
+
)(finalize_parallel_task.s(process_id, job_id, serialized_data))
|
|
258
327
|
|
|
259
328
|
|
|
260
329
|
@celery_app.task(name="fastprocesses.finalize_parallel")
|
|
@@ -274,7 +343,7 @@ def finalize_parallel(
|
|
|
274
343
|
2. Caches the merged result under the same key used by ``CacheResultTask``.
|
|
275
344
|
3. Updates the job status to SUCCESSFUL (or FAILED on error).
|
|
276
345
|
"""
|
|
277
|
-
service = get_process_registry().get_process(process_id)
|
|
346
|
+
service = cast(BaseParallelProcess, get_process_registry().get_process(process_id))
|
|
278
347
|
try:
|
|
279
348
|
update_job_status(
|
|
280
349
|
job_id, 95, "Merging parallel results.", JobStatusCode.RUNNING
|
|
@@ -329,28 +398,43 @@ def execute_scatter_step(
|
|
|
329
398
|
counter shared by all sibling tasks and updates the parent job's progress
|
|
330
399
|
to ``(n_done / total) * 90 %``.
|
|
331
400
|
"""
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
raise ValueError(
|
|
338
|
-
f"Step '{step_name}' not found on process '{process_id}'. "
|
|
339
|
-
f"Available steps: {list(steps)}"
|
|
401
|
+
try:
|
|
402
|
+
data: dict = json.loads(serialized_data)
|
|
403
|
+
service = cast(
|
|
404
|
+
BaseScatterProcess,
|
|
405
|
+
get_process_registry().get_process(process_id),
|
|
340
406
|
)
|
|
341
407
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return await p
|
|
349
|
-
partial = asyncio.run(_await())
|
|
408
|
+
steps = get_parallel_steps(service)
|
|
409
|
+
if step_name not in steps:
|
|
410
|
+
raise ValueError(
|
|
411
|
+
f"Step '{step_name}' not found on process '{process_id}'. "
|
|
412
|
+
f"Available steps: {list(steps)}"
|
|
413
|
+
)
|
|
350
414
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
415
|
+
partial = steps[step_name](data)
|
|
416
|
+
if inspect.isawaitable(partial):
|
|
417
|
+
if asyncio.iscoroutine(partial):
|
|
418
|
+
partial = asyncio.run(partial)
|
|
419
|
+
else:
|
|
420
|
+
|
|
421
|
+
async def _await(p=partial):
|
|
422
|
+
return await p
|
|
423
|
+
|
|
424
|
+
partial = asyncio.run(_await())
|
|
425
|
+
|
|
426
|
+
result = jsonable_encoder(partial)
|
|
427
|
+
_increment_and_report_progress(job_id, total)
|
|
428
|
+
return result
|
|
429
|
+
except Exception as exc:
|
|
430
|
+
update_job_status(
|
|
431
|
+
job_id,
|
|
432
|
+
0,
|
|
433
|
+
f"Scatter step '{step_name}' failed: {exc}",
|
|
434
|
+
JobStatusCode.FAILED,
|
|
435
|
+
process_id=process_id,
|
|
436
|
+
)
|
|
437
|
+
raise
|
|
354
438
|
|
|
355
439
|
|
|
356
440
|
def _run_scatter(
|
|
@@ -385,12 +469,20 @@ def _run_scatter(
|
|
|
385
469
|
f"{total} step(s): {step_names}."
|
|
386
470
|
)
|
|
387
471
|
|
|
472
|
+
execute_scatter_step_task = cast(Any, execute_scatter_step)
|
|
473
|
+
finalize_scatter_task = cast(Any, finalize_scatter)
|
|
388
474
|
chord(
|
|
389
475
|
[
|
|
390
|
-
|
|
476
|
+
execute_scatter_step_task.s(
|
|
477
|
+
process_id,
|
|
478
|
+
job_id,
|
|
479
|
+
total,
|
|
480
|
+
name,
|
|
481
|
+
serialized_input,
|
|
482
|
+
)
|
|
391
483
|
for name in step_names
|
|
392
484
|
]
|
|
393
|
-
)(
|
|
485
|
+
)(finalize_scatter_task.s(step_names, process_id, job_id, serialized_data))
|
|
394
486
|
|
|
395
487
|
|
|
396
488
|
@celery_app.task(name="fastprocesses.finalize_scatter")
|
|
@@ -412,7 +504,7 @@ def finalize_scatter(
|
|
|
412
504
|
2. Caches the merged result under the same key used by ``CacheResultTask``.
|
|
413
505
|
3. Updates the job status to SUCCESSFUL (or FAILED on error).
|
|
414
506
|
"""
|
|
415
|
-
service = get_process_registry().get_process(process_id)
|
|
507
|
+
service = cast(BaseScatterProcess, get_process_registry().get_process(process_id))
|
|
416
508
|
try:
|
|
417
509
|
named_results = dict(zip(step_names, step_results))
|
|
418
510
|
exec_body: dict = json.loads(serialized_data)
|
|
@@ -465,42 +557,23 @@ def execute_process(self, process_id: str, serialized_data: str | bytes):
|
|
|
465
557
|
message (str): A message describing the current progress.
|
|
466
558
|
status (str | None): The current status (e.g., "RUNNING", "SUCCESSFUL").
|
|
467
559
|
"""
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
job_id,
|
|
476
|
-
)
|
|
477
|
-
return
|
|
478
|
-
|
|
479
|
-
try:
|
|
480
|
-
job_info = JobStatusInfo.model_validate(raw_job_info)
|
|
481
|
-
except Exception as exc:
|
|
482
|
-
logger.error(
|
|
483
|
-
"Failed to validate cached job status in progress callback for "
|
|
484
|
-
"job_id={}: {!r}",
|
|
485
|
-
job_id,
|
|
486
|
-
exc,
|
|
487
|
-
)
|
|
488
|
-
return
|
|
489
|
-
|
|
490
|
-
job_info.progress = progress
|
|
491
|
-
job_info.updated = datetime.now(timezone.utc)
|
|
492
|
-
|
|
493
|
-
if message:
|
|
494
|
-
job_info.message = message
|
|
495
|
-
|
|
496
|
-
job_status_cache.put(job_key, job_info)
|
|
497
|
-
logger.debug(f"Updated progress for job {job_id}: {progress}%, {message}")
|
|
560
|
+
update_job_status(
|
|
561
|
+
job_id,
|
|
562
|
+
progress,
|
|
563
|
+
message,
|
|
564
|
+
JobStatusCode.RUNNING,
|
|
565
|
+
process_id=process_id,
|
|
566
|
+
)
|
|
498
567
|
|
|
499
568
|
chord_dispatched = False # True when a parallel/scatter chord is dispatched
|
|
500
569
|
result = None
|
|
501
570
|
job_status = JobStatusCode.RUNNING
|
|
502
571
|
job_message = ""
|
|
503
|
-
|
|
572
|
+
if isinstance(serialized_data, str):
|
|
573
|
+
serialized_data_str: str = serialized_data
|
|
574
|
+
else:
|
|
575
|
+
serialized_data_str = bytes(serialized_data).decode("utf-8")
|
|
576
|
+
data: dict = json.loads(serialized_data_str)
|
|
504
577
|
|
|
505
578
|
logger.info(f"Executing process {process_id} with data {serialized_data[:80]}")
|
|
506
579
|
job_id = self.request.id # Get the task/job ID
|
|
@@ -560,12 +633,12 @@ def execute_process(self, process_id: str, serialized_data: str | bytes):
|
|
|
560
633
|
if isinstance(service, BaseParallelProcess):
|
|
561
634
|
# Data fan-out: split → N parallel subtasks (same op) → merge.
|
|
562
635
|
# Chord dispatched; this task returns immediately.
|
|
563
|
-
_run_parallel(service, process_id, data, job_id,
|
|
636
|
+
_run_parallel(service, process_id, data, job_id, serialized_data_str)
|
|
564
637
|
chord_dispatched = True
|
|
565
638
|
elif isinstance(service, BaseScatterProcess):
|
|
566
639
|
# Operation fan-out: N different steps on same input → merge.
|
|
567
640
|
# Chord dispatched; this task returns immediately.
|
|
568
|
-
_run_scatter(service, process_id, data, job_id,
|
|
641
|
+
_run_scatter(service, process_id, data, job_id, serialized_data_str)
|
|
569
642
|
chord_dispatched = True
|
|
570
643
|
else:
|
|
571
644
|
result = service.run_execute(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|