feat(landscape): HTTP API, worker consumer, wiring, docs

Adds the /landscape routes (slots, slot upload, runs, run detail, results),
mounts them in the router, wires the landscape repository, job publisher,
godal raster IO and Compute into the composition root, and registers the
gis-landscape-runner consumer in the worker.

Also fixes the Dockerfile so the image actually runs: godal's cgo needs g++,
and the runtime base must match the builder's Alpine release or libgdal's
soname does not resolve.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 14:11:57 +05:00
parent d74277552f
commit ad3c9fbdcd
8 changed files with 827 additions and 7 deletions

View File

@@ -35,6 +35,9 @@ tags:
description: Geo dataset upload, processing, and retrieval
- name: Dataset Groups
description: Groups of same-named datasets versioned by period
- name: Landscape
description: >-
Landscape Program (ЛП): raster inputs, graph runs, and computed results
paths:
/healthz:
@@ -750,6 +753,141 @@ paths:
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
/landscape/slots:
get:
tags: [Landscape]
summary: List the Landscape Program input slots
description: |
Every leaf input group of the computation graph, in graph order, with
the dataset currently filling it (any status) or `null` when nothing
has been uploaded yet.
responses:
"200":
description: The input slots
content:
application/json:
schema:
type: array
items: { $ref: "#/components/schemas/LandscapeSlot" }
/landscape/slots/{code}:
parameters:
- name: code
in: path
required: true
description: Slot (dataset group) code, e.g. `lp-z-biodiversity`.
schema:
type: string
post:
tags: [Landscape]
summary: Upload a raster into an input slot
description: |
Multipart upload of a GeoTIFF (`.tif`/`.tiff`, TIFF magic bytes checked)
into the slot's dataset group under the `landscape` category. It goes
through the regular raster upload path, so the COG conversion is
enqueued as usual. An unknown slot code yields 404.
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file:
type: string
format: binary
description: The GeoTIFF to store in the slot.
name:
type: string
description: Dataset name; defaults to the uploaded file name.
start_date:
type: string
format: date
description: Start of the period the raster describes.
end_date:
type: string
format: date
description: >-
End of the period (inclusive); drives the "freshest in
group" rule used to resolve run inputs.
responses:
"201":
description: The created dataset
content:
application/json:
schema: { $ref: "#/components/schemas/Dataset" }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
"422": { $ref: "#/components/responses/ValidationError" }
/landscape/runs:
get:
tags: [Landscape]
summary: List Landscape Program runs (paginated, newest first)
parameters:
- $ref: "#/components/parameters/PageParam"
- $ref: "#/components/parameters/PageSizeParam"
responses:
"200":
description: A page of runs
content:
application/json:
schema: { $ref: "#/components/schemas/LandscapeRunPage" }
"400": { $ref: "#/components/responses/BadRequest" }
post:
tags: [Landscape]
summary: Queue a Landscape Program run
description: |
Validates the params and overrides, persists the run with one pending
node per graph node, and publishes the worker job. Returns 202: the
graph is executed asynchronously, poll `GET /landscape/runs/{id}`.
requestBody:
required: false
content:
application/json:
schema: { $ref: "#/components/schemas/LandscapeRunInput" }
responses:
"202":
description: Run queued
content:
application/json:
schema: { $ref: "#/components/schemas/LandscapeRun" }
"400": { $ref: "#/components/responses/BadRequest" }
"422": { $ref: "#/components/responses/ValidationError" }
/landscape/runs/{id}:
parameters:
- $ref: "#/components/parameters/IdParam"
get:
tags: [Landscape]
summary: Get a Landscape Program run with its nodes
responses:
"200":
description: The run
content:
application/json:
schema: { $ref: "#/components/schemas/LandscapeRun" }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
/landscape/results:
get:
tags: [Landscape]
summary: List the Landscape Program result layers
description: |
One entry per output group of the graph with its freshest `ready`
raster, the COG URL to render it, and the group's SLD URL when one has
been uploaded. `latest` is `null` while nothing has been computed.
responses:
"200":
description: The result layers
content:
application/json:
schema:
type: array
items: { $ref: "#/components/schemas/LandscapeResult" }
components:
parameters:
IdParam:
@@ -1309,3 +1447,239 @@ components:
type: integer
total_pages:
type: integer
LandscapeSlot:
type: object
description: A leaf input group of the Landscape Program graph.
required: [code, title, group_id, latest]
properties:
code:
type: string
description: Dataset group code, e.g. `lp-z-biodiversity`.
title:
type: string
group_id:
type: string
format: uuid
latest:
oneOf:
- $ref: "#/components/schemas/DatasetSummary"
- type: "null"
description: The freshest dataset of the slot's group, any status.
LandscapeParams:
type: object
description: |
Run-time knobs of the graph. Every field is optional; omitted fields
take the defaults shown below.
properties:
window_size:
type: integer
minimum: 256
maximum: 8192
default: 2048
description: Side of the square window the executor tiles rasters into.
tie_priority:
type: array
description: >-
Sector preference when several sectors tie in functional zoning; a
permutation of 1..5 (1 irrigation, 2 snow, 3 crop, 4 pasture,
5 tourism).
items:
type: integer
minimum: 1
maximum: 5
minItems: 5
maxItems: 5
default: [1, 2, 3, 4, 5]
v1_min_s_e12:
type: integer
minimum: 1
maximum: 5
default: 3
description: V1 minimum suitability where E <= 2.
v1_min_s_e3:
type: integer
minimum: 1
maximum: 5
default: 4
description: V1 minimum suitability where E == 3.
v2_min_s_e3:
type: integer
minimum: 1
maximum: 5
default: 3
description: V2 minimum suitability where E == 3.
v2_min_s_e4:
type: integer
minimum: 1
maximum: 5
default: 5
description: V2 minimum suitability where E == 4.
LandscapeRunInput:
type: object
description: Options of a new Landscape Program run. All fields optional.
properties:
from_stage:
type: [integer, "null"]
minimum: 0
maximum: 3
description: >-
Force recomputation from this stage on. Omitted (or null) reuses
every node whose inputs and params are unchanged; `0` recomputes
everything.
overrides:
type: object
description: >-
Pins a specific dataset for a group code instead of resolving the
freshest ready raster. The dataset must exist, be a raster, and be
`ready`.
additionalProperties:
type: string
format: uuid
params:
$ref: "#/components/schemas/LandscapeParams"
examples:
- from_stage: 0
overrides: { "lp-z-lcr": "3f7c1f5e-9a1a-4a54-8b2e-2d0f5c9d1a11" }
params: { window_size: 1024 }
LandscapeDatasetRef:
type: object
description: A dataset bound to one group slot of a node.
required: [group, dataset_id]
properties:
group:
type: string
description: Dataset group code.
dataset_id:
type: string
format: uuid
LandscapeRunNode:
type: object
description: Execution state of one graph node within a run.
required: [id, run_id, node_code, stage, status, fingerprint]
properties:
id:
type: string
format: uuid
run_id:
type: string
format: uuid
node_code:
type: string
description: "Node code `XY`: X = stage, Y = function within the stage."
stage:
type: integer
status:
type: string
enum: [pending, running, skipped, succeeded, failed]
description: >-
`skipped` means a previous run's identical result was reused.
fingerprint:
type: string
description: >-
sha256 over the node code, its resolved inputs and the
node-relevant params; equal fingerprints allow reuse.
inputs:
type: [array, "null"]
items:
$ref: "#/components/schemas/LandscapeDatasetRef"
outputs:
type: [array, "null"]
items:
$ref: "#/components/schemas/LandscapeDatasetRef"
error:
type: [string, "null"]
started_at:
type: [string, "null"]
format: date-time
finished_at:
type: [string, "null"]
format: date-time
LandscapeRun:
type: object
description: One execution of the Landscape Program graph.
required: [id, status, params, overrides, created_at]
properties:
id:
type: string
format: uuid
status:
type: string
enum: [queued, running, succeeded, failed]
from_stage:
type: [integer, "null"]
params:
$ref: "#/components/schemas/LandscapeParams"
overrides:
type: object
additionalProperties:
type: string
format: uuid
error:
type: [string, "null"]
description: Failure reason when status is `failed`.
created_at:
type: string
format: date-time
started_at:
type: [string, "null"]
format: date-time
finished_at:
type: [string, "null"]
format: date-time
nodes:
type: [array, "null"]
items:
$ref: "#/components/schemas/LandscapeRunNode"
LandscapeRunPage:
type: object
required: [data, page, page_size, total, total_pages]
properties:
data:
type: array
items:
$ref: "#/components/schemas/LandscapeRun"
page:
type: integer
page_size:
type: integer
total:
type: integer
total_pages:
type: integer
LandscapeResult:
type: object
description: One computed output group of the graph and its latest layer.
required: [group, title, node, group_id, latest, cog_url, sld_url]
properties:
group:
type: string
description: Dataset group code, e.g. `lp-v1-conservative`.
title:
type: string
node:
type: string
description: Code of the graph node producing this group.
group_id:
type: string
format: uuid
latest:
oneOf:
- $ref: "#/components/schemas/DatasetSummary"
- type: "null"
description: The freshest `ready` raster of the group, if any.
cog_url:
type: [string, "null"]
description: "`/datasets/{id}.cog` for `latest`, or null."
sld_url:
type: [string, "null"]
description: >-
`/dataset-groups/{group_id}/sld` when the group has an SLD, else
null.