From b9c0999c4b10d187483af8895330cebc0bbe31cb Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Thu, 14 Mar 2024 18:40:53 +0100 Subject: [PATCH 1/4] Enable search on /items and add queryables links --- stac_fastapi/pgstac/core.py | 66 ++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/core.py index ef20f25..65a9b50 100644 --- a/stac_fastapi/pgstac/core.py +++ b/stac_fastapi/pgstac/core.py @@ -16,7 +16,13 @@ from stac_fastapi.types.core import AsyncBaseCoreClient from stac_fastapi.types.errors import InvalidQueryParameter, NotFoundError from stac_fastapi.types.requests import get_base_url -from stac_fastapi.types.stac import Collection, Collections, Item, ItemCollection +from stac_fastapi.types.stac import ( + Collection, + Collections, + Item, + ItemCollection, + LandingPage, +) from stac_pydantic.links import Relations from stac_pydantic.shared import MimeTypes @@ -37,6 +43,30 @@ class CoreCrudClient(AsyncBaseCoreClient): """Client for core endpoints defined by stac.""" + async def landing_page(self, **kwargs) -> LandingPage: + """Landing page. + + Called with `GET /`. + + Returns: + API landing page, serving as an entry point to the API. + """ + request: Request = kwargs["request"] + base_url = get_base_url(request) + landing_page = await super().landing_page(**kwargs) + + if self.extension_is_enabled("FilterExtension"): + landing_page["links"].append( + { + "rel": "http://www.opengis.net/def/rel/ogc/1.0/queryables", + "type": "application/schema+json", + "title": "Queryables", + "href": urljoin(base_url, "queryables"), + } + ) + + return landing_page + async def all_collections(self, request: Request, **kwargs) -> Collections: """Read all collections from the database.""" base_url = get_base_url(request) @@ -55,6 +85,18 @@ async def all_collections(self, request: Request, **kwargs) -> Collections: collection_id=coll["id"], request=request ).get_links(extra_links=coll.get("links")) + if self.extension_is_enabled("FilterExtension"): + coll["links"].append( + { + "rel": "http://www.opengis.net/def/rel/ogc/1.0/queryables", + "type": "application/schema+json", + "title": "Queryables", + "href": urljoin( + base_url, f"collections/{coll['id']}/queryables" + ), + } + ) + linked_collections.append(coll) links = [ @@ -107,6 +149,19 @@ async def get_collection( collection_id=collection_id, request=request ).get_links(extra_links=collection.get("links")) + if self.extension_is_enabled("FilterExtension"): + base_url = get_base_url(request) + collection["links"].append( + { + "rel": "http://www.opengis.net/def/rel/ogc/1.0/queryables", + "type": "application/schema+json", + "title": "Queryables", + "href": urljoin( + base_url, f"collections/{collection_id}/queryables" + ), + } + ) + return Collection(**collection) async def _get_base_item( @@ -277,6 +332,15 @@ async def item_collection( "token": token, } + if self.extension_is_enabled("FilterExtension"): + filter_lang = request.query_params.get("filter-lang", None) + filter = request.query_params.get("filter", "").strip() + + if len(filter) > 0 and filter_lang == "cql2-text": + ast = parse_cql2_text(filter) + base_args["filter"] = orjson.loads(to_cql2(ast)) + base_args["filter-lang"] = "cql2-json" + clean = {} for k, v in base_args.items(): if v is not None and v != []: From f7f39db0d491215f5b3e6ccd8c339eea9d2ab1ae Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Thu, 1 Aug 2024 13:07:54 +0200 Subject: [PATCH 2/4] add tests and remove unused --- stac_fastapi/pgstac/core.py | 52 +++++------------------------------- tests/api/test_api.py | 10 +++++++ tests/conftest.py | 5 +++- tests/resources/test_item.py | 8 ++++++ 4 files changed, 29 insertions(+), 46 deletions(-) diff --git a/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/core.py index 2180f2c..5d393c4 100644 --- a/stac_fastapi/pgstac/core.py +++ b/stac_fastapi/pgstac/core.py @@ -18,13 +18,7 @@ from stac_fastapi.types.errors import InvalidQueryParameter, NotFoundError from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType -from stac_fastapi.types.stac import ( - Collection, - Collections, - Item, - ItemCollection, - LandingPage, -) +from stac_fastapi.types.stac import Collection, Collections, Item, ItemCollection from stac_pydantic.links import Relations from stac_pydantic.shared import BBox, MimeTypes @@ -45,30 +39,6 @@ class CoreCrudClient(AsyncBaseCoreClient): """Client for core endpoints defined by stac.""" - async def landing_page(self, **kwargs) -> LandingPage: - """Landing page. - - Called with `GET /`. - - Returns: - API landing page, serving as an entry point to the API. - """ - request: Request = kwargs["request"] - base_url = get_base_url(request) - landing_page = await super().landing_page(**kwargs) - - if self.extension_is_enabled("FilterExtension"): - landing_page["links"].append( - { - "rel": "http://www.opengis.net/def/rel/ogc/1.0/queryables", - "type": "application/schema+json", - "title": "Queryables", - "href": urljoin(base_url, "queryables"), - } - ) - - return landing_page - async def all_collections(self, request: Request, **kwargs) -> Collections: """Read all collections from the database.""" base_url = get_base_url(request) @@ -90,8 +60,8 @@ async def all_collections(self, request: Request, **kwargs) -> Collections: if self.extension_is_enabled("FilterExtension"): coll["links"].append( { - "rel": "http://www.opengis.net/def/rel/ogc/1.0/queryables", - "type": "application/schema+json", + "rel": Relations.queryables.value, + "type": MimeTypes.jsonschema.value, "title": "Queryables", "href": urljoin( base_url, f"collections/{coll['id']}/queryables" @@ -155,8 +125,8 @@ async def get_collection( base_url = get_base_url(request) collection["links"].append( { - "rel": "http://www.opengis.net/def/rel/ogc/1.0/queryables", - "type": "application/schema+json", + "rel": Relations.queryables.value, + "type": MimeTypes.jsonschema.value, "title": "Queryables", "href": urljoin(base_url, f"collections/{collection_id}/queryables"), } @@ -339,8 +309,8 @@ async def item_collection( } if self.extension_is_enabled("FilterExtension"): - filter_lang = request.query_params.get("filter-lang", None) - filter = request.query_params.get("filter", "").strip() + filter_lang = kwargs.get("filter_lang", None) + filter = kwargs.get("filter", "").strip() if len(filter) > 0 and filter_lang == "cql2-text": ast = parse_cql2_text(filter) @@ -439,14 +409,6 @@ async def get_search( # noqa: C901 Returns: ItemCollection containing items which match the search criteria. """ - query_params = str(request.query_params) - - # Kludgy fix because using factory does not allow alias for filter-lang - if filter_lang is None: - match = re.search(r"filter-lang=([a-z0-9-]+)", query_params, re.IGNORECASE) - if match: - filter_lang = match.group(1) - # Parse request parameters base_args = { "collections": collections, diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 5b2d577..2077c35 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -73,6 +73,13 @@ async def test_get_search_content_type(app_client): assert resp.headers["content-type"] == "application/geo+json" +async def test_landing_links(app_client): + """test landing page links.""" + landing = await app_client.get("/") + assert landing.status_code == 200, landing.text + assert "Queryables" in [link.get("title") for link in landing.json()["links"]] + + async def test_get_queryables_content_type(app_client, load_test_collection): resp = await app_client.get("queryables") assert resp.headers["content-type"] == "application/schema+json" @@ -743,6 +750,9 @@ async def test_no_extension( async with AsyncClient(transport=ASGITransport(app=app)) as client: landing = await client.get("http://test/") assert landing.status_code == 200, landing.text + assert "Queryables" not in [ + link.get("title") for link in landing.json()["links"] + ] collection = await client.get("http://test/collections/test-collection") assert collection.status_code == 200, collection.text diff --git a/tests/conftest.py b/tests/conftest.py index 6740026..e5955b7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -137,7 +137,10 @@ def api_client(request, database): items_get_request_model = create_request_model( model_name="ItemCollectionUri", base_model=ItemCollectionUri, - mixins=[TokenPaginationExtension().GET], + mixins=[ + TokenPaginationExtension().GET, + FilterExtension(client=FiltersClient()).GET, + ], request_type="GET", ) search_get_request_model = create_get_request_model(extensions) diff --git a/tests/resources/test_item.py b/tests/resources/test_item.py index 8b52ead..7964266 100644 --- a/tests/resources/test_item.py +++ b/tests/resources/test_item.py @@ -1471,6 +1471,14 @@ async def test_get_filter_cql2text(app_client, load_test_data, load_test_collect resp_json = resp.json() assert len(resp.json()["features"]) == 0 + filter = f"proj:epsg={epsg}" + params = {"filter": filter, "filter-lang": "cql2-text"} + resp = await app_client.get( + f"/collections/{test_item['collection']}/items", params=params + ) + resp_json = resp.json() + assert len(resp.json()["features"]) == 1 + async def test_item_merge_raster_bands( app_client, load_test2_item, load_test2_collection From d07ee5834b510831e07213368328b1020288e0b1 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Thu, 1 Aug 2024 13:21:08 +0200 Subject: [PATCH 3/4] fix --- stac_fastapi/pgstac/core.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/core.py index 5d393c4..648d42a 100644 --- a/stac_fastapi/pgstac/core.py +++ b/stac_fastapi/pgstac/core.py @@ -310,10 +310,9 @@ async def item_collection( if self.extension_is_enabled("FilterExtension"): filter_lang = kwargs.get("filter_lang", None) - filter = kwargs.get("filter", "").strip() - - if len(filter) > 0 and filter_lang == "cql2-text": - ast = parse_cql2_text(filter) + filter = kwargs.get("filter", None) + if filter is not None and filter_lang == "cql2-text": + ast = parse_cql2_text(filter.strip()) base_args["filter"] = orjson.loads(to_cql2(ast)) base_args["filter-lang"] = "cql2-json" From 63d99b6506b35aa659838f249462e197fe78b54b Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Thu, 1 Aug 2024 16:06:28 +0200 Subject: [PATCH 4/4] more tests and update changelog --- CHANGES.md | 2 ++ tests/resources/test_collection.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index ccae3f3..8356053 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## [Unreleased] +- Enable filter extension for `GET /items` requests and add `Queryables` links in `/collections` and `/collections/{collection_id}` responses ([#89](https://github.com/stac-utils/stac-fastapi-pgstac/pull/89)) + ## [3.0.0a4] - 2024-07-10 - Update stac-fastapi libraries to `~=3.0.0b2` diff --git a/tests/resources/test_collection.py b/tests/resources/test_collection.py index ff5c8ad..3a2183b 100644 --- a/tests/resources/test_collection.py +++ b/tests/resources/test_collection.py @@ -260,3 +260,19 @@ async def test_get_collections_forwarded_header(app_client, load_test_collection ) for link in resp.json()["links"]: assert link["href"].startswith("https://test:1234/") + + +@pytest.mark.asyncio +async def test_get_collections_queryables_links(app_client, load_test_collection): + resp = await app_client.get( + "/collections", + ) + assert "Queryables" in [ + link.get("title") for link in resp.json()["collections"][0]["links"] + ] + + collection_id = resp.json()["collections"][0]["id"] + resp = await app_client.get( + f"/collections/{collection_id}", + ) + assert "Queryables" in [link.get("title") for link in resp.json()["links"]]