Skip to content

Commit d53de6e

Browse files
z-mrozujonhealy1
andauthored
Fields extension implementation for collections/{collection}/items (stac-utils#436)
**Description:** This PR implements the [Fields extension](https://github.com/stac-api-extensions/fields) for the /collections/{collection}/items endpoint similarly to how it's implemented for the /search endpoint. **PR Checklist:** - [x] Code is formatted and linted (run `pre-commit run --all-files`) - [x] Tests pass (run `make test`) - [ ] Documentation has been updated to reflect changes, if applicable - [x] Changes are added to the changelog --------- Co-authored-by: jonhealy1 <[email protected]>
1 parent 859732e commit d53de6e

File tree

7 files changed

+114
-3
lines changed

7 files changed

+114
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1313
- `STAC_INDEX_ASSETS` environment variable to allow asset serialization to be configurable. [#433](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/433)
1414
- 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)
1515
- Sort, Query, and Filter extension and functionality to the item collection route. [#437](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/437)
16+
- Added Fields Extension implementation for the `/collections/{collection_id}/items` endpoint. [#436](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/436)
1617

1718
### Changed
1819

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ async def item_collection(
293293
filter_lang: Optional[str] = None,
294294
token: Optional[str] = None,
295295
query: Optional[str] = None,
296+
fields: Optional[List[str]] = None,
296297
**kwargs,
297298
) -> stac_types.ItemCollection:
298299
"""List items within a specific collection.
@@ -314,6 +315,7 @@ async def item_collection(
314315
query (Optional[str]): Optional query string.
315316
filter_expr (Optional[str]): Optional filter expression.
316317
filter_lang (Optional[str]): Optional filter language.
318+
fields (Optional[List[str]]): Fields to include or exclude from the results.
317319
318320
Returns:
319321
ItemCollection: Feature collection with items, paging links, and counts.
@@ -338,6 +340,7 @@ async def item_collection(
338340
query=query,
339341
filter_expr=filter_expr,
340342
filter_lang=filter_lang,
343+
fields=fields,
341344
)
342345

343346
async def get_item(

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
TokenPaginationExtension,
4444
TransactionExtension,
4545
)
46+
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
4647
from stac_fastapi.extensions.core.filter import FilterConformanceClasses
4748
from stac_fastapi.extensions.core.query import QueryConformanceClasses
4849
from stac_fastapi.extensions.core.sort import SortConformanceClasses
@@ -85,8 +86,11 @@
8586
aggregation_extension.POST = EsAggregationExtensionPostRequest
8687
aggregation_extension.GET = EsAggregationExtensionGetRequest
8788

89+
fields_extension = FieldsExtension()
90+
fields_extension.conformance_classes.append(FieldsConformanceClasses.ITEMS)
91+
8892
search_extensions = [
89-
FieldsExtension(),
93+
fields_extension,
9094
QueryExtension(),
9195
SortExtension(),
9296
TokenPaginationExtension(),
@@ -133,6 +137,7 @@
133137
conformance_classes=[QueryConformanceClasses.ITEMS],
134138
),
135139
filter_extension,
140+
FieldsExtension(conformance_classes=[FieldsConformanceClasses.ITEMS]),
136141
],
137142
request_type="GET",
138143
)

stac_fastapi/opensearch/stac_fastapi/opensearch/app.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
TokenPaginationExtension,
3838
TransactionExtension,
3939
)
40+
from stac_fastapi.extensions.core.fields import FieldsConformanceClasses
4041
from stac_fastapi.extensions.core.filter import FilterConformanceClasses
4142
from stac_fastapi.extensions.core.query import QueryConformanceClasses
4243
from stac_fastapi.extensions.core.sort import SortConformanceClasses
@@ -84,8 +85,11 @@
8485
aggregation_extension.POST = EsAggregationExtensionPostRequest
8586
aggregation_extension.GET = EsAggregationExtensionGetRequest
8687

88+
fields_extension = FieldsExtension()
89+
fields_extension.conformance_classes.append(FieldsConformanceClasses.ITEMS)
90+
8791
search_extensions = [
88-
FieldsExtension(),
92+
fields_extension,
8993
QueryExtension(),
9094
SortExtension(),
9195
TokenPaginationExtension(),
@@ -133,6 +137,7 @@
133137
conformance_classes=[QueryConformanceClasses.ITEMS],
134138
),
135139
filter_extension,
140+
FieldsExtension(conformance_classes=[FieldsConformanceClasses.ITEMS]),
136141
],
137142
request_type="GET",
138143
)

stac_fastapi/tests/api/test_api_item_collection.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,71 @@ async def test_item_collection_filter_by_nonexistent_id(app_client, ctx, txn_cli
190190
assert (
191191
len(resp_json["features"]) == 0
192192
), f"Expected no items with ID {non_existent_id}, but found {len(resp_json['features'])} matches"
193+
194+
195+
@pytest.mark.asyncio
196+
async def test_item_collection_fields_extension(app_client, ctx, txn_client):
197+
resp = await app_client.get(
198+
"/collections/test-collection/items",
199+
params={"fields": "+properties.datetime"},
200+
)
201+
assert resp.status_code == 200
202+
resp_json = resp.json()
203+
assert list(resp_json["features"][0]["properties"]) == ["datetime"]
204+
205+
206+
@pytest.mark.asyncio
207+
async def test_item_collection_fields_extension_no_properties_get(
208+
app_client, ctx, txn_client
209+
):
210+
resp = await app_client.get(
211+
"/collections/test-collection/items", params={"fields": "-properties"}
212+
)
213+
assert resp.status_code == 200
214+
resp_json = resp.json()
215+
assert "properties" not in resp_json["features"][0]
216+
217+
218+
@pytest.mark.asyncio
219+
async def test_item_collection_fields_extension_no_null_fields(
220+
app_client, ctx, txn_client
221+
):
222+
resp = await app_client.get("/collections/test-collection/items")
223+
assert resp.status_code == 200
224+
resp_json = resp.json()
225+
# check if no null fields: https://github.com/stac-utils/stac-fastapi-elasticsearch/issues/166
226+
for feature in resp_json["features"]:
227+
# assert "bbox" not in feature["geometry"]
228+
for link in feature["links"]:
229+
assert all(a not in link or link[a] is not None for a in ("title", "asset"))
230+
for asset in feature["assets"]:
231+
assert all(
232+
a not in asset or asset[a] is not None
233+
for a in ("start_datetime", "created")
234+
)
235+
236+
237+
@pytest.mark.asyncio
238+
async def test_item_collection_fields_extension_return_all_properties(
239+
app_client, ctx, txn_client, load_test_data
240+
):
241+
item = load_test_data("test_item.json")
242+
resp = await app_client.get(
243+
"/collections/test-collection/items",
244+
params={"collections": ["test-collection"], "fields": "properties"},
245+
)
246+
assert resp.status_code == 200
247+
resp_json = resp.json()
248+
feature = resp_json["features"][0]
249+
assert len(feature["properties"]) >= len(item["properties"])
250+
for expected_prop, expected_value in item["properties"].items():
251+
if expected_prop in (
252+
"datetime",
253+
"start_datetime",
254+
"end_datetime",
255+
"created",
256+
"updated",
257+
):
258+
assert feature["properties"][expected_prop][0:19] == expected_value[0:19]
259+
else:
260+
assert feature["properties"][expected_prop] == expected_value

stac_fastapi/tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,8 @@ def build_test_app():
362362
aggregation_extension.GET = EsAggregationExtensionGetRequest
363363

364364
search_extensions = [
365-
SortExtension(),
366365
FieldsExtension(),
366+
SortExtension(),
367367
QueryExtension(),
368368
TokenPaginationExtension(),
369369
FilterExtension(),

stac_fastapi/tests/resources/test_item.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,35 @@ async def test_field_extension_exclude_default_includes(app_client, ctx):
869869
assert "gsd" not in resp_json["features"][0]
870870

871871

872+
@pytest.mark.asyncio
873+
async def test_field_extension_get_includes_collection_items(app_client, ctx):
874+
"""Test GET collections/{collection_id}/items with included fields (fields extension)"""
875+
test_item = ctx.item
876+
params = {
877+
"fields": "+properties.proj:epsg,+properties.gsd",
878+
}
879+
resp = await app_client.get(
880+
f"/collections/{test_item['collection']}/items", params=params
881+
)
882+
feat_properties = resp.json()["features"][0]["properties"]
883+
assert not set(feat_properties) - {"proj:epsg", "gsd", "datetime"}
884+
885+
886+
@pytest.mark.asyncio
887+
async def test_field_extension_get_excludes_collection_items(app_client, ctx):
888+
"""Test GET collections/{collection_id}/items with included fields (fields extension)"""
889+
test_item = ctx.item
890+
params = {
891+
"fields": "-properties.proj:epsg,-properties.gsd",
892+
}
893+
resp = await app_client.get(
894+
f"/collections/{test_item['collection']}/items", params=params
895+
)
896+
resp_json = resp.json()
897+
assert "proj:epsg" not in resp_json["features"][0]["properties"].keys()
898+
assert "gsd" not in resp_json["features"][0]["properties"].keys()
899+
900+
872901
@pytest.mark.asyncio
873902
async def test_search_intersects_and_bbox(app_client):
874903
"""Test POST search intersects and bbox are mutually exclusive (core)"""

0 commit comments

Comments
 (0)