From 1825dce9f8c8a478e7cef3f3172aa5f660e7454a Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Tue, 24 Sep 2024 14:14:12 +0200 Subject: [PATCH] simplify Factories (#943) * simplify Factories * fix * fix mosaic * update changelog * update version number * update changelog * update changelog --- CHANGES.md | 45 +++- deployment/aws/lambda/Dockerfile | 2 +- deployment/k8s/charts/Chart.yaml | 2 +- pyproject.toml | 12 +- src/titiler/application/pyproject.toml | 6 +- .../titiler/application/__init__.py | 2 +- .../application/titiler/application/main.py | 3 +- src/titiler/core/tests/test_factories.py | 35 ++- src/titiler/core/titiler/core/__init__.py | 7 +- src/titiler/core/titiler/core/factory.py | 252 +++++++----------- src/titiler/extensions/pyproject.toml | 2 +- src/titiler/extensions/tests/test_viewer.py | 4 +- .../extensions/titiler/extensions/__init__.py | 2 +- .../extensions/titiler/extensions/cogeo.py | 9 +- .../extensions/titiler/extensions/stac.py | 8 +- .../extensions/titiler/extensions/viewer.py | 13 +- .../extensions/titiler/extensions/wms.py | 17 +- src/titiler/mosaic/pyproject.toml | 2 +- src/titiler/mosaic/tests/test_factory.py | 40 +-- src/titiler/mosaic/titiler/mosaic/__init__.py | 2 +- src/titiler/mosaic/titiler/mosaic/factory.py | 170 ++++++------ 21 files changed, 314 insertions(+), 321 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d5c98f0cb..7935b7776 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,12 +1,35 @@ # Release Notes -## Unreleased +## 0.19.0 (TBD) + +### Misc + +* Removed default `WebMercatorQuad` tile matrix set in `/tiles`, `/tilesjson.json`, `/map` and `/WMTSCapabilities.xml` endpoints **breaking change** + + ``` + # Before + /tiles/{z}/{x}/{y} + /tilejson.json + /map + /WMTSCapabilities.xml + + # Now + /tiles/WebMercatorQuad/{z}/{x}/{y} + /WebMercatorQuad/tilejson.json + /WebMercatorQuad/map + /WebMercatorQuad/WMTSCapabilities.xml + ``` + +* Use `@attrs.define` instead of dataclass for factories **breaking change** +* Use `@attrs.define` instead of dataclass for factory extensions **breaking change** + ### titiler.core * Improve XSS security for HTML templates (author @jcary741, https://github.com/developmentseed/titiler/pull/953) -* Remove all default values to the dependencies +* Remove all default values to the dependencies **breaking change** + * `DatasetParams.unscale`: `False` -> `None` (default to `False` in rio-tiler) * `DatasetParams.resampling_method`: `nearest` -> `None` (default to `nearest` in rio-tiler) * `DatasetParams.reproject_method`: `nearest` -> `None` (default to `nearest` in rio-tiler) @@ -40,12 +63,30 @@ * Use `.as_dict()` method when passing option to rio-tiler Reader's methods to avoid parameter conflicts when using custom Readers. +* Renamed `BaseTilerFactory` to `BaseFactory` **breaking change** + +* Removed useless attribute in `BaseFactory` (and moved them to `TilerFactory`) **breaking change** + +### titiler.mosaic + +* Renamed `reader` attribute to `backend` in `MosaicTilerFactory` **breaking change** + ### titiler.extensions * Encode URL for cog_viewer and stac_viewer (author @guillemc23, https://github.com/developmentseed/titiler/pull/961) * Add links for render parameters and `/map` link to **viewer** dashboard (author @hrodmn, https://github.com/developmentseed/titiler/pull/987) +## 0.18.9 (2024-09-23) + +* fix release 0.18.8 + +## 0.18.8 (2024-09-23) + +### titiler.extensions + +* Add links for render parameters and /map link to viewer dashboard (author @hrodmn, https://github.com/developmentseed/titiler/pull/987) + ## 0.18.7 (2024-09-19) * fix Hillshade algorithm (bad `azimuth` angle) (https://github.com/developmentseed/titiler/pull/985) [Backported] diff --git a/deployment/aws/lambda/Dockerfile b/deployment/aws/lambda/Dockerfile index d8b0a47a4..788c69682 100644 --- a/deployment/aws/lambda/Dockerfile +++ b/deployment/aws/lambda/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /tmp RUN yum install -y gcc-c++ RUN python -m pip install pip -U -RUN python -m pip install "titiler.application==0.18.6" "mangum>=0.10.0" -t /asset --no-binary pydantic +RUN python -m pip install "titiler.application==0.18.9" "mangum>=0.10.0" -t /asset --no-binary pydantic # Reduce package size and remove useless files RUN cd /asset && find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done; diff --git a/deployment/k8s/charts/Chart.yaml b/deployment/k8s/charts/Chart.yaml index 7bf02e487..52b7cea77 100644 --- a/deployment/k8s/charts/Chart.yaml +++ b/deployment/k8s/charts/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v1 -appVersion: 0.18.6 +appVersion: 0.18.9 description: A dynamic Web Map tile server name: titiler version: 1.1.3 diff --git a/pyproject.toml b/pyproject.toml index 9cb757cd2..2793e564c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,12 +29,12 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering :: GIS", ] -version="0.18.6" +version="0.18.9" dependencies = [ - "titiler.core==0.18.6", - "titiler.extensions==0.18.6", - "titiler.mosaic==0.18.6", - "titiler.application==0.18.6", + "titiler.core==0.18.9", + "titiler.extensions==0.18.9", + "titiler.mosaic==0.18.9", + "titiler.application==0.18.9", ] [project.urls] @@ -126,7 +126,7 @@ filterwarnings = [ bypass-selection = true [tool.bumpversion] -current_version = "0.18.6" +current_version = "0.18.9" parse = """(?x) (?P\\d+)\\. (?P\\d+)\\. diff --git a/src/titiler/application/pyproject.toml b/src/titiler/application/pyproject.toml index bbaaca65d..5b3a6fa7d 100644 --- a/src/titiler/application/pyproject.toml +++ b/src/titiler/application/pyproject.toml @@ -31,9 +31,9 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.18.6", - "titiler.extensions[cogeo,stac]==0.18.6", - "titiler.mosaic==0.18.6", + "titiler.core==0.18.9", + "titiler.extensions[cogeo,stac]==0.18.9", + "titiler.mosaic==0.18.9", "starlette-cramjam>=0.3,<0.4", "pydantic-settings~=2.0", ] diff --git a/src/titiler/application/titiler/application/__init__.py b/src/titiler/application/titiler/application/__init__.py index 6e61632cf..da005153e 100644 --- a/src/titiler/application/titiler/application/__init__.py +++ b/src/titiler/application/titiler/application/__init__.py @@ -1,3 +1,3 @@ """titiler.application""" -__version__ = "0.18.6" +__version__ = "0.18.9" diff --git a/src/titiler/application/titiler/application/main.py b/src/titiler/application/titiler/application/main.py index a8acafb15..d9441bf39 100644 --- a/src/titiler/application/titiler/application/main.py +++ b/src/titiler/application/titiler/application/main.py @@ -6,7 +6,7 @@ import jinja2 from fastapi import Depends, FastAPI, HTTPException, Security from fastapi.security.api_key import APIKeyQuery -from rio_tiler.io import STACReader +from rio_tiler.io import Reader, STACReader from starlette.middleware.cors import CORSMiddleware from starlette.requests import Request from starlette.responses import HTMLResponse @@ -98,6 +98,7 @@ def validate_access_token(access_token: str = Security(api_key_query)): # Simple Dataset endpoints (e.g Cloud Optimized GeoTIFF) if not api_settings.disable_cog: cog = TilerFactory( + reader=Reader, router_prefix="/cog", extensions=[ cogValidateExtension(), diff --git a/src/titiler/core/tests/test_factories.py b/src/titiler/core/tests/test_factories.py index 6333cb291..7545f9a92 100644 --- a/src/titiler/core/tests/test_factories.py +++ b/src/titiler/core/tests/test_factories.py @@ -5,18 +5,17 @@ import pathlib import warnings import xml.etree.ElementTree as ET -from dataclasses import dataclass from enum import Enum from io import BytesIO from typing import Dict, Optional, Type from unittest.mock import patch from urllib.parse import urlencode -import attr import httpx import morecantile import numpy import pytest +from attrs import define, field from fastapi import Depends, FastAPI, HTTPException, Path, Query, security, status from morecantile.defaults import TileMatrixSets from rasterio.crs import CRS @@ -31,7 +30,7 @@ from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers from titiler.core.factory import ( AlgorithmFactory, - BaseTilerFactory, + BaseFactory, ColorMapFactory, MultiBandTilerFactory, MultiBaseTilerFactory, @@ -49,7 +48,7 @@ def test_TilerFactory(): """Test TilerFactory class.""" cog = TilerFactory() - assert len(cog.router.routes) == 27 + assert len(cog.router.routes) == 20 assert len(cog.supported_tms.list()) == NB_DEFAULT_TMS cog = TilerFactory(router_prefix="something", supported_tms=WEB_TMS) @@ -76,7 +75,7 @@ def test_TilerFactory(): assert response.status_code == 422 cog = TilerFactory(add_preview=False, add_part=False, add_viewer=False) - assert len(cog.router.routes) == 18 + assert len(cog.router.routes) == 12 app = FastAPI() cog = TilerFactory() @@ -723,7 +722,7 @@ def test_MultiBaseTilerFactory(rio): rio.open = mock_rasterio_open stac = MultiBaseTilerFactory(reader=STACReader) - assert len(stac.router.routes) == 29 + assert len(stac.router.routes) == 22 app = FastAPI() app.include_router(stac.router) @@ -1045,20 +1044,20 @@ def test_MultiBaseTilerFactory(rio): assert "(B09 - B01) / (B09 + B01)" in props -@attr.s +@define class BandFileReader(MultiBandReader): """Test MultiBand""" - input: str = attr.ib() - tms: morecantile.TileMatrixSet = attr.ib( + input: str = field() + tms: morecantile.TileMatrixSet = field( default=morecantile.tms.get("WebMercatorQuad") ) - reader_options: Dict = attr.ib(factory=dict) + reader_options: Dict = field(factory=dict) - reader: Type[BaseReader] = attr.ib(default=Reader) + reader: Type[BaseReader] = field(default=Reader) - minzoom: int = attr.ib() - maxzoom: int = attr.ib() + minzoom: int = field() + maxzoom: int = field() @minzoom.default def _minzoom(self): @@ -1093,7 +1092,7 @@ def test_MultiBandTilerFactory(): bands = MultiBandTilerFactory( reader=BandFileReader, path_dependency=CustomPathParams ) - assert len(bands.router.routes) == 28 + assert len(bands.router.routes) == 21 app = FastAPI() app.include_router(bands.router) @@ -1465,7 +1464,7 @@ def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic) ], router_prefix="something", ) - assert len(cog.router.routes) == 27 + assert len(cog.router.routes) == 20 app = FastAPI() app.include_router(cog.router, prefix="/something") @@ -1614,8 +1613,8 @@ def test_algorithm(): def test_path_param_in_prefix(): """Test path params in prefix.""" - @dataclass - class EndpointFactory(BaseTilerFactory): + @define + class EndpointFactory(BaseFactory): def register_routes(self): """register endpoints.""" @@ -1632,7 +1631,7 @@ def route1(param1: int = Path(...), param2: str = Path(...)): return {"value": param2} app = FastAPI() - endpoints = EndpointFactory(reader=Reader, router_prefix="/prefixed/{param1}") + endpoints = EndpointFactory(router_prefix="/prefixed/{param1}") app.include_router(endpoints.router, prefix="/prefixed/{param1}") client = TestClient(app) diff --git a/src/titiler/core/titiler/core/__init__.py b/src/titiler/core/titiler/core/__init__.py index 396515b6e..137a2cda5 100644 --- a/src/titiler/core/titiler/core/__init__.py +++ b/src/titiler/core/titiler/core/__init__.py @@ -1,11 +1,14 @@ """titiler.core""" -__version__ = "0.18.6" +__version__ = "0.18.9" from . import dependencies, errors, factory, routing # noqa from .factory import ( # noqa - BaseTilerFactory, + AlgorithmFactory, + BaseFactory, + ColorMapFactory, MultiBandTilerFactory, MultiBaseTilerFactory, TilerFactory, + TMSFactory, ) diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index 5e9774fcc..1cb9dfe76 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -1,8 +1,6 @@ """TiTiler Router factories.""" import abc -import warnings -from dataclasses import dataclass, field from typing import ( Any, Callable, @@ -20,6 +18,7 @@ import jinja2 import numpy import rasterio +from attrs import define, field from fastapi import APIRouter, Body, Depends, Path, Query from fastapi.dependencies.utils import get_parameterless_sub_dependant from fastapi.params import Depends as DependsFunc @@ -28,7 +27,7 @@ from morecantile import TileMatrixSet from morecantile import tms as morecantile_tms from morecantile.defaults import TileMatrixSets -from pydantic import conint +from pydantic import Field from rio_tiler.colormap import ColorMaps from rio_tiler.colormap import cmap as default_cmap from rio_tiler.constants import WGS84_CRS @@ -81,7 +80,7 @@ Statistics, StatisticsGeoJSON, ) -from titiler.core.resources.enums import ImageType, MediaType, OptionalHeader +from titiler.core.resources.enums import ImageType, MediaType from titiler.core.resources.responses import GeoJSONResponse, JSONResponse, XMLResponse from titiler.core.routing import EndpointScope from titiler.core.utils import render_image @@ -110,103 +109,45 @@ } -@dataclass # type: ignore +@define class FactoryExtension(metaclass=abc.ABCMeta): """Factory Extension.""" @abc.abstractmethod - def register(self, factory: "BaseTilerFactory"): + def register(self, factory: "BaseFactory"): """Register extension to the factory.""" ... -# ref: https://github.com/python/mypy/issues/5374 -@dataclass # type: ignore -class BaseTilerFactory(metaclass=abc.ABCMeta): - """BaseTiler Factory. +@define(kw_only=True) +class BaseFactory(metaclass=abc.ABCMeta): + """Base Factory. Abstract Base Class which defines most inputs used by dynamic tiler. Attributes: - reader (rio_tiler.io.base.BaseReader): A rio-tiler reader (e.g Reader). router (fastapi.APIRouter): Application router to register endpoints to. - path_dependency (Callable): Endpoint dependency defining `path` to pass to the reader init. - dataset_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset overwriting options (e.g nodata). - layer_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset indexes/bands/assets options. - render_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image rendering options (e.g add_mask). - colormap_dependency (Callable): Endpoint dependency defining ColorMap options (e.g colormap_name). - process_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image post-processing options (e.g rescaling, color-formula). - tms_dependency (Callable): Endpoint dependency defining TileMatrixSet to use. - reader_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining BaseReader options. - environment_dependency (Callable): Endpoint dependency to define GDAL environment at runtime. router_prefix (str): prefix where the router will be mounted in the application. - optional_headers(sequence of titiler.core.resources.enums.OptionalHeader): additional headers to return with the response. """ - reader: Type[BaseReader] - # FastAPI router - router: APIRouter = field(default_factory=APIRouter) - - # Path Dependency - path_dependency: Callable[..., Any] = DatasetPathParams - - # Indexes/Expression Dependencies - layer_dependency: Type[DefaultDependency] = BidxExprParams - - # Rasterio Dataset Options (nodata, unscale, resampling, reproject) - dataset_dependency: Type[DefaultDependency] = DatasetParams - - # Post Processing Dependencies (algorithm) - process_dependency: Callable[ - ..., Optional[BaseAlgorithm] - ] = available_algorithms.dependency - - # Image rendering Dependencies - rescale_dependency: Callable[..., Optional[RescaleType]] = RescalingParams - color_formula_dependency: Callable[..., Optional[str]] = ColorFormulaParams - colormap_dependency: Callable[..., Optional[ColorMapType]] = ColorMapParams - render_dependency: Type[DefaultDependency] = ImageRenderingParams - - # Reader dependency - reader_dependency: Type[DefaultDependency] = DefaultDependency - - # GDAL ENV dependency - environment_dependency: Callable[..., Dict] = field(default=lambda: {}) - - # TileMatrixSet dependency - supported_tms: TileMatrixSets = morecantile_tms - default_tms: Optional[str] = None + router: APIRouter = field(factory=APIRouter) # Router Prefix is needed to find the path for /tile if the TilerFactory.router is mounted # with other router (multiple `.../tile` routes). # e.g if you mount the route with `/cog` prefix, set router_prefix to cog and router_prefix: str = "" - # add additional headers in response - optional_headers: List[OptionalHeader] = field(default_factory=list) - # add dependencies to specific routes route_dependencies: List[Tuple[List[EndpointScope], List[DependsFunc]]] = field( - default_factory=list + factory=list ) - extensions: List[FactoryExtension] = field(default_factory=list) + extensions: List[FactoryExtension] = field(factory=list) - templates: Jinja2Templates = DEFAULT_TEMPLATES - - def __post_init__(self): + def __attrs_post_init__(self): """Post Init: register route and configure specific options.""" - # TODO: remove this in 0.19 - if self.default_tms: - warnings.warn( - "`default_tms` attribute is deprecated and will be removed in 0.19.", - DeprecationWarning, - ) - - self.default_tms = self.default_tms or "WebMercatorQuad" - # Register endpoints self.register_routes() @@ -220,7 +161,7 @@ def __post_init__(self): @abc.abstractmethod def register_routes(self): - """Register Tiler Routes.""" + """Register Routes.""" ... def url_for(self, request: Request, name: str, **path_params: Any) -> str: @@ -275,12 +216,21 @@ def add_route_dependencies( route.dependencies.extend(dependencies) # type: ignore -@dataclass -class TilerFactory(BaseTilerFactory): +@define(kw_only=True) +class TilerFactory(BaseFactory): """Tiler Factory. Attributes: reader (rio_tiler.io.base.BaseReader): A rio-tiler reader. Defaults to `rio_tiler.io.Reader`. + path_dependency (Callable): Endpoint dependency defining `path` to pass to the reader init. + dataset_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset overwriting options (e.g nodata). + layer_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset indexes/bands/assets options. + render_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image rendering options (e.g add_mask). + colormap_dependency (Callable): Endpoint dependency defining ColorMap options (e.g colormap_name). + process_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image post-processing options (e.g rescaling, color-formula). + tms_dependency (Callable): Endpoint dependency defining TileMatrixSet to use. + reader_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining BaseReader options. + environment_dependency (Callable): Endpoint dependency to define GDAL environment at runtime. stats_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's statistics method. histogram_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for numpy's histogram method. img_preview_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's preview method. @@ -294,6 +244,21 @@ class TilerFactory(BaseTilerFactory): # Default reader is set to rio_tiler.io.Reader reader: Type[BaseReader] = Reader + # Reader dependency + reader_dependency: Type[DefaultDependency] = DefaultDependency + + # Path Dependency + path_dependency: Callable[..., Any] = DatasetPathParams + + # Indexes/Expression Dependencies + layer_dependency: Type[DefaultDependency] = BidxExprParams + + # Rasterio Dataset Options (nodata, unscale, resampling, reproject) + dataset_dependency: Type[DefaultDependency] = DatasetParams + + # Tile/Tilejson/WMTS Dependencies + tile_dependency: Type[DefaultDependency] = TileParams + # Statistics/Histogram Dependencies stats_dependency: Type[DefaultDependency] = StatisticsParams histogram_dependency: Type[DefaultDependency] = HistogramParams @@ -302,8 +267,24 @@ class TilerFactory(BaseTilerFactory): img_preview_dependency: Type[DefaultDependency] = PreviewParams img_part_dependency: Type[DefaultDependency] = PartFeatureParams - # Tile/Tilejson/WMTS Dependencies - tile_dependency: Type[DefaultDependency] = TileParams + # Post Processing Dependencies (algorithm) + process_dependency: Callable[ + ..., Optional[BaseAlgorithm] + ] = available_algorithms.dependency + + # Image rendering Dependencies + rescale_dependency: Callable[..., Optional[RescaleType]] = RescalingParams + color_formula_dependency: Callable[..., Optional[str]] = ColorFormulaParams + colormap_dependency: Callable[..., Optional[ColorMapType]] = ColorMapParams + render_dependency: Type[DefaultDependency] = ImageRenderingParams + + # GDAL ENV dependency + environment_dependency: Callable[..., Dict] = field(default=lambda: {}) + + # TileMatrixSet dependency + supported_tms: TileMatrixSets = morecantile_tms + + templates: Jinja2Templates = DEFAULT_TEMPLATES # Add/Remove some endpoints add_preview: bool = True @@ -530,18 +511,6 @@ def geojson_statistics( def tile(self): # noqa: C901 """Register /tiles endpoint.""" - @self.router.get(r"/tiles/{z}/{x}/{y}", **img_endpoint_params, deprecated=True) - @self.router.get( - r"/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params, deprecated=True - ) - @self.router.get( - r"/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params, deprecated=True - ) - @self.router.get( - r"/tiles/{z}/{x}/{y}@{scale}x.{format}", - **img_endpoint_params, - deprecated=True, - ) @self.router.get(r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) @self.router.get( r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params @@ -574,10 +543,15 @@ def tile( ], tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", - ] = self.default_tms, + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], scale: Annotated[ - conint(gt=0, le=4), "Tile size scale. 1=256x256, 2=512x512..." + int, + Field( + gt=0, le=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), ] = 1, format: Annotated[ ImageType, @@ -633,13 +607,6 @@ def tile( def tilejson(self): # noqa: C901 """Register /tilejson.json endpoint.""" - @self.router.get( - "/tilejson.json", - response_model=TileJSON, - responses={200: {"description": "Return a tilejson"}}, - response_model_exclude_none=True, - deprecated=True, - ) @self.router.get( "/{tileMatrixSetId}/tilejson.json", response_model=TileJSON, @@ -650,8 +617,10 @@ def tilejson( request: Request, tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", - ] = self.default_tms, + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], src_path=Depends(self.path_dependency), tile_format: Annotated[ Optional[ImageType], @@ -726,15 +695,16 @@ def tilejson( def map_viewer(self): # noqa: C901 """Register /map endpoint.""" - @self.router.get("/map", response_class=HTMLResponse, deprecated=True) @self.router.get("/{tileMatrixSetId}/map", response_class=HTMLResponse) def map_viewer( request: Request, - src_path=Depends(self.path_dependency), tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", - ] = self.default_tms, + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + src_path=Depends(self.path_dependency), tile_format: Annotated[ Optional[ImageType], Query( @@ -788,9 +758,6 @@ def map_viewer( def wmts(self): # noqa: C901 """Register /wmts endpoint.""" - @self.router.get( - "/WMTSCapabilities.xml", response_class=XMLResponse, deprecated=True - ) @self.router.get( "/{tileMatrixSetId}/WMTSCapabilities.xml", response_class=XMLResponse ) @@ -798,8 +765,10 @@ def wmts( request: Request, tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", - ] = self.default_tms, + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], src_path=Depends(self.path_dependency), tile_format: Annotated[ ImageType, @@ -1139,7 +1108,7 @@ def feature_image( return Response(content, media_type=media_type) -@dataclass +@define(kw_only=True) class MultiBaseTilerFactory(TilerFactory): """Custom Tiler Factory for MultiBaseReader classes. @@ -1387,7 +1356,7 @@ def geojson_statistics( return fc.features[0] if isinstance(geojson, Feature) else fc -@dataclass +@define(kw_only=True) class MultiBandTilerFactory(TilerFactory): """Custom Tiler Factory for MultiBandReader classes. @@ -1589,41 +1558,12 @@ def geojson_statistics( return fc.features[0] if isinstance(geojson, Feature) else fc -@dataclass -class TMSFactory: +@define(kw_only=True) +class TMSFactory(BaseFactory): """TileMatrixSet endpoints Factory.""" supported_tms: TileMatrixSets = morecantile_tms - # FastAPI router - router: APIRouter = field(default_factory=APIRouter) - - # Router Prefix is needed to find the path for /tile if the TilerFactory.router is mounted - # with other router (multiple `.../tile` routes). - # e.g if you mount the route with `/cog` prefix, set router_prefix to cog and - router_prefix: str = "" - - def __post_init__(self): - """Post Init: register route and configure specific options.""" - self.register_routes() - - def url_for(self, request: Request, name: str, **path_params: Any) -> str: - """Return full url (with prefix) for a specific endpoint.""" - url_path = self.router.url_path_for(name, **path_params) - base_url = str(request.base_url) - if self.router_prefix: - prefix = self.router_prefix.lstrip("/") - # If we have prefix with custom path param we check and replace them with - # the path params provided - if "{" in prefix: - _, path_format, param_convertors = compile_path(prefix) - prefix, _ = replace_params( - path_format, param_convertors, request.path_params.copy() - ) - base_url += prefix - - return str(url_path.make_absolute_url(base_url=base_url)) - def register_routes(self): """Register TMS endpoint routes.""" @@ -1695,18 +1635,15 @@ async def tilematrixset( return self.supported_tms.get(tileMatrixSetId) -@dataclass -class AlgorithmFactory: +@define(kw_only=True) +class AlgorithmFactory(BaseFactory): """Algorithm endpoints Factory.""" # Supported algorithm supported_algorithm: Algorithms = available_algorithms - # FastAPI router - router: APIRouter = field(default_factory=APIRouter) - - def __post_init__(self): - """Post Init: register routes""" + def register_routes(self): + """Register Algorithm routes.""" def metadata(algorithm: BaseAlgorithm) -> AlgorithmMetadata: """Algorithm Metadata""" @@ -1778,24 +1715,15 @@ def algorithm_metadata( return metadata(self.supported_algorithm.get(algorithm)) -@dataclass -class ColorMapFactory: +@define(kw_only=True) +class ColorMapFactory(BaseFactory): """Colormap endpoints Factory.""" # Supported colormaps supported_colormaps: ColorMaps = default_cmap - # FastAPI router - router: APIRouter = field(default_factory=APIRouter) - - def url_for(self, request: Request, name: str, **path_params: Any) -> str: - """Return full url (with prefix) for a specific endpoint.""" - url_path = self.router.url_path_for(name, **path_params) - base_url = str(request.base_url) - return str(url_path.make_absolute_url(base_url=base_url)) - - def __post_init__(self): # noqa: C901 - """Post Init: register routes""" + def register_routes(self): # noqa: C901 + """Register ColorMap routes.""" @self.router.get( "/colorMaps", diff --git a/src/titiler/extensions/pyproject.toml b/src/titiler/extensions/pyproject.toml index 980d0a5a1..cbcf916a4 100644 --- a/src/titiler/extensions/pyproject.toml +++ b/src/titiler/extensions/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.18.6" + "titiler.core==0.18.9" ] [project.optional-dependencies] diff --git a/src/titiler/extensions/tests/test_viewer.py b/src/titiler/extensions/tests/test_viewer.py index 2a56f119b..18a4cd929 100644 --- a/src/titiler/extensions/tests/test_viewer.py +++ b/src/titiler/extensions/tests/test_viewer.py @@ -25,7 +25,9 @@ def test_cogViewerExtension(): def test_stacViewerExtension(): """Test stacViewerExtension class.""" tiler = MultiBaseTilerFactory(reader=STACReader) - tiler_plus_viewer = MultiBaseTilerFactory(extensions=[stacViewerExtension()]) + tiler_plus_viewer = MultiBaseTilerFactory( + reader=STACReader, extensions=[stacViewerExtension()] + ) assert len(tiler_plus_viewer.router.routes) == len(tiler.router.routes) + 1 app = FastAPI() diff --git a/src/titiler/extensions/titiler/extensions/__init__.py b/src/titiler/extensions/titiler/extensions/__init__.py index 84260a30e..0052e92fa 100644 --- a/src/titiler/extensions/titiler/extensions/__init__.py +++ b/src/titiler/extensions/titiler/extensions/__init__.py @@ -1,6 +1,6 @@ """titiler.extensions""" -__version__ = "0.18.6" +__version__ = "0.18.9" from .cogeo import cogValidateExtension # noqa from .stac import stacExtension # noqa diff --git a/src/titiler/extensions/titiler/extensions/cogeo.py b/src/titiler/extensions/titiler/extensions/cogeo.py index 733afd715..92732a23b 100644 --- a/src/titiler/extensions/titiler/extensions/cogeo.py +++ b/src/titiler/extensions/titiler/extensions/cogeo.py @@ -1,11 +1,10 @@ """rio-cogeo Extension.""" -from dataclasses import dataclass - +from attrs import define from fastapi import Depends, Query from typing_extensions import Annotated -from titiler.core.factory import BaseTilerFactory, FactoryExtension +from titiler.core.factory import FactoryExtension, TilerFactory from titiler.core.resources.responses import JSONResponse try: @@ -16,11 +15,11 @@ Info = None -@dataclass +@define class cogValidateExtension(FactoryExtension): """Add /validate endpoint to a COG TilerFactory.""" - def register(self, factory: BaseTilerFactory): + def register(self, factory: TilerFactory): """Register endpoint to the tiler factory.""" assert ( diff --git a/src/titiler/extensions/titiler/extensions/stac.py b/src/titiler/extensions/titiler/extensions/stac.py index 257a851fe..4c47284d8 100644 --- a/src/titiler/extensions/titiler/extensions/stac.py +++ b/src/titiler/extensions/titiler/extensions/stac.py @@ -1,12 +1,12 @@ """rio-stac Extension.""" -from dataclasses import dataclass from typing import Any, Dict, List, Literal, Optional +from attrs import define from fastapi import Depends, Query from typing_extensions import Annotated, TypedDict -from titiler.core.factory import BaseTilerFactory, FactoryExtension +from titiler.core.factory import FactoryExtension, TilerFactory try: import pystac @@ -33,11 +33,11 @@ class Item(TypedDict, total=False): collection: str -@dataclass +@define class stacExtension(FactoryExtension): """Add /stac endpoint to a COG TilerFactory.""" - def register(self, factory: BaseTilerFactory): + def register(self, factory: TilerFactory): """Register endpoint to the tiler factory.""" assert ( diff --git a/src/titiler/extensions/titiler/extensions/viewer.py b/src/titiler/extensions/titiler/extensions/viewer.py index a673d4ef5..daa3e9bde 100644 --- a/src/titiler/extensions/titiler/extensions/viewer.py +++ b/src/titiler/extensions/titiler/extensions/viewer.py @@ -1,13 +1,12 @@ """titiler Viewer Extensions.""" -from dataclasses import dataclass - import jinja2 +from attrs import define from starlette.requests import Request from starlette.responses import HTMLResponse from starlette.templating import Jinja2Templates -from titiler.core.factory import BaseTilerFactory, FactoryExtension +from titiler.core.factory import FactoryExtension, TilerFactory jinja2_env = jinja2.Environment( loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]) @@ -15,13 +14,13 @@ DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) -@dataclass +@define class cogViewerExtension(FactoryExtension): """Add /viewer endpoint to the TilerFactory.""" templates: Jinja2Templates = DEFAULT_TEMPLATES - def register(self, factory: BaseTilerFactory): + def register(self, factory: TilerFactory): """Register endpoint to the tiler factory.""" @factory.router.get("/viewer", response_class=HTMLResponse) @@ -42,13 +41,13 @@ def cog_viewer(request: Request): ) -@dataclass +@define class stacViewerExtension(FactoryExtension): """Add /viewer endpoint to the TilerFactory.""" templates: Jinja2Templates = DEFAULT_TEMPLATES - def register(self, factory: BaseTilerFactory): + def register(self, factory: TilerFactory): """Register endpoint to the tiler factory.""" @factory.router.get("/viewer", response_class=HTMLResponse) diff --git a/src/titiler/extensions/titiler/extensions/wms.py b/src/titiler/extensions/titiler/extensions/wms.py index 096a8eb13..916d909e6 100644 --- a/src/titiler/extensions/titiler/extensions/wms.py +++ b/src/titiler/extensions/titiler/extensions/wms.py @@ -1,6 +1,6 @@ """wms Extension.""" -from dataclasses import dataclass, field +from dataclasses import dataclass from enum import Enum from typing import Any, Dict, List, Optional, Set, Tuple from urllib.parse import urlencode @@ -8,6 +8,7 @@ import jinja2 import numpy import rasterio +from attrs import define, field from fastapi import Depends, HTTPException from rasterio.crs import CRS from rio_tiler.models import ImageData @@ -18,7 +19,7 @@ from starlette.templating import Jinja2Templates from titiler.core.dependencies import ColorFormulaParams, RescalingParams -from titiler.core.factory import BaseTilerFactory, FactoryExtension +from titiler.core.factory import FactoryExtension, TilerFactory from titiler.core.resources.enums import ImageType, MediaType from titiler.core.utils import render_image @@ -56,13 +57,13 @@ def feed(self, array: numpy.ma.MaskedArray): self.mosaic.mask = mask -@dataclass +@define class wmsExtension(FactoryExtension): """Add /wms endpoint to a TilerFactory.""" - supported_crs: List[str] = field(default_factory=lambda: ["EPSG:4326"]) + supported_crs: List[str] = field(default=["EPSG:4326"]) supported_format: List[str] = field( - default_factory=lambda: [ + default=[ "image/png", "image/jpeg", "image/jpg", @@ -71,12 +72,10 @@ class wmsExtension(FactoryExtension): "image/tiff; application=geotiff", ] ) - supported_version: List[str] = field( - default_factory=lambda: ["1.0.0", "1.1.1", "1.3.0"] - ) + supported_version: List[str] = field(default=["1.0.0", "1.1.1", "1.3.0"]) templates: Jinja2Templates = DEFAULT_TEMPLATES - def register(self, factory: BaseTilerFactory): # noqa: C901 + def register(self, factory: TilerFactory): # noqa: C901 """Register endpoint to the tiler factory.""" @factory.router.get( diff --git a/src/titiler/mosaic/pyproject.toml b/src/titiler/mosaic/pyproject.toml index 575c2b6d5..121218d78 100644 --- a/src/titiler/mosaic/pyproject.toml +++ b/src/titiler/mosaic/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.core==0.18.6", + "titiler.core==0.18.9", "cogeo-mosaic>=7.0,<8.0", ] diff --git a/src/titiler/mosaic/tests/test_factory.py b/src/titiler/mosaic/tests/test_factory.py index d5db4e8ff..5dfa49762 100644 --- a/src/titiler/mosaic/tests/test_factory.py +++ b/src/titiler/mosaic/tests/test_factory.py @@ -43,7 +43,7 @@ def test_MosaicTilerFactory(): optional_headers=[OptionalHeader.x_assets], router_prefix="mosaic", ) - assert len(mosaic.router.routes) == 24 + assert len(mosaic.router.routes) == 16 app = FastAPI() app.include_router(mosaic.router, prefix="/mosaic") @@ -104,10 +104,6 @@ def test_MosaicTilerFactory(): ) assert response.status_code == 200 - response = client.get("/mosaic/tiles/7/37/45", params={"url": mosaic_file}) - assert response.status_code == 200 - assert response.headers["X-Assets"] - response = client.get( "/mosaic/tiles/WebMercatorQuad/7/37/45", params={"url": mosaic_file} ) @@ -122,7 +118,8 @@ def test_MosaicTilerFactory(): # Buffer response = client.get( - "/mosaic/tiles/7/37/45.npy", params={"url": mosaic_file, "buffer": 10} + "/mosaic/tiles/WebMercatorQuad/7/37/45.npy", + params={"url": mosaic_file, "buffer": 10}, ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -130,7 +127,7 @@ def test_MosaicTilerFactory(): assert npy_tile.shape == (4, 276, 276) # mask + data response = client.get( - "/mosaic/tilejson.json", + "/mosaic/WebMercatorQuad/tilejson.json", params={ "url": mosaic_file, "tile_format": "png", @@ -148,7 +145,7 @@ def test_MosaicTilerFactory(): assert body["maxzoom"] == 9 response = client.get( - "/mosaic/tilejson.json", + "/mosaic/WebMercatorQuad/tilejson.json", params={ "url": mosaic_file, "tile_format": "png", @@ -168,7 +165,7 @@ def test_MosaicTilerFactory(): assert "tileMatrixSetId" not in body["tiles"][0] response = client.get( - "/mosaic/WMTSCapabilities.xml", + "/mosaic/WebMercatorQuad/WMTSCapabilities.xml", params={ "url": mosaic_file, "tile_format": "png", @@ -186,7 +183,7 @@ def test_MosaicTilerFactory(): assert response.status_code == 200 response = client.get( - "/mosaic/7/36/45/assets", + "/mosaic/WebMercatorQuad/7/36/45/assets", params={"url": mosaic_file}, ) assert response.status_code == 200 @@ -260,7 +257,7 @@ class BackendParams(DefaultDependency): def test_MosaicTilerFactory_BackendParams(): """Test MosaicTilerFactory factory with Backend dependency.""" mosaic = MosaicTilerFactory( - reader=FileBackend, + backend=FileBackend, backend_dependency=BackendParams, router_prefix="/mosaic", ) @@ -270,7 +267,7 @@ def test_MosaicTilerFactory_BackendParams(): with tmpmosaic() as mosaic_file: response = client.get( - "/mosaic/tilejson.json", + "/mosaic/WebMercatorQuad/tilejson.json", params={"url": mosaic_file}, ) assert response.json()["minzoom"] == 4 @@ -297,14 +294,17 @@ def test_MosaicTilerFactory_PixelSelectionParams(): client = TestClient(app) with tmpmosaic() as mosaic_file: - response = client.get("/mosaic/tiles/7/37/45.npy", params={"url": mosaic_file}) + response = client.get( + "/mosaic/tiles/WebMercatorQuad/7/37/45.npy", params={"url": mosaic_file} + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" npy_tile = numpy.load(BytesIO(response.content)) assert npy_tile.shape == (4, 256, 256) # mask + data response = client.get( - "/mosaic_highest/tiles/7/37/45.npy", params={"url": mosaic_file} + "/mosaic_highest/tiles/WebMercatorQuad/7/37/45.npy", + params={"url": mosaic_file}, ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -324,13 +324,19 @@ def test_MosaicTilerFactory_strict_zoom(monkeypatch): with TestClient(app) as client: with tmpmosaic() as mosaic_file: - response = client.get("/tiles/7/37/45.png", params={"url": mosaic_file}) + response = client.get( + "/tiles/WebMercatorQuad/7/37/45.png", params={"url": mosaic_file} + ) assert response.status_code == 200 - response = client.get("/tiles/6/18/22.png", params={"url": mosaic_file}) + response = client.get( + "/tiles/WebMercatorQuad/6/18/22.png", params={"url": mosaic_file} + ) assert response.status_code == 400 assert "Invalid ZOOM level 6" in response.text - response = client.get("/tiles/11/594/734.png", params={"url": mosaic_file}) + response = client.get( + "/tiles/WebMercatorQuad/11/594/734.png", params={"url": mosaic_file} + ) assert response.status_code == 400 assert "Invalid ZOOM level 11" in response.text diff --git a/src/titiler/mosaic/titiler/mosaic/__init__.py b/src/titiler/mosaic/titiler/mosaic/__init__.py index 3d6450fa1..528d36b61 100644 --- a/src/titiler/mosaic/titiler/mosaic/__init__.py +++ b/src/titiler/mosaic/titiler/mosaic/__init__.py @@ -1,6 +1,6 @@ """titiler.mosaic""" -__version__ = "0.18.6" +__version__ = "0.18.9" from . import errors, factory # noqa from .factory import MosaicTilerFactory # noqa diff --git a/src/titiler/mosaic/titiler/mosaic/factory.py b/src/titiler/mosaic/titiler/mosaic/factory.py index facfc826d..366e10546 100644 --- a/src/titiler/mosaic/titiler/mosaic/factory.py +++ b/src/titiler/mosaic/titiler/mosaic/factory.py @@ -1,11 +1,11 @@ """TiTiler.mosaic Router factories.""" import os -from dataclasses import dataclass -from typing import Callable, Dict, Literal, Optional, Type, Union +from typing import Any, Callable, Dict, List, Literal, Optional, Type, Union from urllib.parse import urlencode import rasterio +from attrs import define, field from cogeo_mosaic.backends import BaseBackend, MosaicBackend from cogeo_mosaic.models import Info as mosaicInfo from cogeo_mosaic.mosaic import MosaicJSON @@ -14,18 +14,34 @@ from geojson_pydantic.geometries import Polygon from morecantile import tms as morecantile_tms from morecantile.defaults import TileMatrixSets -from pydantic import conint +from pydantic import Field from rio_tiler.constants import MAX_THREADS, WGS84_CRS from rio_tiler.io import BaseReader, MultiBandReader, MultiBaseReader, Reader from rio_tiler.models import Bounds from rio_tiler.mosaic.methods import PixelSelectionMethod from rio_tiler.mosaic.methods.base import MosaicMethodBase +from rio_tiler.types import ColorMapType from starlette.requests import Request from starlette.responses import HTMLResponse, Response +from starlette.templating import Jinja2Templates from typing_extensions import Annotated -from titiler.core.dependencies import CoordCRSParams, DefaultDependency, TileParams -from titiler.core.factory import BaseTilerFactory, img_endpoint_params +from titiler.core.algorithm import BaseAlgorithm +from titiler.core.algorithm import algorithms as available_algorithms +from titiler.core.dependencies import ( + BidxExprParams, + ColorFormulaParams, + ColorMapParams, + CoordCRSParams, + DatasetParams, + DatasetPathParams, + DefaultDependency, + ImageRenderingParams, + RescaleType, + RescalingParams, + TileParams, +) +from titiler.core.factory import DEFAULT_TEMPLATES, BaseFactory, 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, JSONResponse, XMLResponse @@ -45,44 +61,59 @@ def PixelSelectionParams( return PixelSelectionMethod[pixel_selection].value() -@dataclass -class MosaicTilerFactory(BaseTilerFactory): - """ - MosaicTiler Factory. +@define(kw_only=True) +class MosaicTilerFactory(BaseFactory): + """MosaicTiler Factory.""" - The main difference with titiler.endpoint.factory.TilerFactory is that this factory - needs the `reader` to be of `cogeo_mosaic.backends.BaseBackend` type (e.g MosaicBackend) and a `dataset_reader` (BaseReader). - """ + backend: Type[BaseBackend] = MosaicBackend + backend_dependency: Type[DefaultDependency] = DefaultDependency - reader: Type[BaseBackend] = MosaicBackend dataset_reader: Union[ Type[BaseReader], Type[MultiBaseReader], Type[MultiBandReader], ] = Reader + reader_dependency: Type[DefaultDependency] = DefaultDependency - backend_dependency: Type[DefaultDependency] = DefaultDependency + # Path Dependency + path_dependency: Callable[..., Any] = DatasetPathParams - pixel_selection_dependency: Callable[..., MosaicMethodBase] = PixelSelectionParams + # Indexes/Expression Dependencies + layer_dependency: Type[DefaultDependency] = BidxExprParams + + # Rasterio Dataset Options (nodata, unscale, resampling, reproject) + dataset_dependency: Type[DefaultDependency] = DatasetParams # Tile/Tilejson/WMTS Dependencies tile_dependency: Type[DefaultDependency] = TileParams + # Post Processing Dependencies (algorithm) + process_dependency: Callable[ + ..., Optional[BaseAlgorithm] + ] = available_algorithms.dependency + + # Image rendering Dependencies + rescale_dependency: Callable[..., Optional[RescaleType]] = RescalingParams + color_formula_dependency: Callable[..., Optional[str]] = ColorFormulaParams + colormap_dependency: Callable[..., Optional[ColorMapType]] = ColorMapParams + render_dependency: Type[DefaultDependency] = ImageRenderingParams + + pixel_selection_dependency: Callable[..., MosaicMethodBase] = PixelSelectionParams + + # GDAL ENV dependency + environment_dependency: Callable[..., Dict] = field(default=lambda: {}) + supported_tms: TileMatrixSets = morecantile_tms - default_tms: Optional[str] = None + + templates: Jinja2Templates = DEFAULT_TEMPLATES + + optional_headers: List[OptionalHeader] = field(factory=list) # Add/Remove some endpoints add_viewer: bool = True def register_routes(self): - """ - This Method register routes to the router. - - Because we wrap the endpoints in a class we cannot define the routes as - methods (because of the self argument). The HACK is to define routes inside - the class method and register them after the class initialization. - - """ + """This Method register routes to the router.""" self.read() self.bounds() @@ -118,7 +149,7 @@ def read( ): """Read a MosaicJSON""" with rasterio.Env(**env): - with self.reader( + with self.backend( src_path, reader=self.dataset_reader, reader_options=reader_params.as_dict(), @@ -145,7 +176,7 @@ def bounds( ): """Return the bounds of the MosaicJSON.""" with rasterio.Env(**env): - with self.reader( + with self.backend( src_path, reader=self.dataset_reader, reader_options=reader_params.as_dict(), @@ -172,7 +203,7 @@ def info( ): """Return basic info.""" with rasterio.Env(**env): - with self.reader( + with self.backend( src_path, reader=self.dataset_reader, reader_options=reader_params.as_dict(), @@ -200,7 +231,7 @@ def info_geojson( ): """Return mosaic's basic info as a GeoJSON feature.""" with rasterio.Env(**env): - with self.reader( + with self.backend( src_path, reader=self.dataset_reader, reader_options=reader_params.as_dict(), @@ -219,18 +250,6 @@ def info_geojson( def tile(self): # noqa: C901 """Register /tiles endpoints.""" - @self.router.get("/tiles/{z}/{x}/{y}", **img_endpoint_params, deprecated=True) - @self.router.get( - "/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params, deprecated=True - ) - @self.router.get( - "/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params, deprecated=True - ) - @self.router.get( - "/tiles/{z}/{x}/{y}@{scale}x.{format}", - **img_endpoint_params, - deprecated=True, - ) @self.router.get("/tiles/{tileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) @self.router.get( "/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params @@ -263,11 +282,15 @@ def tile( ], tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", - ] = self.default_tms, + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], scale: Annotated[ - conint(gt=0, le=4), - "Tile size scale. 1=256x256, 2=512x512...", + int, + Field( + gt=0, le=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), ] = 1, format: Annotated[ ImageType, @@ -303,7 +326,7 @@ def tile( tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader( + with self.backend( src_path, tms=tms, reader=self.dataset_reader, @@ -354,13 +377,6 @@ def tile( def tilejson(self): # noqa: C901 """Add tilejson endpoint.""" - @self.router.get( - "/tilejson.json", - response_model=TileJSON, - responses={200: {"description": "Return a tilejson"}}, - response_model_exclude_none=True, - deprecated=True, - ) @self.router.get( "/{tileMatrixSetId}/tilejson.json", response_model=TileJSON, @@ -371,8 +387,10 @@ def tilejson( request: Request, tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", - ] = self.default_tms, + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], src_path=Depends(self.path_dependency), tile_format: Annotated[ Optional[ImageType], @@ -436,7 +454,7 @@ def tilejson( tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader( + with self.backend( src_path, tms=tms, reader=self.dataset_reader, @@ -458,15 +476,16 @@ def tilejson( def map_viewer(self): # noqa: C901 """Register /map endpoint.""" - @self.router.get("/map", response_class=HTMLResponse, deprecated=True) @self.router.get("/{tileMatrixSetId}/map", response_class=HTMLResponse) def map_viewer( request: Request, - src_path=Depends(self.path_dependency), tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", - ] = self.default_tms, + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + src_path=Depends(self.path_dependency), tile_format: Annotated[ Optional[ImageType], Query( @@ -522,9 +541,6 @@ def map_viewer( def wmts(self): # noqa: C901 """Add wmts endpoint.""" - @self.router.get( - "/WMTSCapabilities.xml", response_class=XMLResponse, deprecated=True - ) @self.router.get( "/{tileMatrixSetId}/WMTSCapabilities.xml", response_class=XMLResponse ) @@ -532,8 +548,10 @@ def wmts( request: Request, tileMatrixSetId: Annotated[ Literal[tuple(self.supported_tms.list())], - f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", - ] = self.default_tms, + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], src_path=Depends(self.path_dependency), tile_format: Annotated[ ImageType, @@ -596,7 +614,7 @@ def wmts( tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader( + with self.backend( src_path, tms=tms, reader=self.dataset_reader, @@ -667,7 +685,7 @@ def point( threads = int(os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS)) with rasterio.Env(**env): - with self.reader( + with self.backend( src_path, reader=self.dataset_reader, reader_options=reader_params.as_dict(), @@ -717,7 +735,7 @@ def assets_for_bbox( ): """Return a list of assets which overlap a bounding box""" with rasterio.Env(**env): - with self.reader( + with self.backend( src_path, reader=self.dataset_reader, reader_options=reader_params.as_dict(), @@ -746,7 +764,7 @@ def assets_for_lon_lat( ): """Return a list of assets which overlap a point""" with rasterio.Env(**env): - with self.reader( + with self.backend( src_path, reader=self.dataset_reader, reader_options=reader_params.as_dict(), @@ -758,15 +776,17 @@ def assets_for_lon_lat( coord_crs=coord_crs or WGS84_CRS, ) - @self.router.get( - "/{z}/{x}/{y}/assets", - responses={200: {"description": "Return list of COGs"}}, - ) @self.router.get( "/{tileMatrixSetId}/{z}/{x}/{y}/assets", responses={200: {"description": "Return list of COGs"}}, ) def assets_for_tile( + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], z: Annotated[ int, Path( @@ -785,10 +805,6 @@ def assets_for_tile( description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.", ), ], - tileMatrixSetId: Annotated[ - Literal[tuple(self.supported_tms.list())], - f"Identifier selecting one of the TileMatrixSetId supported (default: '{self.default_tms}')", - ] = self.default_tms, src_path=Depends(self.path_dependency), backend_params=Depends(self.backend_dependency), reader_params=Depends(self.reader_dependency), @@ -797,7 +813,7 @@ def assets_for_tile( """Return a list of assets which overlap a given tile""" tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader( + with self.backend( src_path, tms=tms, reader=self.dataset_reader,