From 22c5811471b400c45bbf4a408fa0b9a4bbf4e32d Mon Sep 17 00:00:00 2001 From: Jens Verrydt Date: Mon, 30 Jun 2025 14:58:29 +0200 Subject: [PATCH 1/3] Upgrade to become compatible with titiler.core/titiler.mosaic v0.19 [GDD-4068] --- CHANGELOG.md | 6 ++++++ pyproject.toml | 4 ++-- titiler/stacapi/__init__.py | 2 +- titiler/stacapi/factory.py | 30 ++++++++++++++++-------------- titiler/stacapi/main.py | 9 ++++++--- titiler/stacapi/reader.py | 2 -- 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b23a2db..27a06bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +## [0.4.0] - 2025-06-30 + +* Upgrade to become compatible with titiler.core/titiler.mosaic v0.19 (author @jverrydt, https://github.com/developmentseed/titiler-stacapi/pull/32) + ## [0.3.2] - 2025-05-19 * Align ows:Title, Identifier and Abstract in WMTS GetCapabilities (author @jverrydt, https://github.com/developmentseed/titiler-stacapi/pull/31) @@ -28,6 +32,8 @@ * initial release [Unreleased]: +[0.3.2]: +[0.3.1]: [0.3.0]: [0.2.0]: [0.1.1]: diff --git a/pyproject.toml b/pyproject.toml index 47c2db1..aca9366 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,8 +24,8 @@ classifiers = [ ] dependencies = [ "orjson", - "titiler.core>=0.17.0,<0.19", - "titiler.mosaic>=0.17.0,<0.19", + "titiler.core>=0.19.0,<0.20", + "titiler.mosaic>=0.19.0,<0.20", "pystac-client", "pydantic>=2.4,<3.0", "pydantic-settings~=2.0", diff --git a/titiler/stacapi/__init__.py b/titiler/stacapi/__init__.py index f1a1cba..be36dfa 100644 --- a/titiler/stacapi/__init__.py +++ b/titiler/stacapi/__init__.py @@ -1,3 +1,3 @@ """titiler.stacapi""" -__version__ = "0.3.2" +__version__ = "0.4.0" diff --git a/titiler/stacapi/factory.py b/titiler/stacapi/factory.py index d2e436a..c1e2787 100644 --- a/titiler/stacapi/factory.py +++ b/titiler/stacapi/factory.py @@ -4,13 +4,13 @@ import json import os from copy import copy -from dataclasses import dataclass, field from enum import Enum from typing import Any, Callable, Dict, List, Literal, Optional, Type from urllib.parse import urlencode import jinja2 import rasterio +from attrs import define, field from cachetools import TTLCache, cached from cachetools.keys import hashkey from cogeo_mosaic.backends import BaseBackend @@ -38,7 +38,7 @@ DefaultDependency, TileParams, ) -from titiler.core.factory import BaseTilerFactory, img_endpoint_params +from titiler.core.factory import TilerFactory, img_endpoint_params from titiler.core.models.mapbox import TileJSON from titiler.core.resources.enums import ImageType, MediaType, OptionalHeader from titiler.core.resources.responses import GeoJSONResponse, XMLResponse @@ -95,17 +95,17 @@ def get_dependency_params(*, dependency: Callable, query_params: Dict) -> Any: return -@dataclass -class MosaicTilerFactory(BaseTilerFactory): +@define(kw_only=True) +class MosaicTilerFactory(TilerFactory): """Custom MosaicTiler for STACAPI Mosaic Backend.""" path_dependency: Callable[..., APIParams] = STACApiParams search_dependency: Callable[..., Dict] = STACSearchParams - # In this factory, `reader` should be a Mosaic Backend + # In this factory, `backend` should be a Mosaic Backend # https://developmentseed.org/cogeo-mosaic/advanced/backends/ - reader: Type[BaseBackend] = STACAPIBackend + backend: Type[BaseBackend] = STACAPIBackend # Because the endpoints should work with STAC Items, # the `layer_dependency` define which query parameters are mandatory/optional to `display` images @@ -124,6 +124,8 @@ class MosaicTilerFactory(BaseTilerFactory): templates: Jinja2Templates = DEFAULT_TEMPLATES + optional_headers: List[OptionalHeader] = field(factory=list) + def get_base_url(self, request: Request) -> str: """return endpoints base url.""" base_url = str(request.base_url).rstrip("/") @@ -219,7 +221,7 @@ def tile( tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader( + with self.backend( url=api_params["api_url"], headers=api_params.get("headers", {}), tms=tms, @@ -725,15 +727,15 @@ def get_layer_from_collections( # noqa: C901 return layers -@dataclass -class OGCWMTSFactory(BaseTilerFactory): +@define(kw_only=True) +class OGCWMTSFactory(TilerFactory): """Create /wmts endpoint""" path_dependency: Callable[..., APIParams] = STACApiParams # In this factory, `reader` should be a Mosaic Backend # https://developmentseed.org/cogeo-mosaic/advanced/backends/ - reader: Type[BaseBackend] = STACAPIBackend + backend: Type[BaseBackend] = STACAPIBackend query_dependency: Callable[..., Any] = STACQueryParams @@ -751,7 +753,7 @@ class OGCWMTSFactory(BaseTilerFactory): backend_dependency: Type[DefaultDependency] = DefaultDependency supported_format: List[str] = field( - default_factory=lambda: [ + factory=lambda: [ "image/png", "image/jpeg", "image/jpg", @@ -761,7 +763,7 @@ class OGCWMTSFactory(BaseTilerFactory): ] ) - supported_version: List[str] = field(default_factory=lambda: ["1.0.0"]) + supported_version: List[str] = field(factory=lambda: ["1.0.0"]) templates: Jinja2Templates = DEFAULT_TEMPLATES @@ -799,7 +801,7 @@ def get_tile( # noqa: C901 y = int(req["tilerow"]) tms = self.supported_tms.get(tms_id) - with self.reader( + with self.backend( url=stac_url, headers=headers, tms=tms, @@ -1382,7 +1384,7 @@ def WMTS_getTile( tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader( + with self.backend( url=api_params["api_url"], headers=api_params.get("headers", {}), tms=tms, diff --git a/titiler/stacapi/main.py b/titiler/stacapi/main.py index 610801a..e487409 100644 --- a/titiler/stacapi/main.py +++ b/titiler/stacapi/main.py @@ -4,8 +4,10 @@ from typing import Any, Dict, List, Optional import jinja2 +import morecantile from fastapi import Depends, FastAPI from fastapi.responses import ORJSONResponse +from morecantile import TileMatrixSets from starlette.middleware.cors import CORSMiddleware from starlette.requests import Request from starlette.templating import Jinja2Templates @@ -85,9 +87,11 @@ ############################################################################### # OGC WMTS Endpoints +supported_tms = TileMatrixSets( + {"WebMercatorQuad": morecantile.tms.get("WebMercatorQuad")} +) wmts = OGCWMTSFactory( - path_dependency=STACApiParams, - templates=templates, + path_dependency=STACApiParams, templates=templates, supported_tms=supported_tms ) app.include_router( wmts.router, @@ -119,7 +123,6 @@ stac = MultiBaseTilerFactory( reader=STACReader, path_dependency=ItemIdParams, - optional_headers=optional_headers, router_prefix="/collections/{collection_id}/items/{item_id}", add_viewer=True, ) diff --git a/titiler/stacapi/reader.py b/titiler/stacapi/reader.py index 10ef9d7..9a2d4f0 100644 --- a/titiler/stacapi/reader.py +++ b/titiler/stacapi/reader.py @@ -25,8 +25,6 @@ class STACReader(stac.STACReader): """ - input: pystac.Item = attr.ib() - tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) minzoom: int = attr.ib() maxzoom: int = attr.ib() From 642a3af744a8aa245532de283755f54224bad0b6 Mon Sep 17 00:00:00 2001 From: Jens Verrydt Date: Tue, 1 Jul 2025 12:39:30 +0200 Subject: [PATCH 2/3] fix: use as_dict to reader dependency params [GDD-4068] --- titiler/stacapi/factory.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/titiler/stacapi/factory.py b/titiler/stacapi/factory.py index c1e2787..3e05b8d 100644 --- a/titiler/stacapi/factory.py +++ b/titiler/stacapi/factory.py @@ -225,8 +225,8 @@ def tile( url=api_params["api_url"], headers=api_params.get("headers", {}), tms=tms, - reader_options={**reader_params}, - **backend_params, + reader_options={**reader_params.as_dict()}, + **backend_params.as_dict(), ) as src_dst: if MOSAIC_STRICT_ZOOM and ( z < src_dst.minzoom or z > src_dst.maxzoom @@ -244,9 +244,9 @@ def tile( tilesize=scale * 256, pixel_selection=pixel_selection, threads=MOSAIC_THREADS, - **tile_params, - **layer_params, - **dataset_params, + **tile_params.as_dict(), + **layer_params.as_dict(), + **dataset_params.as_dict(), ) if post_process: @@ -262,7 +262,7 @@ def tile( image, output_format=format, colormap=colormap, - **render_params, + **render_params.as_dict(), ) headers: Dict[str, str] = {} @@ -865,9 +865,9 @@ def get_tile( # noqa: C901 search_query=search_query, pixel_selection=pixel_selection, threads=MOSAIC_THREADS, - **tile_params, - **layer_params, - **dataset_params, + **tile_params.as_dict(), + **layer_params.as_dict(), + **dataset_params.as_dict(), ) if post_process := get_dependency_params( @@ -1388,8 +1388,8 @@ def WMTS_getTile( url=api_params["api_url"], headers=api_params.get("headers", {}), tms=tms, - reader_options={**reader_params}, - **backend_params, + reader_options={**reader_params.as_dict()}, + **backend_params.as_dict(), ) as src_dst: if MOSAIC_STRICT_ZOOM and ( z < src_dst.minzoom or z > src_dst.maxzoom @@ -1407,9 +1407,9 @@ def WMTS_getTile( tilesize=256, pixel_selection=pixel_selection, threads=MOSAIC_THREADS, - **tile_params, - **layer_params, - **dataset_params, + **tile_params.as_dict(), + **layer_params.as_dict(), + **dataset_params.as_dict(), ) if post_process: @@ -1425,7 +1425,7 @@ def WMTS_getTile( image, output_format=format, colormap=colormap, - **render_params, + **render_params.as_dict(), ) return Response(content, media_type=media_type) From 022ea5140312cf44c9c23bcbb69e854fb94ba3e4 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Thu, 6 Nov 2025 11:30:38 +0100 Subject: [PATCH 3/3] update and fix --- pyproject.toml | 2 +- titiler/stacapi/reader.py | 70 ++++++--------------------------------- uv.lock | 29 ++++++++-------- 3 files changed, 26 insertions(+), 75 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0cff6f8..4306466 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ known_third_party = [ ] default_section = "THIRDPARTY" -[tool.ruff] +[tool.ruff.lint] select = [ "D1", # pydocstyle errors "E", # pycodestyle errors diff --git a/titiler/stacapi/reader.py b/titiler/stacapi/reader.py index 9a2d4f0..e946199 100644 --- a/titiler/stacapi/reader.py +++ b/titiler/stacapi/reader.py @@ -1,16 +1,13 @@ """Custom STAC reader.""" -from typing import Any, Dict, Optional, Set, Type +from typing import Any, Dict, Optional, Sequence, Set, Type import attr import pystac import rasterio from morecantile import TileMatrixSet -from rasterio.crs import CRS -from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS -from rio_tiler.errors import InvalidAssetName +from rio_tiler.constants import WEB_MERCATOR_TMS from rio_tiler.io import BaseReader, Reader, stac -from rio_tiler.types import AssetInfo from titiler.stacapi.settings import STACSettings @@ -25,11 +22,11 @@ class STACReader(stac.STACReader): """ - tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) - minzoom: int = attr.ib() - maxzoom: int = attr.ib() + input: pystac.Item = attr.ib() - geographic_crs: CRS = attr.ib(default=WGS84_CRS) + tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS) + minzoom: int = attr.ib(default=None) + maxzoom: int = attr.ib(default=None) include_assets: Optional[Set[str]] = attr.ib(default=None) exclude_assets: Optional[Set[str]] = attr.ib(default=None) @@ -37,65 +34,18 @@ class STACReader(stac.STACReader): include_asset_types: Set[str] = attr.ib(default=stac.DEFAULT_VALID_TYPE) exclude_asset_types: Optional[Set[str]] = attr.ib(default=None) + assets: Sequence[str] = attr.ib(init=False) + default_assets: Optional[Sequence[str]] = attr.ib(default=None) + reader: Type[BaseReader] = attr.ib(default=Reader) reader_options: Dict = attr.ib(factory=dict) - fetch_options: Dict = attr.ib(factory=dict) - ctx: Any = attr.ib(default=rasterio.Env) item: pystac.Item = attr.ib(init=False) + fetch_options: Dict = attr.ib(factory=dict) def __attrs_post_init__(self): """Fetch STAC Item and get list of valid assets.""" self.item = self.input super().__attrs_post_init__() - - @minzoom.default - def _minzoom(self): - return self.tms.minzoom - - @maxzoom.default - def _maxzoom(self): - return self.tms.maxzoom - - def _get_asset_info(self, asset: str) -> AssetInfo: - """Validate asset names and return asset's url. - - Args: - asset (str): STAC asset name. - - Returns: - str: STAC asset href. - - """ - if asset not in self.assets: - raise InvalidAssetName( - f"'{asset}' is not valid, should be one of {self.assets}" - ) - - asset_info = self.item.assets[asset] - extras = asset_info.extra_fields - - url = asset_info.get_absolute_href() or asset_info.href - if alternate := stac_config.alternate_url: - url = asset_info.to_dict()["alternate"][alternate]["href"] - - info = AssetInfo( - url=url, - metadata=extras, - ) - - if head := extras.get("file:header_size"): - info["env"] = {"GDAL_INGESTED_BYTES_AT_OPEN": head} - - if bands := extras.get("raster:bands"): - stats = [ - (b["statistics"]["minimum"], b["statistics"]["maximum"]) - for b in bands - if {"minimum", "maximum"}.issubset(b.get("statistics", {})) - ] - if len(stats) == len(bands): - info["dataset_statistics"] = stats - - return info diff --git a/uv.lock b/uv.lock index 6d58676..c1b7681 100644 --- a/uv.lock +++ b/uv.lock @@ -378,7 +378,7 @@ wheels = [ [[package]] name = "cogeo-mosaic" -version = "7.2.0" +version = "8.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -396,9 +396,9 @@ dependencies = [ { name = "shapely" }, { name = "supermorecado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/2a/4efe88ac2b4e20a6827a9aa89322d81366fc5532c96d2644b5814f41350b/cogeo_mosaic-7.2.0.tar.gz", hash = "sha256:1fa0168f83adcd1065ab49e7c67bc82a507bba895717ac596f8e26b08bc249cb", size = 33458, upload-time = "2024-10-04T13:41:36.995Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/dd/64a58399a87d21828ebda01b28f0b66a962ca71380ff10111269834ef499/cogeo_mosaic-8.2.0.tar.gz", hash = "sha256:5dfc79dde66e8563959d9896124209c12a97edb611d87f255550c14840818bfa", size = 33948, upload-time = "2025-05-06T08:42:39.753Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/c7/dad3d92c8306200c20b4c02cbde636e23950081bb0731f6e4b3e059e03e4/cogeo_mosaic-7.2.0-py3-none-any.whl", hash = "sha256:ed63cb8f8cbc02fafd27bc30ca41956319ece55012629483b274832b72d2802a", size = 40123, upload-time = "2024-10-04T13:41:34.923Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/2a8446c97c8dc190cd627eaecd476b5b3a872398c359e32e8316ec1a4d62/cogeo_mosaic-8.2.0-py3-none-any.whl", hash = "sha256:b943cd21e9a8b68135553c5484fb88dd065a23e24349430a39eb5e6487cf11e5", size = 40477, upload-time = "2025-05-06T08:42:38.64Z" }, ] [[package]] @@ -2208,7 +2208,7 @@ wheels = [ [[package]] name = "rio-tiler" -version = "6.8.0" +version = "7.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -2221,10 +2221,11 @@ dependencies = [ { name = "pydantic" }, { name = "pystac" }, { name = "rasterio" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/23/ca11dbead89d003383cdfc0247bb6072856206e3c6c1ae464785e64e62ed/rio_tiler-6.8.0.tar.gz", hash = "sha256:e52bd4dc5f984c707d3b0907c91b99c347f646bc017ad73dd888d156284ddfc7", size = 170562, upload-time = "2024-10-23T11:46:22.401Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/1b/684dd2478fdbf69befa7518936639c37c9fa1694fd75cca5c0430a2ab542/rio_tiler-7.9.2.tar.gz", hash = "sha256:55f96adcffcf67825c83a9906085b4d5b740139ec66432949a0e4c0b4ea6916b", size = 175772, upload-time = "2025-10-09T11:34:12.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/e9/eddfa5c6e1222fb2870211bb9e4bbd9527ee5c02ed89df31f73b3efe5144/rio_tiler-6.8.0-py3-none-any.whl", hash = "sha256:f83cb6242f2a8f2e7d77bcbe157509228230df73914b66d4791f56877b55297b", size = 264255, upload-time = "2024-10-23T11:46:20.508Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/cbb6feed404ab0b2883b81349d8642d96047878394e7195d1bacfe36a277/rio_tiler-7.9.2-py3-none-any.whl", hash = "sha256:aeb078e63b59ef1041c99bdd4f776341ee8e940fa57ca2e37bab498738b49b56", size = 269983, upload-time = "2025-10-09T11:34:14.44Z" }, ] [[package]] @@ -2478,7 +2479,7 @@ wheels = [ [[package]] name = "titiler-core" -version = "0.18.10" +version = "0.19.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi" }, @@ -2492,22 +2493,22 @@ dependencies = [ { name = "simplejson" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/4b/8588b12b5f9ad0adb201c3f562a348a50103c14efc65137a311b19e5e052/titiler.core-0.18.10.tar.gz", hash = "sha256:342fa935c8d9f876002a28609b93a38baeab24da9d42290a68ee1b23bbe76e35", size = 30143, upload-time = "2024-10-17T21:36:52.94Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/02/481c5826a87c3314bc422439679158b721c176fbfdabf733e7147bdac0ba/titiler_core-0.19.3.tar.gz", hash = "sha256:c6baf47387b7135ef09efbe97ed0b070070c6cd79999e9cee6cf4b82b876f8b7", size = 34913, upload-time = "2025-01-09T21:25:39.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/e8/ab12514687191e012d497eb9ca6180f5f5cf0c089a912f8b083ed08772e5/titiler.core-0.18.10-py3-none-any.whl", hash = "sha256:84294841308e057eb6b085938989f4d0f2f3cbb882790784803bd100d3695a42", size = 35147, upload-time = "2024-10-17T21:36:51.452Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/7c4019f11b769b77bec5df4820b3a4aa1af88ce07123befe6fd4a739cb3e/titiler_core-0.19.3-py3-none-any.whl", hash = "sha256:d3f5b6c784ad15127ca55fdc243858ac21344d0b1c80951a3f61ed05ff24e493", size = 40284, upload-time = "2025-01-09T21:25:36.728Z" }, ] [[package]] name = "titiler-mosaic" -version = "0.18.10" +version = "0.19.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cogeo-mosaic" }, { name = "titiler-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/ac/f6b3e267cb0e3f6260960f38ef91b14489e31b1d54a6dc6822d39af8b97f/titiler.mosaic-0.18.10.tar.gz", hash = "sha256:b796a763d802b6ceaa75e932094a11182ba41f8db74bd145ed44e3e5e45e6ef1", size = 7710, upload-time = "2024-10-17T21:36:58.255Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/bc/037c348ec5ae3be7b66664e1ff4e85a9121c58d235f44077e7743b6cbc70/titiler_mosaic-0.19.3.tar.gz", hash = "sha256:9e00ad954a28b7610f10b377d376c5b736721992fd5e259284876fa2c6a58bc3", size = 10407, upload-time = "2025-01-09T21:25:54.877Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/74/4bde72f12dbcd68b28d1aa3310b31d096528263fbc27ab0844064cfcc770/titiler.mosaic-0.18.10-py3-none-any.whl", hash = "sha256:138e2f0091fd92bb0289641dc66a2d729893fb9d64205002c44540df6d791c92", size = 8147, upload-time = "2024-10-17T21:36:56.875Z" }, + { url = "https://files.pythonhosted.org/packages/4f/35/70b5182fffec9778440c56bbb3b13fcc231c3a89697341f2a82b8b717402/titiler_mosaic-0.19.3-py3-none-any.whl", hash = "sha256:f5b669befb6cd65f17fd8eb68d35e911edb977ef6196b1c93bae3d2ade5dfa41", size = 11170, upload-time = "2025-01-09T21:25:52.556Z" }, ] [[package]] @@ -2552,8 +2553,8 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.4,<3.0" }, { name = "pydantic-settings", specifier = "~=2.0" }, { name = "pystac-client" }, - { name = "titiler-core", specifier = ">=0.17.0,<0.19" }, - { name = "titiler-mosaic", specifier = ">=0.17.0,<0.19" }, + { name = "titiler-core", specifier = ">=0.19.0,<0.20" }, + { name = "titiler-mosaic", specifier = ">=0.19.0,<0.20" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.12.0" }, ] provides-extras = ["server"]