Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e24498f
Revert "Enabling asset indexing (#341)"
z-mrozu Sep 9, 2025
99ed336
collections/{collection}/items fields extension implementation
z-mrozu Sep 9, 2025
db3c9ba
Revert "Revert "Enabling asset indexing (#341)""
z-mrozu Sep 9, 2025
67d1cb9
tests
z-mrozu Sep 11, 2025
f5ac531
changelog update
z-mrozu Sep 11, 2025
58725ea
add sort to item collection route
jonhealy1 Sep 11, 2025
6f8b161
dockerfile fix
jonhealy1 Sep 11, 2025
5adc1eb
add tests
jonhealy1 Sep 11, 2025
d19c855
missing imports
jonhealy1 Sep 11, 2025
8c6742f
pass item collection to get search
jonhealy1 Sep 11, 2025
690549e
update
jonhealy1 Sep 11, 2025
09c1eb2
support pagination
jonhealy1 Sep 11, 2025
25e2241
update changelog
jonhealy1 Sep 12, 2025
b1c64a4
fix default sort
jonhealy1 Sep 12, 2025
483466f
test default sort
jonhealy1 Sep 12, 2025
56e97a8
update docstring
jonhealy1 Sep 12, 2025
7963b8c
remove debug print
jonhealy1 Sep 12, 2025
77dfa21
remove request token
jonhealy1 Sep 12, 2025
db0e448
clean up
jonhealy1 Sep 12, 2025
9d66d59
add query to item collection
jonhealy1 Sep 12, 2025
2588f4e
add filter to item collection
jonhealy1 Sep 13, 2025
852225c
remove unused test file
jonhealy1 Sep 13, 2025
1249c7a
fix test
jonhealy1 Sep 13, 2025
e10baad
passing locally
jonhealy1 Sep 13, 2025
3b3b087
add filter extension to os
jonhealy1 Sep 13, 2025
6b58c81
Merge branch 'main' into sort-item-collection
jonhealy1 Sep 13, 2025
62e8d30
lint
jonhealy1 Sep 13, 2025
7355a80
move item collection tests
jonhealy1 Sep 13, 2025
d3a432d
Merge branch 'sort-item-collection' into collections-items-fields-ext…
z-mrozu Sep 15, 2025
4f2ff6d
fields extension update
z-mrozu Sep 15, 2025
3392afb
move item collection fields extension api tests
z-mrozu Sep 15, 2025
573fbff
Merge branch 'main' into collections-items-fields-extension
jonhealy1 Sep 16, 2025
b54ed92
code review fixes
z-mrozu Sep 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `STAC_INDEX_ASSETS` environment variable to allow asset serialization to be configurable. [#433](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/433)
- Added the `ENV_MAX_LIMIT` environment variable to SFEOS, allowing overriding of the `MAX_LIMIT`, which controls the `?limit` parameter for returned items and STAC collections. [#434](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/434)
- Sort, Query, and Filter extension and functionality to the item collection route. [#437](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/437)
- Added Fields Extension implementation for the `/collections/{collection_id}/items` endpoint. [#436](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/436)

### Changed

Expand Down
3 changes: 3 additions & 0 deletions stac_fastapi/core/stac_fastapi/core/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ async def item_collection(
filter_lang: Optional[str] = None,
token: Optional[str] = None,
query: Optional[str] = None,
fields: Optional[List[str]] = None,
**kwargs,
) -> stac_types.ItemCollection:
"""List items within a specific collection.
Expand All @@ -314,6 +315,7 @@ async def item_collection(
query (Optional[str]): Optional query string.
filter_expr (Optional[str]): Optional filter expression.
filter_lang (Optional[str]): Optional filter language.
fields (Optional[List[str]]): Fields to include or exclude from the results.

Returns:
ItemCollection: Feature collection with items, paging links, and counts.
Expand All @@ -338,6 +340,7 @@ async def item_collection(
query=query,
filter_expr=filter_expr,
filter_lang=filter_lang,
fields=fields,
)

async def get_item(
Expand Down
7 changes: 6 additions & 1 deletion stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
TokenPaginationExtension,
TransactionExtension,
)
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
from stac_fastapi.extensions.core.filter import FilterConformanceClasses
from stac_fastapi.extensions.core.query import QueryConformanceClasses
from stac_fastapi.extensions.core.sort import SortConformanceClasses
Expand Down Expand Up @@ -85,8 +86,11 @@
aggregation_extension.POST = EsAggregationExtensionPostRequest
aggregation_extension.GET = EsAggregationExtensionGetRequest

fields_extension = FieldsExtension()
fields_extension.conformance_classes.append(FieldsConformanceClasses.ITEMS)

search_extensions = [
FieldsExtension(),
fields_extension,
QueryExtension(),
SortExtension(),
TokenPaginationExtension(),
Expand Down Expand Up @@ -133,6 +137,7 @@
conformance_classes=[QueryConformanceClasses.ITEMS],
),
filter_extension,
FieldsExtension(conformance_classes=[FieldsConformanceClasses.ITEMS]),
],
request_type="GET",
)
Expand Down
7 changes: 6 additions & 1 deletion stac_fastapi/opensearch/stac_fastapi/opensearch/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
TokenPaginationExtension,
TransactionExtension,
)
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
from stac_fastapi.extensions.core.filter import FilterConformanceClasses
from stac_fastapi.extensions.core.query import QueryConformanceClasses
from stac_fastapi.extensions.core.sort import SortConformanceClasses
Expand Down Expand Up @@ -84,8 +85,11 @@
aggregation_extension.POST = EsAggregationExtensionPostRequest
aggregation_extension.GET = EsAggregationExtensionGetRequest

fields_extension = FieldsExtension()
fields_extension.conformance_classes.append(FieldsConformanceClasses.ITEMS)

search_extensions = [
FieldsExtension(),
fields_extension,
QueryExtension(),
SortExtension(),
TokenPaginationExtension(),
Expand Down Expand Up @@ -133,6 +137,7 @@
conformance_classes=[QueryConformanceClasses.ITEMS],
),
filter_extension,
FieldsExtension(conformance_classes=[FieldsConformanceClasses.ITEMS]),
],
request_type="GET",
)
Expand Down
68 changes: 68 additions & 0 deletions stac_fastapi/tests/api/test_api_item_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,71 @@ async def test_item_collection_filter_by_nonexistent_id(app_client, ctx, txn_cli
assert (
len(resp_json["features"]) == 0
), f"Expected no items with ID {non_existent_id}, but found {len(resp_json['features'])} matches"


@pytest.mark.asyncio
async def test_item_collection_fields_extension(app_client, ctx, txn_client):
resp = await app_client.get(
"/collections/test-collection/items",
params={"fields": "+properties.datetime"},
)
assert resp.status_code == 200
resp_json = resp.json()
assert list(resp_json["features"][0]["properties"]) == ["datetime"]


@pytest.mark.asyncio
async def test_item_collection_fields_extension_no_properties_get(
app_client, ctx, txn_client
):
resp = await app_client.get(
"/collections/test-collection/items", params={"fields": "-properties"}
)
assert resp.status_code == 200
resp_json = resp.json()
assert "properties" not in resp_json["features"][0]


@pytest.mark.asyncio
async def test_item_collection_fields_extension_no_null_fields(
app_client, ctx, txn_client
):
resp = await app_client.get("/collections/test-collection/items")
assert resp.status_code == 200
resp_json = resp.json()
# check if no null fields: https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/166
for feature in resp_json["features"]:
# assert "bbox" not in feature["geometry"]
for link in feature["links"]:
assert all(a not in link or link[a] is not None for a in ("title", "asset"))
for asset in feature["assets"]:
assert all(
a not in asset or asset[a] is not None
for a in ("start_datetime", "created")
)


@pytest.mark.asyncio
async def test_item_collection_fields_extension_return_all_properties(
app_client, ctx, txn_client, load_test_data
):
item = load_test_data("test_item.json")
resp = await app_client.get(
"/collections/test-collection/items",
params={"collections": ["test-collection"], "fields": "properties"},
)
assert resp.status_code == 200
resp_json = resp.json()
feature = resp_json["features"][0]
assert len(feature["properties"]) >= len(item["properties"])
for expected_prop, expected_value in item["properties"].items():
if expected_prop in (
"datetime",
"start_datetime",
"end_datetime",
"created",
"updated",
):
assert feature["properties"][expected_prop][0:19] == expected_value[0:19]
else:
assert feature["properties"][expected_prop] == expected_value
2 changes: 1 addition & 1 deletion stac_fastapi/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,8 @@ def build_test_app():
aggregation_extension.GET = EsAggregationExtensionGetRequest

search_extensions = [
SortExtension(),
FieldsExtension(),
SortExtension(),
QueryExtension(),
TokenPaginationExtension(),
FilterExtension(),
Expand Down
29 changes: 29 additions & 0 deletions stac_fastapi/tests/resources/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,35 @@ async def test_field_extension_exclude_default_includes(app_client, ctx):
assert "gsd" not in resp_json["features"][0]


@pytest.mark.asyncio
async def test_field_extension_get_includes_collection_items(app_client, ctx):
"""Test GET collections/{collection_id}/items with included fields (fields extension)"""
test_item = ctx.item
params = {
"fields": "+properties.proj:epsg,+properties.gsd",
}
resp = await app_client.get(
f"/collections/{test_item['collection']}/items", params=params
)
feat_properties = resp.json()["features"][0]["properties"]
assert not set(feat_properties) - {"proj:epsg", "gsd", "datetime"}


@pytest.mark.asyncio
async def test_field_extension_get_excludes_collection_items(app_client, ctx):
"""Test GET collections/{collection_id}/items with included fields (fields extension)"""
test_item = ctx.item
params = {
"fields": "-properties.proj:epsg,-properties.gsd",
}
resp = await app_client.get(
f"/collections/{test_item['collection']}/items", params=params
)
resp_json = resp.json()
assert "proj:epsg" not in resp_json["features"][0]["properties"].keys()
assert "gsd" not in resp_json["features"][0]["properties"].keys()


@pytest.mark.asyncio
async def test_search_intersects_and_bbox(app_client):
"""Test POST search intersects and bbox are mutually exclusive (core)"""
Expand Down