From 644f074db1484569122c3a5ccfbe229eadf26bad Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Thu, 31 Aug 2023 14:42:37 +0200 Subject: [PATCH 1/2] add tilejson links for layers --- CHANGES.md | 4 ++++ tests/test_mosaic.py | 44 +++++++++++++++++++++++++++++++++------ titiler/pgstac/factory.py | 35 +++++++++++++++++++++++++++++++ titiler/pgstac/main.py | 2 +- 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1291676..0f6888f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # Release Notes +## 0,5,2 (TDB) + +* add `tilejson` URL links for `defaults layers` defined in mosaic's metadata in `/mosaic/register` and `/mosaic/{mosaic_id}/info` response + ## 0.5.1 (2023-08-03) * add `python-dotenv` requirement diff --git a/tests/test_mosaic.py b/tests/test_mosaic.py index a298314..190d76d 100644 --- a/tests/test_mosaic.py +++ b/tests/test_mosaic.py @@ -504,6 +504,41 @@ def test_query_with_metadata(app): assert resp["minzoom"] == 1 assert resp["maxzoom"] == 2 + # Check that `defaults` created `tilejson` URL in links + query = { + "filter": { + "op": "=", + "args": [{"property": "collection"}, "noaa-emergency-response"], + }, + "metadata": { + "name": "mymosaic", + "minzoom": 1, + "maxzoom": 2, + "defaults": { + "one_band": { + "assets": "cog", + "asset_bidx": 1, + }, + "three_bands": { + "assets": "cog", + "asset_bidx": [1, 2, 3], + }, + }, + }, + } + + response = app.post("/mosaic/register", json=query) + assert response.status_code == 200 + resp = response.json() + assert resp["searchid"] + assert ( + len(resp["links"]) == 4 + ) # info, tilejson, tilejson for one_band, tilejson for three_bands + link = resp["links"][2] + assert link["title"] == "TileJSON link for `one_band` layer." + assert "asset_bidx=1" in link["href"] + assert "assets=cog" in link["href"] + @patch("rio_tiler.io.rasterio.rasterio") def test_statistics(rio, app): @@ -572,15 +607,13 @@ def test_mosaic_list(app): assert response.status_code == 200 resp = response.json() assert ["searches", "links", "context"] == list(resp) - assert resp["context"] == {"returned": 5, "limit": 10, "matched": 5} - assert len(resp["searches"]) == 5 + assert len(resp["searches"]) > 0 assert len(resp["links"]) == 1 response = app.get("/mosaic/list?limit=1") assert response.status_code == 200 resp = response.json() assert ["searches", "links", "context"] == list(resp) - assert resp["context"] == {"returned": 1, "limit": 1, "matched": 5} assert len(resp["searches"]) == 1 assert len(resp["links"]) == 2 @@ -588,7 +621,6 @@ def test_mosaic_list(app): assert response.status_code == 200 resp = response.json() assert ["searches", "links", "context"] == list(resp) - assert resp["context"] == {"returned": 1, "limit": 1, "matched": 5} assert len(resp["searches"]) == 1 assert len(resp["links"]) == 3 @@ -645,7 +677,7 @@ def test_mosaic_list(app): response = app.get("/mosaic/list?sortby=lastused") assert response.status_code == 200 resp = response.json() - assert resp["context"] == {"returned": 8, "limit": 10, "matched": 8} + assert resp["context"] dates = [ datetime.strptime(s["search"]["lastused"][0:-6], "%Y-%m-%dT%H:%M:%S.%f") for s in resp["searches"] @@ -655,7 +687,7 @@ def test_mosaic_list(app): response = app.get("/mosaic/list?sortby=-lastused") assert response.status_code == 200 resp = response.json() - assert resp["context"] == {"returned": 8, "limit": 10, "matched": 8} + assert resp["context"] dates = [ datetime.strptime(s["search"]["lastused"][0:-6], "%Y-%m-%dT%H:%M:%S.%f") for s in resp["searches"] diff --git a/titiler/pgstac/factory.py b/titiler/pgstac/factory.py index 55a5dc5..44a2753 100644 --- a/titiler/pgstac/factory.py +++ b/titiler/pgstac/factory.py @@ -678,6 +678,22 @@ def register_search( ) search_info = cursor.fetchone() + layer_links = [] + if search_info.metadata.defaults: + layer_links = [ + model.Link( + title=f"TileJSON link for `{name}` layer.", + rel="tilejson", + href=self.url_for( + request, + "tilejson", + searchid=search_info.id, + ) + + f"?{urlencode(values, doseq=True)}", + ) + for name, values in search_info.metadata.defaults.items() + ] + return model.RegisterResponse( searchid=search_info.id, links=[ @@ -691,6 +707,7 @@ def register_search( rel="tilejson", href=self.url_for(request, "tilejson", searchid=search_info.id), ), + *layer_links, ], ) @@ -713,6 +730,22 @@ def info_search(request: Request, searchid=Depends(self.path_dependency)): if not search_info: raise MosaicNotFoundError(f"SearchId `{searchid}` not found") + layer_links = [] + if search_info.metadata.defaults: + layer_links = [ + model.Link( + title=f"TileJSON link for `{name}` layer.", + rel="tilejson", + href=self.url_for( + request, + "tilejson", + searchid=search_info.id, + ) + + f"?{urlencode(values, doseq=True)}", + ) + for name, values in search_info.metadata.defaults.items() + ] + return model.Info( search=search_info, links=[ @@ -723,9 +756,11 @@ def info_search(request: Request, searchid=Depends(self.path_dependency)): ), ), model.Link( + title="Templated link for TileJSON", rel="tilejson", href=self.url_for(request, "tilejson", searchid=search_info.id), ), + *layer_links, ], ) diff --git a/titiler/pgstac/main.py b/titiler/pgstac/main.py index b8ab5f3..21caecf 100644 --- a/titiler/pgstac/main.py +++ b/titiler/pgstac/main.py @@ -82,7 +82,7 @@ async def lifespan(app: FastAPI): CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, - allow_methods=["GET", "POST"], + allow_methods=["GET", "POST", "OPTIONS"], allow_headers=["*"], ) From 8fec9c37df3c884653b20e9592c959088ef7853d Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Thu, 31 Aug 2023 15:38:26 +0200 Subject: [PATCH 2/2] add more links --- tests/test_mosaic.py | 20 +++-- titiler/pgstac/factory.py | 154 ++++++++++++++++++++++++-------------- 2 files changed, 113 insertions(+), 61 deletions(-) diff --git a/tests/test_mosaic.py b/tests/test_mosaic.py index 190d76d..2570910 100644 --- a/tests/test_mosaic.py +++ b/tests/test_mosaic.py @@ -21,7 +21,12 @@ def test_register(app): resp = response.json() assert resp["searchid"] == search_no_bbox assert resp["links"] - assert [link["rel"] for link in resp["links"]] == ["metadata", "tilejson"] + assert [link["rel"] for link in resp["links"]] == [ + "metadata", + "tilejson", + "map", + "wmts", + ] query = { "collections": ["noaa-emergency-response"], @@ -34,7 +39,12 @@ def test_register(app): resp = response.json() assert resp["searchid"] == search_bbox assert resp["links"] - assert [link["rel"] for link in resp["links"]] == ["metadata", "tilejson"] + assert [link["rel"] for link in resp["links"]] == [ + "metadata", + "tilejson", + "map", + "wmts", + ] def test_info(app): @@ -532,9 +542,9 @@ def test_query_with_metadata(app): resp = response.json() assert resp["searchid"] assert ( - len(resp["links"]) == 4 - ) # info, tilejson, tilejson for one_band, tilejson for three_bands - link = resp["links"][2] + len(resp["links"]) == 6 + ) # info, tilejson, map, wmts tilejson for one_band, tilejson for three_bands + link = resp["links"][-2] assert link["title"] == "TileJSON link for `one_band` layer." assert "asset_bidx=1" in link["href"] assert "assets=cog" in link["href"] diff --git a/titiler/pgstac/factory.py b/titiler/pgstac/factory.py index 44a2753..ca3014d 100644 --- a/titiler/pgstac/factory.py +++ b/titiler/pgstac/factory.py @@ -31,6 +31,7 @@ from starlette.datastructures import QueryParams from starlette.requests import Request from starlette.responses import HTMLResponse, Response +from starlette.routing import NoMatchFound from titiler.core.dependencies import ( AssetsBidxExprParams, @@ -652,7 +653,7 @@ def assets_for_point( **pgstac_params, ) - def _search_routes(self) -> None: + def _search_routes(self) -> None: # noqa: C901 """register search routes.""" @self.router.post( @@ -678,38 +679,59 @@ def register_search( ) search_info = cursor.fetchone() - layer_links = [] - if search_info.metadata.defaults: - layer_links = [ - model.Link( - title=f"TileJSON link for `{name}` layer.", - rel="tilejson", - href=self.url_for( - request, - "tilejson", - searchid=search_info.id, - ) - + f"?{urlencode(values, doseq=True)}", - ) - for name, values in search_info.metadata.defaults.items() - ] + links: List[model.Link] = [ + model.Link( + rel="metadata", + title="Mosaic metadata", + href=self.url_for(request, "info_search", searchid=search_info.id), + ), + model.Link( + rel="tilejson", + title="Link for TileJSON", + href=self.url_for(request, "tilejson", searchid=search_info.id), + ), + ] - return model.RegisterResponse( - searchid=search_info.id, - links=[ + try: + links.append( model.Link( - rel="metadata", + rel="map", + title="Link for Map viewer", href=self.url_for( - request, "info_search", searchid=search_info.id + request, "map_viewer", searchid=search_info.id ), - ), + ) + ) + except NoMatchFound: + pass + + try: + links.append( model.Link( - rel="tilejson", - href=self.url_for(request, "tilejson", searchid=search_info.id), - ), - *layer_links, - ], - ) + rel="wmts", + title="Link for WMTS", + href=self.url_for(request, "wmts", searchid=search_info.id), + ) + ) + except NoMatchFound: + pass + + if search_info.metadata.defaults: + for name, values in search_info.metadata.defaults.items(): + links.append( + model.Link( + title=f"TileJSON link for `{name}` layer.", + rel="tilejson", + href=self.url_for( + request, + "tilejson", + searchid=search_info.id, + ) + + f"?{urlencode(values, doseq=True)}", + ) + ) + + return model.RegisterResponse(searchid=search_info.id, links=links) @self.router.get( "/{searchid}/info", @@ -730,39 +752,59 @@ def info_search(request: Request, searchid=Depends(self.path_dependency)): if not search_info: raise MosaicNotFoundError(f"SearchId `{searchid}` not found") - layer_links = [] - if search_info.metadata.defaults: - layer_links = [ - model.Link( - title=f"TileJSON link for `{name}` layer.", - rel="tilejson", - href=self.url_for( - request, - "tilejson", - searchid=search_info.id, - ) - + f"?{urlencode(values, doseq=True)}", - ) - for name, values in search_info.metadata.defaults.items() - ] + links: List[model.Link] = [ + model.Link( + rel="self", + title="Mosaic metadata", + href=self.url_for(request, "info_search", searchid=search_info.id), + ), + model.Link( + title="Link for TileJSON", + rel="tilejson", + href=self.url_for(request, "tilejson", searchid=search_info.id), + ), + ] - return model.Info( - search=search_info, - links=[ + try: + links.append( model.Link( - rel="self", + rel="map", + title="Link for Map viewer", href=self.url_for( - request, "info_search", searchid=search_info.id + request, "map_viewer", searchid=search_info.id ), - ), + ) + ) + except NoMatchFound: + pass + + try: + links.append( model.Link( - title="Templated link for TileJSON", - rel="tilejson", - href=self.url_for(request, "tilejson", searchid=search_info.id), - ), - *layer_links, - ], - ) + rel="wmts", + title="Link for WMTS", + href=self.url_for(request, "wmts", searchid=search_info.id), + ) + ) + except NoMatchFound: + pass + + if search_info.metadata.defaults: + for name, values in search_info.metadata.defaults.items(): + links.append( + model.Link( + title=f"TileJSON link for `{name}` layer.", + rel="tilejson", + href=self.url_for( + request, + "tilejson", + searchid=search_info.id, + ) + + f"?{urlencode(values, doseq=True)}", + ) + ) + + return model.Info(search=search_info, links=links) def _search_list_routes(self) -> None: """Add mosaic listing route."""