From 3737d7f8480fdf5edf2c0b4d200d5c5645b5d323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Mon, 16 Mar 2026 08:49:13 +0100 Subject: [PATCH 1/9] OGC API Testing --- pygeoapi-config.yml | 136 +++++ pygeoapi-openapi.yml | 493 ++++++++++++++++++ pyproject.toml | 1 + src/eo_api/main.py | 9 + src/eo_api/ogc_api/__init__.py | 6 + src/eo_api/ogc_api/plugins/__init__.py | 1 + .../ogc_api/plugins/providers/__init__.py | 1 + .../ogc_api/plugins/providers/titiler.py | 216 ++++++++ .../plugins/providers/titiler_custom.py | 60 +++ src/eo_api/startup.py | 41 ++ src/eo_api/tiles/__init__.py | 5 + src/eo_api/tiles/titiler.py | 11 + uv.lock | 65 +++ 13 files changed, 1045 insertions(+) create mode 100644 pygeoapi-config.yml create mode 100644 pygeoapi-openapi.yml create mode 100644 src/eo_api/ogc_api/__init__.py create mode 100644 src/eo_api/ogc_api/plugins/__init__.py create mode 100644 src/eo_api/ogc_api/plugins/providers/__init__.py create mode 100644 src/eo_api/ogc_api/plugins/providers/titiler.py create mode 100644 src/eo_api/ogc_api/plugins/providers/titiler_custom.py create mode 100644 src/eo_api/tiles/__init__.py create mode 100644 src/eo_api/tiles/titiler.py diff --git a/pygeoapi-config.yml b/pygeoapi-config.yml new file mode 100644 index 00000000..c4cd224c --- /dev/null +++ b/pygeoapi-config.yml @@ -0,0 +1,136 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2025 Tom Kralidis +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation +# files (the "Software"), to deal in the Software without +# restriction, including without limitation the rights to use, +# copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following +# conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# ================================================================= + +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 + gzip: false + languages: + # First language is the default language + - en-US + - fr-CA + # cors: true + pretty_print: true + limits: + default_items: 20 + max_items: 50 + # templates: + # path: /path/to/Jinja2/templates + # static: /path/to/static/folder # css/js/img + map: + url: https://tile.openstreetmap.org/{z}/{x}/{y}.png + attribution: '© OpenStreetMap contributors' + manager: + name: TinyDB + connection: /tmp/pygeoapi-process-manager.db + output_dir: /tmp/ + # ogc_schemas_location: /opt/schemas.opengis.net + admin: false # enable admin api + +logging: + level: ERROR + #logfile: /tmp/pygeoapi.log + +metadata: + identification: + title: + en: DHIS2 EO API + description: + en: OGC API compliant geospatial data API + keywords: + en: + - geospatial + - data + - api + keywords_type: theme + terms_of_service: https://creativecommons.org/licenses/by/4.0/ + url: https://example.org + license: + name: CC-BY 4.0 license + url: https://creativecommons.org/licenses/by/4.0/ + provider: + name: DHIS2 EO API + url: https://dhis2.org + contact: + name: DHIS2 Climate Team + email: climate@dhis2.org + role: pointOfContact + +resources: + worldpop_population_yearly: + type: collection + title: Population density + description: Population density + keywords: + - population + extents: + spatial: + bbox: [-13.3035, 6.9176, -10.2658, 10.0004] + crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 + temporal: + begin: 2015-01-01T00:00:00Z + end: 2030-12-31T23:59:59Z + trs: http://www.opengis.net/def/uom/ISO-8601/0/Gregorian + resolution: P1Y + default: 2026-01-01T00:00:00Z + links: + - type: text/html + rel: canonical + title: information + href: https://hub.worldpop.org/geodata/summary?id=75360 + hreflang: en-EN + providers: + - type: coverage + name: xarray + data: data/downloads/worldpop_population_yearly.zarr + storage_crs: http://www.opengis.net/def/crs/EPSG/0/4326 + x_field: x + y_field: y + time_field: time + format: + name: zarr + mimetype: application/zip + options: + zarr: + consolidated: true + squeeze: true + - type: tile + name: eo_api.ogc_api.plugins.providers.titiler.TiTilerProvider + data: data/downloads/worldpop_population_yearly.zarr + format: + name: png + mimetype: image/png + options: + scheme: WebMercatorQuad + endpoint: /zarr/tiles + variable: pop_total diff --git a/pygeoapi-openapi.yml b/pygeoapi-openapi.yml new file mode 100644 index 00000000..7899187a --- /dev/null +++ b/pygeoapi-openapi.yml @@ -0,0 +1,493 @@ +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: worldpop_population_yearly + 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/worldpop_population_yearly: + get: + description: Population density + operationId: describeWorldpop_population_yearlyCollection + 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 Population density metadata + tags: + - worldpop_population_yearly + /collections/worldpop_population_yearly/coverage: + get: + description: Population density + operationId: getWorldpop_population_yearlyCoverage + 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 Population density coverage + tags: + - worldpop_population_yearly + /collections/worldpop_population_yearly/tiles: + get: + description: Population density + operationId: describeWorldpop_population_yearly.collection.map.getTileSetsList + parameters: + - $ref: '#/components/parameters/f' + - $ref: '#/components/parameters/lang' + responses: + '200': + $ref: '#/components/responses/Tiles' + '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: Fetch a Population density tiles description + tags: + - worldpop_population_yearly + /collections/worldpop_population_yearly/tiles/{tileMatrixSetId}/{tileMatrix}/{tileRow}/{tileCol}: + get: + description: Population density + operationId: getWorldpop_population_yearly.collection.map.getTile + parameters: + - $ref: https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml#/components/parameters/tileMatrixSetId + - $ref: https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml#/components/parameters/tileMatrix + - $ref: https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml#/components/parameters/tileRow + - $ref: https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml#/components/parameters/tileCol + - description: The optional f parameter indicates the output format which the + server shall provide as part of the response document. + explode: false + in: query + name: f + required: false + schema: + default: png + enum: + - png + type: string + style: form + responses: + '200': + content: + image/png: + schema: + format: binary + type: string + description: successful operation + '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 a Population density tile + tags: + - worldpop_population_yearly + /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: + - &id001 + 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: + - *id001 + - $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: + - *id001 + - $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 +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: Population density + name: worldpop_population_yearly +- 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 d319dfdd..37568a8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ version = "0.1.0" requires-python = ">=3.13" dependencies = [ "titiler-core>=1.2.0", + "titiler-xarray>=0.22.0", "uvicorn>=0.41.0", "python-dotenv>=1.0.1", "pygeoapi>=0.22.0", diff --git a/src/eo_api/main.py b/src/eo_api/main.py index e12ab581..237eefcc 100644 --- a/src/eo_api/main.py +++ b/src/eo_api/main.py @@ -1,10 +1,13 @@ """DHIS2 EO API -- Earth observation data API for DHIS2.""" from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware import eo_api.startup # noqa: F401 # pyright: ignore[reportUnusedImport] from eo_api import data_accessor, data_manager, data_registry, system +from eo_api.ogc_api import ogc_api_app +from eo_api.tiles import tiles_router app = FastAPI() @@ -20,3 +23,9 @@ app.include_router(data_registry.routes.router, prefix='/registry', tags=['Data registry']) app.include_router(data_manager.routes.router, prefix='/manage', tags=['Data manager']) app.include_router(data_accessor.routes.router, prefix='/retrieve', tags=['Data retrieval']) +app.include_router(tiles_router, prefix='/zarr', tags=['Zarr']) + +app.mount("/data", StaticFiles(directory="data/downloads"), name="Data") + +# mount all pygeoapi endpoints to /ogcapi +app.mount(path="/ogcapi", app=ogc_api_app) diff --git a/src/eo_api/ogc_api/__init__.py b/src/eo_api/ogc_api/__init__.py new file mode 100644 index 00000000..25e5d868 --- /dev/null +++ b/src/eo_api/ogc_api/__init__.py @@ -0,0 +1,6 @@ +"""OGC API integration wiring.""" + +from pygeoapi.starlette_app import APP as ogc_api_app + +__all__ = ["ogc_api_app"] + diff --git a/src/eo_api/ogc_api/plugins/__init__.py b/src/eo_api/ogc_api/plugins/__init__.py new file mode 100644 index 00000000..221656ac --- /dev/null +++ b/src/eo_api/ogc_api/plugins/__init__.py @@ -0,0 +1 @@ +"""Custom pygeoapi plugin package for eo_api.""" diff --git a/src/eo_api/ogc_api/plugins/providers/__init__.py b/src/eo_api/ogc_api/plugins/providers/__init__.py new file mode 100644 index 00000000..fb614f98 --- /dev/null +++ b/src/eo_api/ogc_api/plugins/providers/__init__.py @@ -0,0 +1 @@ +"""Custom pygeoapi provider plugins for eo_api.""" diff --git a/src/eo_api/ogc_api/plugins/providers/titiler.py b/src/eo_api/ogc_api/plugins/providers/titiler.py new file mode 100644 index 00000000..adb3d306 --- /dev/null +++ b/src/eo_api/ogc_api/plugins/providers/titiler.py @@ -0,0 +1,216 @@ +"""TiTiler Zarr tile provider plugin for pygeoapi.""" + +from __future__ import annotations + +import base64 +import os +from pathlib import Path +from typing import Any, cast +from urllib.parse import urlencode + +import requests +from pygeoapi.models.provider.base import TileMatrixSetEnum +from pygeoapi.provider.base import ( + ProviderConnectionError, + ProviderGenericError, + ProviderInvalidQueryError, +) +from pygeoapi.provider.tile import BaseTileProvider, ProviderTileNotFoundError +from pygeoapi.util import is_url, url_join + +_DEFAULT_TITILER_BASE_URL = "http://127.0.0.1:8000" +_DEFAULT_TITILER_ENDPOINT = "/zarr/tiles" +_DATETIME_TILESET_SEPARATOR = "~dt~" + + +class TiTilerProvider(BaseTileProvider): + """Bridge pygeoapi OGC API Tiles requests to TiTiler endpoints.""" + + def __init__(self, provider_def: dict[str, Any]) -> None: + format_name = provider_def["format"]["name"].lower() + if format_name not in {"png", "jpeg", "jpg", "webp"}: + raise RuntimeError("TiTiler format must be png, jpeg, jpg, or webp") + + options = dict(provider_def.get("options") or {}) + scheme = options.get("scheme") + schemes = options.get("schemes") + if not schemes: + options["schemes"] = [scheme] if scheme else ["WebMercatorQuad"] + + options.setdefault("endpoint", _DEFAULT_TITILER_ENDPOINT) + options.setdefault( + "endpoint_base", + os.getenv("TITILER_BASE_URL", _DEFAULT_TITILER_BASE_URL), + ) + options.setdefault("timeout", 30) + options.setdefault("datetime_param", "datetime") + options.setdefault("datetime_default", None) + + provider_def = {**provider_def, "options": options} + super().__init__(provider_def) + + self.tile_type = "raster" + self._layer = Path(self.data).stem + + def __repr__(self) -> str: + return f" {self.data}" + + def get_layer(self) -> None: + return None + + def get_fields(self) -> dict[str, Any]: + return {} + + @property + def endpoint(self) -> str: + configured = str(self.options.get("endpoint", _DEFAULT_TITILER_ENDPOINT)) + if is_url(configured): + return configured.rstrip("/") + + base = str(self.options.get("endpoint_base", _DEFAULT_TITILER_BASE_URL)).rstrip("/") + if configured.startswith("/"): + return f"{base}{configured}".rstrip("/") + return f"{base}/{configured}".rstrip("/") + + def get_tiling_schemes(self) -> list[Any]: + configured = set(self.options.get("schemes", [])) + tile_matrix_set_enum = cast(Any, TileMatrixSetEnum) + tile_matrix_set_links = [enum.value for enum in tile_matrix_set_enum if enum.value.tileMatrixSet in configured] + if not tile_matrix_set_links: + raise ProviderConnectionError("Could not identify any valid tiling scheme") + return tile_matrix_set_links + + def get_tiles_service( + self, + baseurl: str | None = None, + servicepath: str | None = None, + dirpath: str | None = None, + tile_type: str | None = None, + ) -> dict[str, list[dict[str, str]]]: + del dirpath, tile_type + + format_name = self.format_type + if servicepath is None: + servicepath = ( + "collections/{dataset}/tiles/{tileMatrixSetId}/" + "{tileMatrix}/{tileRow}/{tileCol}?f=" + f"{format_name}" + ) + + if baseurl and not servicepath.startswith("http"): + self._service_url = url_join(baseurl, servicepath) + else: + self._service_url = servicepath + + query = urlencode(self._titiler_query_params()) + default_scheme = self.options["schemes"][0] + titiler_href = ( + f"{self.endpoint}/{default_scheme}/{{tileMatrix}}/{{tileCol}}/{{tileRow}}.{format_name}?{query}" + ) + + return { + "links": [ + { + "type": self.mimetype, + "rel": "item", + "title": "This collection as image tiles", + "href": self._service_url, + }, + { + "type": self.mimetype, + "rel": "alternate", + "title": "Direct TiTiler tile URL template", + "href": titiler_href, + }, + ] + } + + def get_tiles( + self, + layer: str | None = None, + tileset: str | None = None, + z: int | None = None, + y: int | None = None, + x: int | None = None, + format_: str | None = None, + ) -> bytes | None: + del layer + + if tileset is None: + raise ProviderInvalidQueryError("Missing tileset identifier") + if z is None or y is None or x is None: + raise ProviderInvalidQueryError("Missing tile coordinates") + + normalized_tileset, datetime_ = self._parse_tileset_datetime(tileset) + + try: + z_i = int(z) + y_i = int(y) + x_i = int(x) + except (TypeError, ValueError) as err: + raise ProviderInvalidQueryError("Invalid tile coordinates") from err + + tms = self.get_tilematrixset(normalized_tileset) + if tms is None or not self.is_in_limits(tms, z_i, x_i, y_i): + raise ProviderTileNotFoundError + + tile_format = (format_ or self.format_type).lower() + if tile_format == "jpg": + tile_format = "jpeg" + + request_url = f"{self.endpoint}/{normalized_tileset}/{z_i}/{x_i}/{y_i}.{tile_format}" + + try: + response = requests.get( + request_url, + params=self._titiler_query_params(datetime_), + timeout=int(self.options.get("timeout", 30)), + ) + except requests.RequestException as exc: + raise ProviderConnectionError(str(exc)) from exc + + if response.status_code == 204: + return None + if response.status_code == 404: + raise ProviderTileNotFoundError + if response.status_code < 500 and not response.ok: + raise ProviderInvalidQueryError(response.text) + if response.status_code >= 500: + raise ProviderGenericError(response.text) + + return cast(bytes, response.content) + + def _titiler_query_params(self, datetime_: str | None = None) -> dict[str, Any]: + ignored = { + "scheme", + "schemes", + "endpoint", + "endpoint_base", + "timeout", + "datetime_param", + "datetime_default", + } + params = {"url": self.data} + params.update({k: v for k, v in self.options.items() if k not in ignored}) + effective_datetime = datetime_ if datetime_ is not None else self.options.get("datetime_default") + if effective_datetime is not None: + datetime_param = str(self.options.get("datetime_param", "datetime")) + params[datetime_param] = str(effective_datetime) + return params + + def _parse_tileset_datetime(self, tileset: str) -> tuple[str, str | None]: + if _DATETIME_TILESET_SEPARATOR not in tileset: + return tileset, None + + normalized_tileset, encoded_datetime = tileset.split(_DATETIME_TILESET_SEPARATOR, 1) + if not normalized_tileset or not encoded_datetime: + raise ProviderInvalidQueryError("Invalid tileMatrixSetId datetime encoding") + + padding = "=" * (-len(encoded_datetime) % 4) + try: + datetime_ = base64.urlsafe_b64decode(f"{encoded_datetime}{padding}").decode("utf-8") + except Exception as err: + raise ProviderInvalidQueryError("Invalid tileMatrixSetId datetime encoding") from err + + return normalized_tileset, datetime_ + diff --git a/src/eo_api/ogc_api/plugins/providers/titiler_custom.py b/src/eo_api/ogc_api/plugins/providers/titiler_custom.py new file mode 100644 index 00000000..6d8df178 --- /dev/null +++ b/src/eo_api/ogc_api/plugins/providers/titiler_custom.py @@ -0,0 +1,60 @@ +"""TiTiler provider""" + +from typing import Any +from pygeoapi.provider.tile import BaseTileProvider +from pygeoapi.models.provider.base import TileMatrixSetEnum + +class TiTilerProvider(BaseTileProvider): + """Minimal provider implementation used by pygeoapi plugin loading.""" + + def __init__(self, provider_def: dict[str, Any]) -> None: + options = dict(provider_def.get("options") or {}) + if not options.get("schemes"): + scheme = options.get("scheme") + options["schemes"] = [scheme] if scheme else ["WebMercatorQuad"] + + zoom = dict(options.get("zoom") or {}) + zoom.setdefault("min", 0) + zoom.setdefault("max", 24) + options["zoom"] = zoom + + provider_def = {**provider_def, "options": options} + + super().__init__(provider_def) + + self.tile_type = 'raster' + + def __repr__(self) -> str: + return f" {self.data}" + + def get_layer(self): + raise NotImplementedError() + + def get_tiling_schemes(self) -> list[Any]: + return [ + TileMatrixSetEnum.WEBMERCATORQUAD.value # type: ignore + ] + + def get_tiles_service( + self, + baseurl: str | None = None, + servicepath: str | None = None, + dirpath: str | None = None, + tile_type: str | None = None, + ) -> dict[str, list[dict[str, str]]]: + + return { + "links": [] + } + + def get_tiles( + self, + layer: str | None = None, + tileset: str | None = None, + z: int | None = None, + y: int | None = None, + x: int | None = None, + format_: str | None = None, + ) -> bytes | None: + return None + \ No newline at end of file diff --git a/src/eo_api/startup.py b/src/eo_api/startup.py index 1d1ba517..ab59563e 100644 --- a/src/eo_api/startup.py +++ b/src/eo_api/startup.py @@ -5,6 +5,10 @@ """ import logging +import os +import sys +from pathlib import Path + from dotenv import load_dotenv # noqa: E402 # -- Load .env (must happen before pygeoapi reads PYGEOAPI_CONFIG) ------------ @@ -19,3 +23,40 @@ handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s - %(message)s")) eo_logger.addHandler(handler) eo_logger.propagate = False + + +def _configure_proj_data() -> None: + """Configure PROJ path to match the active Python environment. + + This avoids loading an incompatible system/miniforge proj.db when rasterio + initializes CRS objects during titiler import. + """ + + candidates: list[Path] = [] + + for sys_path in sys.path: + if not sys_path: + continue + candidates.append(Path(sys_path) / "rasterio" / "proj_data") + + try: + from pyproj import datadir + + pyproj_data_dir = datadir.get_data_dir() + if pyproj_data_dir: + candidates.append(Path(pyproj_data_dir)) + except Exception: + pass + + for candidate in candidates: + if candidate.exists(): + proj_path = str(candidate) + os.environ["PROJ_LIB"] = proj_path + os.environ["PROJ_DATA"] = proj_path + eo_logger.info("Configured PROJ data directory: %s", proj_path) + return + + eo_logger.warning("Could not locate a compatible PROJ data directory in the active environment") + + +_configure_proj_data() diff --git a/src/eo_api/tiles/__init__.py b/src/eo_api/tiles/__init__.py new file mode 100644 index 00000000..d83fc515 --- /dev/null +++ b/src/eo_api/tiles/__init__.py @@ -0,0 +1,5 @@ +"""Tile routing utilities for eo_api.""" + +from eo_api.tiles.titiler import tiles_router + +__all__ = ["tiles_router"] diff --git a/src/eo_api/tiles/titiler.py b/src/eo_api/tiles/titiler.py new file mode 100644 index 00000000..86fa3fdb --- /dev/null +++ b/src/eo_api/tiles/titiler.py @@ -0,0 +1,11 @@ +"""TiTiler router definitions.""" + +from titiler.xarray.factory import TilerFactory # pyright: ignore[reportMissingImports] +from titiler.xarray.io import Reader # pyright: ignore[reportMissingImports] +from titiler.xarray.extensions import VariablesExtension # pyright: ignore[reportMissingImports] + +# Xarray-backed TiTiler endpoints (e.g. Zarr datasets). +tiles_router = TilerFactory( + reader=Reader, + extensions=[VariablesExtension()] +).router diff --git a/uv.lock b/uv.lock index 6399c0c3..eb5bd83d 100644 --- a/uv.lock +++ b/uv.lock @@ -814,6 +814,7 @@ dependencies = [ { name = "pygeoapi" }, { name = "python-dotenv" }, { name = "titiler-core" }, + { name = "titiler-xarray" }, { name = "uvicorn" }, { name = "zarr" }, ] @@ -841,6 +842,7 @@ requires-dist = [ { name = "pygeoapi", specifier = ">=0.22.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "titiler-core", specifier = ">=1.2.0" }, + { name = "titiler-xarray", specifier = ">=0.22.0" }, { name = "uvicorn", specifier = ">=0.41.0" }, { name = "zarr", specifier = "==3.1.5" }, ] @@ -1917,6 +1919,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] +[[package]] +name = "obstore" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/18/cab734edaeb495a861cfbdced9fecdc0866ed1a85aa5a9202ec77cf4723e/obstore-0.9.2.tar.gz", hash = "sha256:7ef94323127a971c9dea2484109d6c706eb2b2594a2df13c2dd0a6d21a9a69ae", size = 123731, upload-time = "2026-03-11T19:10:18.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/d2/b98058a552849719df56d59a53f7d97e6507b37fca0399a866534800f9fa/obstore-0.9.2-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:50d9c9d6de601ad4805a5a76a1a3d731f7b899383f96ef57276f97bc35202f95", size = 4105494, upload-time = "2026-03-11T19:09:06.573Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/4386622b94fd028cb2298b4780d5a8e2d959fc4c71e599fb63be869aa83d/obstore-0.9.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:4c6dcd9b76b802a2278e1cd88ad7305caf3c3c16f800b2bf5f86a606e9e83d96", size = 3878429, upload-time = "2026-03-11T19:09:07.962Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/0bfad11f1ee5fb1fbdb7833607212ad2586dbd1824b30cf328af63fe92fc/obstore-0.9.2-cp311-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d46e629beb47565fa67b6ef05919434258d72ef848efa340f911af5de2536da", size = 4041157, upload-time = "2026-03-11T19:09:09.278Z" }, + { url = "https://files.pythonhosted.org/packages/eb/98/bfde825f61a8b2541be9185cd6a4ddbb820de94c79750edc32f9f9dfb795/obstore-0.9.2-cp311-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:350d8cc1cd9564369291396e160ebfa133d705ec349d8c0d444a39158d6ef3e7", size = 4144757, upload-time = "2026-03-11T19:09:10.938Z" }, + { url = "https://files.pythonhosted.org/packages/19/35/1c101f6660ef91e5280c824677d8b5ab11ee25ed52e59b075cd795a86e69/obstore-0.9.2-cp311-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dddd38c9f98fd8eaf11a9805464f0bec7e57d8e04a5e0b0cb17582ec58d2fe41", size = 4427897, upload-time = "2026-03-11T19:09:12.137Z" }, + { url = "https://files.pythonhosted.org/packages/fb/eb/a9bdb64474d4e0ab4e4c0105c959090d6bd7ce38d4a945cae3679ead8c52/obstore-0.9.2-cp311-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca872e88e5c719faf1581632e348a6b01331b4f838d7ac29aff226107088dc35", size = 4336227, upload-time = "2026-03-11T19:09:13.822Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ec/e6d39aa311afec2241adb6f2067d7d6ca2eb4e0aab5a95c47796edadd524/obstore-0.9.2-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ee61ac2af5c32c5282fc13b9eba7ffa332f268cb65bc29134ad8ac45e069871", size = 4229010, upload-time = "2026-03-11T19:09:15.503Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fb/a24fd972b66b2d83829e2e89ccf236a759a82f881f909bf4fbe0b6c398ae/obstore-0.9.2-cp311-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:2f430cf8af76985e7ebb8d5f20c8ccef858c608103af6ea95c870f5380cd62f7", size = 4103835, upload-time = "2026-03-11T19:09:16.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d4/c8cc60c8afc597712bf6c5059d629e050de521d901dad0f554b268c2d77f/obstore-0.9.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1df403f80feef7ac483ed66a2a5a964a469f3756ded533935640c4baf986dd49", size = 4292174, upload-time = "2026-03-11T19:09:18.461Z" }, + { url = "https://files.pythonhosted.org/packages/a7/80/dcf8f31814f25c390aa5501a95b78b9f6456d30cd4625109c2a6a5105ad1/obstore-0.9.2-cp311-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c20f62b7c2f57c6f449215c36af4a8d502082ced2185c0b28f07a5e7c9698181", size = 4276266, upload-time = "2026-03-11T19:09:19.787Z" }, + { url = "https://files.pythonhosted.org/packages/16/71/5f5369fba652c5f83b44381d9e7a3cfe00793301d01802059b52b8663f2c/obstore-0.9.2-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:c296e7d60ee132babb7fd01eab946396fa28eb0d88264b9e60320922174e6010", size = 4264118, upload-time = "2026-03-11T19:09:21.081Z" }, + { url = "https://files.pythonhosted.org/packages/c5/50/a5bd1948f2b2efb1039852542829a33a198be0586da7d4247996d3f15d26/obstore-0.9.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:76f274a170731a4461d0fe3eefde38f3bdaf346011ae020c94a0bd18bfd3c4bc", size = 4446876, upload-time = "2026-03-11T19:09:22.401Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d6/bcc266e391403163ed12dd8cab53012f4db8f5020fb49e3b0a505d7a1bba/obstore-0.9.2-cp311-abi3-win_amd64.whl", hash = "sha256:f644fef2a91973b6c055623692524baf830abb1f8bb3ad348611f0e25224e160", size = 4190639, upload-time = "2026-03-11T19:09:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/9a/da/ea7c5095cf15c026819958f74d3ab7b69aff7ce5bf74188e5df5bba4c252/obstore-0.9.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7161a977e94a94dfd2c4ef66846371bdff46bb8b5f9b91dc29c912deb88a5bb2", size = 4087051, upload-time = "2026-03-11T19:09:24.944Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9f/16d6f41ab87e75a6400959a4708343eaca782b78a5f9de7846c70e2b1381/obstore-0.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e3a31fbd68bbe7e061272420337d5ccaf2df7927c2b44ff768531dda02196746", size = 3869338, upload-time = "2026-03-11T19:09:26.404Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/5f13cc91b054d8c93db77e9113ca4924c4320e988284840c8a98238709e6/obstore-0.9.2-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:928da0d131ea33d0b88aa8c3a0dd3f7423261e0c9495444cc14ce0cf62808558", size = 4037703, upload-time = "2026-03-11T19:09:27.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/a2/669620821881559819b8911c4820defa3ffc30a9e49e9d5aca05bd57da45/obstore-0.9.2-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79667de1f0c7eed64b658b3e696bb0565fba4069f6134db502bf4f5f5835aeee", size = 4135488, upload-time = "2026-03-11T19:09:29.232Z" }, + { url = "https://files.pythonhosted.org/packages/9f/12/019e523e97415b4fcfc35b230b270d452fdf5578a7612034c8043c8f2cbf/obstore-0.9.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7318253bc8d03b64473150dad31e611f5bd70a3cc945e3e1d6ac59a901f397c0", size = 4412922, upload-time = "2026-03-11T19:09:30.462Z" }, + { url = "https://files.pythonhosted.org/packages/a6/52/d4a8c1bf588a10bfd17a5a11ebc6af834850fe174a0369648d534a2acb81/obstore-0.9.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:133507229632fde08bc202ca2c81119b2314662dab7a96f8348e97f8e97ae36a", size = 4337193, upload-time = "2026-03-11T19:09:31.773Z" }, + { url = "https://files.pythonhosted.org/packages/aa/59/46c1bdaeae2904bb1edddbfc78e35cb0521ab7c58fe92b147a981873fcdc/obstore-0.9.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c73f208abcddcd3edb7a739d5cac777bdb6fac12a358c9b251654ec7df7866", size = 4221641, upload-time = "2026-03-11T19:09:33.067Z" }, + { url = "https://files.pythonhosted.org/packages/44/9c/b0203594666d11da31e4a7f25ace0718cb1591792e3c1de5225fbd7c8246/obstore-0.9.2-cp313-cp313t-manylinux_2_24_aarch64.whl", hash = "sha256:857b2e7d78c8fb36dcb7c6f1fa89401429667195186ced746a500e54a6aaecdb", size = 4103500, upload-time = "2026-03-11T19:09:34.687Z" }, + { url = "https://files.pythonhosted.org/packages/95/bc/b215712ef24a21247d6e8a4049a76d95e2dca517b8b24efb496600c333c7/obstore-0.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:24c24fdba5080524ce79b36782a11563ea40d9ae5aa26bb6b81a6d089184e4eb", size = 4290492, upload-time = "2026-03-11T19:09:35.936Z" }, + { url = "https://files.pythonhosted.org/packages/ad/28/5aa0ecdc6c01b6e020f1ff8efcca35493e0c6091a0b72ec1bbb16b5b18a8/obstore-0.9.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:778785266aaaf3a73d44ee15e33b72c7ecf0585efeaf8745a1889cc02930ae59", size = 4272220, upload-time = "2026-03-11T19:09:37.223Z" }, + { url = "https://files.pythonhosted.org/packages/06/65/c47b0f972bc7acd64385a964dfbc2efc7361207f490b4d16da789da26fd5/obstore-0.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:305c415fdb2230a1e096f6f290cf524d030329ad5c5e1c9c41f121e7d2fb27d7", size = 4256524, upload-time = "2026-03-11T19:09:38.592Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/9f826fd49cd17cdbc8d2a7a75698d1cc9d731ca98d645f1ca9366ac93781/obstore-0.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a544aad84ae774fac339c686f8a4d7b187c4927b6e33ebb9758c58991d4f27f", size = 4440986, upload-time = "2026-03-11T19:09:40.231Z" }, + { url = "https://files.pythonhosted.org/packages/b9/24/0af1af62239c539975b6c9095428f7597e8f5f9617e897e58dbf7b63f1c5/obstore-0.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:52da6bd719c4962fdfb3c7504e790a89a9b5d27703ee872db01e2075162706fd", size = 4175182, upload-time = "2026-03-11T19:09:41.617Z" }, + { url = "https://files.pythonhosted.org/packages/fa/63/02ca0378938efd1111aa5d689b527c6f3f0c59f4ee440a7b0bf36c528f46/obstore-0.9.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:1bd4790eaa2bb384b58e1c430b2c8816edd7e60216e813c8120014f742e5d280", size = 4087916, upload-time = "2026-03-11T19:09:43.162Z" }, + { url = "https://files.pythonhosted.org/packages/86/9b/604bfb0ec9f117dbb8e936d64e45d95cd9a1fcb63640453566fb3dc66e9d/obstore-0.9.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6417ac0b5cb32498490ceb7034ea357ea2ea965c855590496d64b2d7808a621", size = 3869703, upload-time = "2026-03-11T19:09:44.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/6a/04bcb394f2a6bb12c4325e6ff3f7ead24592582a593c70669d9cdb5b4e9c/obstore-0.9.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc07d71e2f9cd30d2db6ac15c2b162d5b14f6a0e7f575ad66676335c256b1a80", size = 4038164, upload-time = "2026-03-11T19:09:45.922Z" }, + { url = "https://files.pythonhosted.org/packages/34/39/2cc1c2c2a7027dd32ae010ac2ae4491b5f653f86c499e6ec20a6a54e799d/obstore-0.9.2-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7606d5f5c682cc8be9f55d3b07d282dfc0e0262ddfd31b8a26b0a6a3787e5b78", size = 4135199, upload-time = "2026-03-11T19:09:47.242Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4c/defabe9c19bddf44f22591bcf0fffbc3b2b3202eb5ab99a0d894562f56de/obstore-0.9.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80e870ab402ac0f93799049a6680faacbfc2995c60fa87fd683807ce1366e544", size = 4413291, upload-time = "2026-03-11T19:09:48.934Z" }, + { url = "https://files.pythonhosted.org/packages/10/ce/fcfd0436834657a6617d06f07de7630889036c722d35ed9df7913e6caac7/obstore-0.9.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:534049c4b970e1e49c33b47a3e2a051fdc9727f844c3d4737aac4e4c89939fe4", size = 4337512, upload-time = "2026-03-11T19:09:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/565d0cd60f7ae6bb65bde745e182f745a0520f314b32cb802d5f445ad10a/obstore-0.9.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c903949b9994003bda82b57f938ab88f458e75fd27eed809547533bffad99a77", size = 4221955, upload-time = "2026-03-11T19:09:51.499Z" }, + { url = "https://files.pythonhosted.org/packages/0e/27/3fb7f28277fbc929168ff7e02a36a64a56e1288936ac10fce49420c343f4/obstore-0.9.2-cp314-cp314t-manylinux_2_24_aarch64.whl", hash = "sha256:3f07a060702c8b1af51ca15a92658a34bb3ff2e38625173c5592c5aae7fdbfcd", size = 4103438, upload-time = "2026-03-11T19:09:52.748Z" }, + { url = "https://files.pythonhosted.org/packages/67/8f/53ed223ee069da797b09f45e9dbf4a1ed24743081be1ec1411ab6baf8ce9/obstore-0.9.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:462a864782a8d7a1a60c55ac19ce4ad53668a39e35d16b98b787fe97d3fec193", size = 4290842, upload-time = "2026-03-11T19:09:54.3Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/fc94afca13776c4eb8b7a2f27ecb9ee964156d20d699100b719c6c8b6246/obstore-0.9.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:afe36e0452e753c2fece5e6849dd13f209400d5feca668514c0cca2242b0eee8", size = 4273457, upload-time = "2026-03-11T19:09:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/7a/8e/fb02a7a8d4f966af5e069315075bc4388eb63d9cff1c2f3283f3c5781919/obstore-0.9.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3bfae2c634bca903141ef09d6d65e343402de0470e595799881a47ac7c08b2bd", size = 4256979, upload-time = "2026-03-11T19:09:56.983Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/5621ea304d39b4099d36bfa50dce901eb37b3861e2592d76baa26031d407/obstore-0.9.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:71d4059b5e948fe6e8cfc2b77da9c2fc944dfe0ee98090d985e60dd6ebecd7f6", size = 4441545, upload-time = "2026-03-11T19:09:58.59Z" }, + { url = "https://files.pythonhosted.org/packages/30/44/5a7b98d5d92a2267df7a9a905b3cc4f0ca98fbf207b9fae5179a6838a80b/obstore-0.9.2-cp314-cp314t-win_amd64.whl", hash = "sha256:e75295c9c522dde5020d4ff763315af75a165a8a6b8d7f9ed247ce17b7d7f7b0", size = 4175247, upload-time = "2026-03-11T19:10:00.111Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.39.1" @@ -3324,6 +3373,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/d8/20d2982580c1e13025f7e54391f0b2bbf669cb2b1462f42b64d8fe3cf50c/titiler_core-1.2.0-py3-none-any.whl", hash = "sha256:ba7f34f83b3dab0cae612b88ad087be230bbce2043562e17b8ed9182484c4642", size = 88373, upload-time = "2026-02-09T14:37:52.263Z" }, ] +[[package]] +name = "titiler-xarray" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "obstore" }, + { name = "rioxarray" }, + { name = "titiler-core" }, + { name = "xarray" }, + { name = "zarr" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/b2/e6aec77d4160f610b49e95b9edd2ef585c7f8c83900a0ca66b5c6a02acfc/titiler_xarray-1.2.0.tar.gz", hash = "sha256:7e13b753e636ee5af4db1d7fbc84e8dfb58ba0ae0fdcccefb01d4ffdae82ba8d", size = 32428, upload-time = "2026-02-09T14:37:55.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/d3/a3238916c0016a349f309e4ff4ab119c02063317c26d9eacdf4da136c27a/titiler_xarray-1.2.0-py3-none-any.whl", hash = "sha256:781489360d4562e33dd782187b10706ed619b7e0a0ce13c6ff7f459e6ff75915", size = 34150, upload-time = "2026-02-09T14:37:54.446Z" }, +] + [[package]] name = "toml" version = "0.10.2" From 246421646fc43919c3f21426d453285b04e1fa4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Mon, 16 Mar 2026 08:52:19 +0100 Subject: [PATCH 2/9] Code cleaning --- pygeoapi-config.yml | 136 ----- pygeoapi-openapi.yml | 493 ------------------ src/eo_api/ogc_api/__init__.py | 6 - src/eo_api/ogc_api/plugins/__init__.py | 1 - .../ogc_api/plugins/providers/__init__.py | 1 - .../ogc_api/plugins/providers/titiler.py | 216 -------- .../plugins/providers/titiler_custom.py | 60 --- 7 files changed, 913 deletions(-) delete mode 100644 pygeoapi-config.yml delete mode 100644 pygeoapi-openapi.yml delete mode 100644 src/eo_api/ogc_api/__init__.py delete mode 100644 src/eo_api/ogc_api/plugins/__init__.py delete mode 100644 src/eo_api/ogc_api/plugins/providers/__init__.py delete mode 100644 src/eo_api/ogc_api/plugins/providers/titiler.py delete mode 100644 src/eo_api/ogc_api/plugins/providers/titiler_custom.py diff --git a/pygeoapi-config.yml b/pygeoapi-config.yml deleted file mode 100644 index c4cd224c..00000000 --- a/pygeoapi-config.yml +++ /dev/null @@ -1,136 +0,0 @@ -# ================================================================= -# -# Authors: Tom Kralidis -# -# Copyright (c) 2025 Tom Kralidis -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation -# files (the "Software"), to deal in the Software without -# restriction, including without limitation the rights to use, -# copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following -# conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. -# -# ================================================================= - -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 - gzip: false - languages: - # First language is the default language - - en-US - - fr-CA - # cors: true - pretty_print: true - limits: - default_items: 20 - max_items: 50 - # templates: - # path: /path/to/Jinja2/templates - # static: /path/to/static/folder # css/js/img - map: - url: https://tile.openstreetmap.org/{z}/{x}/{y}.png - attribution: '© OpenStreetMap contributors' - manager: - name: TinyDB - connection: /tmp/pygeoapi-process-manager.db - output_dir: /tmp/ - # ogc_schemas_location: /opt/schemas.opengis.net - admin: false # enable admin api - -logging: - level: ERROR - #logfile: /tmp/pygeoapi.log - -metadata: - identification: - title: - en: DHIS2 EO API - description: - en: OGC API compliant geospatial data API - keywords: - en: - - geospatial - - data - - api - keywords_type: theme - terms_of_service: https://creativecommons.org/licenses/by/4.0/ - url: https://example.org - license: - name: CC-BY 4.0 license - url: https://creativecommons.org/licenses/by/4.0/ - provider: - name: DHIS2 EO API - url: https://dhis2.org - contact: - name: DHIS2 Climate Team - email: climate@dhis2.org - role: pointOfContact - -resources: - worldpop_population_yearly: - type: collection - title: Population density - description: Population density - keywords: - - population - extents: - spatial: - bbox: [-13.3035, 6.9176, -10.2658, 10.0004] - crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84 - temporal: - begin: 2015-01-01T00:00:00Z - end: 2030-12-31T23:59:59Z - trs: http://www.opengis.net/def/uom/ISO-8601/0/Gregorian - resolution: P1Y - default: 2026-01-01T00:00:00Z - links: - - type: text/html - rel: canonical - title: information - href: https://hub.worldpop.org/geodata/summary?id=75360 - hreflang: en-EN - providers: - - type: coverage - name: xarray - data: data/downloads/worldpop_population_yearly.zarr - storage_crs: http://www.opengis.net/def/crs/EPSG/0/4326 - x_field: x - y_field: y - time_field: time - format: - name: zarr - mimetype: application/zip - options: - zarr: - consolidated: true - squeeze: true - - type: tile - name: eo_api.ogc_api.plugins.providers.titiler.TiTilerProvider - data: data/downloads/worldpop_population_yearly.zarr - format: - name: png - mimetype: image/png - options: - scheme: WebMercatorQuad - endpoint: /zarr/tiles - variable: pop_total diff --git a/pygeoapi-openapi.yml b/pygeoapi-openapi.yml deleted file mode 100644 index 7899187a..00000000 --- a/pygeoapi-openapi.yml +++ /dev/null @@ -1,493 +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: worldpop_population_yearly - 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/worldpop_population_yearly: - get: - description: Population density - operationId: describeWorldpop_population_yearlyCollection - 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 Population density metadata - tags: - - worldpop_population_yearly - /collections/worldpop_population_yearly/coverage: - get: - description: Population density - operationId: getWorldpop_population_yearlyCoverage - 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 Population density coverage - tags: - - worldpop_population_yearly - /collections/worldpop_population_yearly/tiles: - get: - description: Population density - operationId: describeWorldpop_population_yearly.collection.map.getTileSetsList - parameters: - - $ref: '#/components/parameters/f' - - $ref: '#/components/parameters/lang' - responses: - '200': - $ref: '#/components/responses/Tiles' - '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: Fetch a Population density tiles description - tags: - - worldpop_population_yearly - /collections/worldpop_population_yearly/tiles/{tileMatrixSetId}/{tileMatrix}/{tileRow}/{tileCol}: - get: - description: Population density - operationId: getWorldpop_population_yearly.collection.map.getTile - parameters: - - $ref: https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml#/components/parameters/tileMatrixSetId - - $ref: https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml#/components/parameters/tileMatrix - - $ref: https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml#/components/parameters/tileRow - - $ref: https://schemas.opengis.net/ogcapi/tiles/part1/1.0/openapi/ogcapi-tiles-1.yaml#/components/parameters/tileCol - - description: The optional f parameter indicates the output format which the - server shall provide as part of the response document. - explode: false - in: query - name: f - required: false - schema: - default: png - enum: - - png - type: string - style: form - responses: - '200': - content: - image/png: - schema: - format: binary - type: string - description: successful operation - '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 a Population density tile - tags: - - worldpop_population_yearly - /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: - - &id001 - 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: - - *id001 - - $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: - - *id001 - - $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 -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: Population density - name: worldpop_population_yearly -- name: coverages -- name: edr -- name: records -- name: features -- name: maps -- name: processes -- name: jobs -- name: tiles -- name: stac - diff --git a/src/eo_api/ogc_api/__init__.py b/src/eo_api/ogc_api/__init__.py deleted file mode 100644 index 25e5d868..00000000 --- a/src/eo_api/ogc_api/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""OGC API integration wiring.""" - -from pygeoapi.starlette_app import APP as ogc_api_app - -__all__ = ["ogc_api_app"] - diff --git a/src/eo_api/ogc_api/plugins/__init__.py b/src/eo_api/ogc_api/plugins/__init__.py deleted file mode 100644 index 221656ac..00000000 --- a/src/eo_api/ogc_api/plugins/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Custom pygeoapi plugin package for eo_api.""" diff --git a/src/eo_api/ogc_api/plugins/providers/__init__.py b/src/eo_api/ogc_api/plugins/providers/__init__.py deleted file mode 100644 index fb614f98..00000000 --- a/src/eo_api/ogc_api/plugins/providers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Custom pygeoapi provider plugins for eo_api.""" diff --git a/src/eo_api/ogc_api/plugins/providers/titiler.py b/src/eo_api/ogc_api/plugins/providers/titiler.py deleted file mode 100644 index adb3d306..00000000 --- a/src/eo_api/ogc_api/plugins/providers/titiler.py +++ /dev/null @@ -1,216 +0,0 @@ -"""TiTiler Zarr tile provider plugin for pygeoapi.""" - -from __future__ import annotations - -import base64 -import os -from pathlib import Path -from typing import Any, cast -from urllib.parse import urlencode - -import requests -from pygeoapi.models.provider.base import TileMatrixSetEnum -from pygeoapi.provider.base import ( - ProviderConnectionError, - ProviderGenericError, - ProviderInvalidQueryError, -) -from pygeoapi.provider.tile import BaseTileProvider, ProviderTileNotFoundError -from pygeoapi.util import is_url, url_join - -_DEFAULT_TITILER_BASE_URL = "http://127.0.0.1:8000" -_DEFAULT_TITILER_ENDPOINT = "/zarr/tiles" -_DATETIME_TILESET_SEPARATOR = "~dt~" - - -class TiTilerProvider(BaseTileProvider): - """Bridge pygeoapi OGC API Tiles requests to TiTiler endpoints.""" - - def __init__(self, provider_def: dict[str, Any]) -> None: - format_name = provider_def["format"]["name"].lower() - if format_name not in {"png", "jpeg", "jpg", "webp"}: - raise RuntimeError("TiTiler format must be png, jpeg, jpg, or webp") - - options = dict(provider_def.get("options") or {}) - scheme = options.get("scheme") - schemes = options.get("schemes") - if not schemes: - options["schemes"] = [scheme] if scheme else ["WebMercatorQuad"] - - options.setdefault("endpoint", _DEFAULT_TITILER_ENDPOINT) - options.setdefault( - "endpoint_base", - os.getenv("TITILER_BASE_URL", _DEFAULT_TITILER_BASE_URL), - ) - options.setdefault("timeout", 30) - options.setdefault("datetime_param", "datetime") - options.setdefault("datetime_default", None) - - provider_def = {**provider_def, "options": options} - super().__init__(provider_def) - - self.tile_type = "raster" - self._layer = Path(self.data).stem - - def __repr__(self) -> str: - return f" {self.data}" - - def get_layer(self) -> None: - return None - - def get_fields(self) -> dict[str, Any]: - return {} - - @property - def endpoint(self) -> str: - configured = str(self.options.get("endpoint", _DEFAULT_TITILER_ENDPOINT)) - if is_url(configured): - return configured.rstrip("/") - - base = str(self.options.get("endpoint_base", _DEFAULT_TITILER_BASE_URL)).rstrip("/") - if configured.startswith("/"): - return f"{base}{configured}".rstrip("/") - return f"{base}/{configured}".rstrip("/") - - def get_tiling_schemes(self) -> list[Any]: - configured = set(self.options.get("schemes", [])) - tile_matrix_set_enum = cast(Any, TileMatrixSetEnum) - tile_matrix_set_links = [enum.value for enum in tile_matrix_set_enum if enum.value.tileMatrixSet in configured] - if not tile_matrix_set_links: - raise ProviderConnectionError("Could not identify any valid tiling scheme") - return tile_matrix_set_links - - def get_tiles_service( - self, - baseurl: str | None = None, - servicepath: str | None = None, - dirpath: str | None = None, - tile_type: str | None = None, - ) -> dict[str, list[dict[str, str]]]: - del dirpath, tile_type - - format_name = self.format_type - if servicepath is None: - servicepath = ( - "collections/{dataset}/tiles/{tileMatrixSetId}/" - "{tileMatrix}/{tileRow}/{tileCol}?f=" - f"{format_name}" - ) - - if baseurl and not servicepath.startswith("http"): - self._service_url = url_join(baseurl, servicepath) - else: - self._service_url = servicepath - - query = urlencode(self._titiler_query_params()) - default_scheme = self.options["schemes"][0] - titiler_href = ( - f"{self.endpoint}/{default_scheme}/{{tileMatrix}}/{{tileCol}}/{{tileRow}}.{format_name}?{query}" - ) - - return { - "links": [ - { - "type": self.mimetype, - "rel": "item", - "title": "This collection as image tiles", - "href": self._service_url, - }, - { - "type": self.mimetype, - "rel": "alternate", - "title": "Direct TiTiler tile URL template", - "href": titiler_href, - }, - ] - } - - def get_tiles( - self, - layer: str | None = None, - tileset: str | None = None, - z: int | None = None, - y: int | None = None, - x: int | None = None, - format_: str | None = None, - ) -> bytes | None: - del layer - - if tileset is None: - raise ProviderInvalidQueryError("Missing tileset identifier") - if z is None or y is None or x is None: - raise ProviderInvalidQueryError("Missing tile coordinates") - - normalized_tileset, datetime_ = self._parse_tileset_datetime(tileset) - - try: - z_i = int(z) - y_i = int(y) - x_i = int(x) - except (TypeError, ValueError) as err: - raise ProviderInvalidQueryError("Invalid tile coordinates") from err - - tms = self.get_tilematrixset(normalized_tileset) - if tms is None or not self.is_in_limits(tms, z_i, x_i, y_i): - raise ProviderTileNotFoundError - - tile_format = (format_ or self.format_type).lower() - if tile_format == "jpg": - tile_format = "jpeg" - - request_url = f"{self.endpoint}/{normalized_tileset}/{z_i}/{x_i}/{y_i}.{tile_format}" - - try: - response = requests.get( - request_url, - params=self._titiler_query_params(datetime_), - timeout=int(self.options.get("timeout", 30)), - ) - except requests.RequestException as exc: - raise ProviderConnectionError(str(exc)) from exc - - if response.status_code == 204: - return None - if response.status_code == 404: - raise ProviderTileNotFoundError - if response.status_code < 500 and not response.ok: - raise ProviderInvalidQueryError(response.text) - if response.status_code >= 500: - raise ProviderGenericError(response.text) - - return cast(bytes, response.content) - - def _titiler_query_params(self, datetime_: str | None = None) -> dict[str, Any]: - ignored = { - "scheme", - "schemes", - "endpoint", - "endpoint_base", - "timeout", - "datetime_param", - "datetime_default", - } - params = {"url": self.data} - params.update({k: v for k, v in self.options.items() if k not in ignored}) - effective_datetime = datetime_ if datetime_ is not None else self.options.get("datetime_default") - if effective_datetime is not None: - datetime_param = str(self.options.get("datetime_param", "datetime")) - params[datetime_param] = str(effective_datetime) - return params - - def _parse_tileset_datetime(self, tileset: str) -> tuple[str, str | None]: - if _DATETIME_TILESET_SEPARATOR not in tileset: - return tileset, None - - normalized_tileset, encoded_datetime = tileset.split(_DATETIME_TILESET_SEPARATOR, 1) - if not normalized_tileset or not encoded_datetime: - raise ProviderInvalidQueryError("Invalid tileMatrixSetId datetime encoding") - - padding = "=" * (-len(encoded_datetime) % 4) - try: - datetime_ = base64.urlsafe_b64decode(f"{encoded_datetime}{padding}").decode("utf-8") - except Exception as err: - raise ProviderInvalidQueryError("Invalid tileMatrixSetId datetime encoding") from err - - return normalized_tileset, datetime_ - diff --git a/src/eo_api/ogc_api/plugins/providers/titiler_custom.py b/src/eo_api/ogc_api/plugins/providers/titiler_custom.py deleted file mode 100644 index 6d8df178..00000000 --- a/src/eo_api/ogc_api/plugins/providers/titiler_custom.py +++ /dev/null @@ -1,60 +0,0 @@ -"""TiTiler provider""" - -from typing import Any -from pygeoapi.provider.tile import BaseTileProvider -from pygeoapi.models.provider.base import TileMatrixSetEnum - -class TiTilerProvider(BaseTileProvider): - """Minimal provider implementation used by pygeoapi plugin loading.""" - - def __init__(self, provider_def: dict[str, Any]) -> None: - options = dict(provider_def.get("options") or {}) - if not options.get("schemes"): - scheme = options.get("scheme") - options["schemes"] = [scheme] if scheme else ["WebMercatorQuad"] - - zoom = dict(options.get("zoom") or {}) - zoom.setdefault("min", 0) - zoom.setdefault("max", 24) - options["zoom"] = zoom - - provider_def = {**provider_def, "options": options} - - super().__init__(provider_def) - - self.tile_type = 'raster' - - def __repr__(self) -> str: - return f" {self.data}" - - def get_layer(self): - raise NotImplementedError() - - def get_tiling_schemes(self) -> list[Any]: - return [ - TileMatrixSetEnum.WEBMERCATORQUAD.value # type: ignore - ] - - def get_tiles_service( - self, - baseurl: str | None = None, - servicepath: str | None = None, - dirpath: str | None = None, - tile_type: str | None = None, - ) -> dict[str, list[dict[str, str]]]: - - return { - "links": [] - } - - def get_tiles( - self, - layer: str | None = None, - tileset: str | None = None, - z: int | None = None, - y: int | None = None, - x: int | None = None, - format_: str | None = None, - ) -> bytes | None: - return None - \ No newline at end of file From bef4fd2803c4bcdb919cd91e4180c7408e1ef2d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Mon, 16 Mar 2026 08:56:11 +0100 Subject: [PATCH 3/9] Code cleaning --- src/eo_api/main.py | 7 ------- src/eo_api/tiles/titiler.py | 10 ++++------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/eo_api/main.py b/src/eo_api/main.py index 237eefcc..dd5d5d5d 100644 --- a/src/eo_api/main.py +++ b/src/eo_api/main.py @@ -1,12 +1,10 @@ """DHIS2 EO API -- Earth observation data API for DHIS2.""" from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware import eo_api.startup # noqa: F401 # pyright: ignore[reportUnusedImport] from eo_api import data_accessor, data_manager, data_registry, system -from eo_api.ogc_api import ogc_api_app from eo_api.tiles import tiles_router app = FastAPI() @@ -24,8 +22,3 @@ app.include_router(data_manager.routes.router, prefix='/manage', tags=['Data manager']) app.include_router(data_accessor.routes.router, prefix='/retrieve', tags=['Data retrieval']) app.include_router(tiles_router, prefix='/zarr', tags=['Zarr']) - -app.mount("/data", StaticFiles(directory="data/downloads"), name="Data") - -# mount all pygeoapi endpoints to /ogcapi -app.mount(path="/ogcapi", app=ogc_api_app) diff --git a/src/eo_api/tiles/titiler.py b/src/eo_api/tiles/titiler.py index 86fa3fdb..aab0f47d 100644 --- a/src/eo_api/tiles/titiler.py +++ b/src/eo_api/tiles/titiler.py @@ -1,10 +1,8 @@ -"""TiTiler router definitions.""" +from titiler.xarray.factory import TilerFactory +from titiler.xarray.io import Reader +from titiler.xarray.extensions import VariablesExtension -from titiler.xarray.factory import TilerFactory # pyright: ignore[reportMissingImports] -from titiler.xarray.io import Reader # pyright: ignore[reportMissingImports] -from titiler.xarray.extensions import VariablesExtension # pyright: ignore[reportMissingImports] - -# Xarray-backed TiTiler endpoints (e.g. Zarr datasets). +# Xarray-backed TiTiler endpoints using zarr reader tiles_router = TilerFactory( reader=Reader, extensions=[VariablesExtension()] From e6941500804e0c56d2c98ab745e8a5857474ae67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Mon, 16 Mar 2026 09:23:02 +0100 Subject: [PATCH 4/9] Lintin fixes --- pyproject.toml | 2 +- src/eo_api/tiles/titiler.py | 2 ++ uv.lock | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 37568a8a..4f9fe6b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "0.1.0" requires-python = ">=3.13" dependencies = [ "titiler-core>=1.2.0", - "titiler-xarray>=0.22.0", + "titiler-xarray>=1.2.0", "uvicorn>=0.41.0", "python-dotenv>=1.0.1", "pygeoapi>=0.22.0", diff --git a/src/eo_api/tiles/titiler.py b/src/eo_api/tiles/titiler.py index aab0f47d..a04b2ed7 100644 --- a/src/eo_api/tiles/titiler.py +++ b/src/eo_api/tiles/titiler.py @@ -1,3 +1,5 @@ + """Xarray-backed TiTiler router configuration for EO-API tile endpoints.""" + from titiler.xarray.factory import TilerFactory from titiler.xarray.io import Reader from titiler.xarray.extensions import VariablesExtension diff --git a/uv.lock b/uv.lock index eb5bd83d..4f11d715 100644 --- a/uv.lock +++ b/uv.lock @@ -842,7 +842,7 @@ requires-dist = [ { name = "pygeoapi", specifier = ">=0.22.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "titiler-core", specifier = ">=1.2.0" }, - { name = "titiler-xarray", specifier = ">=0.22.0" }, + { name = "titiler-xarray", specifier = ">=1.2.0" }, { name = "uvicorn", specifier = ">=0.41.0" }, { name = "zarr", specifier = "==3.1.5" }, ] From 7a64477fd925077f481a5dc5e793e09c0473f4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Mon, 16 Mar 2026 09:33:41 +0100 Subject: [PATCH 5/9] Lint fix --- src/eo_api/tiles/titiler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/eo_api/tiles/titiler.py b/src/eo_api/tiles/titiler.py index a04b2ed7..d5b5d9e3 100644 --- a/src/eo_api/tiles/titiler.py +++ b/src/eo_api/tiles/titiler.py @@ -1,8 +1,8 @@ - """Xarray-backed TiTiler router configuration for EO-API tile endpoints.""" +"""Xarray-backed TiTiler router configuration for EO-API tile endpoints.""" -from titiler.xarray.factory import TilerFactory -from titiler.xarray.io import Reader -from titiler.xarray.extensions import VariablesExtension +from titiler.xarray.factory import TilerFactory # pyright: ignore[reportMissingImports] +from titiler.xarray.io import Reader # pyright: ignore[reportMissingImports] +from titiler.xarray.extensions import VariablesExtension # pyright: ignore[reportMissingImports] # Xarray-backed TiTiler endpoints using zarr reader tiles_router = TilerFactory( From f0d99e51153e91c5c3eb1cd6770cba8a59e3d155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Tue, 7 Apr 2026 15:19:04 +0200 Subject: [PATCH 6/9] TiTiler configuration --- src/eo_api/main.py | 4 ++-- src/eo_api/tiles/__init__.py | 6 +++--- src/eo_api/tiles/titiler.py | 11 +++++------ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/eo_api/main.py b/src/eo_api/main.py index 17f61286..79a3add0 100644 --- a/src/eo_api/main.py +++ b/src/eo_api/main.py @@ -9,7 +9,7 @@ from eo_api.ingestions import routes as ingestion_routes from eo_api.pygeoapi_app import mount_pygeoapi from eo_api.system import routes as system_routes -from eo_api.tiles import tiles_router +from eo_api.tiles import titiler_routes as titiler_routes app = FastAPI() @@ -28,6 +28,6 @@ app.include_router(ingestion_routes.ingestions_router, prefix="/ingestions", tags=["Ingestions"]) app.include_router(ingestion_routes.zarr_router, prefix="/zarr", tags=["Zarr"]) app.include_router(ingestion_routes.sync_router, prefix="/sync", tags=["Sync"]) -app.include_router(tiles_router, prefix='/titiler', tags=['TiTiler']) +app.include_router(titiler_routes.router, prefix='/titiler', tags=["TiTiler"]) mount_pygeoapi(app) diff --git a/src/eo_api/tiles/__init__.py b/src/eo_api/tiles/__init__.py index d83fc515..0a378056 100644 --- a/src/eo_api/tiles/__init__.py +++ b/src/eo_api/tiles/__init__.py @@ -1,5 +1,5 @@ -"""Tile routing utilities for eo_api.""" +"""Tiles package.""" -from eo_api.tiles.titiler import tiles_router +from . import titiler as titiler_routes -__all__ = ["tiles_router"] +__all__ = ["titiler_routes"] \ No newline at end of file diff --git a/src/eo_api/tiles/titiler.py b/src/eo_api/tiles/titiler.py index d5b5d9e3..c192c407 100644 --- a/src/eo_api/tiles/titiler.py +++ b/src/eo_api/tiles/titiler.py @@ -1,11 +1,10 @@ -"""Xarray-backed TiTiler router configuration for EO-API tile endpoints.""" +"""Xarray-backed TiTiler router configuration""" -from titiler.xarray.factory import TilerFactory # pyright: ignore[reportMissingImports] -from titiler.xarray.io import Reader # pyright: ignore[reportMissingImports] -from titiler.xarray.extensions import VariablesExtension # pyright: ignore[reportMissingImports] +from titiler.xarray.factory import TilerFactory +from titiler.xarray.io import Reader +from titiler.xarray.extensions import VariablesExtension -# Xarray-backed TiTiler endpoints using zarr reader -tiles_router = TilerFactory( +router = TilerFactory( reader=Reader, extensions=[VariablesExtension()] ).router From 7121f57addfc65eb693a513bdcf4478fdbc33af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Tue, 7 Apr 2026 15:23:16 +0200 Subject: [PATCH 7/9] TiTiler upgrade --- pyproject.toml | 4 ++-- uv.lock | 27 ++++++++++++++------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4f9fe6b1..a4eb0542 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,8 @@ name = "eo-api" version = "0.1.0" requires-python = ">=3.13" dependencies = [ - "titiler-core>=1.2.0", - "titiler-xarray>=1.2.0", + "titiler-core>=2.0.1", + "titiler-xarray>=2.0.1", "uvicorn>=0.41.0", "python-dotenv>=1.0.1", "pygeoapi>=0.22.0", diff --git a/uv.lock b/uv.lock index 4f11d715..5d95646b 100644 --- a/uv.lock +++ b/uv.lock @@ -841,8 +841,8 @@ requires-dist = [ { name = "prefect", specifier = ">=3.6" }, { name = "pygeoapi", specifier = ">=0.22.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, - { name = "titiler-core", specifier = ">=1.2.0" }, - { name = "titiler-xarray", specifier = ">=1.2.0" }, + { name = "titiler-core", specifier = ">=2.0.1" }, + { name = "titiler-xarray", specifier = ">=2.0.1" }, { name = "uvicorn", specifier = ">=0.41.0" }, { name = "zarr", specifier = "==3.1.5" }, ] @@ -1100,6 +1100,7 @@ dependencies = [ { name = "griffecli" }, { name = "griffelib" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/04/56/28a0accac339c164b52a92c6cfc45a903acc0c174caa5c1713803467b533/griffe-2.0.0.tar.gz", hash = "sha256:c68979cd8395422083a51ea7cf02f9c119d889646d99b7b656ee43725de1b80f", size = 293906, upload-time = "2026-03-23T21:06:53.402Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8b/94/ee21d41e7eb4f823b94603b9d40f86d3c7fde80eacc2c3c71845476dddaa/griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa", size = 5214, upload-time = "2026-02-09T19:09:44.105Z" }, ] @@ -1112,6 +1113,7 @@ dependencies = [ { name = "colorama" }, { name = "griffelib" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/a4/f8/2e129fd4a86e52e58eefe664de05e7d502decf766e7316cc9e70fdec3e18/griffecli-2.0.0.tar.gz", hash = "sha256:312fa5ebb4ce6afc786356e2d0ce85b06c1c20d45abc42d74f0cda65e159f6ef", size = 56213, upload-time = "2026-03-23T21:06:54.8Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ed/d93f7a447bbf7a935d8868e9617cbe1cadf9ee9ee6bd275d3040fbf93d60/griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d", size = 9345, upload-time = "2026-02-09T19:09:42.554Z" }, ] @@ -1120,6 +1122,7 @@ wheels = [ name = "griffelib" version = "2.0.0" source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/06/eccbd311c9e2b3ca45dbc063b93134c57a1ccc7607c5e545264ad092c4a9/griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934", size = 166312, upload-time = "2026-03-23T21:06:55.954Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] @@ -2957,7 +2960,7 @@ wheels = [ [[package]] name = "rio-tiler" -version = "8.0.5" +version = "9.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -2970,11 +2973,10 @@ dependencies = [ { name = "pydantic" }, { name = "pystac" }, { name = "rasterio" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/00/3d4ee47e63eb1848acd996cbc02e915adc78360206847b11d26531029c53/rio_tiler-8.0.5.tar.gz", hash = "sha256:c1ce2b9ef166620541c21fe3a0d911a2127354fa68c17131d73ae4e889522cd9", size = 180790, upload-time = "2026-01-05T13:15:10.484Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/87/a034798912bc99f6a8b1ff576d6e33ce17fd64e0bc0d1d5936173739b21d/rio_tiler-9.0.5.tar.gz", hash = "sha256:5bf0ed036b3aa9a1ca76c12fcaf77d40e5c2bca484a242ec480a5585a2135c34", size = 190895, upload-time = "2026-04-03T09:05:48.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/b1/8ea5458d63ef63565e6e7c38ef419c034f1ba8cdfd69fb0ae9c9c90195a1/rio_tiler-8.0.5-py3-none-any.whl", hash = "sha256:137c558c29be1e7312719d2d865ac74f4198afd02e655cdcba306069380d44c9", size = 276693, upload-time = "2026-01-05T13:15:08.793Z" }, + { url = "https://files.pythonhosted.org/packages/67/aa/44772ec88d0df54bae4196f90edd9cca268720a8850a4250389e9e22599c/rio_tiler-9.0.5-py3-none-any.whl", hash = "sha256:44ebfdbbe9c404011be66dbb33cbcd38650d653bee681ab6d0d555b4af37021f", size = 287490, upload-time = "2026-04-03T09:05:46.896Z" }, ] [[package]] @@ -3354,7 +3356,7 @@ wheels = [ [[package]] name = "titiler-core" -version = "1.2.0" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi" }, @@ -3366,16 +3368,15 @@ dependencies = [ { name = "rasterio" }, { name = "rio-tiler" }, { name = "simplejson" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/8d/dcfa9cd77562c0d3525eab2b4b97d3616099b043fbe85db83734db28de44/titiler_core-1.2.0.tar.gz", hash = "sha256:6ac9d30ecd384832ade13029a49f74ffdcba1485fb511ac9509046bb98a571ee", size = 69857, upload-time = "2026-02-09T14:37:53.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/9b/ae47d292f8a7a21cef9716fa5e33fcba4048e26aa11abfe9228bb2c97c9e/titiler_core-2.0.1.tar.gz", hash = "sha256:590343a8da15f56950870d1ebb637924280530de3640877ebc46ab507e04a930", size = 69016, upload-time = "2026-03-31T13:45:15.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/d8/20d2982580c1e13025f7e54391f0b2bbf669cb2b1462f42b64d8fe3cf50c/titiler_core-1.2.0-py3-none-any.whl", hash = "sha256:ba7f34f83b3dab0cae612b88ad087be230bbce2043562e17b8ed9182484c4642", size = 88373, upload-time = "2026-02-09T14:37:52.263Z" }, + { url = "https://files.pythonhosted.org/packages/ba/32/68b4c9f81e8b1cf0dc57d03a4159d0118decda94af41fdf5a98a2b2ced25/titiler_core-2.0.1-py3-none-any.whl", hash = "sha256:f435cf653a3f9acaeaeb60d1e8cda1474ccd79d4364c9e1e8da91dd08c96a449", size = 87427, upload-time = "2026-03-31T13:45:08.265Z" }, ] [[package]] name = "titiler-xarray" -version = "1.2.0" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "obstore" }, @@ -3384,9 +3385,9 @@ dependencies = [ { name = "xarray" }, { name = "zarr" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/b2/e6aec77d4160f610b49e95b9edd2ef585c7f8c83900a0ca66b5c6a02acfc/titiler_xarray-1.2.0.tar.gz", hash = "sha256:7e13b753e636ee5af4db1d7fbc84e8dfb58ba0ae0fdcccefb01d4ffdae82ba8d", size = 32428, upload-time = "2026-02-09T14:37:55.718Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/9a/edee70131c9976b0fd9818caab72b18fe0256225dafdb65b04609c476c24/titiler_xarray-2.0.1.tar.gz", hash = "sha256:257ee065d30a28e50bd821c45e548a190a530027eaa83e915738535c72386012", size = 32477, upload-time = "2026-03-31T13:45:18.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/d3/a3238916c0016a349f309e4ff4ab119c02063317c26d9eacdf4da136c27a/titiler_xarray-1.2.0-py3-none-any.whl", hash = "sha256:781489360d4562e33dd782187b10706ed619b7e0a0ce13c6ff7f459e6ff75915", size = 34150, upload-time = "2026-02-09T14:37:54.446Z" }, + { url = "https://files.pythonhosted.org/packages/8e/53/1cb5c09c8cf4e04a341ecc8afc4eb99b369db7a9dae1a089ffe89e43c739/titiler_xarray-2.0.1-py3-none-any.whl", hash = "sha256:e87bdacdbc5718cc2b0b1df5224ed90d7900670dbddbe99dbe3dd815f0e720eb", size = 34278, upload-time = "2026-03-31T13:45:12.764Z" }, ] [[package]] From 81ee5c39600a12d8ef93b69ca190b5803e76c6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Tue, 7 Apr 2026 15:25:22 +0200 Subject: [PATCH 8/9] Code cleaning --- src/eo_api/tiles/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eo_api/tiles/__init__.py b/src/eo_api/tiles/__init__.py index 0a378056..3cbdbf4a 100644 --- a/src/eo_api/tiles/__init__.py +++ b/src/eo_api/tiles/__init__.py @@ -2,4 +2,4 @@ from . import titiler as titiler_routes -__all__ = ["titiler_routes"] \ No newline at end of file +__all__ = ["titiler_routes"] From 622c5eedccb2003b91f2dc0f1262e13ad1db8f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Sandvik?= Date: Tue, 7 Apr 2026 15:27:04 +0200 Subject: [PATCH 9/9] Code cleaning --- src/eo_api/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/eo_api/main.py b/src/eo_api/main.py index 79a3add0..9620afac 100644 --- a/src/eo_api/main.py +++ b/src/eo_api/main.py @@ -9,7 +9,7 @@ from eo_api.ingestions import routes as ingestion_routes from eo_api.pygeoapi_app import mount_pygeoapi from eo_api.system import routes as system_routes -from eo_api.tiles import titiler_routes as titiler_routes +from eo_api.tiles import titiler_routes app = FastAPI()