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/climate_api/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: DHIS2 Climate API
description:
en: OGC API compliant geospatial data API
provider:
name: DHIS2 Climate API
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 Climate API, 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 Climate API, 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/climate_api/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: climate_api.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: climate_api.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