diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8210bd36..688fc57c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,5 +9,5 @@ - [ ] Code is formatted and linted (run `pre-commit run --all-files`) - [ ] Tests pass (run `make test`) -- [ ] Documentation has been updated to reflect changes, if applicable, and docs build successfully (run `make docs`) +- [ ] Documentation has been updated to reflect changes, if applicable - [ ] Changes are added to the changelog \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ff957c6f..93f78872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,16 +9,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- Added bbox and datetime parameters and functionality to item_collection https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/127 -- Added collection_id parameter to create_item function https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/127 -- Added item_id and collection_id to update_item https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/127 -- The default Collection objects index can be overridden by the `STAC_COLLECTIONS_INDEX` environment variable. -- The default Item objects index prefix can be overridden by the `STAC_ITEMS_INDEX_PREFIX` environment variable. +- Added bbox and datetime parameters and functionality to item_collection [#127](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/127) +- Added collection_id parameter to create_item function [#127](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/127) +- Added item_id and collection_id to update_item [#127](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/127) +- The default Collection objects index can be overridden by the `STAC_COLLECTIONS_INDEX` environment variable [#128](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/128) +- The default Item objects index prefix can be overridden by the `STAC_ITEMS_INDEX_PREFIX` environment variable [#128](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/128) +- Fields Extension [#129](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/129) ### Changed -- Updated core stac-fastapi libraries to 2.4.3 from 2.3.0 https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/127 +- Updated core stac-fastapi libraries to 2.4.3 from 2.3.0 [#127](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/127) ## [v0.2.0] diff --git a/Makefile b/Makefile index 0690fde5..ce2609bc 100644 --- a/Makefile +++ b/Makefile @@ -60,16 +60,6 @@ pybase-install: install: pybase-install pip install -e ./stac_fastapi/elasticsearch[dev,server] -.PHONY: docs-image -docs-image: - docker-compose -f docker-compose.docs.yml \ - build - -.PHONY: docs -docs: docs-image - docker-compose -f docker-compose.docs.yml \ - run docs - .PHONY: ingest ingest: python3 data_loader/data_loader.py diff --git a/README.md b/README.md index 10d0be8b..1397fb14 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # STAC FastAPI Elasticsearch -Elasticsearch backend for stac-fastapi. +## Elasticsearch backend for stac-fastapi + +### Join our [Gitter](https://gitter.im/stac-fastapi-elasticsearch/community) page -Join our [Gitter](https://gitter.im/stac-fastapi-elasticsearch/community) page +### Check out the public Postman documentation [Postman](https://documenter.getpostman.com/view/12888943/2s8ZDSdRHA) + +### For changes, see the [Changelog](CHANGELOG.md) -For changes, see the [Changelog](CHANGELOG.md). ## Development Environment Setup @@ -30,7 +33,7 @@ pre-commit run --all-files ```shell docker-compose build ``` - + ## Running API on localhost:8080 ```shell @@ -55,14 +58,15 @@ curl -X "POST" "http://localhost:8080/collections" \ }' ``` -Note: this "Collections Transaction" behavior is not part of the STAC API, but may be soon. +Note: this "Collections Transaction" behavior is not part of the STAC API, but may be soon. + ## Testing ```shell make test ``` - + ## Ingest sample data ```shell @@ -71,7 +75,8 @@ make ingest ## Elasticsearch Mappings -Mappings apply to search index, not source. +Mappings apply to search index, not source. + ## Managing Elasticsearch Indices diff --git a/postman_collections/stac-fastapi-elasticsearch.postman_collection.json b/postman_collections/stac-fastapi-elasticsearch.postman_collection.json index a49d5994..eb02cbd2 100644 --- a/postman_collections/stac-fastapi-elasticsearch.postman_collection.json +++ b/postman_collections/stac-fastapi-elasticsearch.postman_collection.json @@ -2,7 +2,8 @@ "info": { "_postman_id": "ca9d0979-4035-45ad-bfba-582a680a05ab", "name": "stac-fastapi-elasticsearch", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "12888943" }, "item": [ { @@ -16,7 +17,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "" ] @@ -35,7 +36,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "collections" ] @@ -54,7 +55,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "collections", "test-collection" @@ -74,7 +75,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "collections", "test-collection", @@ -96,7 +97,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "collections", "test-collection" @@ -116,7 +117,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "collections", "test-collection", @@ -137,7 +138,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "collections", "test-collection", @@ -174,7 +175,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "collections" ] @@ -208,7 +209,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "collections", "test-collection", @@ -218,6 +219,43 @@ }, "response": [] }, + { + "name": "UPDATE item", + "protocolProfileBehavior": { + "disabledSystemHeaders": { + "content-type": true + } + }, + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"type\": \"Feature\",\n \"id\": \"test-item\",\n \"stac_version\": \"1.0.1\",\n \"stac_extensions\": [\n \"https://stac-extensions.github.io/eo/v1.0.0/schema.json\",\n \"https://stac-extensions.github.io/projection/v1.0.0/schema.json\"\n ],\n \"geometry\": {\n \"coordinates\": [\n [\n [\n 152.15052873427666,\n -33.82243006904891\n ],\n [\n 150.1000346138806,\n -34.257132625788756\n ],\n [\n 149.5776607193635,\n -32.514709769700254\n ],\n [\n 151.6262528041627,\n -32.08081674221862\n ],\n [\n 152.15052873427666,\n -33.82243006904891\n ]\n ]\n ],\n \"type\": \"Polygon\"\n },\n \"properties\": {\n \"datetime\": \"2018-02-12T12:30:22Z\",\n \"landsat:scene_id\": \"LC82081612020043LGN00\",\n \"landsat:row\": \"161\",\n \"gsd\": 15,\n \"landsat:revision\": \"00\",\n \"view:sun_azimuth\": -148.83296771,\n \"instrument\": \"OLI_TIRS\",\n \"landsat:product_id\": \"LC08_L1GT_208161_20200212_20200212_01_RT\",\n \"eo:cloud_cover\": 0,\n \"landsat:tier\": \"RT\",\n \"landsat:processing_level\": \"L1GT\",\n \"landsat:column\": \"208\",\n \"platform\": \"landsat-8\",\n \"proj:epsg\": 32756,\n \"view:sun_elevation\": -37.30791534,\n \"view:off_nadir\": 0,\n \"height\": 2500,\n \"width\": 2500\n },\n \"bbox\": [\n 149.57574,\n -34.25796,\n 152.15194,\n -32.07915\n ],\n \"collection\": \"test-collection\",\n \"assets\": {},\n \"links\": [\n {\n \"href\": \"http://localhost:8081/collections/landsat-8-l1/items/LC82081612020043\",\n \"rel\": \"self\",\n \"type\": \"application/geo+json\"\n },\n {\n \"href\": \"http://localhost:8081/collections/landsat-8-l1\",\n \"rel\": \"parent\",\n \"type\": \"application/json\"\n },\n {\n \"href\": \"http://localhost:8081/collections/landsat-8-l1\",\n \"rel\": \"collection\",\n \"type\": \"application/json\"\n },\n {\n \"href\": \"http://localhost:8081/\",\n \"rel\": \"root\",\n \"type\": \"application/json\"\n }\n ]\n}" + }, + "url": { + "raw": "http://localhost:8080/collections/test-collection/items/test-item", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "collections", + "test-collection", + "items", + "test-item" + ] + } + }, + "response": [] + }, { "name": "POST search ", "protocolProfileBehavior": { @@ -236,7 +274,41 @@ ], "body": { "mode": "raw", - "raw": "{\n \"collections\":[\"test-collection\"],\n \"intersects\":{\"type\": \"Point\", \"coordinates\": [150.04, -33.14]}\n}" + "raw": "{\n \"collections\":[\"test-collection\"],\n \"limit\": 2,\n \"intersects\":{\"type\": \"Point\", \"coordinates\": [150.04, -33.14]},\n \"query\": {\n \"gsd\":{\"gt\":10}\n }\n}" + }, + "url": { + "raw": "http://localhost:8080/search", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "search" + ] + } + }, + "response": [] + }, + { + "name": "POST search ", + "protocolProfileBehavior": { + "disabledSystemHeaders": { + "content-type": true + } + }, + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"collections\":[\"test-collection\"],\n \"intersects\":{\n \"type\": \"Polygon\", \n \"coordinates\": [\n [\n [\n 98.0859375,\n -7.362466865535738\n ],\n [\n 95.2734375,\n -44.33956524809713\n ],\n [\n 188.4375,\n -50.28933925329178\n ],\n [\n 168.75,\n 10.487811882056695\n ],\n [\n 98.0859375,\n -7.362466865535738\n ]\n ]\n ]\n }\n}" }, "url": { "raw": "http://localhost:8080/search", @@ -244,7 +316,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "search" ] @@ -270,7 +342,75 @@ ], "body": { "mode": "raw", - "raw": "{\n \"collections\":[\"sentinel-s2-l2a\"],\n \"bbox\": [-69.433594,-10.660608,-47.285156,3.513421]\n}" + "raw": "{\n \"bbox\": [97.504892,-45.254738,174.321298,-2.431580]\n}" + }, + "url": { + "raw": "http://localhost:8080/search", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "search" + ] + } + }, + "response": [] + }, + { + "name": "POST search Fields Include", + "protocolProfileBehavior": { + "disabledSystemHeaders": { + "content-type": true + } + }, + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"bbox\": [97.504892,-45.254738,174.321298,-2.431580],\n \"fields\": {\n \"include\": [\"properties.gsd\"]\n }\n}" + }, + "url": { + "raw": "http://localhost:8080/search", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "search" + ] + } + }, + "response": [] + }, + { + "name": "POST search Fields Exclude", + "protocolProfileBehavior": { + "disabledSystemHeaders": { + "content-type": true + } + }, + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"bbox\": [97.504892,-45.254738,174.321298,-2.431580],\n \"fields\": {\n \"exclude\": [\"properties\"]\n }\n}" }, "url": { "raw": "http://localhost:8080/search", @@ -278,7 +418,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "search" ] @@ -303,7 +443,7 @@ "host": [ "localhost" ], - "port": "8083", + "port": "8080", "path": [ "search" ], diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py index 32054977..84eece90 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py @@ -15,8 +15,9 @@ from stac_fastapi.elasticsearch.database_logic import create_collection_index from stac_fastapi.elasticsearch.extensions import QueryExtension from stac_fastapi.elasticsearch.session import Session -from stac_fastapi.extensions.core import ( # FieldsExtension, +from stac_fastapi.extensions.core import ( ContextExtension, + FieldsExtension, FilterExtension, SortExtension, TokenPaginationExtension, @@ -67,7 +68,7 @@ class FixedQueryExtension(QueryExtension): extensions = [ TransactionExtension(client=TransactionsClient(session=session), settings=settings), BulkTransactionExtension(client=BulkTransactionsClient(session=session)), - # FieldsExtension(), + FieldsExtension(), FixedQueryExtension(), FixedSortExtension(), TokenPaginationExtension(), diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config.py index 8fda8032..e1630d67 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/config.py @@ -37,6 +37,7 @@ class ElasticsearchSettings(ApiSettings): # Fields which are defined by STAC but not included in the database model forbidden_fields: Set[str] = _forbidden_fields + indexed_fields: Set[str] = {"datetime"} @property def create_client(self): @@ -49,6 +50,7 @@ class AsyncElasticsearchSettings(ApiSettings): # Fields which are defined by STAC but not included in the database model forbidden_fields: Set[str] = _forbidden_fields + indexed_fields: Set[str] = {"datetime"} @property def create_client(self): diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py index ff3a53e9..421cf91f 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/core.py @@ -3,10 +3,11 @@ import logging from datetime import datetime as datetime_type from datetime import timezone -from typing import Any, Dict, List, Optional, Type, Union +from typing import Any, Dict, List, Optional, Set, Type, Union from urllib.parse import urljoin import attr +import stac_pydantic from fastapi import HTTPException from overrides import overrides from pydantic import ValidationError @@ -25,6 +26,7 @@ Items, ) from stac_fastapi.types import stac as stac_types +from stac_fastapi.types.config import Settings from stac_fastapi.types.core import ( AsyncBaseCoreClient, AsyncBaseFiltersClient, @@ -240,17 +242,17 @@ async def get_search( # base_args["filter"] = orjson.loads(to_cql2(parse_cql2_text(filter))) # print(f'>>> {base_args["filter"]}') - # if fields: - # includes = set() - # excludes = set() - # for field in fields: - # if field[0] == "-": - # excludes.add(field[1:]) - # elif field[0] == "+": - # includes.add(field[1:]) - # else: - # includes.add(field) - # base_args["fields"] = {"include": includes, "exclude": excludes} + if fields: + includes = set() + excludes = set() + for field in fields: + if field[0] == "-": + excludes.add(field[1:]) + elif field[0] == "+": + includes.add(field[1:]) + else: + includes.add(field) + base_args["fields"] = {"include": includes, "exclude": excludes} # Do the request try: @@ -337,25 +339,25 @@ async def post_search( self.item_serializer.db_to_stac(item, base_url=base_url) for item in items ] - # if self.extension_is_enabled("FieldsExtension"): - # if search_request.query is not None: - # query_include: Set[str] = set( - # [ - # k if k in Settings.get().indexed_fields else f"properties.{k}" - # for k in search_request.query.keys() - # ] - # ) - # if not search_request.fields.include: - # search_request.fields.include = query_include - # else: - # search_request.fields.include.union(query_include) - - # filter_kwargs = search_request.fields.filter_fields - - # response_features = [ - # json.loads(stac_pydantic.Item(**feat).json(**filter_kwargs)) - # for feat in response_features - # ] + if self.extension_is_enabled("FieldsExtension"): + if search_request.query is not None: + query_include: Set[str] = set( + [ + k if k in Settings.get().indexed_fields else f"properties.{k}" + for k in search_request.query.keys() + ] + ) + if not search_request.fields.include: + search_request.fields.include = query_include + else: + search_request.fields.include.union(query_include) + + filter_kwargs = search_request.fields.filter_fields + + items = [ + json.loads(stac_pydantic.Item(**feat).json(**filter_kwargs)) + for feat in items + ] context_obj = None if self.extension_is_enabled("ContextExtension"): diff --git a/stac_fastapi/elasticsearch/tests/api/test_api.py b/stac_fastapi/elasticsearch/tests/api/test_api.py index 40b06423..204dfc40 100644 --- a/stac_fastapi/elasticsearch/tests/api/test_api.py +++ b/stac_fastapi/elasticsearch/tests/api/test_api.py @@ -2,9 +2,7 @@ import uuid from datetime import datetime, timedelta -import pytest - -from ..conftest import MockRequest, create_collection, create_item +from ..conftest import create_collection, create_item ROUTES = { "GET /_mgmt/ping", @@ -110,17 +108,49 @@ async def test_app_context_extension(app_client, ctx, txn_client): assert matched == 1 -@pytest.mark.skip(reason="fields not implemented yet") -async def test_app_fields_extension(load_test_data, app_client, txn_client): - item = load_test_data("test_item.json") - txn_client.create_item(item, request=MockRequest, refresh=True) - +async def test_app_fields_extension(app_client, ctx, txn_client): resp = await app_client.get("/search", params={"collections": ["test-collection"]}) assert resp.status_code == 200 resp_json = resp.json() assert list(resp_json["features"][0]["properties"]) == ["datetime"] - txn_client.delete_item(item["id"], item["collection"]) + +async def test_app_fields_extension_no_properties_get(app_client, ctx, txn_client): + resp = await app_client.get( + "/search", params={"collections": ["test-collection"], "fields": "-properties"} + ) + assert resp.status_code == 200 + resp_json = resp.json() + assert "properties" not in resp_json["features"][0] + + +async def test_app_fields_extension_no_properties_post(app_client, ctx, txn_client): + resp = await app_client.post( + "/search", + json={ + "collections": ["test-collection"], + "fields": {"exclude": ["properties"]}, + }, + ) + assert resp.status_code == 200 + resp_json = resp.json() + assert "properties" not in resp_json["features"][0] + + +async def test_app_fields_extension_return_all_properties(app_client, ctx, txn_client): + item = ctx.item + resp = await app_client.get( + "/search", 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", "created", "updated"): + assert feature["properties"][expected_prop][0:19] == expected_value[0:19] + else: + assert feature["properties"][expected_prop] == expected_value async def test_app_query_extension_gt(app_client, ctx): diff --git a/stac_fastapi/elasticsearch/tests/conftest.py b/stac_fastapi/elasticsearch/tests/conftest.py index 72f67451..b755425c 100644 --- a/stac_fastapi/elasticsearch/tests/conftest.py +++ b/stac_fastapi/elasticsearch/tests/conftest.py @@ -24,6 +24,7 @@ from stac_fastapi.elasticsearch.database_logic import create_collection_index from stac_fastapi.extensions.core import ( # FieldsExtension, ContextExtension, + FieldsExtension, TokenPaginationExtension, TransactionExtension, ) @@ -160,7 +161,7 @@ async def app(): ), ContextExtension(), FixedSortExtension(), - # FieldsExtension(), + FieldsExtension(), FixedQueryExtension(), TokenPaginationExtension(), FixedFilterExtension(), diff --git a/stac_fastapi/elasticsearch/tests/resources/test_item.py b/stac_fastapi/elasticsearch/tests/resources/test_item.py index 21f7d87e..6e0d9b94 100644 --- a/stac_fastapi/elasticsearch/tests/resources/test_item.py +++ b/stac_fastapi/elasticsearch/tests/resources/test_item.py @@ -99,7 +99,6 @@ async def test_update_new_item(app_client, ctx): test_item = ctx.item test_item["id"] = "a" - # note: this endpoint is wrong in stac-fastapi -- should be /collections/{c_id}/items/{item_id} resp = await app_client.put( f"/collections/{test_item['collection']}/items/{test_item['id']}", json=test_item, @@ -650,30 +649,16 @@ async def test_pagination_token_idempotent(app_client, ctx, txn_client): ] -@pytest.mark.skip(reason="fields not implemented") -async def test_field_extension_get_includes(app_client, load_test_data): +async def test_field_extension_get_includes(app_client): """Test GET search with included fields (fields extension)""" - test_item = load_test_data("test_item.json") - resp = await app_client.post( - f"/collections/{test_item['collection']}/items", json=test_item - ) - assert resp.status_code == 200 - params = {"fields": "+properties.proj:epsg,+properties.gsd"} resp = await app_client.get("/search", params=params) feat_properties = resp.json()["features"][0]["properties"] assert not set(feat_properties) - {"proj:epsg", "gsd", "datetime"} -@pytest.mark.skip(reason="fields not implemented") -async def test_field_extension_get_excludes(app_client, load_test_data): +async def test_field_extension_get_excludes(app_client): """Test GET search with included fields (fields extension)""" - test_item = load_test_data("test_item.json") - resp = await app_client.post( - f"/collections/{test_item['collection']}/items", json=test_item - ) - assert resp.status_code == 200 - params = {"fields": "-properties.proj:epsg,-properties.gsd"} resp = await app_client.get("/search", params=params) resp_json = resp.json() @@ -681,15 +666,8 @@ async def test_field_extension_get_excludes(app_client, load_test_data): assert "gsd" not in resp_json["features"][0]["properties"].keys() -@pytest.mark.skip(reason="fields not implemented") -async def test_field_extension_post(app_client, load_test_data): +async def test_field_extension_post(app_client): """Test POST search with included and excluded fields (fields extension)""" - test_item = load_test_data("test_item.json") - resp = await app_client.post( - f"/collections/{test_item['collection']}/items", json=test_item - ) - assert resp.status_code == 200 - body = { "fields": { "exclude": ["assets.B1"], @@ -707,15 +685,8 @@ async def test_field_extension_post(app_client, load_test_data): } -@pytest.mark.skip(reason="fields not implemented") async def test_field_extension_exclude_and_include(app_client, load_test_data): """Test POST search including/excluding same field (fields extension)""" - test_item = load_test_data("test_item.json") - resp = await app_client.post( - f"/collections/{test_item['collection']}/items", json=test_item - ) - assert resp.status_code == 200 - body = { "fields": { "exclude": ["properties.eo:cloud_cover"], @@ -728,15 +699,8 @@ async def test_field_extension_exclude_and_include(app_client, load_test_data): assert "eo:cloud_cover" not in resp_json["features"][0]["properties"] -@pytest.mark.skip(reason="fields not implemented") async def test_field_extension_exclude_default_includes(app_client, load_test_data): """Test POST search excluding a forbidden field (fields extension)""" - test_item = load_test_data("test_item.json") - resp = await app_client.post( - f"/collections/{test_item['collection']}/items", json=test_item - ) - assert resp.status_code == 200 - body = {"fields": {"exclude": ["gsd"]}} resp = await app_client.post("/search", json=body)