Skip to content

Commit 2588f4e

Browse files
committed
add filter to item collection
1 parent 9d66d59 commit 2588f4e

File tree

5 files changed

+132
-5
lines changed

5 files changed

+132
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010

1111
### Added
1212

13-
- Sortby functionality to the item collection route. [#437](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/437)
13+
- Sort extension and sortby functionality to the item collection route. [#437](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/437)
14+
- Query extension and query functionality to the item collection route. [#437](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/437)
15+
- Filter extension and filter functionality to the item collection route. [#437](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/437)
1416

1517
### Changed
1618

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,10 +284,13 @@ async def get_collection(
284284
async def item_collection(
285285
self,
286286
collection_id: str,
287+
request: Request,
287288
bbox: Optional[BBox] = None,
288289
datetime: Optional[str] = None,
289290
limit: Optional[int] = None,
290291
sortby: Optional[str] = None,
292+
filter_expr: Optional[str] = None,
293+
filter_lang: Optional[str] = None,
291294
token: Optional[str] = None,
292295
query: Optional[str] = None,
293296
**kwargs,
@@ -300,6 +303,7 @@ async def item_collection(
300303
301304
Args:
302305
collection_id (str): ID of the collection to list items from.
306+
request (Request): FastAPI Request object.
303307
bbox (Optional[BBox]): Optional bounding box filter.
304308
datetime (Optional[str]): Optional datetime or interval filter.
305309
limit (Optional[int]): Optional page size. Defaults to env ``STAC_ITEM_LIMIT`` when unset.
@@ -308,16 +312,15 @@ async def item_collection(
308312
imply ascending order.
309313
token (Optional[str]): Optional pagination token.
310314
query (Optional[str]): Optional query string.
311-
**kwargs: Must include ``request`` (FastAPI Request).
315+
filter_expr (Optional[str]): Optional filter expression.
316+
filter_lang (Optional[str]): Optional filter language.
312317
313318
Returns:
314319
ItemCollection: Feature collection with items, paging links, and counts.
315320
316321
Raises:
317322
HTTPException: 404 if the collection does not exist.
318323
"""
319-
request: Request = kwargs["request"]
320-
321324
try:
322325
await self.get_collection(collection_id=collection_id, request=request)
323326
except Exception:
@@ -333,6 +336,8 @@ async def item_collection(
333336
token=token,
334337
sortby=sortby,
335338
query=query,
339+
filter_expr=filter_expr,
340+
filter_lang=filter_lang,
336341
)
337342

338343
async def get_item(
@@ -521,9 +526,14 @@ async def post_search(
521526
search=search, op=operator, field=field, value=value
522527
)
523528

524-
# only cql2_json is supported here
529+
# Apply CQL2 filter (support both 'filter_expr' and canonical 'filter')
530+
cql2_filter = None
525531
if hasattr(search_request, "filter_expr"):
526532
cql2_filter = getattr(search_request, "filter_expr", None)
533+
if cql2_filter is None and hasattr(search_request, "filter"):
534+
cql2_filter = getattr(search_request, "filter", None)
535+
536+
if cql2_filter is not None:
527537
try:
528538
search = await self.database.apply_cql2_filter(search, cql2_filter)
529539
except Exception as e:

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
QueryExtension(
133133
conformance_classes=[QueryConformanceClasses.ITEMS],
134134
),
135+
filter_extension,
135136
],
136137
request_type="GET",
137138
)

stac_fastapi/tests/api/test_api.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import random
34
import uuid
@@ -1634,3 +1635,57 @@ async def test_item_collection_query(app_client, txn_client, ctx):
16341635
resp_json = resp.json()
16351636
ids = [f["id"] for f in resp_json["features"]]
16361637
assert test_item["id"] in ids
1638+
1639+
1640+
@pytest.mark.asyncio
1641+
async def test_filter_by_id(app_client, ctx):
1642+
"""Test filtering items by ID using the filter parameter."""
1643+
# Get the test item and collection from the context
1644+
item = ctx.item
1645+
collection_id = item["collection"]
1646+
item_id = item["id"]
1647+
1648+
# Create a filter to match the item by ID
1649+
filter_body = {"op": "=", "args": [{"property": "id"}, item_id]}
1650+
1651+
# Make the request with the filter
1652+
params = [("filter", json.dumps(filter_body)), ("filter-lang", "cql2-json")]
1653+
1654+
resp = await app_client.get(
1655+
f"/collections/{collection_id}/items",
1656+
params=params,
1657+
)
1658+
1659+
# Verify the response
1660+
assert resp.status_code == 200
1661+
resp_json = resp.json()
1662+
1663+
# Should find exactly one matching item
1664+
assert len(resp_json["features"]) == 1
1665+
assert resp_json["features"][0]["id"] == item_id
1666+
assert resp_json["features"][0]["collection"] == collection_id
1667+
1668+
1669+
@pytest.mark.asyncio
1670+
async def test_filter_by_nonexistent_id(app_client, ctx):
1671+
"""Test filtering with a non-existent ID returns no results."""
1672+
collection_id = ctx.item["collection"]
1673+
1674+
# Create a filter with a non-existent ID
1675+
filter_body = {
1676+
"op": "=",
1677+
"args": [{"property": "id"}, "this-id-does-not-exist-12345"],
1678+
}
1679+
1680+
# Make the request with the filter
1681+
params = [("filter", json.dumps(filter_body)), ("filter-lang", "cql2-json")]
1682+
1683+
resp = await app_client.get(
1684+
f"/collections/{collection_id}/items",
1685+
params=params,
1686+
)
1687+
1688+
# Verify the response
1689+
assert resp.status_code == 200
1690+
resp_json = resp.json()
1691+
assert len(resp_json["features"]) == 0
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Tests for STAC API filter extension."""
2+
3+
import json
4+
5+
import pytest
6+
7+
8+
@pytest.mark.asyncio
9+
async def test_filter_by_id(app_client, ctx):
10+
"""Test filtering items by ID using the filter parameter."""
11+
# Get the test item and collection from the context
12+
item = ctx.item
13+
collection_id = item["collection"]
14+
item_id = item["id"]
15+
16+
# Create a filter to match the item by ID
17+
filter_body = {"op": "=", "args": [{"property": "id"}, item_id]}
18+
19+
# Make the request with the filter
20+
params = [("filter", json.dumps(filter_body)), ("filter-lang", "cql2-json")]
21+
22+
resp = await app_client.get(
23+
f"/collections/{collection_id}/items",
24+
params=params,
25+
)
26+
27+
# Verify the response
28+
assert resp.status_code == 200
29+
resp_json = resp.json()
30+
31+
# Should find exactly one matching item
32+
assert len(resp_json["features"]) == 1
33+
assert resp_json["features"][0]["id"] == item_id
34+
assert resp_json["features"][0]["collection"] == collection_id
35+
36+
37+
@pytest.mark.asyncio
38+
async def test_filter_by_nonexistent_id(app_client, ctx):
39+
"""Test filtering with a non-existent ID returns no results."""
40+
collection_id = ctx.item["collection"]
41+
42+
# Create a filter with a non-existent ID
43+
filter_body = {
44+
"op": "=",
45+
"args": [{"property": "id"}, "this-id-does-not-exist-12345"],
46+
}
47+
48+
# Make the request with the filter
49+
params = [("filter", json.dumps(filter_body)), ("filter-lang", "cql2-json")]
50+
51+
resp = await app_client.get(
52+
f"/collections/{collection_id}/items",
53+
params=params,
54+
)
55+
56+
# Verify the response
57+
assert resp.status_code == 200
58+
resp_json = resp.json()
59+
assert len(resp_json["features"]) == 0

0 commit comments

Comments
 (0)