diff --git a/pyproject.toml b/pyproject.toml index 4816f62..56ed0d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,9 +24,10 @@ classifiers = [ dynamic = ["version"] dependencies = [ "orjson", - "titiler.core>=1.1,<1.2", - "titiler.mosaic>=1.1,<1.2", - "titiler.extensions>=1.1,<1.2", + "titiler-core>=2.0.0a2,<2.1", + "titiler-mosaic>=2.0.0a2,<2.1", + "titiler-extensions>=2.0.0a2,<2.1", + "rio-tiler>=9.0.0a4,<10.0", "pystac-client", "pydantic>=2.4,<3.0", "pydantic-settings~=2.0", diff --git a/tests/fixtures/catalog.json b/tests/fixtures/catalog.json index 6d3c3a4..415e237 100644 --- a/tests/fixtures/catalog.json +++ b/tests/fixtures/catalog.json @@ -81,9 +81,8 @@ "visual": { "title": "Visual Image", "assets": [ - "visual" + "visual|indexes=1,2,3" ], - "asset_bidx": "visual|1,2,3", "minmax_zoom": [ 8, 22 @@ -98,9 +97,8 @@ "color": { "title": "Colored Image", "assets": [ - "visual" + "visual|indexes=1" ], - "asset_bidx": "visual|1", "colormap": { "1": [0, 0, 0, 255], "1000": [255, 255, 255, 255] @@ -109,14 +107,13 @@ "visualr": { "title": "Rescaled Image", "assets": [ - "visual" + "visual|indexes=1" ], - "asset_bidx": "visual|1", "rescale": [ [0, 100] ] } - }, + }, "description": "Maxar OpenData | Cyclone Mocha, a category five cyclone with 130 mph winds and torrential rain, hit parts of Myanmar and Bangladesh, forcing mass evacuations ahead of the storm. The cyclone, one of the most powerful to hit the region in the last decade, made landfall on Sunday, May 14, 2023, near Sittwe in Myanmar's Rakhine state. Rain and a storm surge caused widespread flooding in low-lying areas. The United National Office Coordination of Humanitarian Affairs stated that there had been extensive damage among already vulnerable communities and that communications with the affected areas have been difficult.", "item_assets": { "visual": { diff --git a/tests/fixtures/catalog_old_renders.json b/tests/fixtures/catalog_old_renders.json new file mode 100644 index 0000000..6d3c3a4 --- /dev/null +++ b/tests/fixtures/catalog_old_renders.json @@ -0,0 +1,343 @@ +{ + "collections": [ + { + "id": "MAXAR_BayofBengal_Cyclone_Mocha_May_23", + "type": "Collection", + "links": [ + { + "rel": "items", + "type": "application/geo+json", + "href": "https://stac.endpoint.io/collections/MAXAR_BayofBengal_Cyclone_Mocha_May_23/items" + }, + { + "rel": "parent", + "type": "application/json", + "href": "https://stac.endpoint.io/" + }, + { + "rel": "root", + "type": "application/json", + "href": "https://stac.endpoint.io/" + }, + { + "rel": "self", + "type": "application/json", + "href": "https://stac.endpoint.io/collections/MAXAR_BayofBengal_Cyclone_Mocha_May_23" + } + ], + "title": "Bay of Bengal Cyclone Mocha 2023", + "extent": { + "spatial": { + "bbox": [ + [ + 91.831615, + 19.982078842323997, + 92.97426268500965, + 21.666101 + ], + [ + 92.567815, + 20.18811887678192, + 92.74417544237298, + 20.62968532404085 + ], + [ + 92.72278776887262, + 20.104801, + 92.893524, + 20.630214 + ], + [ + 92.75855246040959, + 19.982078842323997, + 92.89682495377032, + 20.514473160464657 + ], + [ + 92.84253515935835, + 19.984656587012033, + 92.97426268500965, + 20.514418665444474 + ], + [ + 91.831615, + 21.518411, + 91.957078, + 21.666101 + ] + ] + }, + "temporal": { + "interval": [ + [ + "2023-01-03T04:30:17Z", + "2023-05-22T04:35:25Z" + ] + ] + } + }, + "license": "CC-BY-NC-4.0", + "renders": { + "visual": { + "title": "Visual Image", + "assets": [ + "visual" + ], + "asset_bidx": "visual|1,2,3", + "minmax_zoom": [ + 8, + 22 + ], + "tilematrixsets": { + "WebMercatorQuad": [ + 8, + 22 + ] + } + }, + "color": { + "title": "Colored Image", + "assets": [ + "visual" + ], + "asset_bidx": "visual|1", + "colormap": { + "1": [0, 0, 0, 255], + "1000": [255, 255, 255, 255] + } + }, + "visualr": { + "title": "Rescaled Image", + "assets": [ + "visual" + ], + "asset_bidx": "visual|1", + "rescale": [ + [0, 100] + ] + } + }, + "description": "Maxar OpenData | Cyclone Mocha, a category five cyclone with 130 mph winds and torrential rain, hit parts of Myanmar and Bangladesh, forcing mass evacuations ahead of the storm. The cyclone, one of the most powerful to hit the region in the last decade, made landfall on Sunday, May 14, 2023, near Sittwe in Myanmar's Rakhine state. Rain and a storm surge caused widespread flooding in low-lying areas. The United National Office Coordination of Humanitarian Affairs stated that there had been extensive damage among already vulnerable communities and that communications with the affected areas have been difficult.", + "item_assets": { + "visual": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "visual" + ], + "title": "Visual Image" + }, + "data-mask": { + "type": "application/geopackage+sqlite3", + "roles": [ + "data-mask" + ], + "title": "Data Mask" + }, + "ms_analytic": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "title": "Multispectral Image" + }, + "pan_analytic": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "title": "Panchromatic Image" + } + }, + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json", + "https://stac-extensions.github.io/render/v1.0.0/schema.json" + ] + }, + { + "id": "MAXAR_BayofBengal_Cyclone_Mocha_May_23_cube_dimensions", + "type": "Collection", + "links": [ + { + "rel": "items", + "type": "application/geo+json", + "href": "https://stac.endpoint.io/collections/MAXAR_BayofBengal_Cyclone_Mocha_May_23/items" + }, + { + "rel": "parent", + "type": "application/json", + "href": "https://stac.endpoint.io/" + }, + { + "rel": "root", + "type": "application/json", + "href": "https://stac.endpoint.io/" + }, + { + "rel": "self", + "type": "application/json", + "href": "https://stac.endpoint.io/collections/MAXAR_BayofBengal_Cyclone_Mocha_May_23" + } + ], + "title": "Bay of Bengal Cyclone Mocha 2023", + "cube:dimensions": { + "time": { + "type": "temporal", + "extent": [ + "2023-01-03T04:30:17Z", + "2023-05-22T04:35:25Z" + ], + "values": [ + "2023-01-03T04:30:17Z", + "2023-02-03T04:30:17Z", + "2023-03-03T04:30:17Z", + "2023-04-03T04:30:17Z", + "2023-05-03T04:30:17Z", + "2023-05-22T04:35:25Z" + ] + } + }, + "extent": { + "spatial": { + "bbox": [ + [ + 91.831615, + 19.982078842323997, + 92.97426268500965, + 21.666101 + ], + [ + 92.567815, + 20.18811887678192, + 92.74417544237298, + 20.62968532404085 + ], + [ + 92.72278776887262, + 20.104801, + 92.893524, + 20.630214 + ], + [ + 92.75855246040959, + 19.982078842323997, + 92.89682495377032, + 20.514473160464657 + ], + [ + 92.84253515935835, + 19.984656587012033, + 92.97426268500965, + 20.514418665444474 + ], + [ + 91.831615, + 21.518411, + 91.957078, + 21.666101 + ] + ] + }, + "temporal": { + "interval": [ + [ + "2023-01-03T04:30:17Z", + "2023-05-22T04:35:25Z" + ] + ] + } + }, + "license": "CC-BY-NC-4.0", + "renders": { + "visual": { + "title": "Visual Image", + "assets": [ + "visual" + ], + "asset_bidx": "visual|1,2,3", + "minmax_zoom": [ + 8, + 22 + ], + "tilematrixsets": { + "WebMercatorQuad": [ + 8, + 22 + ] + } + } + }, + "description": "Maxar OpenData | Cyclone Mocha, a category five cyclone with 130 mph winds and torrential rain, hit parts of Myanmar and Bangladesh, forcing mass evacuations ahead of the storm. The cyclone, one of the most powerful to hit the region in the last decade, made landfall on Sunday, May 14, 2023, near Sittwe in Myanmar's Rakhine state. Rain and a storm surge caused widespread flooding in low-lying areas. The United National Office Coordination of Humanitarian Affairs stated that there had been extensive damage among already vulnerable communities and that communications with the affected areas have been difficult.", + "item_assets": { + "visual": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "visual" + ], + "title": "Visual Image" + }, + "data-mask": { + "type": "application/geopackage+sqlite3", + "roles": [ + "data-mask" + ], + "title": "Data Mask" + }, + "ms_analytic": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "title": "Multispectral Image" + }, + "pan_analytic": { + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "title": "Panchromatic Image" + } + }, + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json", + "https://stac-extensions.github.io/render/v1.0.0/schema.json", + "https://stac-extensions.github.io/datacube/v2.2.0/schema.json" + ] + } + ], + "links": [ + { + "rel": "root", + "type": "application/json", + "href": "https://stac.endpoint.io/" + }, + { + "rel": "parent", + "type": "application/json", + "href": "https://stac.endpoint.io/" + }, + { + "rel": "self", + "type": "application/json", + "href": "https://stac.endpoint.io/collections" + }, + { + "rel": "data", + "type": "application/json", + "href": "https://stac.endpoint.io/collections" + }, + { + "rel": "aggregate", + "type": "application/json", + "title": "Aggregate", + "href": "https://stac.endpoint.io/aggregate" + }, + { + "rel": "aggregations", + "type": "application/json", + "title": "Aggregations", + "href": "https://stac.endpoint.io/aggregations" + } + ] +} diff --git a/tests/fixtures/noaa-emergency-response.json b/tests/fixtures/noaa-emergency-response.json index 41ed177..c176322 100644 --- a/tests/fixtures/noaa-emergency-response.json +++ b/tests/fixtures/noaa-emergency-response.json @@ -1 +1 @@ -{"id":"noaa-emergency-response", "title": "NOAA Emergency Response Imagery", "description":"NOAA Emergency Response Imagery hosted on AWS Public Dataset.","stac_version":"1.0.0","license":"public-domain","links":[],"extent":{"spatial":{"bbox":[[-180,-90,180,90]]},"temporal":{"interval":[["2005-01-01T00:00:00Z",null]]}}} +{"id":"noaa-emergency-response", "type": "Collection", "title": "NOAA Emergency Response Imagery", "description":"NOAA Emergency Response Imagery hosted on AWS Public Dataset.","stac_version":"1.0.0","license":"public-domain","links":[],"extent":{"spatial":{"bbox":[[-87.00,35.00,-84.00,37.00]]},"temporal":{"interval":[["2005-01-01T00:00:00Z",null]]}}} diff --git a/tests/test_collections.py b/tests/test_collections.py index 1844a4e..90faf1e 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -4,22 +4,30 @@ import os from unittest.mock import patch +import pystac + from .conftest import mock_rasterio_open item_json = os.path.join( os.path.dirname(__file__), "fixtures", "20200307aC0853900w361030.json" ) +collection_json = os.path.join( + os.path.dirname(__file__), "fixtures", "noaa-emergency-response.json" +) +@patch("titiler.stacapi.factory.STACAPIBackend._get_collection") @patch("titiler.stacapi.factory.STACAPIBackend.get_assets") @patch("rio_tiler.io.rasterio.rasterio") -def test_stac_collections(rio, get_assets, app): +def test_stac_collections(rio, get_assets, _get_collection, app): """test STAC items endpoints.""" rio.open = mock_rasterio_open with open(item_json, "r") as f: get_assets.return_value = [json.loads(f.read())] + _get_collection.return_value = pystac.Collection.from_file(collection_json) + response = app.get( "/collections/noaa-emergency-response/tiles/WebMercatorQuad/15/8589/12849.png", params={ @@ -42,3 +50,32 @@ def test_stac_collections(rio, get_assets, app): assert resp["minzoom"] == 12 assert resp["maxzoom"] == 14 assert "?assets=cog" in resp["tiles"][0] + assert resp["bounds"] == [-87.00, 35.00, -84.00, 37.00] + + response = app.get( + "/collections/noaa-emergency-response/WebMercatorQuad/tilejson.json", + params={"assets": "cog", "minzoom": 12, "maxzoom": 14, "bbox": "0,0,1,1"}, + ) + assert response.status_code == 200 + resp = response.json() + assert resp["minzoom"] == 12 + assert resp["maxzoom"] == 14 + assert "?assets=cog" in resp["tiles"][0] + assert resp["bounds"] == [0.0, 0.0, 1.0, 1.0] + + response = app.get( + "/collections/noaa-emergency-response/info", + ) + assert response.status_code == 200 + resp = response.json() + assert resp["bounds"] == [-87.0, 35.0, -84.0, 37.0] + assert not resp["renders"] + + response = app.get( + "/collections/noaa-emergency-response/info", + params={"bbox": "0,0,1,1"}, + ) + assert response.status_code == 200 + resp = response.json() + assert resp["bounds"] == [0.0, 0.0, 1.0, 1.0] + assert not resp["renders"] diff --git a/tests/test_items.py b/tests/test_items.py index da96630..19b09d1 100644 --- a/tests/test_items.py +++ b/tests/test_items.py @@ -5,7 +5,6 @@ from unittest.mock import patch import pystac -import pytest from .conftest import mock_rasterio_open @@ -29,10 +28,15 @@ def test_stac_items(get_stac_item, rio, app): assert response.status_code == 200 assert response.json() == ["cog"] - with pytest.warns(UserWarning): - response = app.get( - "/collections/noaa-emergency-response/items/20200307aC0853900w361030/info", - ) + response = app.get( + "/collections/noaa-emergency-response/items/20200307aC0853900w361030/info", + ) + assert response.status_code == 422 + + response = app.get( + "/collections/noaa-emergency-response/items/20200307aC0853900w361030/info", + params={"assets": ":all:"}, + ) assert response.status_code == 200 assert response.json()["cog"] diff --git a/tests/test_render.py b/tests/test_render.py index 1daaece..2723705 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -11,6 +11,9 @@ from titiler.stacapi.factory import get_dependency_params, get_layer_from_collections catalog_json = os.path.join(os.path.dirname(__file__), "fixtures", "catalog.json") +catalog_json_old = os.path.join( + os.path.dirname(__file__), "fixtures", "catalog_old_renders.json" +) @patch("titiler.stacapi.factory.Client") @@ -32,7 +35,7 @@ def test_render(client): visual = collections_render["MAXAR_BayofBengal_Cyclone_Mocha_May_23_visual"] assert visual["bbox"] assert visual["time"] - assert visual["render"]["asset_bidx"] + assert visual["render"]["assets"] color = collections_render["MAXAR_BayofBengal_Cyclone_Mocha_May_23_color"]["render"] assert isinstance(color["colormap"], str) @@ -52,3 +55,26 @@ def test_render(client): query_params=visualr, ) assert rendering.rescale + + +@patch("titiler.stacapi.factory.Client") +def test_old_render(client): + """test STAC items endpoints.""" + + with open(catalog_json_old, "r") as f: + collections = [ + pystac.Collection.from_dict(c) for c in json.loads(f.read())["collections"] + ] + client.open.return_value.get_collections.return_value = collections + + collections_render = get_layer_from_collections( + APIParams(url="https://something.stac"), + None, + ) + assert len(collections_render) == 4 + + visual = collections_render["MAXAR_BayofBengal_Cyclone_Mocha_May_23_visual"] + assert visual["bbox"] + assert visual["time"] + assert visual["render"]["assets"] == ["visual|indexes=1,2,3"] + assert "asset_bidx" not in visual["render"] diff --git a/tests/test_wmts.py b/tests/test_wmts.py index c8171a0..9bbd97b 100644 --- a/tests/test_wmts.py +++ b/tests/test_wmts.py @@ -102,8 +102,7 @@ def test_wmts_getcapabilities(client, app): params = layer.resourceURLs[0]["template"].split("?")[1] query = parse_qs(params) - assert query["assets"] == ["visual"] - assert query["asset_bidx"] == ["visual|1,2,3"] + assert query["assets"] == ["visual|indexes=1,2,3"] layer = wmts["MAXAR_BayofBengal_Cyclone_Mocha_May_23_cube_dimensions_visual"] assert "TIME" in layer.dimensions @@ -331,11 +330,12 @@ def test_wmts_gettile_param_override(client, item_search, rio, app): "TILEROW": 7188, "TILECOL": 12375, "TIME": "2023-01-05", - "expression": "(where(visual_invalid >= 0))", + "assets": "visual_invalid", + "expression": "(where(b1 >= 0))", }, ) - assert response.status_code == 500 - assert "Could not find any valid assets" in response.json()["detail"] + assert response.status_code == 404 + assert "visual_invalid is not valid" in response.json()["detail"] response = app.get( "/wmts", diff --git a/titiler/stacapi/backend.py b/titiler/stacapi/backend.py index a2fc344..de9e5b9 100644 --- a/titiler/stacapi/backend.py +++ b/titiler/stacapi/backend.py @@ -5,6 +5,7 @@ from typing import Any import attr +import pystac from cachetools import TTLCache, cached from cachetools.keys import hashkey from geojson_pydantic import Point, Polygon @@ -164,13 +165,39 @@ def get_assets( @cached( # type: ignore ttl_cache, - key=lambda self: hashkey( + key=lambda self, collection_id: hashkey( + collection_id, self.api_params["url"], json.dumps(self.input), json.dumps(self.api_params.get("headers", {})), ), lock=Lock(), ) + def _get_collection(self, collection_id) -> pystac.Collection: + stac_api_io = StacApiIO( + max_retries=Retry( + total=retry_config.retry, + backoff_factor=retry_config.retry_factor, + ), + headers=self.api_params.get("headers", {}), + ) + client = Client.open(f"{self.api_params['url']}", stac_io=stac_api_io) + return client.get_collection(collection_id) + + def get_geographic_bounds(self, crs: CRS) -> BBox: + """Override method to fetch bounds from collection metadata.""" + if not self.input.get("bbox") and ( + collections := self.input.get("collections", []) + ): + if len(collections) == 1: + collection = self._get_collection(collections[0]) + if collection.extent.spatial: + if collection.extent.spatial.bboxes[0]: + self.bounds = list(collection.extent.spatial.bboxes[0]) + self.crs = WGS84_CRS + + return super().get_geographic_bounds(crs) + def info(self) -> MosaicInfo: # type: ignore """Mosaic info.""" renders = {} @@ -179,16 +206,8 @@ def info(self) -> MosaicInfo: # type: ignore if collections := self.input.get("collections", []): if len(collections) == 1: - stac_api_io = StacApiIO( - max_retries=Retry( - total=retry_config.retry, - backoff_factor=retry_config.retry_factor, - ), - headers=self.api_params.get("headers", {}), - ) - client = Client.open(f"{self.api_params['url']}", stac_io=stac_api_io) - collection = client.get_collection(collections[0]) - if collection.extent.spatial: + collection = self._get_collection(collections[0]) + if not self.input.get("bbox") and collection.extent.spatial: bounds = tuple(collection.extent.spatial.bboxes[0]) crs = WGS84_CRS renders = collection.extra_fields.get("renders", {}) diff --git a/titiler/stacapi/compat.py b/titiler/stacapi/compat.py new file mode 100644 index 0000000..310559f --- /dev/null +++ b/titiler/stacapi/compat.py @@ -0,0 +1,36 @@ +"""compat tools for titiler 2.0""" + +from rio_tiler.utils import cast_to_sequence + + +# NOTE: This is the same as titiler.extensions.render._adapt_render_for_v2 +def _adapt_render_for_v2(render: dict) -> None: + """adapt render dict from titiler 1.0 to 2.0.""" + if assets := render.get("assets"): + assets_with_options: dict[str, list] = { + asset: [] for asset in cast_to_sequence(assets) + } + + # adapt for titiler V2 + if asset_bidx := render.pop("asset_bidx", None): + asset_bidx = cast_to_sequence(asset_bidx) + for v in asset_bidx: + asset, bidx = v.split("|") + if asset in assets_with_options: + assets_with_options[asset].append(f"indexes={bidx}") + + # asset_expression + if asset_expr := render.pop("asset_expression", None): + asset_expr = cast_to_sequence(asset_expr) + for v in asset_expr: + asset, expr = v.split("|") + if asset in assets_with_options: + assets_with_options[asset].append(f"expression={expr}") + + new_assets = [] + for asset, options in assets_with_options.items(): + if options: + asset = asset + "|" + "&".join(options) + new_assets.append(asset) + + render["assets"] = new_assets diff --git a/titiler/stacapi/factory.py b/titiler/stacapi/factory.py index bd9dace..375f4b9 100644 --- a/titiler/stacapi/factory.py +++ b/titiler/stacapi/factory.py @@ -38,7 +38,7 @@ from titiler.core.algorithm import BaseAlgorithm from titiler.core.algorithm import algorithms as available_algorithms from titiler.core.dependencies import ( - AssetsBidxExprParams, + AssetsExprParams, ColorMapParams, DatasetParams, DefaultDependency, @@ -51,6 +51,7 @@ from titiler.core.utils import render_image, tms_limits from titiler.mosaic.factory import PixelSelectionParams from titiler.stacapi.backend import STACAPIBackend +from titiler.stacapi.compat import _adapt_render_for_v2 from titiler.stacapi.dependencies import ( APIParams, BackendParams, @@ -162,6 +163,9 @@ def get_layer_from_collections( # noqa: C901 if "renders" in collection.extra_fields: for name, render in collection.extra_fields["renders"].items(): + # convert asset_bidx/asset_expression to asset key + _adapt_render_for_v2(render) + tilematrixsets: dict[str, tuple[int, int] | None] = render.pop( "tilematrixsets", None ) @@ -316,8 +320,8 @@ class OGCEndpointsFactory(BaseFactory): # Because the endpoints should work with STAC Items, # the `layer_dependency` define which query parameters are mandatory/optional to `display` images - # Defaults to `titiler.core.dependencies.AssetsBidxExprParams`, `assets=` or `expression=` is required - layer_dependency: Type[DefaultDependency] = AssetsBidxExprParams + # Defaults to `titiler.core.dependencies.AssetsExprParams`, `assets=` is required + layer_dependency: Type[DefaultDependency] = AssetsExprParams # Rasterio Dataset Options (nodata, unscale, resampling, reproject) dataset_dependency: Type[DefaultDependency] = DatasetParams diff --git a/titiler/stacapi/main.py b/titiler/stacapi/main.py index 3e51bd1..8975676 100644 --- a/titiler/stacapi/main.py +++ b/titiler/stacapi/main.py @@ -18,7 +18,7 @@ from starlette.templating import Jinja2Templates from titiler.core import __version__ as titiler_version -from titiler.core.dependencies import AssetsBidxExprParams +from titiler.core.dependencies import AssetsExprParams from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers from titiler.core.factory import ( AlgorithmFactory, @@ -36,6 +36,7 @@ from titiler.mosaic.factory import MosaicTilerFactory from titiler.stacapi import __version__ as titiler_stacapi_version from titiler.stacapi.backend import STACAPIBackend +from titiler.stacapi.compat import _adapt_render_for_v2 from titiler.stacapi.dependencies import ( BackendParams, CollectionSearch, @@ -141,6 +142,15 @@ # Notes: # - The `path_dependency` is set to `STACCollectionSearchParams` which define `{collection_id}` # `Path` dependency and other Query parameters used to construct STAC API Search request. + + +def _get_renders_collection(obj) -> dict: + renders = obj.info().renders or {} + for render in renders.values(): + _adapt_render_for_v2(render) + return renders + + collection = MosaicTilerFactory( path_dependency=CollectionSearch, backend=STACAPIBackend, @@ -148,12 +158,12 @@ dataset_reader=SimpleSTACReader, assets_accessor_dependency=STACAPIExtensionParams, optional_headers=optional_headers, - layer_dependency=AssetsBidxExprParams, + layer_dependency=AssetsExprParams, router_prefix="/collections/{collection_id}", add_viewer=True, extensions=[ wmtsExtensionMosaic( - get_renders=lambda obj: obj.info().renders or {} # type: ignore [attr-defined] + get_renders=_get_renders_collection # type: ignore [attr-defined] ), ], templates=templates, @@ -169,13 +179,22 @@ # but in this project we use a custom `path_dependency=ItemIdParams`, which define `{collection_id}` and `{item_id}` as # `Path` dependencies. Then the `ItemIdParams` dependency will fetch the STAC API endpoint to get the STAC Item. The Item # will then be used in our custom `STACReader`. + + +def _get_renders_item(obj) -> dict: + renders = obj.item.properties.get("renders", {}) + for render in renders.values(): + _adapt_render_for_v2(render) + return renders + + stac = MultiBaseTilerFactory( reader=STACAPIReader, path_dependency=ItemIdParams, router_prefix="/collections/{collection_id}/items/{item_id}", add_viewer=True, extensions=[ - wmtsExtension(get_renders=lambda obj: obj.item.properties.get("renders", {})), # type: ignore [attr-defined] + wmtsExtension(get_renders=_get_renders_item), # type: ignore [attr-defined] ], templates=templates, ) diff --git a/titiler/stacapi/reader.py b/titiler/stacapi/reader.py index b6fa3f0..aec0799 100644 --- a/titiler/stacapi/reader.py +++ b/titiler/stacapi/reader.py @@ -135,7 +135,7 @@ def _parse_vrt_asset(self, asset: str) -> tuple[str, str | None]: return asset, None - def _get_asset_info(self, asset: str) -> AssetInfo: + def _get_asset_info(self, asset: str) -> AssetInfo: # noqa: C901 """Validate asset names and return asset's url. Args: @@ -146,28 +146,46 @@ def _get_asset_info(self, asset: str) -> AssetInfo: """ asset, vrt_options = self._parse_vrt_asset(asset) + + method_options: dict[str, Any] = {} + + # NOTE: asset can be in form of + # "{asset_name}|some_option=some_value&another_option=another_value" + if "|" in asset: + asset, params = asset.split("|", 1) + # NOTE: Construct method options from params + if params: + for param in params.split("&"): + key, value = param.split("=", 1) + if key == "indexes": + method_options["indexes"] = list(map(int, value.split(","))) + elif key == "expression": + method_options["expression"] = value + if asset not in self.assets: raise InvalidAssetName( f"{asset} is not valid. Should be one of {self.assets}" ) + asset_modified = "expression" in method_options or vrt_options + asset_info = self.input["assets"][asset] - info = AssetInfo( - url=asset_info["href"], - env={}, - ) + info = { + "url": asset_info["href"], + "name": asset, + "media_type": asset_info.get("type"), + "reader_options": {}, + "method_options": method_options, + } if STAC_ALTERNATE_KEY and "alternate" in asset_info: if alternate := asset_info["alternate"].get(STAC_ALTERNATE_KEY): info["url"] = alternate["href"] - if media_type := asset_info.get("type"): - info["media_type"] = media_type - if header_size := asset_info.get("file:header_size"): info["env"]["GDAL_INGESTED_BYTES_AT_OPEN"] = header_size - if bands := asset_info.get("raster:bands"): + if (bands := asset_info.get("raster:bands")) and not asset_modified: stats = [ (b["statistics"]["minimum"], b["statistics"]["maximum"]) for b in bands diff --git a/uv.lock b/uv.lock index adc4888..7ea169c 100644 --- a/uv.lock +++ b/uv.lock @@ -550,15 +550,15 @@ toml = [ [[package]] name = "cssselect2" -version = "0.8.0" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tinycss2" }, { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" }, ] [[package]] @@ -624,7 +624,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.128.8" +version = "0.129.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -633,9 +633,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/72/0df5c58c954742f31a7054e2dd1143bae0b408b7f36b59b85f928f9b456c/fastapi-0.128.8.tar.gz", hash = "sha256:3171f9f328c4a218f0a8d2ba8310ac3a55d1ee12c28c949650288aee25966007", size = 375523, upload-time = "2026-02-11T15:19:36.69Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/47/75f6bea02e797abff1bca968d5997793898032d9923c1935ae2efdece642/fastapi-0.129.0.tar.gz", hash = "sha256:61315cebd2e65df5f97ec298c888f9de30430dd0612d59d6480beafbc10655af", size = 375450, upload-time = "2026-02-12T13:54:52.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/37/37b07e276f8923c69a5df266bfcb5bac4ba8b55dfe4a126720f8c48681d1/fastapi-0.128.8-py3-none-any.whl", hash = "sha256:5618f492d0fe973a778f8fec97723f598aa9deee495040a8d51aaf3cf123ecf1", size = 103630, upload-time = "2026-02-11T15:19:35.209Z" }, + { url = "https://files.pythonhosted.org/packages/9e/dd/d0ee25348ac58245ee9f90b6f3cbb666bf01f69be7e0911f9851bddbda16/fastapi-0.129.0-py3-none-any.whl", hash = "sha256:b4946880e48f462692b31c083be0432275cbfb6e2274566b1be91479cc1a84ec", size = 102950, upload-time = "2026-02-12T13:54:54.528Z" }, ] [[package]] @@ -649,11 +649,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.3" +version = "3.21.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/71/74364ff065ca78914d8bd90b312fe78ddc5e11372d38bc9cb7104f887ce1/filelock-3.21.2.tar.gz", hash = "sha256:cfd218cfccf8b947fce7837da312ec3359d10ef2a47c8602edd59e0bacffb708", size = 31486, upload-time = "2026-02-13T01:27:15.223Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, + { url = "https://files.pythonhosted.org/packages/98/73/3a18f1e1276810e81477c431009b55eeccebbd7301d28a350b77aacf3c33/filelock-3.21.2-py3-none-any.whl", hash = "sha256:d6cd4dbef3e1bb63bc16500fc5aa100f16e405bbff3fb4231711851be50c1560", size = 21479, upload-time = "2026-02-13T01:27:13.611Z" }, ] [[package]] @@ -1838,11 +1838,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/25/ccd8e88fcd16a4eb6343a8b4b9635e6f3928a7ebcd82822a14d20e3ca29f/platformdirs-4.7.0.tar.gz", hash = "sha256:fd1a5f8599c85d49b9ac7d6e450bc2f1aaf4a23f1fe86d09952fe20ad365cf36", size = 23118, upload-time = "2026-02-12T22:21:53.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e3/1eddccb2c39ecfbe09b3add42a04abcc3fa5b468aa4224998ffb8a7e9c8f/platformdirs-4.7.0-py3-none-any.whl", hash = "sha256:1ed8db354e344c5bb6039cd727f096af975194b508e37177719d562b2b540ee6", size = 18983, upload-time = "2026-02-12T22:21:52.237Z" }, ] [[package]] @@ -2551,7 +2551,7 @@ wheels = [ [[package]] name = "rio-tiler" -version = "8.0.5" +version = "9.0.0a4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -2565,11 +2565,10 @@ dependencies = [ { name = "pystac" }, { name = "rasterio", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "rasterio", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { 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/c0/ba/f065ae284ea8a3d6f0415a8afea6d962a54a0ffbfd0ff583a798c0590b66/rio_tiler-9.0.0a4.tar.gz", hash = "sha256:6ef221368f0f11a7c6198c97a32ee6515b8610e50ee81057f121b958b6c12261", size = 181898, upload-time = "2026-02-11T16:23:37.796Z" } 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/72/8d/5d3a96d273e0644538d346270fcfc67ff5f0ed83ad98665ab36159c32aeb/rio_tiler-9.0.0a4-py3-none-any.whl", hash = "sha256:a7699b878cb86492a94910d7c6348ba96220ea6bde7acec6aca830a792f57503", size = 277769, upload-time = "2026-02-11T16:23:33.096Z" }, ] [[package]] @@ -2787,7 +2786,7 @@ wheels = [ [[package]] name = "titiler-core" -version = "1.1.1" +version = "2.0.0a2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi" }, @@ -2800,35 +2799,35 @@ dependencies = [ { name = "rasterio", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "rio-tiler" }, { name = "simplejson" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/68/c6ea6a4de4673934b74e7c9cd626917ecccca491e717730e286340b2482c/titiler_core-1.1.1.tar.gz", hash = "sha256:69568d86d1e1bffd211d3089fb6f006ae24653d84749f8f85b2319d4307d8358", size = 68572, upload-time = "2026-01-22T15:53:18.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/10/70fcb76b1596b9be1723a7a86c9ee1f2c3625d04db0e702075804b4cb3d8/titiler_core-2.0.0a2.tar.gz", hash = "sha256:2b9aaf74eef938cfabf3689c36623a2f2e67033b4291ee45821d49c42f5258ab", size = 68241, upload-time = "2026-02-13T09:07:13.368Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/8e/e284715e92608cfe4f858880418759550fae4fc37559bc865de1f4f41d31/titiler_core-1.1.1-py3-none-any.whl", hash = "sha256:638897f0f54bf3f1e4151f551aeccd2eea8baf8b7728ccbef57ee24d677167fa", size = 86775, upload-time = "2026-01-22T15:53:19.404Z" }, + { url = "https://files.pythonhosted.org/packages/92/b8/023fb15e0906bf90463735a0e0a7f9cae19c52f700176598a4a0c4a96a76/titiler_core-2.0.0a2-py3-none-any.whl", hash = "sha256:40b3c5bdac151fa855de51f60a5ab45da71e07d4f64be8aaa1aac6277b9355e0", size = 86687, upload-time = "2026-02-13T09:07:14.205Z" }, ] [[package]] name = "titiler-extensions" -version = "1.1.1" +version = "2.0.0a2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "titiler-core" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/f0/3435d1aafa3f730fe19cce6022aa997f3f2c9a67d98274fc890645a49a6d/titiler_extensions-1.1.1.tar.gz", hash = "sha256:7963cfdbd3bd94a2a8934e3208091fbaef6fcb45dde796ddcd3823085cc4b22f", size = 31270, upload-time = "2026-01-22T15:53:29.628Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/6b/3c0f513fd4f21cc81e5cd9677268d7a52d6423c9655d59846d4ece140323/titiler_extensions-2.0.0a2.tar.gz", hash = "sha256:2393fd820d2987f2e801538a2d51955669a2a78ff6ea5884379e6bc5a1cc1c38", size = 31660, upload-time = "2026-02-13T09:07:24.059Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/31/2c44d0726ddfb1f0ad56ae5a80bd73f2d911190d2de06ba245a1e14bbef2/titiler_extensions-1.1.1-py3-none-any.whl", hash = "sha256:10d5a7ec7dd1efde49ef51103bb03a8e211a31a35bb6335688a2a960f5ed4732", size = 38011, upload-time = "2026-01-22T15:53:28.267Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c6/b91861e52fd12f62004c6c678707369d3742acda5a7e2679d47079b6000a/titiler_extensions-2.0.0a2-py3-none-any.whl", hash = "sha256:db0eccd8232efc137fd229bc6881e4cdac4707ec0e3e6b0ee4d5afb17de3e3ee", size = 38441, upload-time = "2026-02-13T09:07:25.284Z" }, ] [[package]] name = "titiler-mosaic" -version = "1.1.1" +version = "2.0.0a2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "titiler-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/ff/526c70f3214dc4112d4abe1edbf62e52f75385a851821fb79abc73a35b4b/titiler_mosaic-1.1.1.tar.gz", hash = "sha256:d7d9f5f1be8c22ad51b8bd063e3d11d2289d058e4ae6856bcf8f13760d815b1c", size = 14761, upload-time = "2026-01-22T15:53:23.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/33/37745500040b2043b155e6dc918fbe1a27ec29ab32e7ab88c52a6abdcb1c/titiler_mosaic-2.0.0a2.tar.gz", hash = "sha256:1692092ffd8b501698acb382ec62ab4b1ee9d4a39ff7549624b0817eb338d4bf", size = 14647, upload-time = "2026-02-13T09:07:19.052Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/5f/f56fb8d9d437f39c0fd7b6aa89c355054992269adfa263acdaeae40f444d/titiler_mosaic-1.1.1-py3-none-any.whl", hash = "sha256:e78c2e346fd5badd8fd4104c1219068b9e83961ba223aa1a767c66b2b6c0b47b", size = 17492, upload-time = "2026-01-22T15:53:22.991Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6b/246bc98a3dff2d5765eb985080b2528044b31f338b79495362928aa04d9a/titiler_mosaic-2.0.0a2-py3-none-any.whl", hash = "sha256:b6467d17bc6808e709946cf21b719c161bf691f277cf4aee86128ca0571b8d8c", size = 17389, upload-time = "2026-02-13T09:07:18.183Z" }, ] [[package]] @@ -2839,6 +2838,7 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pystac-client" }, + { name = "rio-tiler" }, { name = "titiler-core" }, { name = "titiler-extensions" }, { name = "titiler-mosaic" }, @@ -2873,9 +2873,10 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.4,<3.0" }, { name = "pydantic-settings", specifier = "~=2.0" }, { name = "pystac-client" }, - { name = "titiler-core", specifier = ">=1.1,<1.2" }, - { name = "titiler-extensions", specifier = ">=1.1,<1.2" }, - { name = "titiler-mosaic", specifier = ">=1.1,<1.2" }, + { name = "rio-tiler", specifier = ">=9.0.0a4,<10.0" }, + { name = "titiler-core", specifier = ">=2.0.0a2,<2.1" }, + { name = "titiler-extensions", specifier = ">=2.0.0a2,<2.1" }, + { name = "titiler-mosaic", specifier = ">=2.0.0a2,<2.1" }, { name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.12.0" }, ] provides-extras = ["server"]