-
Notifications
You must be signed in to change notification settings - Fork 1
docs: add focused process job lifecycle example #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # Geoprocessing job lifecycle | ||
|
|
||
| ## Server contract first | ||
|
|
||
| `HonuaGeoprocessing` operates registered OGC API Processes. It does not upload | ||
| or execute arbitrary Python code. | ||
|
|
||
| | Item | Contract | | ||
| | --- | --- | | ||
| | Capability | `process.ogc-api-processes` | | ||
| | Submit | `POST /ogc/processes/processes/{processId}/execution` | | ||
| | Status | `GET /ogc/processes/jobs/{jobId}` | | ||
| | Results | `GET /ogc/processes/jobs/{jobId}/results` | | ||
| | Cancel | `DELETE /ogc/processes/jobs/{jobId}` (best effort) | | ||
| | Auth | `HonuaClient` API key, bearer token, or refreshable auth provider | | ||
| | Fixture | `honua-samples/jobs/geoprocessing-job-lifecycle.json#job-page-fixture:geometry-buffer-v1` | | ||
| | Maturity | Source preview; not yet published to PyPI | | ||
|
|
||
| Inspect the process description before constructing `inputs`; those schemas are | ||
| defined by the registered server process. The focused example therefore takes | ||
| an explicit process id and JSON input object rather than embedding a payload | ||
| that only works for one deployment: | ||
|
|
||
| ```bash | ||
| python examples/geoprocessing_job_lifecycle.py \ | ||
| --process-id geometry.buffer \ | ||
| --inputs-json '{"inputGeoJson":"{}","distance":100}' | ||
| ``` | ||
|
|
||
| That command belongs to the example script. The product `honua` CLI currently | ||
| has no process/job subcommands. | ||
|
|
||
| The example calls the real | ||
| [`HonuaGeoprocessing.submit_inputs`](reference/honua-sdk/clients.md#honua_sdk.geoprocessing.HonuaGeoprocessing.submit_inputs), | ||
| [`wait`](reference/honua-sdk/clients.md#honua_sdk.geoprocessing.HonuaGeoprocessing.wait), and | ||
| [`results`](reference/honua-sdk/clients.md#honua_sdk.geoprocessing.HonuaGeoprocessing.results) | ||
| methods. The wait has a deadline and performs a best-effort dismiss on timeout. | ||
| Use `dismiss(job_id)` only from an explicit operator cancellation action. | ||
|
|
||
| The shared, server-first JS/Python/.NET task contract, expected status sequence, | ||
| and semantic assertion live in `honua-samples/jobs/geoprocessing-job-lifecycle.json`. | ||
|
|
||
| ## Custom Python batch jobs | ||
|
|
||
| Custom-code authoring is a separate production project. The current server | ||
| contract is AWS-Batch-only and pins runtime, allowlisted repository, full commit | ||
| SHA, `module:function` entrypoint, dependency manifest, declared scope, and a | ||
| server-assigned output prefix. The Python SDK has no custom-code authoring or | ||
| submission API, and no current Studio Python editor/publish flow is admitted. | ||
| Do not substitute `submit_inputs()` for that missing product surface. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| """Submit one registered OGC process and wait for its bounded result. | ||
|
|
||
| This example deliberately accepts the process id and input object from the | ||
| caller. Process input schemas are server-defined; inventing a universal buffer | ||
| payload here would make the example look portable when it is not. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import json | ||
| import os | ||
| from collections.abc import Mapping | ||
| from typing import Any | ||
|
|
||
| import httpx | ||
|
|
||
| from honua_sdk import HonuaClient | ||
|
|
||
|
|
||
| def run_job( | ||
| base_url: str, | ||
| process_id: str, | ||
| inputs: Mapping[str, Any], | ||
| *, | ||
| api_key: str | None = None, | ||
| poll_interval: float = 0.5, | ||
| deadline_seconds: float = 30.0, | ||
| transport: httpx.BaseTransport | None = None, | ||
| ) -> dict[str, Any]: | ||
| """Submit, wait, and fetch results for one registered process. | ||
|
|
||
| ``deadline_seconds`` bounds polling. The SDK performs a best-effort | ||
| dismiss when that wait expires. An operator-triggered cancellation should | ||
| call ``client.geoprocessing().dismiss(job_id)`` explicitly. | ||
| """ | ||
|
|
||
| options: dict[str, Any] = {"api_key": api_key, "timeout": 10.0} | ||
| if transport is not None: | ||
| options["transport"] = transport | ||
|
|
||
| with HonuaClient(base_url, **options) as client: | ||
| geoprocessing = client.geoprocessing() | ||
| submitted = geoprocessing.submit_inputs(process_id, dict(inputs)) | ||
| terminal = geoprocessing.wait( | ||
| submitted, | ||
| poll_interval=poll_interval, | ||
| timeout=deadline_seconds, | ||
| ) | ||
| return geoprocessing.results(terminal.job_id) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the server transitions a job to Useful? React with 👍 / 👎. |
||
|
|
||
|
|
||
| def _object_json(value: str) -> dict[str, Any]: | ||
| parsed = json.loads(value) | ||
| if not isinstance(parsed, dict): | ||
| raise argparse.ArgumentTypeError("--inputs-json must decode to a JSON object") | ||
| return parsed | ||
|
|
||
|
|
||
| def main() -> int: | ||
| parser = argparse.ArgumentParser( | ||
| description="Submit a registered Honua OGC process and collect its result.", | ||
| ) | ||
| parser.add_argument( | ||
| "--base-url", | ||
| default=os.environ.get("HONUA_BASE_URL", "http://127.0.0.1:8080"), | ||
| ) | ||
| parser.add_argument("--api-key", default=os.environ.get("HONUA_API_KEY")) | ||
| parser.add_argument("--process-id", required=True) | ||
| parser.add_argument("--inputs-json", required=True, type=_object_json) | ||
| parser.add_argument("--poll-interval", type=float, default=0.5) | ||
| parser.add_argument("--deadline-seconds", type=float, default=30.0) | ||
| args = parser.parse_args() | ||
|
|
||
| result = run_job( | ||
| args.base_url, | ||
| args.process_id, | ||
| args.inputs_json, | ||
| api_key=args.api_key, | ||
| poll_interval=args.poll_interval, | ||
| deadline_seconds=args.deadline_seconds, | ||
| ) | ||
| print(json.dumps(result, indent=2, sort_keys=True)) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
|
|
||
| import httpx | ||
|
|
||
| from examples.geoprocessing_job_lifecycle import run_job | ||
|
|
||
|
|
||
| def test_focused_example_submits_polls_and_fetches_results() -> None: | ||
| requests: list[tuple[str, str]] = [] | ||
|
|
||
| def handler(request: httpx.Request) -> httpx.Response: | ||
| requests.append((request.method, request.url.path)) | ||
| if request.method == "POST": | ||
| assert json.loads(request.content) == { | ||
| "inputs": {"inputGeoJson": "{}", "distance": 100}, | ||
| "response": "document", | ||
| } | ||
| return httpx.Response( | ||
| 201, | ||
| headers={"Location": "http://example.test/ogc/processes/jobs/job-1"}, | ||
| json={ | ||
| "jobID": "job-1", | ||
| "processID": "geometry.buffer", | ||
| "status": "accepted", | ||
| }, | ||
| ) | ||
| if request.url.path == "/ogc/processes/jobs/job-1": | ||
| return httpx.Response( | ||
| 200, | ||
| json={ | ||
| "jobID": "job-1", | ||
| "processID": "geometry.buffer", | ||
| "status": "successful", | ||
| }, | ||
| ) | ||
| if request.url.path == "/ogc/processes/jobs/job-1/results": | ||
| return httpx.Response( | ||
| 200, | ||
| json={"outputFeatureLayer": {"value": {"type": "FeatureCollection", "features": []}}}, | ||
| ) | ||
| raise AssertionError(f"unexpected request: {request.method} {request.url.path}") | ||
|
|
||
| result = run_job( | ||
| "http://example.test", | ||
| "geometry.buffer", | ||
| {"inputGeoJson": "{}", "distance": 100}, | ||
| poll_interval=0.0, | ||
| deadline_seconds=1.0, | ||
| transport=httpx.MockTransport(handler), | ||
| ) | ||
|
|
||
| assert result["outputFeatureLayer"]["value"]["type"] == "FeatureCollection" | ||
| assert requests == [ | ||
| ("POST", "/ogc/processes/processes/geometry.buffer/execution"), | ||
| ("GET", "/ogc/processes/jobs/job-1"), | ||
| ("GET", "/ogc/processes/jobs/job-1/results"), | ||
| ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this documented command is copied for
geometry.buffer,inputGeoJsondecodes only to{}, whereas the process contract requires a serialized GeoJSONFeatureCollection. The real server will therefore fail the showcased job instead of demonstrating the successful lifecycle; use at least an empty{"type":"FeatureCollection","features":[]}value or make the payload an explicit placeholder.Useful? React with 👍 / 👎.