Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/geoprocessing-job-lifecycle.md
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}'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Provide a valid FeatureCollection in the runnable command

When this documented command is copied for geometry.buffer, inputGeoJson decodes only to {}, whereas the process contract requires a serialized GeoJSON FeatureCollection. 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 👍 / 👎.

```

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.
8 changes: 8 additions & 0 deletions docs/reference/honua-sdk/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ and [Retries and timeouts](../../retries-and-timeouts.md) for the retry policy,
::: honua_sdk.AsyncHonuaClient
::: honua_sdk.HonuaGeocodingClient
::: honua_sdk.AsyncHonuaGeocodingClient

## OGC API Processes

The process clients operate process definitions already registered on Honua.
They do not provide a custom-code upload, packaging, or local execution API.

::: honua_sdk.geoprocessing.HonuaGeoprocessing
::: honua_sdk.geoprocessing.AsyncHonuaGeoprocessing
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The checked-in examples target the seeded `test_service` layer used by local Hon
| FastAPI spatial service | App developer exposing async Honua-backed API routes | `fastapi`, `uvicorn` | `uvicorn examples.fastapi_spatial_service:create_app --factory --reload` | local `/features` and `/summary` routes |
| Async feature service | App developer fronting Honua with a pooled async client | `fastapi`, `uvicorn` | `uvicorn examples.async_feature_service.service:create_app --factory --reload` | local `/services` and `/features` routes |
| Protocol clients | SDK developer checking protocol wrappers | core SDK, optional `honua-sdk[grpc]` and `honua-sdk[geopandas]` | `python examples/protocol_clients.py` | printed protocol response examples |
| Geoprocessing job lifecycle | Developer submitting one registered OGC process and collecting its result | core SDK | `python examples/geoprocessing_job_lifecycle.py --process-id <id> --inputs-json <json>` | job receipt and result JSON |

## Validation

Expand Down
88 changes: 88 additions & 0 deletions examples/geoprocessing_job_lifecycle.py
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject failed terminal jobs before fetching results

When the server transitions a job to failed or dismissed, wait() returns that terminal status, but this example immediately requests /results and either surfaces an unrelated HTTP error or treats partial error output as success. Check terminal.succeeded and raise GeoprocessingJobError, matching the SDK's existing execute* helpers, before fetching results.

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())
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ nav:
- Protocol examples: protocol-examples.md
- Protocol parity: protocol-parity.md
- Authentication: auth.md
- Geoprocessing job lifecycle: geoprocessing-job-lifecycle.md
- Compatibility: compatibility.md
- SDK capability coverage: sdk-coverage.md
- Troubleshooting: troubleshooting.md
Expand Down
59 changes: 59 additions & 0 deletions tests/test_geoprocessing_job_lifecycle_example.py
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"),
]
Loading