OGC API and pygeoapi¶
OGC API overview¶
OGC API is a family of standards from the Open Geospatial Consortium that define RESTful interfaces for geospatial data. Each standard covers a specific data type or interaction pattern:
| Standard | Purpose |
|---|---|
| Features | Vector feature access (GeoJSON, etc.) |
| Coverages | Gridded / raster data |
| EDR | Environmental Data Retrieval (point, trajectory, corridor queries) |
| Processes | Server-side processing / workflows |
| Maps | Rendered map images |
| Tiles | Tiled data (vector and map tiles) |
| Records | Catalogue / metadata search |
All standards share a common core: JSON/HTML responses, OpenAPI-described endpoints, and content negotiation. The full specification catalogue is at https://ogcapi.ogc.org.
pygeoapi¶
pygeoapi is a Python server that implements the OGC API standards listed above. It is the OGC Reference Implementation for OGC API - Features.
In this project pygeoapi is mounted as a sub-application at /ogcapi. The integration is minimal -- a single re-export in src/open_climate_service/routers/ogcapi.py:
from pygeoapi.starlette_app import APP as pygeoapi_app
app = pygeoapi_app # mounted by the main FastAPI app
All dataset and behaviour configuration happens in YAML, not Python code.
- pygeoapi docs: https://docs.pygeoapi.io
- Source: https://github.com/geopython/pygeoapi
Configuration¶
pygeoapi is configured through a single generated YAML file whose path is set by the PYGEOAPI_CONFIG environment variable. In this repo that generated file lives under data/pygeoapi/pygeoapi-config.yml, and it is derived from the checked-in base config at config/pygeoapi/base.yml.
Top-level sections¶
server: # host, port, URL, limits, CORS, languages, admin toggle
logging: # log level and optional log file
metadata: # service identification, contact, license
resources: # datasets and processes exposed by the API
server¶
Controls runtime behaviour -- bind address, public URL, response encoding, language negotiation, pagination limits, and the optional admin API.
server:
bind:
host: 127.0.0.1
port: 5000
url: http://127.0.0.1:8000/ogcapi
mimetype: application/json; charset=UTF-8
encoding: utf-8
languages:
- en-US
- fr-CA
limits:
default_items: 20
max_items: 50
admin: false
metadata¶
Service-level identification, contact details, and license. Supports multilingual values.
metadata:
identification:
title:
en: Open Climate Service
description:
en: OGC API compliant geospatial data API
provider:
name: Open Climate Service
url: https://dhis2.org
contact:
name: DHIS2 Climate Team
email: climate@dhis2.org
resources¶
Each key under resources defines a collection or process. A collection needs at minimum a type, title, description, extents, and one or more providers.
resources:
lakes:
type: collection
title: Large Lakes
description: lakes of the world, public domain
extents:
spatial:
bbox: [-180, -90, 180, 90]
crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
providers:
- type: feature
name: GeoJSON
data: tests/data/ne_110m_lakes.geojson
id_field: id
Full configuration reference: https://docs.pygeoapi.io/en/latest/configuration.html
Resource types¶
The type field on a provider determines which OGC API standard the collection exposes.
| Provider type | OGC API standard | Description |
|---|---|---|
feature |
Features | Vector data (points, lines, polygons). Backends include CSV, GeoJSON, PostGIS, Elasticsearch, and others. |
coverage |
Coverages | Gridded / raster data. Backends include rasterio, xarray, and S3-hosted COGs. |
map |
Maps | Rendered map images, typically proxied from an upstream WMS via WMSFacade. |
process |
Processes | Server-side processing tasks. In Open Climate Service, the native /processes surface is authoritative; pygeoapi process support is not the primary process runtime. |
A single collection can have multiple providers (e.g. both feature and tile on the same resource).
CQL filtering¶
pygeoapi supports CQL2 text filters on collections backed by a CQL-capable provider. Filters are passed as query parameters:
The dhis2-org-units-cql collection exposes this capability. Its filterable properties are name, code, shortName, level, and openingDate.
Supported operators¶
| Category | Operators | Example |
|---|---|---|
| Comparison | =, <>, <, <=, >, >= |
level=2 |
| Pattern matching | LIKE, ILIKE (% = any chars, _ = single char) |
name LIKE '%Hospital%' |
| Range | BETWEEN ... AND ... |
level BETWEEN 2 AND 3 |
| Set membership | IN (...) |
level IN (1,2) |
| Null checks | IS NULL, IS NOT NULL |
code IS NOT NULL |
| Logical | AND, OR, NOT |
level=3 AND name LIKE '%CH%' |
String values must be enclosed in single quotes.
Example queries¶
Exact match on level:
String match on name:
LIKE (case-sensitive pattern):
ILIKE (case-insensitive pattern):
Combined filter with AND:
BETWEEN range:
IN set membership:
NULL check combined with comparison:
Processes¶
OGC API - Processes exposes server-side processing tasks. Each process defines typed inputs and outputs and can be executed synchronously or asynchronously via POST.
For Open Climate Service, the canonical process surface is now the native /processes API. The /ogcapi/processes paths below describe the older pygeoapi-centered process surface and should not be treated as the primary entrypoint for current native process work.
Available processes¶
| Process | ID | Description |
|---|---|---|
| Zonal statistics | zonal-statistics |
Compute zonal stats from GeoJSON features and a raster source |
| ERA5-Land | era5-land-download |
Download ERA5-Land hourly climate data (temperature, precipitation, etc.) |
| CHIRPS3 | chirps3-download |
Download CHIRPS3 daily precipitation data |
| CHIRPS3 -> DHIS2 pipeline | chirps3-dhis2-pipeline |
Fetch features, download CHIRPS3, aggregate by feature, generate DHIS2 dataValueSet (optional auto-import) |
Endpoints¶
| Method | Path | Description |
|---|---|---|
| GET | /ogcapi/processes |
List all available processes |
| GET | /ogcapi/processes/{processId} |
Describe a process (inputs, outputs, metadata) |
| POST | /ogcapi/processes/{processId}/execution |
Execute a process (sync or async) |
| GET | /ogcapi/jobs |
List all jobs |
| GET | /ogcapi/jobs/{jobId} |
Get job status |
| GET | /ogcapi/jobs/{jobId}/results |
Get job results |
| DELETE | /ogcapi/jobs/{jobId} |
Cancel or delete a job |
Common inputs (download processes)¶
era5-land-download and chirps3-download share these inputs:
| Input | Type | Required | Description |
|---|---|---|---|
start |
string | yes | Start date in YYYY-MM format |
end |
string | yes | End date in YYYY-MM format |
bbox |
array[number] | yes | Bounding box [west, south, east, north] |
dry_run |
boolean | no | If true (default), return data without pushing to DHIS2 |
Note: chirps3-dhis2-pipeline has its own contract (start_date, end_date, feature-source selectors, and output options).
ERA5-Land (era5-land-download)¶
Downloads ERA5-Land hourly climate data via the DestinE Earth Data Hub. Authentication uses ~/.netrc — see setup_guide.md for registration and setup.
Additional inputs:
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
variables |
array[string] | no | ["2m_temperature", "total_precipitation"] |
ERA5-Land variable names |
Example request:
curl -X POST http://localhost:8000/ogcapi/processes/era5-land-download/execution \
-H "Content-Type: application/json" \
-d '{
"inputs": {
"start": "2024-01",
"end": "2024-03",
"bbox": [32.0, -2.0, 35.0, 1.0],
"variables": ["2m_temperature"],
"dry_run": true
}
}'
CHIRPS3 (chirps3-download)¶
Downloads CHIRPS3 daily precipitation data.
Additional inputs:
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
stage |
string | no | "final" |
Product stage: "final" or "prelim" |
Example request:
curl -X POST http://localhost:8000/ogcapi/processes/chirps3-download/execution \
-H "Content-Type: application/json" \
-d '{
"inputs": {
"start": "2024-01",
"end": "2024-03",
"bbox": [32.0, -2.0, 35.0, 1.0],
"stage": "final",
"dry_run": true
}
}'
Zonal statistics (zonal-statistics)¶
Calculates statistics over raster values for each input GeoJSON feature.
This can be used with:
- features from the
sierra-leone-districtscollection - raster from the
sierra-leone-populationcollection (tests/data/sle_pop_2026_CN_1km_R2025A_UA_v1.tif)
Inputs:
| Input | Type | Required | Default | Description |
|---|---|---|---|---|
geojson |
object or string | yes | - | GeoJSON FeatureCollection object, or path/URL to a GeoJSON file |
raster |
string | yes | - | Raster path or URL |
band |
integer | no | 1 |
1-based band index |
stats |
array[string] | no | ["mean"] |
Any of: count, sum, mean, min, max, median, std |
feature_id_property |
string | no | "id" |
Fallback property key for feature IDs |
output_property |
string | no | "zonal_statistics" |
Property name for computed statistics |
all_touched |
boolean | no | false |
Include all pixels touched by geometry |
include_nodata |
boolean | no | false |
Include nodata values in calculations |
nodata |
number | no | raster nodata | Optional nodata override |
Example request using local Sierra Leone resources:
curl -X POST http://localhost:8000/ogcapi/processes/zonal-statistics/execution \
-H "Content-Type: application/json" \
-d '{
"inputs": {
"geojson": "tests/data/sierra_leone_districts.geojson",
"raster": "tests/data/sle_pop_2026_CN_1km_R2025A_UA_v1.tif",
"stats": ["count", "sum", "mean", "min", "max"],
"output_property": "population_stats"
}
}'
Example response shape:
{
"id": "features",
"value": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "district-id",
"properties": {
"name": "District name",
"population_stats": {
"count": 1234.0,
"sum": 567890.12,
"mean": 460.2,
"min": 1.0,
"max": 999.0
}
},
"geometry": { "type": "Polygon", "coordinates": [] }
}
]
}
}
Process output¶
All processes return a JSON object with:
{
"status": "completed",
"files": ["path/to/file1.nc", "path/to/file2.nc"],
"summary": {
"file_count": 2,
"start": "2024-01",
"end": "2024-03"
},
"message": "Data downloaded (dry run)"
}
CHIRPS3 to DHIS2 pipeline (chirps3-dhis2-pipeline)¶
Runs four steps in one execution:
- Get features (from DHIS2 or provided GeoJSON)
- Fetch CHIRPS3 data for union bbox
- Process dataset (spatial + temporal aggregation)
- Generate DHIS2
dataValueSetpayload (optional auto-import)
Example request using DHIS2 org units as source features:
curl -X POST http://localhost:8000/ogcapi/processes/chirps3-dhis2-pipeline/execution \
-H "Content-Type: application/json" \
-d '{
"inputs": {
"start_date": "2024-01-01",
"end_date": "2024-03-31",
"org_unit_level": 3,
"data_element": "DEMO_DATA_ELEMENT_UID",
"temporal_resolution": "monthly",
"temporal_reducer": "sum",
"spatial_reducer": "mean",
"stage": "final",
"dry_run": true,
"auto_import": false
}
}'
The response includes:
files: downloaded CHIRPS3 monthly filesdataValueSet: DHIS2-compatible payload (dataValuesarray)importResponse: populated only whenauto_import=trueanddry_run=false
Notes:
parent_org_unitis optional. For large DHIS2 instances, preferparent_org_unit+org_unit_level(or explicitorg_unit_ids) to avoid fetching very large feature sets.org_unit_levelalone runs across the full level by default.category_option_comboandattribute_option_comboare optional. If omitted, they are not sent indataValues, allowing DHIS2 defaults where supported.temporal_resolutionsupportsdaily,weekly, andmonthly.
Async execution and job management¶
Climate data downloads (ERA5-Land, CHIRPS3) can take minutes. To avoid HTTP timeouts, processes support asynchronous execution via the Prefer: respond-async header.
Submitting an async request¶
Add the Prefer: respond-async header to a normal execution request. The server returns 201 Created with a Location header pointing to the job status endpoint.
curl -X POST http://localhost:8000/ogcapi/processes/chirps3-download/execution \
-H "Prefer: respond-async" \
-H "Content-Type: application/json" \
-d '{
"inputs": {
"start": "2024-01",
"end": "2024-01",
"bbox": [32, -2, 35, 1]
}
}'
Response (201 Created):
{
"jobID": "abc123",
"status": "accepted",
"type": "process",
"message": "Job accepted",
"...": "..."
}
The Location response header contains the job URL, e.g. /ogcapi/jobs/abc123.
Polling job status¶
The status field progresses through: accepted -> running -> successful (or failed).
Retrieving results¶
Once status is successful:
Listing all jobs¶
Deleting a job¶
Synchronous execution (default)¶
Without the Prefer header, requests execute synchronously and return results directly. This is unchanged from before.
Plugin system¶
pygeoapi uses a plugin architecture so that new data backends, output formats, and processing tasks can be added without modifying the core.
Plugin categories¶
| Category | Base class | Purpose |
|---|---|---|
| provider | pygeoapi.provider.base.BaseProvider |
Data access (read features, coverages, tiles, etc.) |
| formatter | pygeoapi.formatter.base.BaseFormatter |
Output format conversion (e.g. CSV export) |
| process | pygeoapi.process.base.BaseProcessor |
Server-side processing logic |
| process_manager | pygeoapi.process.manager.base.BaseManager |
Job tracking and async execution |
How loading works¶
In the YAML config the name field on a provider or processor identifies the plugin. pygeoapi resolves it in two ways:
- Short name -- a built-in alias registered in pygeoapi's plugin registry (e.g.
GeoJSON,CSV,rasterio). - Dotted Python path -- a fully-qualified class name for custom plugins (e.g.
mypackage.providers.MyProvider).
Plugin directory layout¶
Custom plugins live under src/open_climate_service/routers/ogcapi/plugins/, organized by type:
plugins/
__init__.py
providers/ # Data access plugins (BaseProvider subclasses)
__init__.py
dhis2_common.py # Shared DHIS2 models and helpers
dhis2_org_units.py # Feature provider for DHIS2 org units
dhis2_org_units_cql.py # Feature provider with CQL filter support
dhis2eo.py # EDR provider stub
processes/ # Processing plugins (BaseProcessor subclasses)
__init__.py
schemas.py # Pydantic models for process inputs/outputs
era5_land.py # ERA5-Land download processor
chirps3.py # CHIRPS3 download processor
Creating a custom provider¶
A custom provider subclasses the appropriate base class and implements the required methods.
from pygeoapi.provider.base import BaseProvider
class MyProvider(BaseProvider):
def __init__(self, provider_def):
super().__init__(provider_def)
def get(self, identifier, **kwargs):
...
def query(self, **kwargs):
...
Reference it in the config by dotted path:
providers:
- type: feature
name: open_climate_service.routers.ogcapi.plugins.providers.my_provider.MyProvider
data: /path/to/data
Creating a custom processor¶
A custom processor subclasses BaseProcessor, defines PROCESS_METADATA, and implements execute():
from pygeoapi.process.base import BaseProcessor
PROCESS_METADATA = {
"version": "0.1.0",
"id": "my-process",
"title": "My Process",
"jobControlOptions": ["sync-execute"],
"inputs": { ... },
"outputs": { ... },
}
class MyProcessor(BaseProcessor):
def __init__(self, processor_def):
super().__init__(processor_def, PROCESS_METADATA)
def execute(self, data, outputs=None):
# Validate inputs, run processing, return (mimetype, result)
return "application/json", {"status": "completed"}
Reference it in the config:
resources:
my-process:
type: process
processor:
name: open_climate_service.routers.ogcapi.plugins.processes.my_process.MyProcessor
References¶
- OGC API standards catalogue: https://ogcapi.ogc.org
- OGC API - Features spec: https://ogcapi.ogc.org/features/
- OGC API - Coverages spec: https://ogcapi.ogc.org/coverages/
- OGC API - EDR spec: https://ogcapi.ogc.org/edr/
- OGC API - Processes spec: https://ogcapi.ogc.org/processes/
- pygeoapi documentation: https://docs.pygeoapi.io
- pygeoapi configuration guide: https://docs.pygeoapi.io/en/latest/configuration.html
- pygeoapi data publishing guide: https://docs.pygeoapi.io/en/latest/data-publishing/
- pygeoapi plugins: https://docs.pygeoapi.io/en/latest/plugins.html
- Community plugins wiki: https://github.com/geopython/pygeoapi/wiki/CommunityPlugins
- pygeoapi source: https://github.com/geopython/pygeoapi