diff --git a/CHANGES.md b/CHANGES.md index dfecdc558..bfb6451b1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ * add `MultiBaseTilerFactory` and `MultiBandTilerFactory` custom tiler factories (https://github.com/developmentseed/titiler/pull/230) * Update STAC tiler to use the new `MultiBaseTilerFactory` factory * depreciate *empty* GET endpoint for MosaicTilerFactory read (https://github.com/developmentseed/titiler/pull/232) +* better `debug` configuration and make reponse headers metadata optional (https://github.com/developmentseed/titiler/pull/232) **breaking change** diff --git a/tests/routes/test_cog.py b/tests/routes/test_cog.py index c1963cb5a..f76f41543 100644 --- a/tests/routes/test_cog.py +++ b/tests/routes/test_cog.py @@ -129,10 +129,6 @@ def test_tile(rio, app): meta = parse_img(response.content) assert meta["width"] == 256 assert meta["height"] == 256 - timing = response.headers["server-timing"] - assert "dataread;dur" in timing - assert "postprocess;dur" in timing - assert "format;dur" in timing response = app.get( "/cog/tiles/8/87/48@2x?url=https://myurl.com/cog.tif&rescale=0,1000&color_formula=Gamma R 3" @@ -278,10 +274,6 @@ def test_preview(rio, app): assert meta["width"] == 256 assert meta["height"] == 256 assert meta["driver"] == "JPEG" - timing = response.headers["server-timing"] - assert "dataread;dur" in timing - assert "postprocess;dur" in timing - assert "format;dur" in timing response = app.get( "/cog/preview.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" @@ -339,10 +331,6 @@ def test_part(rio, app): assert meta["width"] == 256 assert meta["height"] == 73 assert meta["driver"] == "PNG" - timing = response.headers["server-timing"] - assert "dataread;dur" in timing - assert "postprocess;dur" in timing - assert "format;dur" in timing response = app.get( "/cog/crop/-56.228,72.715,-54.547,73.188.jpg?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256&return_mask=false" @@ -394,8 +382,6 @@ def test_point(rio, app): assert response.headers["content-type"] == "application/json" body = response.json() assert body["coordinates"] == [-56.228, 72.715] - timing = response.headers["server-timing"] - assert "dataread;dur" in timing def test_file_not_found_error(app): diff --git a/tests/routes/test_mosaic.py b/tests/routes/test_mosaic.py index 38b0df485..2f4db51de 100644 --- a/tests/routes/test_mosaic.py +++ b/tests/routes/test_mosaic.py @@ -105,10 +105,6 @@ def test_point(app): assert len(body["values"]) == 1 assert body["values"][0][0].endswith(".tif") assert body["values"][0][1] == [9943, 9127, 9603] - timing = response.headers["server-timing"] - assert "mosaicread;dur" in timing - assert "dataread;dur" in timing - assert "total;dur" in timing def test_tile(app): @@ -126,15 +122,8 @@ def test_tile(app): ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" - assert response.headers["X-Assets"] meta = parse_img(response.content) assert meta["width"] == meta["height"] == 256 - timing = response.headers["server-timing"] - assert "mosaicread;dur" in timing - assert "dataread;dur" in timing - assert "postprocess;dur" in timing - assert "format;dur" in timing - assert "total;dur" in timing response = app.get( f"/mosaicjson/tiles/{tile.z}/{tile.x}/{tile.y}@2x", diff --git a/tests/test_factories.py b/tests/test_factories.py index a69cef148..ce4c455a9 100644 --- a/tests/test_factories.py +++ b/tests/test_factories.py @@ -1,26 +1,117 @@ # """Test TiTiler Tiler Factories.""" -from rio_tiler.io import COGReader +import os +import tempfile +from contextlib import contextmanager +from cogeo_mosaic.backends import FileBackend +from cogeo_mosaic.mosaic import MosaicJSON -def test_TilerFactory(set_env): +from titiler.dependencies import TMSParams, WebMercatorTMSParams +from titiler.endpoints import factory +from titiler.resources.enums import OptionalHeaders + +from .conftest import DATA_DIR + +from fastapi import FastAPI + +from starlette.testclient import TestClient + +assets = [os.path.join(DATA_DIR, asset) for asset in ["cog1.tif", "cog2.tif"]] + + +def test_TilerFactory(): """Test TilerFactory class.""" - from titiler.dependencies import TMSParams - from titiler.endpoints import factory + cog = factory.TilerFactory() + assert len(cog.router.routes) == 21 + assert cog.tms_dependency == TMSParams + + cog = factory.TilerFactory(add_preview=False, add_part=False) + assert len(cog.router.routes) == 17 + + app = FastAPI() + cog = factory.TilerFactory(optional_headers=[OptionalHeaders.server_timing]) + app.include_router(cog.router) + client = TestClient(app) - app = factory.TilerFactory(reader=COGReader) - assert len(app.router.routes) == 21 - assert app.tms_dependency == TMSParams + 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 - app = factory.TilerFactory(reader=COGReader, add_preview=False, add_part=False) - assert len(app.router.routes) == 17 + response = client.get( + f"/preview?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=256" + ) + 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"/crop/-56.228,72.715,-54.547,73.188.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=256" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + timing = response.headers["server-timing"] + assert "dataread;dur" in timing + assert "postprocess;dur" in timing + assert "format;dur" in timing -def test_MosaicTilerFactory(set_env): + response = client.get(f"/point/-56.228,72.715?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + timing = response.headers["server-timing"] + assert "dataread;dur" in timing + + +@contextmanager +def tmpmosaic(): + """Create a Temporary MosaicJSON file.""" + fileobj = tempfile.NamedTemporaryFile(suffix=".json.gz", delete=False) + fileobj.close() + mosaic_def = MosaicJSON.from_urls(assets) + with FileBackend(fileobj.name, mosaic_def=mosaic_def) as mosaic: + mosaic.write(overwrite=True) + + try: + yield fileobj.name + finally: + os.remove(fileobj.name) + + +def test_MosaicTilerFactory(): """Test MosaicTilerFactory class.""" - from titiler.dependencies import WebMercatorTMSParams - from titiler.endpoints import factory + mosaic = factory.MosaicTilerFactory( + optional_headers=[OptionalHeaders.server_timing, OptionalHeaders.x_assets], + router_prefix="mosaic", + ) + assert len(mosaic.router.routes) == 19 + assert mosaic.tms_dependency == WebMercatorTMSParams + + app = FastAPI() + app.include_router(mosaic.router, prefix="/mosaic") + client = TestClient(app) + + with tmpmosaic() as mosaic_file: + response = client.get( + "/mosaic/point/-74.53125,45.9956935", params={"url": mosaic_file}, + ) + assert response.status_code == 200 + timing = response.headers["server-timing"] + assert "mosaicread;dur" in timing + assert "dataread;dur" in timing + + response = client.get("/mosaic/tiles/7/37/45", params={"url": mosaic_file}) + assert response.status_code == 200 - app = factory.MosaicTilerFactory() - assert len(app.router.routes) == 19 - assert app.tms_dependency == WebMercatorTMSParams + assert response.headers["X-Assets"] + timing = response.headers["server-timing"] + assert "mosaicread;dur" in timing + assert "dataread;dur" in timing + assert "postprocess;dur" in timing + assert "format;dur" in timing diff --git a/tests/test_main.py b/tests/test_main.py index 13c21d757..e6ceee2f2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,4 +6,3 @@ def test_health(app): response = app.get("/healthz") assert response.status_code == 200 assert response.json() == {"ping": "pong!"} - assert response.headers["server-timing"] diff --git a/titiler/endpoints/factory.py b/titiler/endpoints/factory.py index ae91118be..fc9259e9d 100644 --- a/titiler/endpoints/factory.py +++ b/titiler/endpoints/factory.py @@ -36,7 +36,12 @@ ) from ..models.mapbox import TileJSON from ..models.OGC import TileMatrixSetList -from ..resources.enums import ImageType, MimeTypes, PixelSelectionMethod +from ..resources.enums import ( + ImageType, + MimeTypes, + OptionalHeaders, + PixelSelectionMethod, +) from ..resources.responses import GeoJSONResponse, XMLResponse from ..templates import templates @@ -100,6 +105,9 @@ class BaseTilerFactory(metaclass=abc.ABCMeta): # Add specific GDAL environement (e.g {"AWS_REQUEST_PAYER": "requester"}) gdal_config: Dict = field(default_factory=dict) + # add additional headers in response + optional_headers: List[OptionalHeaders] = field(default_factory=list) + def __post_init__(self): """Post Init: register route and configure specific options.""" self.register_routes() @@ -341,9 +349,10 @@ def tile( ) timings.append(("format", round(t.elapsed * 1000, 2))) - headers["Server-Timing"] = ", ".join( - [f"{name};dur={time}" for (name, time) in timings] - ) + if OptionalHeaders.server_timing in self.optional_headers: + headers["Server-Timing"] = ", ".join( + [f"{name};dur={time}" for (name, time) in timings] + ) return Response(content, media_type=format.mimetype, headers=headers) @@ -544,9 +553,10 @@ def point( ) timings.append(("dataread", round(t.elapsed * 1000, 2))) - response.headers["Server-Timing"] = ", ".join( - [f"{name};dur={time}" for (name, time) in timings] - ) + if OptionalHeaders.server_timing in self.optional_headers: + response.headers["Server-Timing"] = ", ".join( + [f"{name};dur={time}" for (name, time) in timings] + ) return {"coordinates": [lon, lat], "values": values} @@ -606,7 +616,7 @@ def preview( ) timings.append(("format", round(t.elapsed * 1000, 2))) - if timings: + if OptionalHeaders.server_timing in self.optional_headers: headers["Server-Timing"] = ", ".join( [f"{name};dur={time}" for (name, time) in timings] ) @@ -674,7 +684,7 @@ def part( ) timings.append(("format", round(t.elapsed * 1000, 2))) - if timings: + if OptionalHeaders.server_timing in self.optional_headers: headers["Server-Timing"] = ", ".join( [f"{name};dur={time}" for (name, time) in timings] ) @@ -934,6 +944,9 @@ class MosaicTilerFactory(BaseTilerFactory): # BaseBackend does not support other TMS than WebMercator tms_dependency: Callable[..., TileMatrixSet] = WebMercatorTMSParams + # Add X-Assets in response headers + add_assets_headers: bool = False + def register_routes(self): """ This Method register routes to the router. @@ -1127,11 +1140,13 @@ def tile( ) timings.append(("format", round(t.elapsed * 1000, 2))) - headers["Server-Timing"] = ", ".join( - [f"{name};dur={time}" for (name, time) in timings] - ) + if OptionalHeaders.server_timing in self.optional_headers: + headers["Server-Timing"] = ", ".join( + [f"{name};dur={time}" for (name, time) in timings] + ) - headers["X-Assets"] = ",".join(data.assets) + if OptionalHeaders.x_assets in self.optional_headers: + headers["X-Assets"] = ",".join(data.assets) return Response(content, media_type=format.mimetype, headers=headers) @@ -1340,9 +1355,10 @@ def point( ) timings.append(("dataread", round((t.elapsed - mosaic_read) * 1000, 2))) - response.headers["Server-Timing"] = ", ".join( - [f"{name};dur={time}" for (name, time) in timings] - ) + if OptionalHeaders.server_timing in self.optional_headers: + response.headers["Server-Timing"] = ", ".join( + [f"{name};dur={time}" for (name, time) in timings] + ) return {"coordinates": [lon, lat], "values": values} diff --git a/titiler/main.py b/titiler/main.py index 3aef83b9f..ea0d10a71 100644 --- a/titiler/main.py +++ b/titiler/main.py @@ -56,9 +56,9 @@ app.add_middleware(BrotliMiddleware, minimum_size=0, gzip_fallback=True) app.add_middleware(CacheControlMiddleware, cachecontrol=api_settings.cachecontrol) -app.add_middleware(TotalTimeMiddleware) if api_settings.debug: - app.add_middleware(LoggerMiddleware) + app.add_middleware(TotalTimeMiddleware) + app.add_middleware(LoggerMiddleware, headers=True, querystrings=True) @app.get("/healthz", description="Health Check", tags=["Health Check"]) diff --git a/titiler/resources/enums.py b/titiler/resources/enums.py index 912265ccd..5f8fe3b96 100644 --- a/titiler/resources/enums.py +++ b/titiler/resources/enums.py @@ -77,3 +77,10 @@ class PixelSelectionMethod(str, Enum): def method(self): """Return rio-tiler-mosaic pixel selection class""" return getattr(defaults, f"{self._value_.title()}Method") + + +class OptionalHeaders(str, Enum): + """Optional Headers in responses.""" + + server_timing = "Server-Timing" + x_assets = "X-Assets"