Skip to content

Commit 6bd1c58

Browse files
committed
feat(spp_api_v2_gis): implement QGIS plugin team recommendations for OGC Processes
Address consumer feedback from the QGIS plugin team: - Always include geometries_failed in batch summary responses (#1) - Always populate computed_at with current UTC timestamp, even for empty results (#2) - Add Retry-After: 5 header to async 201 responses and in-progress job status (#3/#8) - Track batch progress per-geometry via on_progress callback in job execution (#4) - Add x-openspp-batch-limit: 100 extension to spatial-statistics process description (#5)
1 parent ae741c5 commit 6bd1c58

File tree

5 files changed

+57
-6
lines changed

5 files changed

+57
-6
lines changed

spp_api_v2_gis/models/process_job.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ def execute_process(self):
8282
This method is called by the job worker. It runs the appropriate
8383
SpatialQueryService method based on process_id, stores the results,
8484
and updates the job status accordingly.
85+
86+
For batch spatial-statistics requests, progress is updated per geometry.
8587
"""
8688
self.ensure_one()
8789

@@ -101,7 +103,11 @@ def execute_process(self):
101103
inputs = self.inputs or {}
102104

103105
if self.process_id == "spatial-statistics":
104-
results = run_spatial_statistics(service, inputs)
106+
geometry = inputs.get("geometry")
107+
if isinstance(geometry, list) and len(geometry) > 1:
108+
results = self._execute_batch_with_progress(service, inputs)
109+
else:
110+
results = run_spatial_statistics(service, inputs)
105111
elif self.process_id == "proximity-statistics":
106112
results = run_proximity_statistics(service, inputs)
107113
else:
@@ -111,6 +117,7 @@ def execute_process(self):
111117
{
112118
"status": "successful",
113119
"finished_at": fields.Datetime.now(),
120+
"progress": 100,
114121
"results": results,
115122
}
116123
)
@@ -125,6 +132,29 @@ def execute_process(self):
125132
}
126133
)
127134

135+
def _execute_batch_with_progress(self, service, inputs):
136+
"""Execute batch spatial-statistics with per-geometry progress updates.
137+
138+
Calls the service's batch method with a progress callback so that
139+
the job record is updated after each geometry is processed.
140+
"""
141+
geometry = inputs["geometry"]
142+
geometries = [{"id": g["id"], "geometry": g["value"]} for g in geometry]
143+
total = len(geometries)
144+
145+
def on_progress(completed):
146+
self.write({"progress": int(completed / total * 100)})
147+
148+
result = service.query_statistics_batch(
149+
geometries=geometries,
150+
filters=inputs.get("filters"),
151+
variables=inputs.get("variables"),
152+
on_progress=on_progress,
153+
)
154+
for item in result.get("results", []):
155+
item.pop("registrant_ids", None)
156+
return result
157+
128158
@api.model
129159
def cron_cleanup_jobs(self):
130160
"""Clean up old and stale jobs.

spp_api_v2_gis/routers/jobs.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from odoo.addons.fastapi.dependencies import odoo_env
1818
from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client
1919

20-
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, status
20+
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, Response, status
2121

2222
from ..schemas.processes import JobList, StatusInfo
2323
from ._helpers import build_status_info, check_gis_scope, get_base_url
@@ -103,7 +103,18 @@ async def get_job_status(
103103

104104
job = _get_job_or_404(env, job_id, api_client)
105105
base_url = get_base_url(request)
106-
return build_status_info(job, base_url)
106+
status_info = build_status_info(job, base_url)
107+
108+
# Add Retry-After header for in-progress jobs to guide client polling
109+
if job.status in ("accepted", "running"):
110+
return Response(
111+
content=status_info.model_dump_json(by_alias=True, exclude_none=True),
112+
status_code=200,
113+
media_type="application/json",
114+
headers={"Retry-After": "5"},
115+
)
116+
117+
return status_info
107118

108119

109120
@jobs_router.get(

spp_api_v2_gis/routers/processes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,5 +294,6 @@ def _execute_async(env, api_client, process_id, inputs, request):
294294
media_type="application/json",
295295
headers={
296296
"Location": f"{base_url}/gis/ogc/jobs/{job_id}",
297+
"Retry-After": "5",
297298
},
298299
)

spp_api_v2_gis/services/process_registry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def _build_spatial_statistics_description(self):
140140
),
141141
"version": "1.0.0",
142142
"jobControlOptions": ["sync-execute", "async-execute", "dismiss"],
143+
"x-openspp-batch-limit": 100,
143144
"inputs": {
144145
"geometry": {
145146
"title": "Query Geometry",

spp_api_v2_gis/services/spatial_query_service.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import json
55
import logging
6+
from datetime import UTC, datetime
67

78
from odoo.addons.spp_aggregation.services import build_explicit_scope
89

@@ -30,7 +31,7 @@ def __init__(self, env):
3031
"""
3132
self.env = env
3233

33-
def query_statistics_batch(self, geometries, filters=None, variables=None):
34+
def query_statistics_batch(self, geometries, filters=None, variables=None, on_progress=None):
3435
"""Execute spatial query for multiple geometries.
3536
3637
Queries each geometry individually and computes an aggregate summary.
@@ -39,6 +40,7 @@ def query_statistics_batch(self, geometries, filters=None, variables=None):
3940
geometries: List of dicts with 'id' and 'geometry' keys
4041
filters: Additional filters for registrants (dict)
4142
variables: List of statistic names to compute
43+
on_progress: Optional callback(completed_count) called after each geometry
4244
4345
Returns:
4446
dict: Batch results with per-geometry results and summary
@@ -47,6 +49,7 @@ def query_statistics_batch(self, geometries, filters=None, variables=None):
4749
"""
4850
results = []
4951
all_registrant_ids = set()
52+
geometries_failed = 0
5053

5154
for item in geometries:
5255
geometry_id = item["id"]
@@ -76,6 +79,7 @@ def query_statistics_batch(self, geometries, filters=None, variables=None):
7679
)
7780
except Exception as e:
7881
_logger.warning("Batch query failed for geometry '%s': %s", geometry_id, e)
82+
geometries_failed += 1
7983
results.append(
8084
{
8185
"id": geometry_id,
@@ -89,6 +93,9 @@ def query_statistics_batch(self, geometries, filters=None, variables=None):
8993
}
9094
)
9195

96+
if on_progress:
97+
on_progress(len(results))
98+
9299
# Compute summary by aggregating unique registrants with metadata
93100
summary_stats_with_metadata = {"statistics": {}}
94101
if all_registrant_ids:
@@ -97,6 +104,7 @@ def query_statistics_batch(self, geometries, filters=None, variables=None):
97104
summary = {
98105
"total_count": len(all_registrant_ids),
99106
"geometries_queried": len(geometries),
107+
"geometries_failed": geometries_failed,
100108
"statistics": summary_stats_with_metadata.get("statistics", {}),
101109
"access_level": summary_stats_with_metadata.get("access_level"),
102110
"from_cache": summary_stats_with_metadata.get("from_cache", False),
@@ -327,7 +335,7 @@ def _compute_statistics(self, registrant_ids, variables):
327335
"statistics": self._get_empty_statistics(),
328336
"access_level": None,
329337
"from_cache": False,
330-
"computed_at": None,
338+
"computed_at": datetime.now(UTC).isoformat(),
331339
}
332340

333341
if "spp.aggregation.service" not in self.env:
@@ -368,7 +376,7 @@ def _compute_via_aggregation_service(self, registrant_ids, variables):
368376
"statistics": {},
369377
"access_level": None,
370378
"from_cache": False,
371-
"computed_at": None,
379+
"computed_at": datetime.now(UTC).isoformat(),
372380
}
373381

374382
# Call AggregationService (no sudo - let service determine access level from calling user)

0 commit comments

Comments
 (0)