Skip to content

Commit 6c55a6d

Browse files
Merge pull request #32 from jverrydt/main
Upgrade to become compatible with titiler.core/titiler.mosaic v0.19
2 parents 7a1ffd4 + 022ea51 commit 6c55a6d

File tree

6 files changed

+66
-109
lines changed

6 files changed

+66
-109
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* add support for python version 3.13
99
* bump minimum python version to 3.11
1010
* update docker image to python:3.13
11+
* Upgrade to become compatible with titiler.core/titiler.mosaic v0.19 (author @jverrydt, https://github.com/developmentseed/titiler-stacapi/pull/32)
1112

1213
## [0.3.3] - 2025-11-06
1314

@@ -39,6 +40,8 @@
3940
* initial release
4041

4142
[Unreleased]: <https://github.com/developmentseed/titiler-stacapi/compare/0.3.0..main>
43+
[0.3.2]: <https://github.com/developmentseed/titiler-stacapi/compare/0.3.1..0.3.2>
44+
[0.3.1]: <https://github.com/developmentseed/titiler-stacapi/compare/0.3.0..0.3.1>
4245
[0.3.0]: <https://github.com/developmentseed/titiler-stacapi/compare/0.2.0..0.3.0>
4346
[0.2.0]: <https://github.com/developmentseed/titiler-stacapi/compare/0.1.1..0.2.0>
4447
[0.1.1]: <https://github.com/developmentseed/titiler-stacapi/compare/0.1.0..0.1.1>

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ classifiers = [
2323
dynamic = ["version"]
2424
dependencies = [
2525
"orjson",
26-
"titiler.core>=0.17.0,<0.19",
27-
"titiler.mosaic>=0.17.0,<0.19",
26+
"titiler.core>=0.19.0,<0.20",
27+
"titiler.mosaic>=0.19.0,<0.20",
2828
"pystac-client",
2929
"pydantic>=2.4,<3.0",
3030
"pydantic-settings~=2.0",
@@ -96,7 +96,7 @@ known_third_party = [
9696
]
9797
default_section = "THIRDPARTY"
9898

99-
[tool.ruff]
99+
[tool.ruff.lint]
100100
select = [
101101
"D1", # pydocstyle errors
102102
"E", # pycodestyle errors

titiler/stacapi/factory.py

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
import json
55
import os
66
from copy import copy
7-
from dataclasses import dataclass, field
87
from enum import Enum
98
from typing import Any, Callable, Dict, List, Literal, Optional, Type
109
from urllib.parse import urlencode
1110

1211
import jinja2
1312
import rasterio
13+
from attrs import define, field
1414
from cachetools import TTLCache, cached
1515
from cachetools.keys import hashkey
1616
from cogeo_mosaic.backends import BaseBackend
@@ -38,7 +38,7 @@
3838
DefaultDependency,
3939
TileParams,
4040
)
41-
from titiler.core.factory import BaseTilerFactory, img_endpoint_params
41+
from titiler.core.factory import TilerFactory, img_endpoint_params
4242
from titiler.core.models.mapbox import TileJSON
4343
from titiler.core.resources.enums import ImageType, MediaType, OptionalHeader
4444
from titiler.core.resources.responses import GeoJSONResponse, XMLResponse
@@ -95,17 +95,17 @@ def get_dependency_params(*, dependency: Callable, query_params: Dict) -> Any:
9595
return
9696

9797

98-
@dataclass
99-
class MosaicTilerFactory(BaseTilerFactory):
98+
@define(kw_only=True)
99+
class MosaicTilerFactory(TilerFactory):
100100
"""Custom MosaicTiler for STACAPI Mosaic Backend."""
101101

102102
path_dependency: Callable[..., APIParams] = STACApiParams
103103

104104
search_dependency: Callable[..., Dict] = STACSearchParams
105105

106-
# In this factory, `reader` should be a Mosaic Backend
106+
# In this factory, `backend` should be a Mosaic Backend
107107
# https://developmentseed.org/cogeo-mosaic/advanced/backends/
108-
reader: Type[BaseBackend] = STACAPIBackend
108+
backend: Type[BaseBackend] = STACAPIBackend
109109

110110
# Because the endpoints should work with STAC Items,
111111
# the `layer_dependency` define which query parameters are mandatory/optional to `display` images
@@ -124,6 +124,8 @@ class MosaicTilerFactory(BaseTilerFactory):
124124

125125
templates: Jinja2Templates = DEFAULT_TEMPLATES
126126

127+
optional_headers: List[OptionalHeader] = field(factory=list)
128+
127129
def get_base_url(self, request: Request) -> str:
128130
"""return endpoints base url."""
129131
base_url = str(request.base_url).rstrip("/")
@@ -219,12 +221,12 @@ def tile(
219221

220222
tms = self.supported_tms.get(tileMatrixSetId)
221223
with rasterio.Env(**env):
222-
with self.reader(
224+
with self.backend(
223225
url=api_params["api_url"],
224226
headers=api_params.get("headers", {}),
225227
tms=tms,
226-
reader_options={**reader_params},
227-
**backend_params,
228+
reader_options={**reader_params.as_dict()},
229+
**backend_params.as_dict(),
228230
) as src_dst:
229231
if MOSAIC_STRICT_ZOOM and (
230232
z < src_dst.minzoom or z > src_dst.maxzoom
@@ -242,9 +244,9 @@ def tile(
242244
tilesize=scale * 256,
243245
pixel_selection=pixel_selection,
244246
threads=MOSAIC_THREADS,
245-
**tile_params,
246-
**layer_params,
247-
**dataset_params,
247+
**tile_params.as_dict(),
248+
**layer_params.as_dict(),
249+
**dataset_params.as_dict(),
248250
)
249251

250252
if post_process:
@@ -260,7 +262,7 @@ def tile(
260262
image,
261263
output_format=format,
262264
colormap=colormap,
263-
**render_params,
265+
**render_params.as_dict(),
264266
)
265267

266268
headers: Dict[str, str] = {}
@@ -724,15 +726,15 @@ def get_layer_from_collections( # noqa: C901
724726
return layers
725727

726728

727-
@dataclass
728-
class OGCWMTSFactory(BaseTilerFactory):
729+
@define(kw_only=True)
730+
class OGCWMTSFactory(TilerFactory):
729731
"""Create /wmts endpoint"""
730732

731733
path_dependency: Callable[..., APIParams] = STACApiParams
732734

733735
# In this factory, `reader` should be a Mosaic Backend
734736
# https://developmentseed.org/cogeo-mosaic/advanced/backends/
735-
reader: Type[BaseBackend] = STACAPIBackend
737+
backend: Type[BaseBackend] = STACAPIBackend
736738

737739
query_dependency: Callable[..., Any] = STACQueryParams
738740

@@ -750,7 +752,7 @@ class OGCWMTSFactory(BaseTilerFactory):
750752
backend_dependency: Type[DefaultDependency] = DefaultDependency
751753

752754
supported_format: List[str] = field(
753-
default_factory=lambda: [
755+
factory=lambda: [
754756
"image/png",
755757
"image/jpeg",
756758
"image/jpg",
@@ -760,7 +762,7 @@ class OGCWMTSFactory(BaseTilerFactory):
760762
]
761763
)
762764

763-
supported_version: List[str] = field(default_factory=lambda: ["1.0.0"])
765+
supported_version: List[str] = field(factory=lambda: ["1.0.0"])
764766

765767
templates: Jinja2Templates = DEFAULT_TEMPLATES
766768

@@ -798,7 +800,7 @@ def get_tile( # noqa: C901
798800
y = int(req["tilerow"])
799801

800802
tms = self.supported_tms.get(tms_id)
801-
with self.reader(
803+
with self.backend(
802804
url=stac_url,
803805
headers=headers,
804806
tms=tms,
@@ -868,9 +870,9 @@ def get_tile( # noqa: C901
868870
search_query=search_query,
869871
pixel_selection=pixel_selection,
870872
threads=MOSAIC_THREADS,
871-
**tile_params,
872-
**layer_params,
873-
**dataset_params,
873+
**tile_params.as_dict(),
874+
**layer_params.as_dict(),
875+
**dataset_params.as_dict(),
874876
)
875877

876878
if post_process := get_dependency_params(
@@ -1387,12 +1389,12 @@ def WMTS_getTile(
13871389

13881390
tms = self.supported_tms.get(tileMatrixSetId)
13891391
with rasterio.Env(**env):
1390-
with self.reader(
1392+
with self.backend(
13911393
url=api_params["api_url"],
13921394
headers=api_params.get("headers", {}),
13931395
tms=tms,
1394-
reader_options={**reader_params},
1395-
**backend_params,
1396+
reader_options={**reader_params.as_dict()},
1397+
**backend_params.as_dict(),
13961398
) as src_dst:
13971399
if MOSAIC_STRICT_ZOOM and (
13981400
z < src_dst.minzoom or z > src_dst.maxzoom
@@ -1410,9 +1412,9 @@ def WMTS_getTile(
14101412
tilesize=256,
14111413
pixel_selection=pixel_selection,
14121414
threads=MOSAIC_THREADS,
1413-
**tile_params,
1414-
**layer_params,
1415-
**dataset_params,
1415+
**tile_params.as_dict(),
1416+
**layer_params.as_dict(),
1417+
**dataset_params.as_dict(),
14161418
)
14171419

14181420
if post_process:
@@ -1428,7 +1430,7 @@ def WMTS_getTile(
14281430
image,
14291431
output_format=format,
14301432
colormap=colormap,
1431-
**render_params,
1433+
**render_params.as_dict(),
14321434
)
14331435

14341436
return Response(content, media_type=media_type)

titiler/stacapi/main.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
from typing import Any, Dict, List, Optional
44

55
import jinja2
6+
import morecantile
67
from fastapi import Depends, FastAPI
78
from fastapi.responses import ORJSONResponse
9+
from morecantile import TileMatrixSets
810
from starlette.middleware.cors import CORSMiddleware
911
from starlette.requests import Request
1012
from starlette.templating import Jinja2Templates
@@ -84,9 +86,11 @@
8486

8587
###############################################################################
8688
# OGC WMTS Endpoints
89+
supported_tms = TileMatrixSets(
90+
{"WebMercatorQuad": morecantile.tms.get("WebMercatorQuad")}
91+
)
8792
wmts = OGCWMTSFactory(
88-
path_dependency=STACApiParams,
89-
templates=templates,
93+
path_dependency=STACApiParams, templates=templates, supported_tms=supported_tms
9094
)
9195
app.include_router(
9296
wmts.router,
@@ -118,7 +122,6 @@
118122
stac = MultiBaseTilerFactory(
119123
reader=STACReader,
120124
path_dependency=ItemIdParams,
121-
optional_headers=optional_headers,
122125
router_prefix="/collections/{collection_id}/items/{item_id}",
123126
add_viewer=True,
124127
)

titiler/stacapi/reader.py

Lines changed: 8 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
"""Custom STAC reader."""
22

3-
from typing import Any, Dict, Optional, Set, Type
3+
from typing import Any, Dict, Optional, Sequence, Set, Type
44

55
import attr
66
import pystac
77
import rasterio
88
from morecantile import TileMatrixSet
9-
from rasterio.crs import CRS
10-
from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS
11-
from rio_tiler.errors import InvalidAssetName
9+
from rio_tiler.constants import WEB_MERCATOR_TMS
1210
from rio_tiler.io import BaseReader, Reader, stac
13-
from rio_tiler.types import AssetInfo
1411

1512
from titiler.stacapi.settings import STACSettings
1613

@@ -28,76 +25,27 @@ class STACReader(stac.STACReader):
2825
input: pystac.Item = attr.ib()
2926

3027
tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
31-
minzoom: int = attr.ib()
32-
maxzoom: int = attr.ib()
33-
34-
geographic_crs: CRS = attr.ib(default=WGS84_CRS)
28+
minzoom: int = attr.ib(default=None)
29+
maxzoom: int = attr.ib(default=None)
3530

3631
include_assets: Optional[Set[str]] = attr.ib(default=None)
3732
exclude_assets: Optional[Set[str]] = attr.ib(default=None)
3833

3934
include_asset_types: Set[str] = attr.ib(default=stac.DEFAULT_VALID_TYPE)
4035
exclude_asset_types: Optional[Set[str]] = attr.ib(default=None)
4136

37+
assets: Sequence[str] = attr.ib(init=False)
38+
default_assets: Optional[Sequence[str]] = attr.ib(default=None)
39+
4240
reader: Type[BaseReader] = attr.ib(default=Reader)
4341
reader_options: Dict = attr.ib(factory=dict)
4442

45-
fetch_options: Dict = attr.ib(factory=dict)
46-
4743
ctx: Any = attr.ib(default=rasterio.Env)
4844

4945
item: pystac.Item = attr.ib(init=False)
46+
fetch_options: Dict = attr.ib(factory=dict)
5047

5148
def __attrs_post_init__(self):
5249
"""Fetch STAC Item and get list of valid assets."""
5350
self.item = self.input
5451
super().__attrs_post_init__()
55-
56-
@minzoom.default
57-
def _minzoom(self):
58-
return self.tms.minzoom
59-
60-
@maxzoom.default
61-
def _maxzoom(self):
62-
return self.tms.maxzoom
63-
64-
def _get_asset_info(self, asset: str) -> AssetInfo:
65-
"""Validate asset names and return asset's url.
66-
67-
Args:
68-
asset (str): STAC asset name.
69-
70-
Returns:
71-
str: STAC asset href.
72-
73-
"""
74-
if asset not in self.assets:
75-
raise InvalidAssetName(
76-
f"'{asset}' is not valid, should be one of {self.assets}"
77-
)
78-
79-
asset_info = self.item.assets[asset]
80-
extras = asset_info.extra_fields
81-
82-
url = asset_info.get_absolute_href() or asset_info.href
83-
if alternate := stac_config.alternate_url:
84-
url = asset_info.to_dict()["alternate"][alternate]["href"]
85-
86-
info = AssetInfo(
87-
url=url,
88-
metadata=extras,
89-
)
90-
91-
if head := extras.get("file:header_size"):
92-
info["env"] = {"GDAL_INGESTED_BYTES_AT_OPEN": head}
93-
94-
if bands := extras.get("raster:bands"):
95-
stats = [
96-
(b["statistics"]["minimum"], b["statistics"]["maximum"])
97-
for b in bands
98-
if {"minimum", "maximum"}.issubset(b.get("statistics", {}))
99-
]
100-
if len(stats) == len(bands):
101-
info["dataset_statistics"] = stats
102-
103-
return info

0 commit comments

Comments
 (0)