From c89360c0e044be796e13d3abdf0e4b799639ba80 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Wed, 25 Sep 2024 09:15:52 +0200 Subject: [PATCH] fix filter extension implementation (#149) --- CHANGES.md | 2 + docker-compose.yml | 5 +- stac_fastapi/pgstac/core.py | 21 ++++--- tests/resources/test_item.py | 113 +++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e339b0e3..66031dde 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,8 @@ - Fix Docker compose file, so example data can be loaded into database (author @zstatmanweil, https://github.com/stac-utils/stac-fastapi-pgstac/pull/142) +- Fix `filter` extension implementation in `CoreCrudClient` + ## [3.0.0] - 2024-08-02 - 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)) diff --git a/docker-compose.yml b/docker-compose.yml index d39942ed..ec1080aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: '3' services: app: container_name: stac-fastapi-pgstac @@ -67,8 +66,8 @@ services: - ./scripts:/app/scripts command: > /bin/sh -c " - ./scripts/wait-for-it.sh -t 60 app:8082 && - python -m pip install requests && + ./scripts/wait-for-it.sh -t 60 app:8082 && + python -m pip install requests && python /app/scripts/ingest_joplin.py http://app:8082 " depends_on: diff --git a/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/core.py index 648d42a1..7a39b50a 100644 --- a/stac_fastapi/pgstac/core.py +++ b/stac_fastapi/pgstac/core.py @@ -310,11 +310,14 @@ async def item_collection( 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" + filter_query = kwargs.get("filter", None) + if filter_query: + if filter_lang == "cql2-text": + filter_query = to_cql2(parse_cql2_text(filter_query)) + filter_lang = "cql2-json" + + base_args["filter"] = orjson.loads(filter_query) + base_args["filter-lang"] = filter_lang clean = {} for k, v in base_args.items(): @@ -420,9 +423,11 @@ async def get_search( # noqa: C901 if filter: if filter_lang == "cql2-text": - ast = parse_cql2_text(filter) - base_args["filter"] = orjson.loads(to_cql2(ast)) - base_args["filter-lang"] = "cql2-json" + filter = to_cql2(parse_cql2_text(filter)) + filter_lang = "cql2-json" + + base_args["filter"] = orjson.loads(filter) + base_args["filter-lang"] = filter_lang if datetime: base_args["datetime"] = format_datetime_range(datetime) diff --git a/tests/resources/test_item.py b/tests/resources/test_item.py index 79642660..a0771164 100644 --- a/tests/resources/test_item.py +++ b/tests/resources/test_item.py @@ -1540,3 +1540,116 @@ async def test_get_collection_items_duplicate_forwarded_headers( ) for link in resp.json()["features"][0]["links"]: assert link["href"].startswith("https://test:1234/") + + +async def test_get_filter_extension(app_client, load_test_data, load_test_collection): + """Test GET with Filter extension""" + test_item = load_test_data("test_item.json") + collection_id = test_item["collection"] + ids = [] + + # Ingest 5 items + for _ in range(5): + uid = str(uuid.uuid4()) + test_item["id"] = uid + resp = await app_client.post( + f"/collections/{collection_id}/items", json=test_item + ) + assert resp.status_code == 201 + ids.append(uid) + + search_id = ids[2] + + # SEARCH + # CQL2-JSON + resp = await app_client.get( + "/search", + params={ + "filter-lang": "cql2-json", + "filter": json.dumps({"op": "in", "args": [{"property": "id"}, [search_id]]}), + }, + ) + assert resp.status_code == 200 + fc = resp.json() + assert len(fc["features"]) == 1 + assert fc["features"][0]["id"] == search_id + + # CQL-JSON + resp = await app_client.get( + "/search", + params={ + "filter-lang": "cql-json", + "filter": json.dumps( + { + "eq": [ + {"property": "id"}, + search_id, + ], + }, + ), + }, + ) + assert resp.status_code == 200 + fc = resp.json() + assert len(fc["features"]) == 1 + assert fc["features"][0]["id"] == search_id + + # CQL2-TEXT + resp = await app_client.get( + "/search", + params={ + "filter-lang": "cql2-text", + "filter": f"id='{search_id}'", + }, + ) + assert resp.status_code == 200 + fc = resp.json() + assert len(fc["features"]) == 1 + assert fc["features"][0]["id"] == search_id + + # ITEM COLLECTION + # CQL2-JSON + resp = await app_client.get( + f"/collections/{collection_id}/items", + params={ + "filter-lang": "cql2-json", + "filter": json.dumps({"op": "in", "args": [{"property": "id"}, [search_id]]}), + }, + ) + assert resp.status_code == 200 + fc = resp.json() + assert len(fc["features"]) == 1 + assert fc["features"][0]["id"] == search_id + + # CQL-JSON + resp = await app_client.get( + f"/collections/{collection_id}/items", + params={ + "filter-lang": "cql-json", + "filter": json.dumps( + { + "eq": [ + {"property": "id"}, + search_id, + ], + }, + ), + }, + ) + assert resp.status_code == 200 + fc = resp.json() + assert len(fc["features"]) == 1 + assert fc["features"][0]["id"] == search_id + + # CQL2-TEXT + resp = await app_client.get( + f"/collections/{collection_id}/items", + params={ + "filter-lang": "cql2-text", + "filter": f"id='{search_id}'", + }, + ) + assert resp.status_code == 200 + fc = resp.json() + assert len(fc["features"]) == 1 + assert fc["features"][0]["id"] == search_id