From 5b1de8445c9e6a247e7b60b8d955587949cca26b Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Tue, 23 Nov 2021 17:52:29 +0100 Subject: [PATCH 1/5] add method to add more dependencies to specific endpoints --- src/titiler/core/tests/test_factories.py | 102 +++++++++++++++++- src/titiler/core/titiler/core/factory.py | 41 ++++++- .../core/titiler/core/models/routes.py | 13 +++ 3 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/titiler/core/titiler/core/models/routes.py diff --git a/src/titiler/core/tests/test_factories.py b/src/titiler/core/tests/test_factories.py index 7a4fd1b2c..141814575 100644 --- a/src/titiler/core/tests/test_factories.py +++ b/src/titiler/core/tests/test_factories.py @@ -9,6 +9,8 @@ import attr import morecantile +import pytest +from requests.auth import HTTPBasicAuth from rio_tiler.io import BaseReader, COGReader, MultiBandReader, STACReader from titiler.core.dependencies import TMSParams, WebMercatorTMSParams @@ -23,7 +25,7 @@ from .conftest import DATA_DIR, mock_rasterio_open, parse_img -from fastapi import FastAPI, Query +from fastapi import Depends, FastAPI, HTTPException, Query, security, status from starlette.testclient import TestClient @@ -1131,3 +1133,101 @@ def test_TMSFactory(): body = response.json() assert body["type"] == "TileMatrixSetType" assert body["identifier"] == "WebMercatorQuad" + + +def test_TilerFactory_WithDependencies(): + """Test TilerFactory class.""" + + http_basic = security.HTTPBasic() + + def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic)): + print(credentials) + if credentials.username == "bob": + return True + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="You're not Bob", + headers={"WWW-Authenticate": "Basic"}, + ) + + cog = TilerFactory( + route_dependencies=[ + ( + [ + {"path": "/bounds", "method": "GET"}, + {"path": "/tiles/{z}/{x}/{y}", "method": "GET"}, + ], + [Depends(must_be_bob)], + ), + ], + ) + assert len(cog.router.routes) == 25 + assert cog.tms_dependency == TMSParams + + app = FastAPI() + app.include_router(cog.router, prefix="/something") + client = TestClient(app) + + response = client.get(f"/something/tilejson.json?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + assert response.json()["tilejson"] + + auth_bob = HTTPBasicAuth(username="bob", password="ILoveSponge") + auth_notbob = HTTPBasicAuth(username="notbob", password="IHateSponge") + + response = client.get( + f"/something/bounds?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_bob + ) + assert response.status_code == 200 + + with pytest.raises(HTTPException): + client.get( + f"/something/bounds?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_notbob + ) + + # response = client.get(f"/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000") + # assert response.status_code == 200 + # assert response.headers["content-type"] == "image/jpeg" + # timing = response.headers["server-timing"] + # assert "dataread;dur" in timing + # assert "postprocess;dur" in timing + # assert "format;dur" in timing + + # response = client.get( + # f"/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=-3.4028235e+38,3.4028235e+38" + # ) + # assert response.status_code == 200 + # assert response.headers["content-type"] == "image/jpeg" + # timing = response.headers["server-timing"] + # assert "dataread;dur" in timing + # assert "postprocess;dur" in timing + # assert "format;dur" in timing + + # response = client.get( + # f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1&return_mask=false" + # ) + # assert response.status_code == 200 + # assert response.headers["content-type"] == "image/tiff; application=geotiff" + # meta = parse_img(response.content) + # assert meta["dtype"] == "uint16" + # assert meta["count"] == 3 + # assert meta["width"] == 256 + # assert meta["height"] == 256 + + # response = client.get( + # f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif&expression=b1,b1,b1&return_mask=false" + # ) + # assert response.status_code == 200 + # assert response.headers["content-type"] == "image/tiff; application=geotiff" + # meta = parse_img(response.content) + # assert meta["dtype"] == "int32" + # assert meta["count"] == 3 + # assert meta["width"] == 256 + # assert meta["height"] == 256 + + # response = client.get( + # f"/tiles/8/84/47?url={DATA_DIR}/cog.tif&bidx=1&rescale=0,1000&colormap_name=viridis" + # ) + # assert response.status_code == 200 + # assert response.headers["content-type"] == "image/png" diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index 91d49af5a..edda5eb40 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -2,7 +2,7 @@ import abc from dataclasses import dataclass, field -from typing import Any, Callable, Dict, List, Optional, Type, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union from urllib.parse import urlencode import rasterio @@ -46,14 +46,17 @@ Statistics, StatisticsGeoJSON, ) +from titiler.core.models.routes import EndpointScope from titiler.core.resources.enums import ImageType, MediaType, OptionalHeader from titiler.core.resources.responses import GeoJSONResponse, JSONResponse, XMLResponse from titiler.core.utils import Timer -from fastapi import APIRouter, Body, Depends, Path, Query +from fastapi import APIRouter, Body, Depends, Path, Query, params +from fastapi.dependencies.utils import get_parameterless_sub_dependant from starlette.requests import Request from starlette.responses import Response +from starlette.routing import Match from starlette.templating import Jinja2Templates try: @@ -149,10 +152,18 @@ class BaseTilerFactory(metaclass=abc.ABCMeta): # 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[params.Depends]]] = field( + default_factory=list + ) + def __post_init__(self): """Post Init: register route and configure specific options.""" self.register_routes() + for scopes, dependencies in self.route_dependencies: + self.add_route_dependencies(scopes=scopes, dependencies=dependencies) + @abc.abstractmethod def register_routes(self): """Register Tiler Routes.""" @@ -166,6 +177,32 @@ def url_for(self, request: Request, name: str, **path_params: Any) -> str: base_url += self.router_prefix.lstrip("/") return url_path.make_absolute_url(base_url=base_url) + def add_route_dependencies( + self, + scopes: List[EndpointScope], + dependencies=List[params.Depends], + ): + """Add dependencies to routes. + + Allows a developer to add dependencies to a route after the route has been defined. + + """ + for route in self.router.routes: + for scope in scopes: + match, _ = route.matches({"type": "http", **scope}) + if match != Match.FULL: + continue + + # Mimicking how APIRoute handles dependencies: + # https://github.com/tiangolo/fastapi/blob/1760da0efa55585c19835d81afa8ca386036c325/fastapi/routing.py#L408-L412 + for depends in dependencies[::-1]: + route.dependant.dependencies.insert( # type: ignore + 0, + get_parameterless_sub_dependant( + depends=depends, path=route.path_format # type: ignore + ), + ) + @dataclass class TilerFactory(BaseTilerFactory): diff --git a/src/titiler/core/titiler/core/models/routes.py b/src/titiler/core/titiler/core/models/routes.py new file mode 100644 index 000000000..18a3dc566 --- /dev/null +++ b/src/titiler/core/titiler/core/models/routes.py @@ -0,0 +1,13 @@ +"""models for routes.""" + +from typing import Optional, TypedDict + + +class EndpointScope(TypedDict, total=False): + """Define endpoint.""" + + # More strict version of Starlette's Scope + # https://github.com/encode/starlette/blob/6af5c515e0a896cbf3f86ee043b88f6c24200bcf/starlette/types.py#L3 + path: str + method: str + type: Optional[str] # http or websocket From 14286ce011972a899bc5cd4ffaded77510bd3b95 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Tue, 23 Nov 2021 21:33:19 +0100 Subject: [PATCH 2/5] Update src/titiler/core/titiler/core/factory.py Co-authored-by: Anthony Lukach --- src/titiler/core/titiler/core/factory.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index edda5eb40..343f72b48 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -203,6 +203,12 @@ def add_route_dependencies( ), ) + # Register dependencies directly on route so that they aren't ignored if + # the routes are later associated with an app (e.g. app.include_router(router)) + # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/applications.py#L337-L360 + # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678 + route.dependencies.extend(dependencies) + @dataclass class TilerFactory(BaseTilerFactory): From c7b6f4e699c1af172731cf8d651cf7e3f214da8d Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Tue, 23 Nov 2021 21:49:47 +0100 Subject: [PATCH 3/5] fix indent and moar tests --- src/titiler/core/tests/test_factories.py | 107 +++++++++++------------ src/titiler/core/titiler/core/factory.py | 10 +-- 2 files changed, 58 insertions(+), 59 deletions(-) diff --git a/src/titiler/core/tests/test_factories.py b/src/titiler/core/tests/test_factories.py index 141814575..472ac3055 100644 --- a/src/titiler/core/tests/test_factories.py +++ b/src/titiler/core/tests/test_factories.py @@ -9,7 +9,6 @@ import attr import morecantile -import pytest from requests.auth import HTTPBasicAuth from rio_tiler.io import BaseReader, COGReader, MultiBandReader, STACReader @@ -1141,7 +1140,6 @@ def test_TilerFactory_WithDependencies(): http_basic = security.HTTPBasic() def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic)): - print(credentials) if credentials.username == "bob": return True raise HTTPException( @@ -1160,6 +1158,7 @@ def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic) [Depends(must_be_bob)], ), ], + router_prefix="something", ) assert len(cog.router.routes) == 25 assert cog.tms_dependency == TMSParams @@ -1168,66 +1167,66 @@ def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic) app.include_router(cog.router, prefix="/something") client = TestClient(app) + auth_bob = HTTPBasicAuth(username="bob", password="ILoveSponge") + auth_notbob = HTTPBasicAuth(username="notbob", password="IHateSponge") + response = client.get(f"/something/tilejson.json?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] - auth_bob = HTTPBasicAuth(username="bob", password="ILoveSponge") - auth_notbob = HTTPBasicAuth(username="notbob", password="IHateSponge") - response = client.get( f"/something/bounds?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_bob ) assert response.status_code == 200 - with pytest.raises(HTTPException): - client.get( - f"/something/bounds?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_notbob - ) + response = client.get( + f"/something/bounds?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_notbob + ) + assert response.status_code == 401 + assert response.json()["detail"] == "You're not Bob" + + response = client.get( + f"/something/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_bob + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/jpeg" + + response = client.get( + f"/something/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000", + auth=auth_notbob, + ) + assert response.status_code == 401 + assert response.json()["detail"] == "You're not Bob" + + response = client.get( + f"/something/tiles/8/87/48.jpeg?url={DATA_DIR}/cog.tif&rescale=0,1000" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/jpeg" + + cog = TilerFactory(router_prefix="something") + cog.add_route_dependencies( + scopes=[{"path": "/bounds", "method": "GET"}], + dependencies=[Depends(must_be_bob)], + ) + + app = FastAPI() + app.include_router(cog.router, prefix="/something") + client = TestClient(app) + + response = client.get(f"/something/tilejson.json?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + assert response.json()["tilejson"] + + response = client.get( + f"/something/bounds?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_bob + ) + assert response.status_code == 200 - # response = client.get(f"/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000") - # assert response.status_code == 200 - # assert response.headers["content-type"] == "image/jpeg" - # timing = response.headers["server-timing"] - # assert "dataread;dur" in timing - # assert "postprocess;dur" in timing - # assert "format;dur" in timing - - # response = client.get( - # f"/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=-3.4028235e+38,3.4028235e+38" - # ) - # assert response.status_code == 200 - # assert response.headers["content-type"] == "image/jpeg" - # timing = response.headers["server-timing"] - # assert "dataread;dur" in timing - # assert "postprocess;dur" in timing - # assert "format;dur" in timing - - # response = client.get( - # f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1&return_mask=false" - # ) - # assert response.status_code == 200 - # assert response.headers["content-type"] == "image/tiff; application=geotiff" - # meta = parse_img(response.content) - # assert meta["dtype"] == "uint16" - # assert meta["count"] == 3 - # assert meta["width"] == 256 - # assert meta["height"] == 256 - - # response = client.get( - # f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif&expression=b1,b1,b1&return_mask=false" - # ) - # assert response.status_code == 200 - # assert response.headers["content-type"] == "image/tiff; application=geotiff" - # meta = parse_img(response.content) - # assert meta["dtype"] == "int32" - # assert meta["count"] == 3 - # assert meta["width"] == 256 - # assert meta["height"] == 256 - - # response = client.get( - # f"/tiles/8/84/47?url={DATA_DIR}/cog.tif&bidx=1&rescale=0,1000&colormap_name=viridis" - # ) - # assert response.status_code == 200 - # assert response.headers["content-type"] == "image/png" + response = client.get( + f"/something/bounds?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_notbob + ) + assert response.status_code == 401 + assert response.json()["detail"] == "You're not Bob" diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index 343f72b48..c0438c70b 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -203,11 +203,11 @@ def add_route_dependencies( ), ) - # Register dependencies directly on route so that they aren't ignored if - # the routes are later associated with an app (e.g. app.include_router(router)) - # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/applications.py#L337-L360 - # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678 - route.dependencies.extend(dependencies) + # Register dependencies directly on route so that they aren't ignored if + # the routes are later associated with an app (e.g. app.include_router(router)) + # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/applications.py#L337-L360 + # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678 + route.dependencies.extend(dependencies) # type: ignore @dataclass From 3ec94c664a0a18ef0bb35b2dac675cbf863c5006 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Tue, 23 Nov 2021 22:20:08 +0100 Subject: [PATCH 4/5] fix python 3.7 TypeDict and add routing.add_route_dependencies --- src/titiler/core/setup.py | 1 + ...test_CustomAPIRoute.py => test_routing.py} | 57 ++++++++++++++++++- src/titiler/core/titiler/core/factory.py | 3 +- .../core/titiler/core/models/routes.py | 13 ----- src/titiler/core/titiler/core/routing.py | 55 +++++++++++++++++- 5 files changed, 112 insertions(+), 17 deletions(-) rename src/titiler/core/tests/{test_CustomAPIRoute.py => test_routing.py} (71%) delete mode 100644 src/titiler/core/titiler/core/models/routes.py diff --git a/src/titiler/core/setup.py b/src/titiler/core/setup.py index dfeb6d4c4..ec6b47429 100644 --- a/src/titiler/core/setup.py +++ b/src/titiler/core/setup.py @@ -15,6 +15,7 @@ "rio-tiler>=3.0.0a6,<3.1", "simplejson", "importlib_resources>=1.1.0;python_version<'3.9'", + "typing_extensions;python_version<'3.8'", ] extra_reqs = { "test": ["pytest", "pytest-cov", "pytest-asyncio", "requests"], diff --git a/src/titiler/core/tests/test_CustomAPIRoute.py b/src/titiler/core/tests/test_routing.py similarity index 71% rename from src/titiler/core/tests/test_CustomAPIRoute.py rename to src/titiler/core/tests/test_routing.py index a900c697f..05a3444c9 100644 --- a/src/titiler/core/tests/test_CustomAPIRoute.py +++ b/src/titiler/core/tests/test_routing.py @@ -5,10 +5,11 @@ import pytest import rasterio from rasterio._env import get_gdal_config +from requests.auth import HTTPBasicAuth -from titiler.core.routing import apiroute_factory +from titiler.core.routing import add_route_dependencies, apiroute_factory -from fastapi import APIRouter, FastAPI +from fastapi import APIRouter, Depends, FastAPI, HTTPException, security, status from starlette.testclient import TestClient @@ -126,3 +127,55 @@ async def home3(): response = client.get("/afuture") assert response.json()["env"] == "FALSE" + + +def test_register_deps(): + """Test add_route_dependencies.""" + + http_basic = security.HTTPBasic() + + def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic)): + if credentials.username == "bob": + return True + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="You're not Bob", + headers={"WWW-Authenticate": "Basic"}, + ) + + app = FastAPI() + + @app.get("/one") + def one(): + """one.""" + return "one" + + @app.get("/two") + def two(): + """two.""" + return "two" + + auth_bob = HTTPBasicAuth(username="bob", password="ILoveSponge") + auth_notbob = HTTPBasicAuth(username="notbob", password="IHateSponge") + + add_route_dependencies( + app.routes, + scopes=[ + {"path": "/one", "method": "GET"}, + ], + dependencies=[Depends(must_be_bob)], + ) + + client = TestClient(app) + + response = client.get("/one", auth=auth_bob) + assert response.status_code == 200 + + response = client.get("/one", auth=auth_notbob) + assert response.status_code == 401 + + response = client.get("/two", auth=auth_bob) + assert response.status_code == 200 + + response = client.get("/two", auth=auth_notbob) + assert response.status_code == 200 diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index c0438c70b..bd0942674 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -46,9 +46,9 @@ Statistics, StatisticsGeoJSON, ) -from titiler.core.models.routes import EndpointScope from titiler.core.resources.enums import ImageType, MediaType, OptionalHeader from titiler.core.resources.responses import GeoJSONResponse, JSONResponse, XMLResponse +from titiler.core.routing import EndpointScope from titiler.core.utils import Timer from fastapi import APIRouter, Body, Depends, Path, Query, params @@ -179,6 +179,7 @@ def url_for(self, request: Request, name: str, **path_params: Any) -> str: def add_route_dependencies( self, + *, scopes: List[EndpointScope], dependencies=List[params.Depends], ): diff --git a/src/titiler/core/titiler/core/models/routes.py b/src/titiler/core/titiler/core/models/routes.py deleted file mode 100644 index 18a3dc566..000000000 --- a/src/titiler/core/titiler/core/models/routes.py +++ /dev/null @@ -1,13 +0,0 @@ -"""models for routes.""" - -from typing import Optional, TypedDict - - -class EndpointScope(TypedDict, total=False): - """Define endpoint.""" - - # More strict version of Starlette's Scope - # https://github.com/encode/starlette/blob/6af5c515e0a896cbf3f86ee043b88f6c24200bcf/starlette/types.py#L3 - path: str - method: str - type: Optional[str] # http or websocket diff --git a/src/titiler/core/titiler/core/routing.py b/src/titiler/core/titiler/core/routing.py index 318ea66ec..98cb8ba50 100644 --- a/src/titiler/core/titiler/core/routing.py +++ b/src/titiler/core/titiler/core/routing.py @@ -1,14 +1,23 @@ """Custom routing classes.""" +import sys import warnings -from typing import Callable, Dict, Optional, Type +from typing import Callable, Dict, List, Optional, Type import rasterio +from fastapi import params +from fastapi.dependencies.utils import get_parameterless_sub_dependant from fastapi.routing import APIRoute from starlette.requests import Request from starlette.responses import Response +from starlette.routing import BaseRoute, Match + +if sys.version_info >= (3, 8): + from typing import TypedDict # pylint: disable=no-name-in-module +else: + from typing_extensions import TypedDict def apiroute_factory(env: Optional[Dict] = None) -> Type[APIRoute]: @@ -45,3 +54,47 @@ async def custom_route_handler(request: Request) -> Response: return custom_route_handler return EnvAPIRoute + + +class EndpointScope(TypedDict, total=False): + """Define endpoint.""" + + # More strict version of Starlette's Scope + # https://github.com/encode/starlette/blob/6af5c515e0a896cbf3f86ee043b88f6c24200bcf/starlette/types.py#L3 + path: str + method: str + type: Optional[str] # http or websocket + + +def add_route_dependencies( + routes: List[BaseRoute], + *, + scopes: List[EndpointScope], + dependencies=List[params.Depends], +): + """Add dependencies to routes. + + Allows a developer to add dependencies to a route after the route has been defined. + + """ + for route in routes: + for scope in scopes: + match, _ = route.matches({"type": "http", **scope}) # type: ignore + if match != Match.FULL: + continue + + # Mimicking how APIRoute handles dependencies: + # https://github.com/tiangolo/fastapi/blob/1760da0efa55585c19835d81afa8ca386036c325/fastapi/routing.py#L408-L412 + for depends in dependencies[::-1]: + route.dependant.dependencies.insert( # type: ignore + 0, + get_parameterless_sub_dependant( + depends=depends, path=route.path_format # type: ignore + ), + ) + + # Register dependencies directly on route so that they aren't ignored if + # the routes are later associated with an app (e.g. app.include_router(router)) + # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/applications.py#L337-L360 + # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678 + route.dependencies.extend(dependencies) # type: ignore From e873528739388e5024fed24a16568cbc22d9a1d7 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Wed, 24 Nov 2021 08:28:24 +0100 Subject: [PATCH 5/5] update changelog --- CHANGES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 84294fc7d..b6a380c6a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # Release Notes -## 0.4.0a2 (TBD) +## 0.4.0a2 (2021-11-24) ### titiler.core @@ -8,6 +8,7 @@ * remove `additional_dependency` attribute in `BaseTileFactory`. This also remove `**kwargs` in endpoints **breaking** * remove `reader_options` attribute in `BaseTileFactory` **breaking** * `tms_dependency` default to `titiler.core.dependencies.TMSParams` which should supports all morecantile's TMS. +* add `route_dependencies` attribute to `BaseTilerFactory` to allow customizing route dependencies (author @alukach, https://github.com/developmentseed/titiler/pull/406) ### titiler.mosaic