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.
Files changed (33) hide show
  1. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/PKG-INFO +2 -2
  2. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/README.md +1 -1
  3. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/pyproject.toml +1 -1
  4. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/base_process.py +10 -3
  5. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/worker/celery_app.py +3 -1
  6. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/worker/chord_tasks.py +13 -3
  7. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/worker/job_status.py +82 -1
  8. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/worker/pipeline.py +19 -4
  9. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/AUTHORS.md +0 -0
  10. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/__init__.py +0 -0
  11. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/api/__init__.py +0 -0
  12. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/api/manager.py +0 -0
  13. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/api/router.py +0 -0
  14. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/api/server.py +0 -0
  15. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/common.py +0 -0
  16. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/__init__.py +0 -0
  17. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/cache.py +0 -0
  18. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/config.py +0 -0
  19. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/exceptions.py +0 -0
  20. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/logging.py +0 -0
  21. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/models.py +0 -0
  22. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/output_protocol.py +0 -0
  23. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/output_schema_resolver.py +0 -0
  24. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/outputs_handler.py +0 -0
  25. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/redis_connection.py +0 -0
  26. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/core/types.py +0 -0
  27. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/processes/__init__.py +0 -0
  28. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/processes/process_registry.py +0 -0
  29. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/py.typed +0 -0
  30. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/static/style.css +0 -0
  31. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/templates/landing.html +0 -0
  32. {fastprocesses-0.22.0 → fastprocesses-0.22.2}/src/fastprocesses/worker/__init__.py +0 -0
  33. {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.0
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.0
42
+ ## Version: 0.22.2
43
43
 
44
44
  ### Description
45
45
 
@@ -4,7 +4,7 @@ A library to create a FastAPI-based OGC API Processes wrapper around existing pr
4
4
 
5
5
  AI helped to create this code.
6
6
 
7
- ## Version: 0.22.0
7
+ ## Version: 0.22.2
8
8
 
9
9
  ### Description
10
10
 
@@ -79,7 +79,7 @@ exclude = [
79
79
 
80
80
  [tool.poetry]
81
81
  name = "fastprocesses"
82
- version = "0.22.0"
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(self, exec_body: Dict[str, Any]) -> Dict[str, Any]:
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(self, inputs: Dict[str, Any]) -> bool:
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(process, process_id, data, job_id)
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
- _increment_and_report_progress(job_id, total)
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
- exec_body: dict = payload["original_input"] if payload else {}
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(**exec_body)
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(job_id, 0, "Validating inputs.", JobStatusCode.RUNNING)
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(job_id, 0, "Resolving remote inputs.", JobStatusCode.RUNNING)
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: