fastprocesses 0.22.1__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.1 → fastprocesses-0.22.2}/PKG-INFO +4 -3
  2. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/README.md +1 -1
  3. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/pyproject.toml +1 -1
  4. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/worker/chord_tasks.py +13 -3
  5. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/worker/job_status.py +82 -1
  6. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/AUTHORS.md +0 -0
  7. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/__init__.py +0 -0
  8. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/api/__init__.py +0 -0
  9. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/api/manager.py +0 -0
  10. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/api/router.py +0 -0
  11. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/api/server.py +0 -0
  12. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/common.py +0 -0
  13. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/__init__.py +0 -0
  14. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/base_process.py +0 -0
  15. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/cache.py +0 -0
  16. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/config.py +0 -0
  17. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/exceptions.py +0 -0
  18. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/logging.py +0 -0
  19. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/models.py +0 -0
  20. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/output_protocol.py +0 -0
  21. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/output_schema_resolver.py +0 -0
  22. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/outputs_handler.py +0 -0
  23. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/redis_connection.py +0 -0
  24. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/core/types.py +0 -0
  25. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/processes/__init__.py +0 -0
  26. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/processes/process_registry.py +0 -0
  27. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/py.typed +0 -0
  28. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/static/style.css +0 -0
  29. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/templates/landing.html +0 -0
  30. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/worker/__init__.py +0 -0
  31. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/worker/celery_app.py +0 -0
  32. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/worker/executors.py +0 -0
  33. {fastprocesses-0.22.1 → fastprocesses-0.22.2}/src/fastprocesses/worker/pipeline.py +0 -0
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: fastprocesses
3
- Version: 0.22.1
3
+ Version: 0.22.2
4
4
  Summary: A library to create a FastAPI-based OGC API Processes wrapper around existing projects.
5
+ License-File: AUTHORS.md
5
6
  Author: Stefan Schuhart
6
7
  Author-email: stefan.schuhart@gv.hamburg.de
7
8
  Requires-Python: >=3.12
@@ -38,7 +39,7 @@ A library to create a FastAPI-based OGC API Processes wrapper around existing pr
38
39
 
39
40
  AI helped to create this code.
40
41
 
41
- ## Version: 0.22.1
42
+ ## Version: 0.22.2
42
43
 
43
44
  ### Description
44
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.1
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.1"
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"
@@ -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
+ )