diff --git a/CHANGES.md b/CHANGES.md index ccae3f33..8356053c 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/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/core.py index 63e21824..648d42a1 100644 --- a/stac_fastapi/pgstac/core.py +++ b/stac_fastapi/pgstac/core.py @@ -57,6 +57,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": Relations.queryables.value, + "type": MimeTypes.jsonschema.value, + "title": "Queryables", + "href": urljoin( + base_url, f"collections/{coll['id']}/queryables" + ), + } + ) + linked_collections.append(coll) links = [ @@ -109,6 +121,17 @@ 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": Relations.queryables.value, + "type": MimeTypes.jsonschema.value, + "title": "Queryables", + "href": urljoin(base_url, f"collections/{collection_id}/queryables"), + } + ) + return Collection(**collection) async def _get_base_item( @@ -285,6 +308,14 @@ async def item_collection( "token": token, } + if self.extension_is_enabled("FilterExtension"): + filter_lang = kwargs.get("filter_lang", None) + 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" + clean = {} for k, v in base_args.items(): if v is not None and v != []: @@ -377,14 +408,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 5b2d577a..2077c352 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 67400269..e5955b75 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_collection.py b/tests/resources/test_collection.py index ff5c8ade..3a2183b1 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"]] diff --git a/tests/resources/test_item.py b/tests/resources/test_item.py index 8b52ead0..79642660 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