openapi: 3.1.1 info: title: GIS API version: "1.0.0" summary: Hierarchical categories and geo datasets with async processing. description: | HTTP API for hierarchical categories and geo datasets (vector, vector_with_kato, raster). Datasets are processed asynchronously after upload, dispatched by `file_type`: - `vector` — the attribute table is extracted and, with the geometry, assembled into the `.geojson` output. - `raster` — converted to a Cloud-Optimized GeoTIFF (served at `GET /datasets/{id}.cog`); `bbox` is derived from the raster extent. - `vector_with_kato` — columns are detected for selection; the client then submits a KATO/year mapping, unpivoted into observations. Poll `GET /datasets/{id}/status` (optionally long-polling) to follow progress through the status lifecycle: `pending → processing|parsing → awaiting_mapping → extracting → ready` (or `failed`). license: name: Proprietary servers: - url: https://dssgis.dwh.kz/ description: Production tags: - name: Health description: Liveness and readiness probes - name: Categories description: Hierarchical dataset categories - name: Datasets description: Geo dataset upload, processing, and retrieval paths: /healthz: get: tags: [Health] summary: Liveness probe responses: "200": description: Process is alive content: application/json: schema: type: object properties: status: type: string /readyz: get: tags: [Health] summary: Readiness probe description: Runs dependency checks (Postgres, S3/MinIO, RabbitMQ). responses: "200": description: All dependencies reachable content: application/json: schema: { $ref: "#/components/schemas/Readiness" } "503": description: One or more dependencies unavailable content: application/json: schema: { $ref: "#/components/schemas/Readiness" } /categories: get: tags: [Categories] summary: List categories parameters: - name: parent_id in: query required: false description: Filter to the direct children of this category. schema: type: string format: uuid responses: "200": description: Categories ordered by name content: application/json: schema: type: array items: { $ref: "#/components/schemas/Category" } "400": { $ref: "#/components/responses/BadRequest" } post: tags: [Categories] summary: Create a category requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CategoryInput" } responses: "201": description: Created content: application/json: schema: { $ref: "#/components/schemas/Category" } "400": { $ref: "#/components/responses/BadRequest" } "409": { $ref: "#/components/responses/Conflict" } "422": { $ref: "#/components/responses/ValidationError" } /categories/{id}: parameters: - $ref: "#/components/parameters/IdParam" get: tags: [Categories] summary: Get a category responses: "200": description: The category content: application/json: schema: { $ref: "#/components/schemas/Category" } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } put: tags: [Categories] summary: Update a category description: | Updates name, description, and parent. The new parent must exist and must not create a cycle (a category cannot be its own ancestor). requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/CategoryInput" } responses: "200": description: Updated content: application/json: schema: { $ref: "#/components/schemas/Category" } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "422": { $ref: "#/components/responses/ValidationError" } delete: tags: [Categories] summary: Delete a category description: Fails with 409 if datasets or child categories still reference it. responses: "204": { description: Deleted } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } /datasets: get: tags: [Datasets] summary: List datasets (paginated summaries) parameters: - $ref: "#/components/parameters/PageParam" - $ref: "#/components/parameters/PageSizeParam" - name: category_id in: query required: false description: Filter to a category by id. schema: type: string format: uuid - name: category_code in: query required: false description: >- Filter to a category by its code (slug). An unknown code yields an empty page. schema: type: string - name: file_type in: query required: false description: Filter by file type. schema: type: string enum: [vector_with_kato, vector, raster] - name: automated in: query required: false description: Filter by the automated flag. schema: type: boolean - name: status in: query required: false description: Filter by lifecycle status. schema: type: string enum: [pending, parsing, processing, awaiting_mapping, extracting, ready, failed] responses: "200": description: A page of dataset summaries content: application/json: schema: { $ref: "#/components/schemas/DatasetSummaryPage" } "400": { $ref: "#/components/responses/BadRequest" } post: tags: [Datasets] summary: Upload a dataset description: | Multipart upload, validated by `file_type`, extension, and a content magic-byte check, then processed asynchronously (poll `GET /datasets/{id}/status`). Allowed extensions: `vector`/`vector_with_kato` = `.geojson`/`.gpkg`/`.zip`; `raster` = `.tif`/`.tiff`. requestBody: required: true content: multipart/form-data: schema: type: object required: [file, file_type, category_id] properties: file: type: string format: binary description: The geo file. file_type: $ref: "#/components/schemas/FileType" category_id: type: string format: uuid name: type: string description: Display name; defaults to the filename if omitted. description: type: string unit: type: string meta: type: string description: Arbitrary user JSON (sent as a JSON string). automated: type: boolean responses: "201": description: Dataset created; processing enqueued content: application/json: schema: { $ref: "#/components/schemas/Dataset" } "400": { $ref: "#/components/responses/BadRequest" } "422": { $ref: "#/components/responses/ValidationError" } /datasets/{id}: parameters: - $ref: "#/components/parameters/IdParam" get: tags: [Datasets] summary: Get a dataset description: | Dataset metadata, plus `bbox` for rasters. The heavy `geometry` and `properties` fields are not included; fetch geometry via `GET /datasets/{id}.geojson` and a raster's COG via `GET /datasets/{id}.cog`. responses: "200": description: The dataset content: application/json: schema: { $ref: "#/components/schemas/Dataset" } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } delete: tags: [Datasets] summary: Delete a dataset description: Removes the row and the stored object(s). responses: "204": { description: Deleted } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } /datasets/{id}.geojson: parameters: - $ref: "#/components/parameters/IdParam" get: tags: [Datasets] summary: Get a dataset as GeoJSON (vector / vector_with_kato) description: | Assembles a GeoJSON `FeatureCollection` (RFC 7946) from the dataset. A plain `vector` dataset has no KATO mapping or observations, so the result is a single Feature wrapping the dataset's own (dissolved) geometry. Its extracted attribute table (e.g. a GeoPackage's table data, stored in `properties`) is exposed as the Feature's top-level properties: a single row becomes the properties object directly, multiple rows are kept under a `rows` key. An empty collection is returned when the dataset has no geometry. A `vector_with_kato` dataset always ignores any geometry it carries and joins the `districts` table on KATO code: one Feature is emitted per KATO, its boundary taken from the matching district and the per-year observation values nested under a `data` object (keyed by date) alongside `kato` and `name`. KATO codes with no matching district are skipped. Only `ready` datasets are served: a dataset still being processed returns 409. Other file types (e.g. `raster`) return 422. responses: "200": description: The dataset as a GeoJSON FeatureCollection content: application/geo+json: schema: { $ref: "#/components/schemas/GeoJSONFeatureCollection" } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "422": { $ref: "#/components/responses/ValidationError" } /datasets/{id}.cog: parameters: - $ref: "#/components/parameters/IdParam" get: tags: [Datasets] summary: Get a raster dataset's Cloud-Optimized GeoTIFF description: | Streams the raster dataset's Cloud-Optimized GeoTIFF (`image/tiff`), produced when a `raster` upload is converted. Only `raster` datasets are served: other file types return 422, and a raster whose COG has not been produced yet returns 409. responses: "200": description: The Cloud-Optimized GeoTIFF content: image/tiff: schema: { type: string, format: binary } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "422": { $ref: "#/components/responses/ValidationError" } /datasets/{id}/status: parameters: - $ref: "#/components/parameters/IdParam" get: tags: [Datasets] summary: Get processing status (supports long polling) description: | Returns the current status immediately. If `current` is supplied, the request is held until the status differs from it, or until `wait` seconds elapse (then the unchanged status is returned). parameters: - name: current in: query required: false description: The client's last-known status; enables long polling. schema: { $ref: "#/components/schemas/DatasetStatus" } - name: wait in: query required: false description: Max seconds to hold the request (default 25, max 60). schema: type: integer minimum: 0 maximum: 60 default: 25 responses: "200": description: The dataset's status content: application/json: schema: { $ref: "#/components/schemas/DatasetStatusInfo" } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } /datasets/{id}/download: parameters: - $ref: "#/components/parameters/IdParam" get: tags: [Datasets] summary: Download the original uploaded file responses: "200": description: The file stream content: application/octet-stream: schema: type: string format: binary "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } /datasets/{id}/mapping: parameters: - $ref: "#/components/parameters/IdParam" post: tags: [Datasets] summary: Set the KATO column and year mapping (vector_with_kato) description: | Valid only for `vector_with_kato` datasets in `awaiting_mapping` (or `ready`, to re-map). The KATO column and every mapped column must be among the dataset's detected `attribute_columns`. Moves the dataset to `extracting`; observations are produced asynchronously. requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/MappingInput" } responses: "200": description: Mapping saved; extraction enqueued content: application/json: schema: { $ref: "#/components/schemas/Dataset" } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "422": { $ref: "#/components/responses/ValidationError" } /datasets/{id}/observations: parameters: - $ref: "#/components/parameters/IdParam" get: tags: [Datasets] summary: List a dataset's observations (paginated) description: Long-format values unpivoted from a vector_with_kato dataset. parameters: - name: kato_code in: query required: false description: Filter to a single KATO code. schema: type: string - $ref: "#/components/parameters/PageParam" - $ref: "#/components/parameters/PageSizeParam" responses: "200": description: A page of observations content: application/json: schema: { $ref: "#/components/schemas/ObservationPage" } "400": { $ref: "#/components/responses/BadRequest" } "404": { $ref: "#/components/responses/NotFound" } components: parameters: IdParam: name: id in: path required: true schema: type: string format: uuid PageParam: name: page in: query required: false description: 1-based page number. schema: type: integer minimum: 1 default: 1 PageSizeParam: name: page_size in: query required: false description: Items per page (default 20, max 100). schema: type: integer minimum: 1 maximum: 100 default: 20 responses: BadRequest: description: Malformed request (e.g. invalid UUID or query parameter) content: application/json: schema: { $ref: "#/components/schemas/Error" } NotFound: description: Resource not found content: application/json: schema: { $ref: "#/components/schemas/Error" } Conflict: description: Operation conflicts with existing data (e.g. foreign key) content: application/json: schema: { $ref: "#/components/schemas/Error" } ValidationError: description: Input failed validation content: application/json: schema: oneOf: - { $ref: "#/components/schemas/Error" } - { $ref: "#/components/schemas/ValidationErrors" } schemas: Error: type: object required: [error] properties: error: type: string ValidationErrors: type: object description: Per-field validation messages. properties: errors: type: object additionalProperties: type: string Readiness: type: object properties: ready: type: boolean components: type: object additionalProperties: type: string FileType: type: string enum: [vector_with_kato, vector, raster] DatasetStatus: type: string description: Dataset processing lifecycle status. enum: [pending, processing, parsing, awaiting_mapping, extracting, ready, failed] Category: type: object required: [id, code, name, description, created_at, updated_at] properties: id: type: string format: uuid parent_id: type: [string, "null"] format: uuid code: type: string description: Slug (lowercase latin letters, digits, and dashes). name: type: string description: type: string created_at: type: string format: date-time updated_at: type: string format: date-time CategoryInput: type: object required: [code, name] properties: parent_id: type: [string, "null"] format: uuid code: type: string maxLength: 255 pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" description: Slug (lowercase latin letters, digits, and dashes). name: type: string maxLength: 255 description: type: string maxLength: 2000 AttributeColumn: type: object required: [name] properties: name: type: string samples: type: array items: type: string YearColumn: type: object required: [column, date] properties: column: type: string date: type: string format: date GeoJSONGeometry: type: [object, "null"] description: A GeoJSON geometry object (e.g. Point, Polygon). properties: type: type: string coordinates: true GeoJSONFeature: type: object description: A GeoJSON Feature (RFC 7946). required: [type, geometry, properties] properties: type: type: string enum: [Feature] geometry: $ref: "#/components/schemas/GeoJSONGeometry" properties: type: [object, "null"] description: | Arbitrary key/value map. For per-KATO features (no dataset geometry) this holds `kato`, `name`, and one entry per mapped year keyed by date (YYYY-MM-DD). For the single-feature case (dataset has its own geometry) it holds only the observations, keyed by KATO code, each mapping to its date->value pairs. additionalProperties: true GeoJSONFeatureCollection: type: object description: A GeoJSON FeatureCollection (RFC 7946). required: [type, features] properties: type: type: string enum: [FeatureCollection] features: type: array items: $ref: "#/components/schemas/GeoJSONFeature" DatasetSummary: type: object required: [id, category_id, name, file_type, size_bytes, status, created_at, updated_at] properties: id: type: string format: uuid category_id: type: string format: uuid name: type: string description: type: [string, "null"] unit: type: [string, "null"] file_type: $ref: "#/components/schemas/FileType" size_bytes: type: integer format: int64 status: $ref: "#/components/schemas/DatasetStatus" created_at: type: string format: date-time updated_at: type: string format: date-time Dataset: type: object required: [id, category_id, name, filename, storage_key, file_type, size_bytes, content_type, automated, status, created_at, updated_at] properties: id: type: string format: uuid category_id: type: string format: uuid name: type: string description: type: [string, "null"] unit: type: [string, "null"] filename: type: string storage_key: type: string cog_storage_key: type: [string, "null"] description: Set for rasters once the Cloud-Optimized GeoTIFF is produced. file_type: $ref: "#/components/schemas/FileType" size_bytes: type: integer format: int64 content_type: type: string meta: type: [object, "null"] description: Arbitrary user-supplied JSON. automated: type: boolean status: $ref: "#/components/schemas/DatasetStatus" attribute_columns: type: [array, "null"] description: Detected columns (vector_with_kato), for mapping. items: $ref: "#/components/schemas/AttributeColumn" kato_column: type: [string, "null"] year_columns: type: [array, "null"] items: $ref: "#/components/schemas/YearColumn" parse_error: type: [string, "null"] description: Failure reason when status is `failed`. bbox: type: array description: "[minX, minY, maxX, maxY]; present only for rasters." items: type: number minItems: 4 maxItems: 4 created_at: type: string format: date-time updated_at: type: string format: date-time MappingInput: type: object required: [kato_column, year_columns] properties: kato_column: type: string examples: ["като"] year_columns: type: array minItems: 1 items: $ref: "#/components/schemas/YearColumn" examples: - kato_column: "като" year_columns: - { column: F_2023, date: "2023-01-01" } - { column: D_2025, date: "2025-01-01" } Observation: type: object required: [id, dataset_id, kato_code, date] properties: id: type: string format: uuid dataset_id: type: string format: uuid kato_code: type: string date: type: string format: date value: type: [number, "null"] description: Numeric cell value, or null when non-numeric/empty. value_text: type: [string, "null"] description: Non-numeric cell value, or null. DatasetStatusInfo: type: object required: [id, status] properties: id: type: string format: uuid status: $ref: "#/components/schemas/DatasetStatus" parse_error: type: [string, "null"] DatasetSummaryPage: type: object required: [data, page, page_size, total, total_pages] properties: data: type: array items: $ref: "#/components/schemas/DatasetSummary" page: type: integer page_size: type: integer total: type: integer total_pages: type: integer ObservationPage: type: object required: [data, page, page_size, total, total_pages] properties: data: type: array items: $ref: "#/components/schemas/Observation" page: type: integer page_size: type: integer total: type: integer total_pages: type: integer