diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e3e6abe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.venv/ +.git/ +__pycache__/ +.env +*.egg-info/ +.DS_Store +.ruff_cache/ +.mypy_cache/ +.pytest_cache/ +tests/ +.claude/ diff --git a/.env.example b/.env.example index cd4a24c..30847dd 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,5 @@ PYGEOAPI_CONFIG=pygeoapi-config.yml -PYGEOAPI_OPENAPI=pygeoapi-openapi.yml \ No newline at end of file +PYGEOAPI_OPENAPI=pygeoapi-openapi.yml +DHIS2_BASE_URL=https://play.im.dhis2.org/stable-2-42-4/api +DHIS2_USERNAME=admin +DHIS2_PASSWORD=district \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9499a74 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Create .env from example + run: cp .env.example .env + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: uv sync --group dev + + - name: Run linting + run: make lint + + - name: Generate OpenAPI spec + run: make openapi + + - name: Run tests + run: make test diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..3f334fb --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,72 @@ +name: Build and push Docker image + +on: + push: + branches: [main] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: dhis2/eo-api + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Create .env from example + run: cp .env.example .env + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: uv sync + + - name: Generate OpenAPI spec + run: make openapi + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push + id: push + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v2 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.gitignore b/.gitignore index 67c3344..e654634 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__/ .venv/ .env eo_api.egg-info/ +pygeoapi-openapi.yml diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d39637e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,7 @@ +# CLAUDE.md + +## Commit conventions + +- Use conventional commits (e.g. `feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`) +- No Co-Authored-By or other attribution lines +- Never use emojis anywhere — not in commits, code, comments, or responses diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5855c49 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM ghcr.io/astral-sh/uv:0.10-python3.13-trixie-slim + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git curl \ + build-essential cython3 \ + libgeos-dev libproj-dev && \ + rm -rf /var/lib/apt/lists/* + +RUN groupadd --system eo && useradd --system --gid eo --create-home eo + +WORKDIR /app + +COPY pyproject.toml uv.lock .python-version ./ +COPY src/ src/ + +RUN uv sync --frozen --no-dev + +COPY pygeoapi-config.yml pygeoapi-openapi.yml ./ + +ENV PYGEOAPI_CONFIG=/app/pygeoapi-config.yml +ENV PYGEOAPI_OPENAPI=/app/pygeoapi-openapi.yml +ENV PORT=8000 + +USER eo + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:${PORT}/health || exit 1 + +CMD /app/.venv/bin/uvicorn eo_api.main:app --host 0.0.0.0 --port ${PORT} diff --git a/Makefile b/Makefile index 185182a..217d93d 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,27 @@ -sync: +.DEFAULT_GOAL := help + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}' + +sync: ## Install dependencies with uv uv sync -run: - uv run uvicorn main:app --reload +run: openapi ## Start the app with uvicorn + uv run uvicorn eo_api.main:app --reload + +lint: ## Run ruff linting and formatting (autofix) + uv run ruff check --fix . + uv run ruff format . + +test: ## Run tests with pytest + uv run pytest tests/ + +openapi: ## Generate pygeoapi OpenAPI spec + @set -a && . ./.env && set +a && \ + PYTHONPATH="$(PWD)/src" uv run pygeoapi openapi generate ./pygeoapi-config.yml > pygeoapi-openapi.yml + +start: openapi ## Start the Docker stack (builds images first) + docker compose up --build -openapi: - PYTHONPATH="$(PWD)" uv run pygeoapi openapi generate ./pygeoapi-config.yml > pygeoapi-openapi.yml \ No newline at end of file +restart: openapi ## Tear down, rebuild, and start the Docker stack from scratch + docker compose down -v && docker compose build --no-cache && docker compose up diff --git a/README.md b/README.md index 995f0fb..ce64dec 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,15 @@ Install dependencies (requires [uv](https://docs.astral.sh/uv/)): Environment variables are loaded automatically from `.env` (via `python-dotenv`). Copy `.env.example` to `.env` and adjust values as needed. +Key environment variables (used by the OGC API DHIS2 plugin): + +- `DHIS2_BASE_URL` -- DHIS2 API base URL (defaults to play server in `.env.example`) +- `DHIS2_USERNAME` -- DHIS2 username +- `DHIS2_PASSWORD` -- DHIS2 password + Start the app: -`uv run uvicorn main:app --reload` +`uv run uvicorn eo_api.main:app --reload` ### Using pip (alternative) @@ -25,7 +31,7 @@ If you can't use uv (e.g. mixed conda/forge environments): python -m venv .venv source .venv/bin/activate pip install -e . -uvicorn main:app --reload +uvicorn eo_api.main:app --reload ``` ### Using conda @@ -34,13 +40,18 @@ uvicorn main:app --reload conda create -n dhis2-eo-api python=3.13 conda activate dhis2-eo-api pip install -e . -uvicorn main:app --reload +uvicorn eo_api.main:app --reload ``` ### Makefile targets -- `make sync` — install dependencies with uv -- `make run` — start the app with uv +- `make sync` -- install dependencies with uv +- `make run` -- start the app with uvicorn +- `make lint` -- run ruff linting and format checks +- `make test` -- run tests with pytest +- `make openapi` -- generate pygeoapi OpenAPI spec +- `make start` -- start the Docker stack (builds images first) +- `make restart` -- tear down, rebuild, and start the Docker stack from scratch ### pygeoapi instructions diff --git a/compose.ghcr.yml b/compose.ghcr.yml new file mode 100644 index 0000000..f6d0075 --- /dev/null +++ b/compose.ghcr.yml @@ -0,0 +1,8 @@ +services: + api: + image: ghcr.io/dhis2/eo-api:latest + env_file: .env + ports: + - "${PORT:-8000}:${PORT:-8000}" + init: true + restart: unless-stopped diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..ca0fe61 --- /dev/null +++ b/compose.yml @@ -0,0 +1,8 @@ +services: + api: + build: . + env_file: .env + ports: + - "${PORT:-8000}:${PORT:-8000}" + init: true + restart: unless-stopped diff --git a/docs/ogcapi.md b/docs/ogcapi.md new file mode 100644 index 0000000..f40a8f0 --- /dev/null +++ b/docs/ogcapi.md @@ -0,0 +1,286 @@ +# OGC API and pygeoapi + +## OGC API overview + +OGC API is a family of standards from the [Open Geospatial Consortium](https://www.ogc.org/) that define RESTful interfaces for geospatial data. Each standard covers a specific data type or interaction pattern: + +| Standard | Purpose | +|---|---| +| [Features](https://ogcapi.ogc.org/features/) | Vector feature access (GeoJSON, etc.) | +| [Coverages](https://ogcapi.ogc.org/coverages/) | Gridded / raster data | +| [EDR](https://ogcapi.ogc.org/edr/) | Environmental Data Retrieval (point, trajectory, corridor queries) | +| [Processes](https://ogcapi.ogc.org/processes/) | Server-side processing / workflows | +| [Maps](https://ogcapi.ogc.org/maps/) | Rendered map images | +| [Tiles](https://ogcapi.ogc.org/tiles/) | Tiled data (vector and map tiles) | +| [Records](https://ogcapi.ogc.org/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 . + +## pygeoapi + +[pygeoapi](https://pygeoapi.io) 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/eo_api/routers/ogcapi.py`: + +```python +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: +- Source: + +## Configuration + +pygeoapi is configured through a single YAML file whose path is set by the `PYGEOAPI_CONFIG` environment variable. The repo ships a default config at `pygeoapi-config.yml`. + +### Top-level sections + +```yaml +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. + +```yaml +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. + +```yaml +metadata: + identification: + title: + en: DHIS2 EO API + description: + en: OGC API compliant geospatial data API + provider: + name: DHIS2 EO 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`. + +```yaml +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: + +## 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. Defined by a `processor` rather than a `providers` list. | + +A single collection can have multiple providers (e.g. both `feature` and `tile` on the same resource). + +## CQL filtering + +pygeoapi supports [CQL2](https://docs.ogc.org/is/21-065r2/21-065r2.html) text filters on collections backed by a CQL-capable provider. Filters are passed as query parameters: + +``` +?filter= +``` + +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: + +``` +/ogcapi/collections/dhis2-org-units-cql/items?filter=level=2 +``` + +String match on name: + +``` +/ogcapi/collections/dhis2-org-units-cql/items?filter=name='0002 CH Mittaphap' +``` + +LIKE (case-sensitive pattern): + +``` +/ogcapi/collections/dhis2-org-units-cql/items?filter=name LIKE '%Hospital%' +``` + +ILIKE (case-insensitive pattern): + +``` +/ogcapi/collections/dhis2-org-units-cql/items?filter=name ILIKE '%hospital%' +``` + +Combined filter with AND: + +``` +/ogcapi/collections/dhis2-org-units-cql/items?filter=level=3 AND name LIKE '%CH%' +``` + +BETWEEN range: + +``` +/ogcapi/collections/dhis2-org-units-cql/items?filter=level BETWEEN 2 AND 3 +``` + +IN set membership: + +``` +/ogcapi/collections/dhis2-org-units-cql/items?filter=level IN (1,2) +``` + +NULL check combined with comparison: + +``` +/ogcapi/collections/dhis2-org-units-cql/items?filter=code IS NULL AND level=5 +``` + +## 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: + +1. **Short name** -- a built-in alias registered in pygeoapi's plugin registry (e.g. `GeoJSON`, `CSV`, `rasterio`, `HelloWorld`). +2. **Dotted Python path** -- a fully-qualified class name for custom plugins (e.g. `mypackage.providers.MyProvider`). + +### Creating a custom plugin + +A custom provider needs to subclass the appropriate base class and implement the required methods. + +```python +from pygeoapi.provider.base import BaseProvider + + +class MyProvider(BaseProvider): + """Custom feature provider.""" + + def __init__(self, provider_def): + super().__init__(provider_def) + # provider_def contains the YAML provider block + + def get(self, identifier, **kwargs): + # Return a single feature by ID + ... + + def query(self, **kwargs): + # Return a FeatureCollection matching the query parameters + ... +``` + +Reference it in the config by dotted path: + +```yaml +providers: + - type: feature + name: mypackage.providers.MyProvider + data: /path/to/data +``` + +For processes, subclass `BaseProcessor` and set `PROCESS_METADATA` as a class-level dict describing inputs and outputs: + +```python +from pygeoapi.process.base import BaseProcessor + +PROCESS_METADATA = { + "version": "0.1.0", + "id": "my-process", + "title": "My Process", + "inputs": { ... }, + "outputs": { ... }, +} + + +class MyProcessor(BaseProcessor): + def __init__(self, processor_def): + super().__init__(processor_def, PROCESS_METADATA) + + def execute(self, data): + # Process input data and return results + ... +``` + +## References + +- OGC API standards catalogue: +- OGC API - Features spec: +- OGC API - Coverages spec: +- OGC API - EDR spec: +- OGC API - Processes spec: +- pygeoapi documentation: +- pygeoapi configuration guide: +- pygeoapi data publishing guide: +- pygeoapi plugins: +- Community plugins wiki: +- pygeoapi source: diff --git a/main.py b/main.py deleted file mode 100644 index 5656432..0000000 --- a/main.py +++ /dev/null @@ -1,40 +0,0 @@ -from dotenv import load_dotenv -from fastapi import FastAPI -from titiler.core.factory import (TilerFactory) - -from starlette.middleware.cors import CORSMiddleware - -load_dotenv() - -from pygeoapi.starlette_app import APP as pygeoapi_app - -app = FastAPI() - -# Bsed on: -# https://docs.pygeoapi.io/en/stable/administration.html -# https://dive.pygeoapi.io/advanced/downstream-applications/#starlette-and-fastapi -# https://developmentseed.org/titiler/user_guide/getting_started/#4-create-your-titiler-application -# https://github.com/developmentseed/titiler/blob/main/src/titiler/application/titiler/application/main.py - -# Add CORS middleware -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # Allows all origins (for development - be more specific in production) - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# mount all pygeoapi endpoints to /ogcapi -app.mount(path="/ogcapi", app=pygeoapi_app) - -# Create a TilerFactory for Cloud-Optimized GeoTIFFs -cog = TilerFactory() - -# Register all the COG endpoints automatically -app.include_router(cog.router, prefix="/cog", tags=["Cloud Optimized GeoTIFF"]) - -# Optional: Add a welcome message for the root endpoint -@app.get("/") -def read_index(): - return {"message": "Welcome to DHIS2 EO API"} \ No newline at end of file diff --git a/plugins/dhis2eo.py b/plugins/dhis2eo.py deleted file mode 100644 index c148a43..0000000 --- a/plugins/dhis2eo.py +++ /dev/null @@ -1,39 +0,0 @@ -from pygeoapi.provider.base_edr import BaseEDRProvider - - -class DHIS2EOProvider(BaseEDRProvider): - """Minimal EDR provider example.""" - - def get_fields(self): - return { - 'value': { - 'type': 'number', - 'title': 'Value', - 'x-ogc-unit': 'mm/day', - } - } - - def position(self, **kwargs): - return { - 'type': 'Coverage', - 'domain': { - 'type': 'Domain', - 'domainType': 'Point', - 'axes': { - 'Long': {'values': [0.0]}, - 'Lat': {'values': [0.0]}, - }, - }, - 'parameters': { - 'value': {'type': 'Parameter'} - }, - 'ranges': { - 'value': { - 'type': 'NdArray', - 'dataType': 'float', - 'axisNames': ['Long', 'Lat'], - 'shape': [1, 1], - 'values': [10.0], - } - }, - } \ No newline at end of file diff --git a/plugins/dhis2orgUnits.py b/plugins/dhis2orgUnits.py deleted file mode 100644 index 4d34e09..0000000 --- a/plugins/dhis2orgUnits.py +++ /dev/null @@ -1,47 +0,0 @@ -from pygeoapi.provider.base import BaseProvider - -class DHIS2OrgUnitsProvider(BaseProvider): - """DHIS2 Organization Units Provider""" - - def __init__(self, provider_def): - """Inherit from parent class""" - - super().__init__(provider_def) - - def get_fields(self): - - # open dat file and return fields and their datatypes - return { - 'field1': 'string', - 'field2': 'string' - } - - def query(self, offset=0, limit=10, resulttype='results', - bbox=[], datetime_=None, properties=[], sortby=[], - select_properties=[], skip_geometry=False, **kwargs): - - # optionally specify the output filename pygeoapi can use as part - # of the response (HTTP Content-Disposition header) - self.filename = 'my-cool-filename.dat' - - # open data file (self.data) and process, return - return { - 'type': 'FeatureCollection', - 'features': [{ - 'type': 'Feature', - 'id': '371', - 'geometry': { - 'type': 'Point', - 'coordinates': [ -75, 45 ] - }, - 'properties': { - 'stn_id': '35', - 'datetime': '2001-10-30T14:24:55Z', - 'value': '89.9' - } - }] - } - - def get_schema(): - # return a `dict` of a JSON schema (inline or reference) - return ('application/geo+json', {'$ref': 'https://geojson.org/schema/Feature.json'}) \ No newline at end of file diff --git a/pygeoapi-config.yml b/pygeoapi-config.yml index d4222cc..5f922bf 100644 --- a/pygeoapi-config.yml +++ b/pygeoapi-config.yml @@ -99,7 +99,23 @@ resources: crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 providers: - type: feature - name: plugins.dhis2orgUnits.DHIS2OrgUnitsProvider + name: eo_api.routers.ogcapi.plugins.dhis2_org_units.DHIS2OrgUnitsProvider + data: tests/data/ + id_field: id + + dhis2-org-units-cql: + type: collection + title: DHIS2 organization units (CQL) + description: Get organization units from DHIS2 with CQL filter support + keywords: + - dhis2 + extents: + spatial: + bbox: [-180, -90, 180, 90] + crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 + providers: + - type: feature + name: eo_api.routers.ogcapi.plugins.dhis2_org_units_cql.DHIS2OrgUnitsCqlProvider data: tests/data/ id_field: id @@ -122,7 +138,7 @@ resources: hreflang: en-EN providers: - type: edr - name: plugins.dhis2eo.DHIS2EOProvider + name: eo_api.routers.ogcapi.plugins.dhis2eo.DHIS2EOProvider data: tests/data/ era5-land: @@ -143,7 +159,7 @@ resources: hreflang: en-EN providers: - type: edr - name: plugins.dhis2eo.DHIS2EOProvider + name: eo_api.routers.ogcapi.plugins.dhis2eo.DHIS2EOProvider data: tests/data/ obs: diff --git a/pygeoapi-openapi.yml b/pygeoapi-openapi.yml deleted file mode 100644 index ed37cf6..0000000 --- a/pygeoapi-openapi.yml +++ /dev/null @@ -1,1352 +0,0 @@ -components: - parameters: - bbox: - description: Only features that have a geometry that intersects the bounding - box are selected.The bounding box is provided as four or six numbers, depending - on whether the coordinate reference system includes a vertical axis (height - or depth). - explode: false - in: query - name: bbox - required: false - schema: - items: - type: number - maxItems: 6 - minItems: 4 - type: array - style: form - bbox-crs: - description: Indicates the coordinate reference system for the given bbox coordinates. - explode: false - in: query - name: bbox-crs - required: false - schema: - format: uri - type: string - style: form - bbox-crs-epsg: - description: Indicates the EPSG for the given bbox coordinates. - explode: false - in: query - name: bbox-crs - required: false - schema: - default: 4326 - type: integer - style: form - crs: - description: Indicates the coordinate reference system for the results. - explode: false - in: query - name: crs - required: false - schema: - format: uri - type: string - style: form - f: - description: The optional f parameter indicates the output format which the - server shall provide as part of the response document. The default format - is GeoJSON. - explode: false - in: query - name: f - required: false - schema: - default: json - enum: - - json - - html - - jsonld - type: string - style: form - lang: - description: The optional lang parameter instructs the server return a response - in a certain language, if supported. If the language is not among the available - values, the Accept-Language header language will be used if it is supported. - If the header is missing, the default server language is used. Note that providers - may only support a single language (or often no language at all), that can - be different from the server language. Language strings can be written in - a complex (e.g. "fr-CA,fr;q=0.9,en-US;q=0.8,en;q=0.7"), simple (e.g. "de") - or locale-like (e.g. "de-CH" or "fr_BE") fashion. - in: query - name: lang - required: false - schema: - default: en-US - enum: - - en-US - - fr-CA - type: string - offset: - description: The optional offset parameter indicates the index within the result - set from which the server shall begin presenting results in the response document. The - first element has an index of 0 (default). - explode: false - in: query - name: offset - required: false - schema: - default: 0 - minimum: 0 - type: integer - style: form - resourceId: - description: Configuration resource identifier - in: path - name: resourceId - required: true - schema: - default: dhis2-org-units - type: string - skipGeometry: - description: This option can be used to skip response geometries for each feature. - explode: false - in: query - name: skipGeometry - required: false - schema: - default: false - type: boolean - style: form - vendorSpecificParameters: - description: Additional "free-form" parameters that are not explicitly defined - in: query - name: vendorSpecificParameters - schema: - additionalProperties: true - type: object - style: form - responses: - '200': - description: successful operation - '204': - description: no content - Queryables: - content: - application/json: - schema: - $ref: '#/components/schemas/queryables' - description: successful queryables operation - Tiles: - content: - application/json: - schema: - $ref: '#/components/schemas/tiles' - description: Retrieves the tiles description for this collection - default: - content: - application/json: - schema: - $ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/schemas/exception.yaml - description: Unexpected error - schemas: - queryable: - properties: - description: - description: a human-readable narrative describing the queryable - type: string - language: - default: en - description: the language used for the title and description - type: string - queryable: - description: the token that may be used in a CQL predicate - type: string - title: - description: a human readable title for the queryable - type: string - type: - description: the data type of the queryable - type: string - type-ref: - description: a reference to the formal definition of the type - format: url - type: string - required: - - queryable - - type - type: object - queryables: - properties: - queryables: - items: - $ref: '#/components/schemas/queryable' - type: array - required: - - queryables - type: object - tilematrixsetlink: - properties: - tileMatrixSet: - type: string - tileMatrixSetURI: - type: string - required: - - tileMatrixSet - type: object - tiles: - properties: - links: - items: - $ref: https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml#/components/schemas/link - type: array - tileMatrixSetLinks: - items: - $ref: '#/components/schemas/tilematrixsetlink' - type: array - required: - - tileMatrixSetLinks - - links - type: object -info: - contact: - name: DHIS2 EO API - url: https://dhis2.org - x-ogc-serviceContact: - addresses: [] - emails: - - value: climate@dhis2.org - hoursOfService: pointOfContact - name: DHIS2 Climate Team - description: OGC API compliant geospatial data API - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - termsOfService: https://creativecommons.org/licenses/by/4.0/ - title: DHIS2 EO API - version: 0.22.0 - x-keywords: - - geospatial - - data - - api -openapi: 3.0.2 -paths: - /: - get: - description: Landing page - operationId: getLandingPage - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/LandingPage - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Landing page - tags: - - server - /collections: - get: - description: Collections - operationId: getCollections - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/LandingPage - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Collections - tags: - - server - /collections/chirps-precipitation: - get: - description: CHIRPS v3 daily precipitation - operationId: describeChirps-precipitationCollection - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get CHIRPS v3 daily precipitation metadata - tags: - - chirps-precipitation - /collections/chirps-precipitation/area: - get: - description: CHIRPS v3 daily precipitation - operationId: queryAreaChirps-precipitation - parameters: - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/areaCoords.yaml - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/datetime - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/parameter-name.yaml - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/z.yaml - - $ref: '#/components/parameters/f' - responses: - '200': - content: - application/prs.coverage+json: - schema: - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/schemas/coverageJSON.yaml - description: Response - summary: query CHIRPS v3 daily precipitation by area - tags: - - chirps-precipitation - /collections/chirps-precipitation/cube: - get: - description: CHIRPS v3 daily precipitation - operationId: queryCubeChirps-precipitation - parameters: - - description: Only features that have a geometry that intersects the bounding - box are selected.The bounding box is provided as four or six numbers, depending - on whether the coordinate reference system includes a vertical axis (height - or depth). - explode: false - in: query - name: bbox - required: true - schema: - items: - type: number - maxItems: 6 - minItems: 4 - type: array - style: form - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/datetime - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/parameter-name.yaml - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/z.yaml - - $ref: '#/components/parameters/f' - responses: - '200': - content: - application/prs.coverage+json: - schema: - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/schemas/coverageJSON.yaml - description: Response - summary: query CHIRPS v3 daily precipitation by cube - tags: - - chirps-precipitation - /collections/chirps-precipitation/position: - get: - description: CHIRPS v3 daily precipitation - operationId: queryPositionChirps-precipitation - parameters: - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/positionCoords.yaml - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/datetime - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/parameter-name.yaml - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/z.yaml - - $ref: '#/components/parameters/f' - responses: - '200': - content: - application/prs.coverage+json: - schema: - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/schemas/coverageJSON.yaml - description: Response - summary: query CHIRPS v3 daily precipitation by position - tags: - - chirps-precipitation - /collections/dhis2-org-units: - get: - description: Get organization units from DHIS2 - operationId: describeDhis2-org-unitsCollection - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get DHIS2 organization units metadata - tags: - - dhis2-org-units - /collections/dhis2-org-units/items: - get: - description: Get organization units from DHIS2 - operationId: getDhis2-org-unitsFeatures - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - - $ref: '#/components/parameters/bbox' - - description: The optional limit parameter limits the number of items that - are presented in the response document (maximum=50, default=20). - explode: false - in: query - name: limit - required: false - schema: - default: 20 - maximum: 50 - minimum: 1 - type: integer - style: form - - $ref: '#/components/parameters/crs' - - $ref: '#/components/parameters/bbox-crs' - - description: The properties that should be included for each feature. The - parameter value is a comma-separated list of property names. - explode: false - in: query - name: properties - required: false - schema: - items: - enum: [] - type: string - type: array - style: form - - $ref: '#/components/parameters/vendorSpecificParameters' - - $ref: '#/components/parameters/skipGeometry' - - $ref: https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi/parameters/sortby.yaml - - $ref: '#/components/parameters/offset' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get DHIS2 organization units items - tags: - - dhis2-org-units - options: - description: Get organization units from DHIS2 - operationId: optionsDhis2-org-unitsFeatures - responses: - '200': - description: options response - summary: Options for DHIS2 organization units items - tags: - - dhis2-org-units - post: - description: Get organization units from DHIS2 - operationId: getCQL2Dhis2-org-unitsFeatures - requestBody: - content: - application/json: - schema: - $ref: https://schemas.opengis.net/cql2/1.0/cql2.json - description: Get items with CQL2 - required: true - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get DHIS2 organization units items with CQL2 - tags: - - dhis2-org-units - /collections/dhis2-org-units/items/{featureId}: - get: - description: Get organization units from DHIS2 - operationId: getDhis2-org-unitsFeature - parameters: - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId - - $ref: '#/components/parameters/crs' - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Feature - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get DHIS2 organization units item by id - tags: - - dhis2-org-units - options: - description: Get organization units from DHIS2 - operationId: optionsDhis2-org-unitsFeature - parameters: - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId - responses: - '200': - description: options response - summary: Options for DHIS2 organization units item by id - tags: - - dhis2-org-units - /collections/era5-land: - get: - description: ERA5-Land hourly data on single levels from 1981 to present - operationId: describeEra5-landCollection - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get ERA5-Land hourly metadata - tags: - - era5-land - /collections/era5-land/area: - get: - description: ERA5-Land hourly data on single levels from 1981 to present - operationId: queryAreaEra5-land - parameters: - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/areaCoords.yaml - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/datetime - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/parameter-name.yaml - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/z.yaml - - $ref: '#/components/parameters/f' - responses: - '200': - content: - application/prs.coverage+json: - schema: - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/schemas/coverageJSON.yaml - description: Response - summary: query ERA5-Land hourly data on single levels from 1981 to present by - area - tags: - - era5-land - /collections/era5-land/cube: - get: - description: ERA5-Land hourly data on single levels from 1981 to present - operationId: queryCubeEra5-land - parameters: - - description: Only features that have a geometry that intersects the bounding - box are selected.The bounding box is provided as four or six numbers, depending - on whether the coordinate reference system includes a vertical axis (height - or depth). - explode: false - in: query - name: bbox - required: true - schema: - items: - type: number - maxItems: 6 - minItems: 4 - type: array - style: form - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/datetime - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/parameter-name.yaml - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/z.yaml - - $ref: '#/components/parameters/f' - responses: - '200': - content: - application/prs.coverage+json: - schema: - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/schemas/coverageJSON.yaml - description: Response - summary: query ERA5-Land hourly data on single levels from 1981 to present by - cube - tags: - - era5-land - /collections/era5-land/position: - get: - description: ERA5-Land hourly data on single levels from 1981 to present - operationId: queryPositionEra5-land - parameters: - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/positionCoords.yaml - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/datetime - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/parameter-name.yaml - - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/parameters/z.yaml - - $ref: '#/components/parameters/f' - responses: - '200': - content: - application/prs.coverage+json: - schema: - $ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/schemas/coverageJSON.yaml - description: Response - summary: query ERA5-Land hourly data on single levels from 1981 to present by - position - tags: - - era5-land - /collections/gdps-temperature: - get: - description: Global Deterministic Prediction System sample - operationId: describeGdps-temperatureCollection - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Global Deterministic Prediction System sample metadata - tags: - - gdps-temperature - /collections/gdps-temperature/coverage: - get: - description: Global Deterministic Prediction System sample - operationId: getGdps-temperatureCoverage - parameters: - - $ref: '#/components/parameters/lang' - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/bbox' - - $ref: '#/components/parameters/bbox-crs' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Global Deterministic Prediction System sample coverage - tags: - - gdps-temperature - /collections/lakes: - get: - description: lakes of the world, public domain - operationId: describeLakesCollection - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Large Lakes metadata - tags: - - lakes - /collections/lakes/items: - get: - description: lakes of the world, public domain - operationId: getLakesFeatures - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - - $ref: '#/components/parameters/bbox' - - description: The optional limit parameter limits the number of items that - are presented in the response document (maximum=50, default=20). - explode: false - in: query - name: limit - required: false - schema: - default: 20 - maximum: 50 - minimum: 1 - type: integer - style: form - - $ref: '#/components/parameters/crs' - - $ref: '#/components/parameters/bbox-crs' - - &id001 - description: The properties that should be included for each feature. The - parameter value is a comma-separated list of property names. - explode: false - in: query - name: properties - required: false - schema: - items: - enum: - - id - - scalerank - - name - - name_alt - - admin - - featureclass - type: string - type: array - style: form - - $ref: '#/components/parameters/vendorSpecificParameters' - - $ref: '#/components/parameters/skipGeometry' - - $ref: https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi/parameters/sortby.yaml - - $ref: '#/components/parameters/offset' - - explode: false - in: query - name: id - required: false - schema: - type: integer - style: form - - explode: false - in: query - name: scalerank - required: false - schema: - type: integer - style: form - - explode: false - in: query - name: name - required: false - schema: - type: string - style: form - - explode: false - in: query - name: name_alt - required: false - schema: - type: string - style: form - - explode: false - in: query - name: admin - required: false - schema: - type: string - style: form - - explode: false - in: query - name: featureclass - required: false - schema: - type: string - style: form - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Large Lakes items - tags: - - lakes - options: - description: lakes of the world, public domain - operationId: optionsLakesFeatures - responses: - '200': - description: options response - summary: Options for Large Lakes items - tags: - - lakes - post: - description: lakes of the world, public domain - operationId: getCQL2LakesFeatures - requestBody: - content: - application/json: - schema: - $ref: https://schemas.opengis.net/cql2/1.0/cql2.json - description: Get items with CQL2 - required: true - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Large Lakes items with CQL2 - tags: - - lakes - /collections/lakes/items/{featureId}: - get: - description: lakes of the world, public domain - operationId: getLakesFeature - parameters: - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId - - $ref: '#/components/parameters/crs' - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Feature - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Large Lakes item by id - tags: - - lakes - options: - description: lakes of the world, public domain - operationId: optionsLakesFeature - parameters: - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId - responses: - '200': - description: options response - summary: Options for Large Lakes item by id - tags: - - lakes - /collections/lakes/queryables: - get: - description: lakes of the world, public domain - operationId: getLakesQueryables - parameters: - - *id001 - - $ref: '#/components/parameters/f' - - &id003 - description: The profile to be applied to a given request - explode: false - in: query - name: profile - required: false - schema: - enum: - - actual-domain - - valid-domain - type: string - style: form - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: '#/components/responses/Queryables' - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Large Lakes queryables - tags: - - lakes - /collections/lakes/schema: - get: - description: lakes of the world, public domain - operationId: getLakesSchema - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: '#/components/responses/Queryables' - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Large Lakes schema - tags: - - lakes - /collections/mapserver_world_map: - get: - description: MapServer demo WMS world map - operationId: describeMapserver_world_mapCollection - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get MapServer demo WMS world map metadata - tags: - - mapserver_world_map - /collections/mapserver_world_map/map: - get: - description: MapServer demo WMS world map map - operationId: getMap - parameters: - - $ref: '#/components/parameters/bbox' - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/datetime - - description: Response image width - explode: false - in: query - name: width - required: false - schema: - type: integer - style: form - - description: Response image height - explode: false - in: query - name: height - required: false - schema: - type: integer - style: form - - description: Background transparency of map (default=true). - explode: false - in: query - name: transparent - required: false - schema: - default: true - type: boolean - style: form - - $ref: '#/components/parameters/bbox-crs-epsg' - - description: The optional f parameter indicates the output format which the - server shall provide as part of the response document. The default format - is GeoJSON. - explode: false - in: query - name: f - required: false - schema: - default: png - enum: - - png - type: string - style: form - responses: - '200': - content: - application/json: {} - description: Response - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get map - tags: - - mapserver_world_map - /collections/obs: - get: - description: My cool observations - operationId: describeObsCollection - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Collection - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Observations metadata - tags: - - obs - /collections/obs/items: - get: - description: My cool observations - operationId: getObsFeatures - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - - $ref: '#/components/parameters/bbox' - - description: The optional limit parameter limits the number of items that - are presented in the response document (maximum=50, default=20). - explode: false - in: query - name: limit - required: false - schema: - default: 20 - maximum: 50 - minimum: 1 - type: integer - style: form - - $ref: '#/components/parameters/crs' - - $ref: '#/components/parameters/bbox-crs' - - &id002 - description: The properties that should be included for each feature. The - parameter value is a comma-separated list of property names. - explode: false - in: query - name: properties - required: false - schema: - items: - enum: - - id - - stn_id - - datetime - - value - type: string - type: array - style: form - - $ref: '#/components/parameters/vendorSpecificParameters' - - $ref: '#/components/parameters/skipGeometry' - - $ref: https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi/parameters/sortby.yaml - - $ref: '#/components/parameters/offset' - - explode: false - in: query - name: id - required: false - schema: - type: string - style: form - - explode: false - in: query - name: stn_id - required: false - schema: - type: integer - style: form - - explode: false - in: query - name: datetime - required: false - schema: - type: string - style: form - - explode: false - in: query - name: value - required: false - schema: - type: number - style: form - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Observations items - tags: - - obs - options: - description: My cool observations - operationId: optionsObsFeatures - responses: - '200': - description: options response - summary: Options for Observations items - tags: - - obs - post: - description: My cool observations - operationId: getCQL2ObsFeatures - requestBody: - content: - application/json: - schema: - $ref: https://schemas.opengis.net/cql2/1.0/cql2.json - description: Get items with CQL2 - required: true - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Features - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Observations items with CQL2 - tags: - - obs - /collections/obs/items/{featureId}: - get: - description: My cool observations - operationId: getObsFeature - parameters: - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId - - $ref: '#/components/parameters/crs' - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/Feature - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Observations item by id - tags: - - obs - options: - description: My cool observations - operationId: optionsObsFeature - parameters: - - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/parameters/featureId - responses: - '200': - description: options response - summary: Options for Observations item by id - tags: - - obs - /collections/obs/queryables: - get: - description: My cool observations - operationId: getObsQueryables - parameters: - - *id002 - - $ref: '#/components/parameters/f' - - *id003 - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: '#/components/responses/Queryables' - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Observations queryables - tags: - - obs - /collections/obs/schema: - get: - description: My cool observations - operationId: getObsSchema - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: '#/components/responses/Queryables' - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '404': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/NotFound - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: Get Observations schema - tags: - - obs - /conformance: - get: - description: API conformance definition - operationId: getConformanceDeclaration - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/LandingPage - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - '500': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/ServerError - summary: API conformance definition - tags: - - server - /jobs: - get: - description: Retrieve a list of jobs - operationId: getJobs - responses: - '200': - $ref: '#/components/responses/200' - '404': - $ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/NotFound.yaml - default: - $ref: '#/components/responses/default' - summary: Retrieve jobs list - tags: - - jobs - /jobs/{jobId}: - delete: - description: Cancel / delete job - operationId: deleteJob - parameters: - - &id004 - description: job identifier - in: path - name: jobId - required: true - schema: - type: string - responses: - '204': - $ref: '#/components/responses/204' - '404': - $ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/NotFound.yaml - default: - $ref: '#/components/responses/default' - summary: Cancel / delete job - tags: - - jobs - get: - description: Retrieve job details - operationId: getJob - parameters: - - *id004 - - $ref: '#/components/parameters/f' - responses: - '200': - $ref: '#/components/responses/200' - '404': - $ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/NotFound.yaml - default: - $ref: '#/components/responses/default' - summary: Retrieve job details - tags: - - jobs - /jobs/{jobId}/results: - get: - description: Retrieve job results - operationId: getJobResults - parameters: - - *id004 - - $ref: '#/components/parameters/f' - responses: - '200': - $ref: '#/components/responses/200' - '404': - $ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/NotFound.yaml - default: - $ref: '#/components/responses/default' - summary: Retrieve job results - tags: - - jobs - /openapi: - get: - description: This document - operationId: getOpenapi - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - - description: UI to render the OpenAPI document - explode: false - in: query - name: ui - required: false - schema: - default: swagger - enum: - - swagger - - redoc - type: string - style: form - responses: - '200': - $ref: '#/components/responses/200' - '400': - $ref: https://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/ogcapi-features-1.yaml#/components/responses/InvalidParameter - default: - $ref: '#/components/responses/default' - summary: This document - tags: - - server - /processes: - get: - description: Processes - operationId: getProcesses - parameters: - - $ref: '#/components/parameters/f' - responses: - '200': - $ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/ProcessList.yaml - default: - $ref: '#/components/responses/default' - summary: Processes - tags: - - server - /processes/hello-world: - get: - description: An example process that takes a name as input, and echoes it back - as output. Intended to demonstrate a simple process with a single literal - input. - operationId: describeHello-worldProcess - parameters: - - $ref: '#/components/parameters/f' - responses: - '200': - $ref: '#/components/responses/200' - default: - $ref: '#/components/responses/default' - summary: Get process metadata - tags: - - hello-world - /processes/hello-world/execution: - post: - description: An example process that takes a name as input, and echoes it back - as output. Intended to demonstrate a simple process with a single literal - input. - operationId: executeHello-worldJob - parameters: - - description: Indicates client preferences, including whether the client is - capable of asynchronous processing. - in: header - name: Prefer - required: false - schema: - enum: - - respond-async - type: string - requestBody: - content: - application/json: - example: - inputs: - message: An optional message. - name: World - schema: - $ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/schemas/execute.yaml - description: Mandatory execute request JSON - required: true - responses: - '200': - content: - application/json: - schema: - type: object - description: Process output schema - '201': - $ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/ExecuteAsync.yaml - '404': - $ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/NotFound.yaml - '500': - $ref: https://schemas.opengis.net/ogcapi/processes/part1/1.0/openapi/responses/ServerError.yaml - default: - $ref: '#/components/responses/default' - summary: Process Hello World execution - tags: - - hello-world -servers: -- description: OGC API compliant geospatial data API - url: http://127.0.0.1:8000/ogcapi -tags: -- description: OGC API compliant geospatial data API - externalDocs: - description: information - url: https://example.org - name: server -- description: Get organization units from DHIS2 - name: dhis2-org-units -- description: CHIRPS v3 daily precipitation - name: chirps-precipitation -- description: ERA5-Land hourly data on single levels from 1981 to present - name: era5-land -- description: My cool observations - name: obs -- description: lakes of the world, public domain - name: lakes -- description: MapServer demo WMS world map - name: mapserver_world_map -- description: Global Deterministic Prediction System sample - name: gdps-temperature -- name: coverages -- name: edr -- name: records -- name: features -- name: maps -- name: processes -- name: jobs -- name: tiles -- name: stac - diff --git a/pyproject.toml b/pyproject.toml index 6fb619e..28f601d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["uv_build>=0.9.0"] +build-backend = "uv_build" + [project] name = "eo-api" version = "0.1.0" @@ -9,4 +13,76 @@ dependencies = [ "pygeoapi>=0.22.0", "dhis2eo @ git+https://github.com/dhis2/dhis2eo.git@v1.1.0", "dhis2-client @ git+https://github.com/dhis2/dhis2-python-client.git@V0.3.0", -] \ No newline at end of file + "geojson-pydantic>=2.1.0", + "httpx>=0.28.1", +] + +[tool.ruff] +target-version = "py313" +line-length = 120 + +[tool.ruff.lint] +fixable = ["ALL"] +select = ["E", "W", "F", "I", "D"] +ignore = ["D203", "D213"] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.per-file-ignores] +"tests/**/*.py" = ["D"] +"**/__init__.py" = ["D104"] +"src/**/*.py" = ["D102", "D105", "D107"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +docstring-code-format = true +docstring-code-line-length = "dynamic" + +[tool.mypy] +python_version = "3.13" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +check_untyped_defs = true +no_implicit_optional = true +warn_unused_ignores = true +strict_equality = true +mypy_path = ["src"] + +[[tool.mypy.overrides]] +module = "tests.*" +disallow_untyped_defs = false + +[[tool.mypy.overrides]] +module = ["dhis2eo.*", "dhis2_client.*", "pygeoapi.*", "titiler.*"] +ignore_missing_imports = true + +[tool.pyright] +include = ["src", "tests"] +exclude = ["**/.venv"] +pythonVersion = "3.13" +typeCheckingMode = "strict" +useLibraryCodeForTypes = true +reportPrivateUsage = false +reportUnusedFunction = false +reportUnknownMemberType = false +reportUnknownArgumentType = false +reportUnknownParameterType = false +reportUnknownVariableType = false +reportMissingTypeArgument = false +reportMissingTypeStubs = false +reportUnknownLambdaType = false +reportMissingImports = "warning" +reportMissingModuleSource = false + +[dependency-groups] +dev = [ + "httpx>=0.28.0", + "mypy>=1.19.1", + "pyright>=1.1.408", + "pytest>=8.0.0", + "ruff>=0.15.2", +] diff --git a/src/eo_api/__init__.py b/src/eo_api/__init__.py new file mode 100644 index 0000000..b18cf39 --- /dev/null +++ b/src/eo_api/__init__.py @@ -0,0 +1,6 @@ +try: + from importlib.metadata import version as _get_version + + __version__ = _get_version("eo-api") +except Exception: + __version__ = "unknown" diff --git a/src/eo_api/main.py b/src/eo_api/main.py new file mode 100644 index 0000000..a2c92e6 --- /dev/null +++ b/src/eo_api/main.py @@ -0,0 +1,28 @@ +"""DHIS2 EO API - Earth observation data API for DHIS2. + +load_dotenv() is called before pygeoapi import because pygeoapi +reads PYGEOAPI_CONFIG and PYGEOAPI_OPENAPI at import time. +""" + +from dotenv import load_dotenv + +load_dotenv() + +from fastapi import FastAPI # noqa: E402 +from fastapi.middleware.cors import CORSMiddleware # noqa: E402 + +from eo_api.routers import cog, ogcapi, root # noqa: E402 + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(root.router) +app.include_router(cog.router, prefix="/cog", tags=["Cloud Optimized GeoTIFF"]) +app.mount(path="/ogcapi", app=ogcapi.app) diff --git a/plugins/__init__.py b/src/eo_api/routers/__init__.py similarity index 100% rename from plugins/__init__.py rename to src/eo_api/routers/__init__.py diff --git a/src/eo_api/routers/cog.py b/src/eo_api/routers/cog.py new file mode 100644 index 0000000..36cd858 --- /dev/null +++ b/src/eo_api/routers/cog.py @@ -0,0 +1,19 @@ +"""Cloud Optimized GeoTIFF (COG) endpoints powered by titiler.""" + +from titiler.core.factory import TilerFactory + +cog = TilerFactory( + # router_prefix should match the prefix used in app.include_router() + router_prefix="/cog", + # Endpoints to register (all True by default except add_ogc_maps) + add_preview=True, + add_part=True, + add_viewer=True, + # GDAL environment variables applied to every request + # environment_dependency=lambda: { + # "GDAL_DISABLE_READDIR_ON_OPEN": "EMPTY_DIR", + # "GDAL_HTTP_MERGE_CONSECUTIVE_RANGES": "YES", + # }, +) + +router = cog.router diff --git a/src/eo_api/routers/ogcapi/__init__.py b/src/eo_api/routers/ogcapi/__init__.py new file mode 100644 index 0000000..460e4f5 --- /dev/null +++ b/src/eo_api/routers/ogcapi/__init__.py @@ -0,0 +1,48 @@ +"""OGC API endpoints (pygeoapi). + +Unlike titiler (configured in Python), pygeoapi is almost entirely +YAML-config-driven. The config file is located via the ``PYGEOAPI_CONFIG`` +environment variable and controls: + +- **resources** -- datasets exposed as OGC API collections. + Each resource declares a type (feature, coverage, map, process) and a + provider that handles the backend I/O (e.g. Elasticsearch, PostGIS, + rasterio, xarray). +- **server settings** -- gzip compression, CORS headers, response limits, + language negotiation, and the optional admin API. +- **metadata** -- service identification, contact info, and license. +- **API rules** -- URL path encoding, property inclusion/exclusion, and + custom API behaviour overrides. + +Adding or changing datasets therefore means editing the YAML file, not +this module. + +References: +---------- +- Configuration guide: https://docs.pygeoapi.io/en/latest/configuration.html +- Data publishing: https://docs.pygeoapi.io/en/latest/data-publishing/ +""" + +import logging + +from pygeoapi.starlette_app import APP as pygeoapi_app +from pygeoapi.starlette_app import CONFIG + +from eo_api.routers.ogcapi.plugins.dhis2_common import fetch_bbox + +logger = logging.getLogger(__name__) + +try: + bbox = fetch_bbox() + if bbox is not None: + CONFIG["resources"]["dhis2-org-units"]["extents"]["spatial"]["bbox"] = [bbox] + CONFIG["resources"]["dhis2-org-units-cql"]["extents"]["spatial"]["bbox"] = [bbox] + logger.info("DHIS2 org-units bbox set to %s", bbox) + else: + logger.info("No level-1 org unit geometry found, skipping bbox") +except Exception: + logger.warning("Failed to fetch DHIS2 bbox, using config default", exc_info=True) + +# pygeoapi exposes a ready-made Starlette app; we re-export it so the +# main application can mount it with app.mount(). +app = pygeoapi_app diff --git a/src/eo_api/routers/ogcapi/plugins/__init__.py b/src/eo_api/routers/ogcapi/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/eo_api/routers/ogcapi/plugins/dhis2_common.py b/src/eo_api/routers/ogcapi/plugins/dhis2_common.py new file mode 100644 index 0000000..2f101fb --- /dev/null +++ b/src/eo_api/routers/ogcapi/plugins/dhis2_common.py @@ -0,0 +1,142 @@ +"""Shared DHIS2 models, constants, and helpers for org-unit providers.""" + +import os +from datetime import datetime +from typing import Any + +import httpx +from geojson_pydantic import Feature +from geojson_pydantic.geometries import Geometry +from pydantic import BaseModel, Field + +DHIS2_BASE_URL = os.environ["DHIS2_BASE_URL"] +DHIS2_AUTH = (os.environ["DHIS2_USERNAME"], os.environ["DHIS2_PASSWORD"]) +DHIS2_FIELDS = "id,code,name,shortName,level,openingDate,geometry" + + +class DHIS2OrgUnit(BaseModel): + """Organisation unit as returned by the DHIS2 API.""" + + id: str + name: str | None = None + code: str | None = None + shortName: str | None = None + level: int | None = None + openingDate: datetime | None = None + geometry: Geometry | None = None + + +class OrgUnitProperties(BaseModel): + """Feature properties for a DHIS2 org unit.""" + + name: str | None = Field(None, title="Name") + code: str | None = Field(None, title="Code") + shortName: str | None = Field(None, title="Short name") + level: int | None = Field(None, title="Level") + openingDate: str | None = Field(None, title="Opening date") + + +def schema_to_fields(model: type[BaseModel]) -> dict[str, dict[str, str]]: + """Convert a Pydantic model's JSON Schema to pygeoapi field definitions.""" + schema = model.model_json_schema() + fields: dict[str, dict[str, str]] = {} + for name, prop in schema["properties"].items(): + if "anyOf" in prop: + types = [t for t in prop["anyOf"] if t.get("type") != "null"] + field_type = types[0]["type"] if types else "string" + else: + field_type = prop.get("type", "string") + field_def: dict[str, str] = {"type": field_type} + if "title" in prop: + field_def["title"] = prop["title"] + fields[name] = field_def + return fields + + +def flatten_coords(coords: list) -> list[list[float]]: + """Recursively flatten nested coordinate arrays into a list of [x, y] points.""" + if coords and isinstance(coords[0], (int, float)): + return [coords] + result: list[list[float]] = [] + for item in coords: + result.extend(flatten_coords(item)) + return result + + +def compute_bbox(geometry: Geometry) -> tuple[float, float, float, float]: + """Compute bounding box from a GeoJSON geometry.""" + coords = flatten_coords(geometry.model_dump()["coordinates"]) + xs = [c[0] for c in coords] + ys = [c[1] for c in coords] + return (min(xs), min(ys), max(xs), max(ys)) + + +def fetch_bbox() -> list[float] | None: + """Compute bounding box from level-1 org unit geometries.""" + response = httpx.get( + f"{DHIS2_BASE_URL}/organisationUnits", + params={ + "paging": "false", + "fields": "geometry", + "filter": "level:eq:1", + }, + auth=DHIS2_AUTH, + follow_redirects=True, + ) + response.raise_for_status() + all_coords: list[list[float]] = [] + for ou in response.json()["organisationUnits"]: + geom = ou.get("geometry") + if geom and geom.get("coordinates"): + all_coords.extend(flatten_coords(geom["coordinates"])) + if not all_coords: + return None + xs = [c[0] for c in all_coords] + ys = [c[1] for c in all_coords] + return [min(xs), min(ys), max(xs), max(ys)] + + +def fetch_org_units() -> list[DHIS2OrgUnit]: + """Fetch all organisation units from the DHIS2 API.""" + response = httpx.get( + f"{DHIS2_BASE_URL}/organisationUnits", + params={"paging": "false", "fields": DHIS2_FIELDS}, + auth=DHIS2_AUTH, + follow_redirects=True, + ) + response.raise_for_status() + return [DHIS2OrgUnit.model_validate(ou) for ou in response.json()["organisationUnits"]] + + +def org_unit_to_feature(org_unit: DHIS2OrgUnit) -> Feature: + """Convert a DHIS2 org unit to a GeoJSON Feature.""" + props = OrgUnitProperties( + name=org_unit.name, + code=org_unit.code, + shortName=org_unit.shortName, + level=org_unit.level, + openingDate=org_unit.openingDate.isoformat() if org_unit.openingDate else None, + ) + bbox = None + if org_unit.geometry and org_unit.geometry.type in ("Polygon", "MultiPolygon"): + bbox = compute_bbox(org_unit.geometry) + return Feature( + type="Feature", + id=org_unit.id, + geometry=org_unit.geometry, + properties=props.model_dump(), + bbox=bbox, + ) + + +def get_single_org_unit(identifier: str) -> dict[str, Any]: + """Fetch a single org unit by ID and return as a feature dict.""" + response = httpx.get( + f"{DHIS2_BASE_URL}/organisationUnits/{identifier}", + params={"fields": DHIS2_FIELDS}, + auth=DHIS2_AUTH, + follow_redirects=True, + ) + response.raise_for_status() + org_unit = DHIS2OrgUnit.model_validate(response.json()) + return org_unit_to_feature(org_unit).model_dump() diff --git a/src/eo_api/routers/ogcapi/plugins/dhis2_org_units.py b/src/eo_api/routers/ogcapi/plugins/dhis2_org_units.py new file mode 100644 index 0000000..3c78e05 --- /dev/null +++ b/src/eo_api/routers/ogcapi/plugins/dhis2_org_units.py @@ -0,0 +1,67 @@ +"""DHIS2 Organization Units feature provider for pygeoapi.""" + +from typing import Any + +from geojson_pydantic import FeatureCollection +from pygeoapi.provider.base import BaseProvider, SchemaType + +from eo_api.routers.ogcapi.plugins.dhis2_common import ( + OrgUnitProperties, + fetch_org_units, + get_single_org_unit, + org_unit_to_feature, + schema_to_fields, +) + + +class DHIS2OrgUnitsProvider(BaseProvider): + """DHIS2 Organization Units Provider.""" + + def __init__(self, provider_def: dict[str, Any]) -> None: + """Inherit from parent class.""" + super().__init__(provider_def) + self.get_fields() + + def get_fields(self) -> dict[str, dict[str, str]]: + """Return fields and their datatypes.""" + if not self._fields: + self._fields = schema_to_fields(OrgUnitProperties) + return self._fields + + def get(self, identifier: str, **kwargs: Any) -> dict[str, Any]: + """Return a single feature by identifier.""" + return get_single_org_unit(identifier) + + def query( + self, + offset: int = 0, + limit: int = 10, + resulttype: str = "results", + bbox: list[float] | None = None, + datetime_: str | None = None, + properties: list[str] | None = None, + sortby: list[str] | None = None, + select_properties: list[str] | None = None, + skip_geometry: bool = False, + **kwargs: Any, + ) -> dict[str, Any]: + """Return feature collection matching the query parameters.""" + org_units = fetch_org_units() + number_matched = len(org_units) + page = org_units[offset : offset + limit] + + fc = FeatureCollection( + type="FeatureCollection", + features=[org_unit_to_feature(ou) for ou in page], + ) + result = fc.model_dump() + result["numberMatched"] = number_matched + result["numberReturned"] = len(page) + return result + + def get_schema(self, schema_type: SchemaType = SchemaType.item) -> tuple[str, dict[str, Any]]: + """Return a JSON schema for the provider.""" + return ( + "application/geo+json", + {"$ref": "https://geojson.org/schema/Feature.json"}, + ) diff --git a/src/eo_api/routers/ogcapi/plugins/dhis2_org_units_cql.py b/src/eo_api/routers/ogcapi/plugins/dhis2_org_units_cql.py new file mode 100644 index 0000000..68cbf78 --- /dev/null +++ b/src/eo_api/routers/ogcapi/plugins/dhis2_org_units_cql.py @@ -0,0 +1,73 @@ +"""DHIS2 Organization Units feature provider with CQL filter support.""" + +from typing import Any + +from pygeoapi.provider.base import BaseProvider, SchemaType +from pygeofilter.backends.native.evaluate import NativeEvaluator + +from eo_api.routers.ogcapi.plugins.dhis2_common import ( + OrgUnitProperties, + fetch_org_units, + get_single_org_unit, + org_unit_to_feature, + schema_to_fields, +) + + +class DHIS2OrgUnitsCqlProvider(BaseProvider): + """DHIS2 Organization Units Provider with CQL filter support.""" + + def __init__(self, provider_def: dict[str, Any]) -> None: + """Inherit from parent class.""" + super().__init__(provider_def) + self.get_fields() + + def get_fields(self) -> dict[str, dict[str, str]]: + """Return fields and their datatypes.""" + if not self._fields: + self._fields = schema_to_fields(OrgUnitProperties) + return self._fields + + def get(self, identifier: str, **kwargs: Any) -> dict[str, Any]: + """Return a single feature by identifier.""" + return get_single_org_unit(identifier) + + def query( + self, + offset: int = 0, + limit: int = 10, + resulttype: str = "results", + bbox: list[float] | None = None, + datetime_: str | None = None, + properties: list[str] | None = None, + sortby: list[str] | None = None, + select_properties: list[str] | None = None, + skip_geometry: bool = False, + filterq: list | None = None, + **kwargs: Any, + ) -> dict[str, Any]: + """Return feature collection matching the query parameters.""" + org_units = fetch_org_units() + features = [org_unit_to_feature(ou).model_dump() for ou in org_units] + + if filterq: + evaluator = NativeEvaluator(use_getattr=False) + match = evaluator.evaluate(filterq) + features = [f for f in features if match(f["properties"])] + + number_matched = len(features) + page = features[offset : offset + limit] + + return { + "type": "FeatureCollection", + "features": page, + "numberMatched": number_matched, + "numberReturned": len(page), + } + + def get_schema(self, schema_type: SchemaType = SchemaType.item) -> tuple[str, dict[str, Any]]: + """Return a JSON schema for the provider.""" + return ( + "application/geo+json", + {"$ref": "https://geojson.org/schema/Feature.json"}, + ) diff --git a/src/eo_api/routers/ogcapi/plugins/dhis2eo.py b/src/eo_api/routers/ogcapi/plugins/dhis2eo.py new file mode 100644 index 0000000..3153f24 --- /dev/null +++ b/src/eo_api/routers/ogcapi/plugins/dhis2eo.py @@ -0,0 +1,43 @@ +"""DHIS2 EO EDR provider for pygeoapi.""" + +from typing import Any + +from pygeoapi.provider.base_edr import BaseEDRProvider + + +class DHIS2EOProvider(BaseEDRProvider): + """Minimal EDR provider example.""" + + def get_fields(self) -> dict[str, dict[str, Any]]: + """Return available fields.""" + return { + "value": { + "type": "number", + "title": "Value", + "x-ogc-unit": "mm/day", + } + } + + def position(self, **kwargs: Any) -> dict[str, Any]: + """Return coverage data for a point position.""" + return { + "type": "Coverage", + "domain": { + "type": "Domain", + "domainType": "Point", + "axes": { + "Long": {"values": [0.0]}, + "Lat": {"values": [0.0]}, + }, + }, + "parameters": {"value": {"type": "Parameter"}}, + "ranges": { + "value": { + "type": "NdArray", + "dataType": "float", + "axisNames": ["Long", "Lat"], + "shape": [1, 1], + "values": [10.0], + } + }, + } diff --git a/src/eo_api/routers/root.py b/src/eo_api/routers/root.py new file mode 100644 index 0000000..0d4cc5f --- /dev/null +++ b/src/eo_api/routers/root.py @@ -0,0 +1,34 @@ +"""Root API endpoints.""" + +import sys +from importlib.metadata import version + +from fastapi import APIRouter + +from eo_api.schemas import AppInfo, HealthStatus, Status, StatusMessage + +router = APIRouter(tags=["System"]) + + +@router.get("/") +def read_index() -> StatusMessage: + """Return a welcome message for the root endpoint.""" + return StatusMessage(message="Welcome to DHIS2 EO API") + + +@router.get("/health") +def health() -> HealthStatus: + """Return health status for container health checks.""" + return HealthStatus(status=Status.HEALTHY) + + +@router.get("/info") +def info() -> AppInfo: + """Return application version and environment info.""" + return AppInfo( + app_version=version("eo-api"), + python_version=sys.version, + titiler_version=version("titiler.core"), + pygeoapi_version=version("pygeoapi"), + uvicorn_version=version("uvicorn"), + ) diff --git a/src/eo_api/schemas.py b/src/eo_api/schemas.py new file mode 100644 index 0000000..39c0402 --- /dev/null +++ b/src/eo_api/schemas.py @@ -0,0 +1,35 @@ +"""Pydantic response models.""" + +from enum import StrEnum + +from pydantic import BaseModel + + +class StatusMessage(BaseModel): + """Simple status message response.""" + + message: str + + +class Status(StrEnum): + """Health check status values.""" + + HEALTHY = "healthy" + DEGRADED = "degraded" + UNHEALTHY = "unhealthy" + + +class HealthStatus(BaseModel): + """Health check response.""" + + status: Status + + +class AppInfo(BaseModel): + """Application version and environment info.""" + + app_version: str + python_version: str + titiler_version: str + pygeoapi_version: str + uvicorn_version: str diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..08eb7de --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +import pytest +from fastapi.testclient import TestClient + +from eo_api.main import app + + +@pytest.fixture +def client(): + return TestClient(app) diff --git a/tests/test_root.py b/tests/test_root.py new file mode 100644 index 0000000..e45752c --- /dev/null +++ b/tests/test_root.py @@ -0,0 +1,23 @@ +from eo_api.schemas import HealthStatus, StatusMessage + + +def test_root_returns_200(client): + response = client.get("/") + assert response.status_code == 200 + + +def test_root_returns_welcome_message(client): + response = client.get("/") + result = StatusMessage.model_validate(response.json()) + assert result.message == "Welcome to DHIS2 EO API" + + +def test_health_returns_200(client): + response = client.get("/health") + assert response.status_code == 200 + + +def test_health_returns_healthy_status(client): + response = client.get("/health") + result = HealthStatus.model_validate(response.json()) + assert result.status == "healthy" diff --git a/uv.lock b/uv.lock index eb02787..4af86c3 100644 --- a/uv.lock +++ b/uv.lock @@ -581,26 +581,48 @@ wheels = [ [[package]] name = "eo-api" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "dhis2-client" }, { name = "dhis2eo" }, + { name = "geojson-pydantic" }, + { name = "httpx" }, { name = "pygeoapi" }, { name = "python-dotenv" }, { name = "titiler-core" }, { name = "uvicorn" }, ] +[package.dev-dependencies] +dev = [ + { name = "httpx" }, + { name = "mypy" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "ruff" }, +] + [package.metadata] requires-dist = [ { name = "dhis2-client", git = "https://github.com/dhis2/dhis2-python-client.git?rev=V0.3.0" }, { name = "dhis2eo", git = "https://github.com/dhis2/dhis2eo.git?rev=v1.1.0" }, + { name = "geojson-pydantic", specifier = ">=2.1.0" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "pygeoapi", specifier = ">=0.22.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "titiler-core", specifier = ">=1.2.0" }, { name = "uvicorn", specifier = ">=0.41.0" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "httpx", specifier = ">=0.28.0" }, + { name = "mypy", specifier = ">=1.19.1" }, + { name = "pyright", specifier = ">=1.1.408" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "ruff", specifier = ">=0.15.2" }, +] + [[package]] name = "fastapi" version = "0.131.0" @@ -824,6 +846,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -940,6 +971,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, ] +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + [[package]] name = "locket" version = "1.0.0" @@ -1136,6 +1214,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/cf/be4e93afbfa0def2cd6fac9302071db0bd6d0617999ecbf53f92b9398de3/multiurl-0.3.7-py3-none-any.whl", hash = "sha256:054f42974064f103be0ed55b43f0c32fc435a47dc7353a9adaffa643b99fa380", size = 21524, upload-time = "2025-07-29T11:57:03.191Z" }, ] +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "netcdf4" version = "1.7.4" @@ -1163,6 +1277,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/2e/39d5e9179c543f2e6e149a65908f83afd9b6d64379a90789b323111761db/netcdf4-1.7.4-cp314-cp314t-win_arm64.whl", hash = "sha256:034220887d48da032cb2db5958f69759dbb04eb33e279ec6390571d4aea734fe", size = 2531682, upload-time = "2026-01-05T02:27:37.062Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "numexpr" version = "2.14.1" @@ -1318,6 +1441,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, ] +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + [[package]] name = "pdbufr" version = "0.14.1" @@ -1415,6 +1547,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1548,6 +1689,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c3/7f/c803c39fa76fe055bc4154fb6e897185ad21946820a2227283e0a20eeb35/pygeoif-1.6.0-py3-none-any.whl", hash = "sha256:02f84807dadbaf1941c4bb2a9ef1ebac99b1b0404597d2602efdbb58910c69c9", size = 27976, upload-time = "2025-10-01T10:02:12.19Z" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + [[package]] name = "pymeeus" version = "0.5.12" @@ -1647,6 +1797,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" }, ] +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + [[package]] name = "pyshp" version = "3.0.3" @@ -1668,6 +1831,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/b4/a9430e72bfc3c458e1fcf8363890994e483052ab052ed93912be4e5b32c8/pystac-1.14.3-py3-none-any.whl", hash = "sha256:2f60005f521d541fb801428307098f223c14697b3faf4d2f0209afb6a43f39e5", size = 208506, upload-time = "2026-01-09T12:38:40.721Z" }, ] +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1979,6 +2158,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, ] +[[package]] +name = "ruff" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/04/eab13a954e763b0606f460443fcbf6bb5a0faf06890ea3754ff16523dce5/ruff-0.15.2.tar.gz", hash = "sha256:14b965afee0969e68bb871eba625343b8673375f457af4abe98553e8bbb98342", size = 4558148, upload-time = "2026-02-19T22:32:20.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/70/3a4dc6d09b13cb3e695f28307e5d889b2e1a66b7af9c5e257e796695b0e6/ruff-0.15.2-py3-none-linux_armv6l.whl", hash = "sha256:120691a6fdae2f16d65435648160f5b81a9625288f75544dc40637436b5d3c0d", size = 10430565, upload-time = "2026-02-19T22:32:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/bb8457b56185ece1305c666dc895832946d24055be90692381c31d57466d/ruff-0.15.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a89056d831256099658b6bba4037ac6dd06f49d194199215befe2bb10457ea5e", size = 10820354, upload-time = "2026-02-19T22:32:07.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/e0532d7f9c9e0b14c46f61b14afd563298b8b83f337b6789ddd987e46121/ruff-0.15.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e36dee3a64be0ebd23c86ffa3aa3fd3ac9a712ff295e192243f814a830b6bd87", size = 10170767, upload-time = "2026-02-19T22:32:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/da1aa341d3af017a21c7a62fb5ec31d4e7ad0a93ab80e3a508316efbcb23/ruff-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9fb47b6d9764677f8c0a193c0943ce9a05d6763523f132325af8a858eadc2b9", size = 10529591, upload-time = "2026-02-19T22:32:02.547Z" }, + { url = "https://files.pythonhosted.org/packages/93/74/184fbf38e9f3510231fbc5e437e808f0b48c42d1df9434b208821efcd8d6/ruff-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f376990f9d0d6442ea9014b19621d8f2aaf2b8e39fdbfc79220b7f0c596c9b80", size = 10260771, upload-time = "2026-02-19T22:32:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/05/ac/605c20b8e059a0bc4b42360414baa4892ff278cec1c91fff4be0dceedefd/ruff-0.15.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dcc987551952d73cbf5c88d9fdee815618d497e4df86cd4c4824cc59d5dd75f", size = 11045791, upload-time = "2026-02-19T22:32:31.642Z" }, + { url = "https://files.pythonhosted.org/packages/fd/52/db6e419908f45a894924d410ac77d64bdd98ff86901d833364251bd08e22/ruff-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42a47fd785cbe8c01b9ff45031af875d101b040ad8f4de7bbb716487c74c9a77", size = 11879271, upload-time = "2026-02-19T22:32:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d8/7992b18f2008bdc9231d0f10b16df7dda964dbf639e2b8b4c1b4e91b83af/ruff-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe9f49354866e575b4c6943856989f966421870e85cd2ac94dccb0a9dcb2fea", size = 11303707, upload-time = "2026-02-19T22:32:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/d7/02/849b46184bcfdd4b64cde61752cc9a146c54759ed036edd11857e9b8443b/ruff-0.15.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7a672c82b5f9887576087d97be5ce439f04bbaf548ee987b92d3a7dede41d3a", size = 11149151, upload-time = "2026-02-19T22:32:44.234Z" }, + { url = "https://files.pythonhosted.org/packages/70/04/f5284e388bab60d1d3b99614a5a9aeb03e0f333847e2429bebd2aaa1feec/ruff-0.15.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ecc64f46f7019e2bcc3cdc05d4a7da958b629a5ab7033195e11a438403d956", size = 11091132, upload-time = "2026-02-19T22:32:24.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ae/88d844a21110e14d92cf73d57363fab59b727ebeabe78009b9ccb23500af/ruff-0.15.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8dcf243b15b561c655c1ef2f2b0050e5d50db37fe90115507f6ff37d865dc8b4", size = 10504717, upload-time = "2026-02-19T22:32:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/64/27/867076a6ada7f2b9c8292884ab44d08fd2ba71bd2b5364d4136f3cd537e1/ruff-0.15.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dab6941c862c05739774677c6273166d2510d254dac0695c0e3f5efa1b5585de", size = 10263122, upload-time = "2026-02-19T22:32:10.036Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ef/faf9321d550f8ebf0c6373696e70d1758e20ccdc3951ad7af00c0956be7c/ruff-0.15.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b9164f57fc36058e9a6806eb92af185b0697c9fe4c7c52caa431c6554521e5c", size = 10735295, upload-time = "2026-02-19T22:32:39.227Z" }, + { url = "https://files.pythonhosted.org/packages/2f/55/e8089fec62e050ba84d71b70e7834b97709ca9b7aba10c1a0b196e493f97/ruff-0.15.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80d24fcae24d42659db7e335b9e1531697a7102c19185b8dc4a028b952865fd8", size = 11241641, upload-time = "2026-02-19T22:32:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/01/1c30526460f4d23222d0fabd5888868262fd0e2b71a00570ca26483cd993/ruff-0.15.2-py3-none-win32.whl", hash = "sha256:fd5ff9e5f519a7e1bd99cbe8daa324010a74f5e2ebc97c6242c08f26f3714f6f", size = 10507885, upload-time = "2026-02-19T22:32:15.635Z" }, + { url = "https://files.pythonhosted.org/packages/5c/10/3d18e3bbdf8fc50bbb4ac3cc45970aa5a9753c5cb51bf9ed9a3cd8b79fa3/ruff-0.15.2-py3-none-win_amd64.whl", hash = "sha256:d20014e3dfa400f3ff84830dfb5755ece2de45ab62ecea4af6b7262d0fb4f7c5", size = 11623725, upload-time = "2026-02-19T22:32:04.947Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/097c0798b1dab9f8affe73da9642bb4500e098cb27fd8dc9724816ac747b/ruff-0.15.2-py3-none-win_arm64.whl", hash = "sha256:cabddc5822acdc8f7b5527b36ceac55cc51eec7b1946e60181de8fe83ca8876e", size = 10941649, upload-time = "2026-02-19T22:32:18.108Z" }, +] + [[package]] name = "shapely" version = "2.1.2"