Skip to content

Commit 1a78fa9

Browse files
committed
cql2-text support, tests
1 parent 6e83227 commit 1a78fa9

File tree

4 files changed

+103
-51
lines changed

4 files changed

+103
-51
lines changed

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 37 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,8 @@ async def all_collections(
266266
if field[0] == "-":
267267
excludes.add(field[1:])
268268
else:
269-
includes.add(field[1:] if field[0] in "+ " else field)
269+
include_field = field[1:] if field[0] in "+ " else field
270+
includes.add(include_field)
270271

271272
sort = None
272273
if sortby:
@@ -314,34 +315,41 @@ async def all_collections(
314315
detail=f"Only 'cql2-json' and 'cql2-text' filter languages are supported for collections. Got '{filter_lang}'.",
315316
)
316317

317-
# Handle different filter formats
318-
try:
319-
if filter_lang == "cql2-text" or filter_lang is None:
320-
# For cql2-text or when no filter_lang is specified, try both formats
321-
try:
322-
# First try to parse as JSON
323-
parsed_filter = orjson.loads(unquote_plus(filter_expr))
324-
except Exception:
325-
# If that fails, use pygeofilter to convert CQL2-text to CQL2-JSON
326-
try:
327-
# Parse CQL2-text and convert to CQL2-JSON
328-
text_filter = unquote_plus(filter_expr)
329-
parsed_ast = parse_cql2_text(text_filter)
330-
parsed_filter = to_cql2(parsed_ast)
331-
except Exception as e:
332-
# If parsing fails, provide a helpful error message
333-
raise HTTPException(
334-
status_code=400,
335-
detail=f"Invalid CQL2-text filter: {e}. Please check your syntax.",
336-
)
337-
else:
338-
# For explicit cql2-json, parse as JSON
339-
parsed_filter = orjson.loads(unquote_plus(filter_expr))
340-
except Exception as e:
341-
# Catch any other parsing errors
342-
raise HTTPException(
343-
status_code=400, detail=f"Error parsing filter: {e}"
344-
)
318+
# # Handle different filter formats
319+
# try:
320+
# if filter_lang == "cql2-text" or filter_lang is None:
321+
# # For cql2-text or when no filter_lang is specified, try both formats
322+
# try:
323+
# # First try to parse as JSON
324+
# parsed_filter = orjson.loads(unquote_plus(filter_expr))
325+
# except Exception:
326+
# # If that fails, use pygeofilter to convert CQL2-text to CQL2-JSON
327+
# try:
328+
# # Parse CQL2-text and convert to CQL2-JSON
329+
# text_filter = unquote_plus(filter_expr)
330+
# parsed_ast = parse_cql2_text(text_filter)
331+
# parsed_filter = to_cql2(parsed_ast)
332+
# except Exception as e:
333+
# # If parsing fails, provide a helpful error message
334+
# raise HTTPException(
335+
# status_code=400,
336+
# detail=f"Invalid CQL2-text filter: {e}. Please check your syntax.",
337+
# )
338+
# else:
339+
# # For explicit cql2-json, parse as JSON
340+
# parsed_filter = orjson.loads(unquote_plus(filter_expr))
341+
# except Exception as e:
342+
# # Catch any other parsing errors
343+
# raise HTTPException(
344+
# status_code=400, detail=f"Error parsing filter: {e}"
345+
# )
346+
347+
# Handle both cql2-json and cql2-text
348+
parsed_filter = orjson.loads(
349+
unquote_plus(filter_expr)
350+
if filter_lang == "cql2-json" or filter_lang is None
351+
else to_cql2(parse_cql2_text(filter_expr))
352+
)
345353
except Exception as e:
346354
raise HTTPException(
347355
status_code=400, detail=f"Invalid filter parameter: {e}"
@@ -404,31 +412,9 @@ async def post_all_collections(
404412
Returns:
405413
A Collections object containing all the collections in the database and links to various resources.
406414
"""
407-
# Debug print
408-
print("search_request: ", search_request)
409-
# Set the postbody attribute on the request object for PagingLinks
410415
request.postbody = search_request.model_dump(exclude_unset=True)
411416

412417
fields = None
413-
# Check for fields attribute (legacy format)
414-
if hasattr(search_request, "fields") and search_request.fields:
415-
fields = []
416-
417-
# Handle include fields
418-
if (
419-
hasattr(search_request.fields, "include")
420-
and search_request.fields.include
421-
):
422-
for field in search_request.fields.include:
423-
fields.append(f"+{field}")
424-
425-
# Handle exclude fields
426-
if (
427-
hasattr(search_request.fields, "exclude")
428-
and search_request.fields.exclude
429-
):
430-
for field in search_request.fields.exclude:
431-
fields.append(f"-{field}")
432418

433419
# Check for field attribute (ExtendedSearch format)
434420
if hasattr(search_request, "field") and search_request.field:

stac_fastapi/core/stac_fastapi/core/extensions/collections_search.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ async def collections_search_post_endpoint(
151151
collections = await self.client.post_all_collections(
152152
search_request=search_request, request=request
153153
)
154+
154155
return collections
155156

156157
@classmethod

stac_fastapi/tests/api/test_api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
"GET /collections/{collection_id}/aggregate",
4949
"POST /collections/{collection_id}/aggregations",
5050
"POST /collections/{collection_id}/aggregate",
51+
"GET /collections-search",
52+
"POST /collections-search",
5153
}
5254

5355

stac_fastapi/tests/api/test_api_search_collections.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,69 @@ async def test_collections_post(app_client, txn_client, ctx):
675675
c for c in resp_json["collections"] if c["id"].startswith(test_prefix)
676676
]
677677

678+
# Debug print to see what's in the collections
679+
print(
680+
"Collection keys:",
681+
test_collections[0].keys() if test_collections else "No collections found",
682+
)
683+
678684
# Check that stac_version is excluded from the collections
679685
for collection in test_collections:
680686
assert "stac_version" not in collection
687+
688+
689+
@pytest.mark.asyncio
690+
async def test_collections_search_cql2_text(app_client, txn_client, ctx):
691+
"""Test collections search with CQL2-text filter."""
692+
# Create a unique prefix for test collections
693+
test_prefix = f"test-{uuid.uuid4()}"
694+
695+
# Create test collections
696+
collection_data = ctx.collection.copy()
697+
collection_data["id"] = f"{test_prefix}-collection"
698+
await create_collection(txn_client, collection_data)
699+
await refresh_indices(txn_client)
700+
701+
# Test GET search with CQL2-text filter
702+
collection_id = collection_data["id"]
703+
resp = await app_client.get(
704+
f"/collections-search?filter-lang=cql2-text&filter=id='{collection_id}'"
705+
)
706+
assert resp.status_code == 200
707+
resp_json = resp.json()
708+
709+
# Debug print to see what's in the response
710+
print("Collections in response:", [c["id"] for c in resp_json["collections"]])
711+
712+
# Filter collections to only include the ones with our test prefix
713+
filtered_collections = [
714+
c for c in resp_json["collections"] if c["id"].startswith(test_prefix)
715+
]
716+
717+
# Check that only the filtered collection is returned
718+
assert len(filtered_collections) == 1
719+
assert filtered_collections[0]["id"] == collection_id
720+
721+
# Test GET search with more complex CQL2-text filter (LIKE operator)
722+
test_prefix_escaped = test_prefix.replace("-", "\\-")
723+
resp = await app_client.get(
724+
f"/collections-search?filter-lang=cql2-text&filter=id LIKE '{test_prefix_escaped}%'"
725+
)
726+
assert resp.status_code == 200
727+
resp_json = resp.json()
728+
729+
# Debug print to see what's in the response
730+
print(
731+
"Collections in response (LIKE):", [c["id"] for c in resp_json["collections"]]
732+
)
733+
734+
# Filter collections to only include the ones with our test prefix
735+
filtered_collections = [
736+
c for c in resp_json["collections"] if c["id"].startswith(test_prefix)
737+
]
738+
739+
# Check that all test collections are returned
740+
assert (
741+
len(filtered_collections) == 1
742+
) # We only created one collection with this prefix
743+
assert filtered_collections[0]["id"] == collection_id

0 commit comments

Comments
 (0)