Skip to content

Commit e815654

Browse files
committed
Merge branch 'main' of github.com:stac-utils/stac-fastapi-elasticsearch into landing_page_id
2 parents 586f5e9 + 7405916 commit e815654

File tree

23 files changed

+585
-226
lines changed

23 files changed

+585
-226
lines changed

CHANGELOG.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,32 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1111

1212
- Added configurable landing page ID `STAC_FASTAPI_LANDING_PAGE_ID` [#352](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/352)
1313
- Updated dynamic mapping for items to map long values to double versus float [#326](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/326)
14+
15+
### Changed
16+
17+
### Fixed
18+
19+
## [v4.1.0] - 2025-05-09
20+
21+
### Added
22+
23+
- Added logging to bulk insertion methods to provide detailed feedback on errors encountered during operations. [#364](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/364)
24+
- Introduced the `RAISE_ON_BULK_ERROR` environment variable to control whether bulk insertion methods raise exceptions on errors (`true`) or log warnings and continue processing (`false`). [#364](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/364)
25+
- Added code coverage reporting to the test suite using pytest-cov. [#87](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/87)
26+
27+
### Changed
28+
29+
- Updated dynamic mapping for items to map long values to double versus float. [#326](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/326)
1430
- Extended Datetime Search to search on start_datetime and end_datetime as well as datetime fields. [#182](https://github.com/stac-utils/stac-fastapi-elasticsearch/pull/182)
31+
- Changed item update operation to use Elasticsearch index API instead of delete and create for better efficiency and atomicity. [#75](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/issues/75)
32+
- Bulk insertion via `BulkTransactionsClient` now strictly validates all STAC Items using the Pydantic model before insertion. Any invalid item will immediately raise a `ValidationError`, ensuring consistent validation with single-item inserts and preventing invalid STAC Items from being stored. This validation is enforced regardless of the `RAISE_ON_BULK_ERROR` setting. [#368](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/368)
1533

1634
### Changed
1735

1836
### Fixed
1937

38+
- Refactored `create_item` and `update_item` methods to share unified logic, ensuring consistent conflict detection, validation, and database operations. [#368](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/368)
39+
2040
## [v4.0.0] - 2025-04-23
2141

2242
### Added
@@ -352,7 +372,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
352372
- Use genexp in execute_search and get_all_collections to return results.
353373
- Added db_to_stac serializer to item_collection method in core.py.
354374

355-
[Unreleased]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v4.0.0...main
375+
[Unreleased]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v4.1.0...main
376+
[v4.1.0]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v4.0.0...v4.1.0
356377
[v4.0.0]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v3.2.5...v4.0.0
357378
[v3.2.5]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v3.2.4...v3.2.5
358379
[v3.2.4]: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/compare/v3.2.3...v3.2.4

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ test-opensearch:
7575

7676
.PHONY: test
7777
test:
78-
-$(run_es) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh elasticsearch:9200 && cd stac_fastapi/tests/ && pytest'
78+
-$(run_es) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh elasticsearch:9200 && cd stac_fastapi/tests/ && pytest --cov=stac_fastapi --cov-report=term-missing'
7979
docker compose down
8080

81-
-$(run_os) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh opensearch:9202 && cd stac_fastapi/tests/ && pytest'
81+
-$(run_os) /bin/bash -c 'export && ./scripts/wait-for-it-es.sh opensearch:9202 && cd stac_fastapi/tests/ && pytest --cov=stac_fastapi --cov-report=term-missing'
8282
docker compose down
8383

8484
.PHONY: run-database-es

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ You can customize additional settings in your `.env` file:
114114
| `BACKEND` | Tests-related variable | `elasticsearch` or `opensearch` based on the backend | Optional |
115115
| `ELASTICSEARCH_VERSION` | Version of Elasticsearch to use. | `8.11.0` | Optional |
116116
| `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
117-
| `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional |
117+
| `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional
118+
| `RAISE_ON_BULK_ERROR` | Controls whether bulk insert operations raise exceptions on errors. If set to `true`, the operation will stop and raise an exception when an error occurs. If set to `false`, errors will be logged, and the operation will continue. **Note:** STAC Item and ItemCollection validation errors will always raise, regardless of this flag. | `false` | Optional |
118119

119120
> [!NOTE]
120121
> The variables `ES_HOST`, `ES_PORT`, `ES_USE_SSL`, and `ES_VERIFY_CERTS` apply to both Elasticsearch and OpenSearch backends, so there is no need to rename the key names to `OS_` even if you're using OpenSearch.

compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
environment:
1010
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
1111
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
12-
- STAC_FASTAPI_VERSION=4.0.0
12+
- STAC_FASTAPI_VERSION=4.1.0
1313
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-elasticsearch
1414
- APP_HOST=0.0.0.0
1515
- APP_PORT=8080
@@ -42,7 +42,7 @@ services:
4242
environment:
4343
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
4444
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
45-
- STAC_FASTAPI_VERSION=4.0.0
45+
- STAC_FASTAPI_VERSION=4.1.0
4646
- APP_HOST=0.0.0.0
4747
- APP_PORT=8082
4848
- RELOAD=true

examples/auth/compose.basic_auth.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
environment:
1010
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
1111
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
12-
- STAC_FASTAPI_VERSION=4.0.0
12+
- STAC_FASTAPI_VERSION=4.1.0
1313
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-elasticsearch
1414
- APP_HOST=0.0.0.0
1515
- APP_PORT=8080
@@ -43,7 +43,7 @@ services:
4343
environment:
4444
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
4545
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
46-
- STAC_FASTAPI_VERSION=4.0.0
46+
- STAC_FASTAPI_VERSION=4.1.0
4747
- APP_HOST=0.0.0.0
4848
- APP_PORT=8082
4949
- RELOAD=true

examples/auth/compose.oauth2.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
environment:
1010
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
1111
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
12-
- STAC_FASTAPI_VERSION=4.0.0
12+
- STAC_FASTAPI_VERSION=4.1.0
1313
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-elasticsearch
1414
- APP_HOST=0.0.0.0
1515
- APP_PORT=8080
@@ -44,7 +44,7 @@ services:
4444
environment:
4545
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
4646
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
47-
- STAC_FASTAPI_VERSION=4.0.0
47+
- STAC_FASTAPI_VERSION=4.1.0
4848
- APP_HOST=0.0.0.0
4949
- APP_PORT=8082
5050
- RELOAD=true

examples/auth/compose.route_dependencies.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
environment:
1010
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
1111
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
12-
- STAC_FASTAPI_VERSION=4.0.0
12+
- STAC_FASTAPI_VERSION=4.1.0
1313
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-elasticsearch
1414
- APP_HOST=0.0.0.0
1515
- APP_PORT=8080
@@ -43,7 +43,7 @@ services:
4343
environment:
4444
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
4545
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
46-
- STAC_FASTAPI_VERSION=4.0.0
46+
- STAC_FASTAPI_VERSION=4.1.0
4747
- APP_HOST=0.0.0.0
4848
- APP_PORT=8082
4949
- RELOAD=true

examples/rate_limit/compose.rate_limit.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ services:
99
environment:
1010
- STAC_FASTAPI_TITLE=stac-fastapi-elasticsearch
1111
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Elasticsearch backend
12-
- STAC_FASTAPI_VERSION=4.0.0
12+
- STAC_FASTAPI_VERSION=4.1.0
1313
- STAC_FASTAPI_LANDING_PAGE_ID=stac-fastapi-elasticsearch
1414
- APP_HOST=0.0.0.0
1515
- APP_PORT=8080
@@ -43,7 +43,7 @@ services:
4343
environment:
4444
- STAC_FASTAPI_TITLE=stac-fastapi-opensearch
4545
- STAC_FASTAPI_DESCRIPTION=A STAC FastAPI with an Opensearch backend
46-
- STAC_FASTAPI_VERSION=4.0.0
46+
- STAC_FASTAPI_VERSION=4.1.0
4747
- APP_HOST=0.0.0.0
4848
- APP_PORT=8082
4949
- RELOAD=true

stac_fastapi/core/stac_fastapi/core/core.py

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -676,46 +676,65 @@ class TransactionsClient(AsyncBaseTransactionsClient):
676676
@overrides
677677
async def create_item(
678678
self, collection_id: str, item: Union[Item, ItemCollection], **kwargs
679-
) -> Optional[stac_types.Item]:
680-
"""Create an item in the collection.
679+
) -> Union[stac_types.Item, str]:
680+
"""
681+
Create an item or a feature collection of items in the specified collection.
681682
682683
Args:
683-
collection_id (str): The id of the collection to add the item to.
684-
item (stac_types.Item): The item to be added to the collection.
685-
kwargs: Additional keyword arguments.
684+
collection_id (str): The ID of the collection to add the item(s) to.
685+
item (Union[Item, ItemCollection]): A single item or a collection of items to be added.
686+
**kwargs: Additional keyword arguments, such as `request` and `refresh`.
686687
687688
Returns:
688-
stac_types.Item: The created item.
689+
Union[stac_types.Item, str]: The created item if a single item is added, or a summary string
690+
indicating the number of items successfully added and errors if a collection of items is added.
689691
690692
Raises:
691-
NotFound: If the specified collection is not found in the database.
692-
ConflictError: If the item in the specified collection already exists.
693-
693+
NotFoundError: If the specified collection is not found in the database.
694+
ConflictError: If an item with the same ID already exists in the collection.
694695
"""
695-
item = item.model_dump(mode="json")
696-
base_url = str(kwargs["request"].base_url)
696+
request = kwargs.get("request")
697+
base_url = str(request.base_url)
697698

698-
# If a feature collection is posted
699-
if item["type"] == "FeatureCollection":
699+
# Convert Pydantic model to dict for uniform processing
700+
item_dict = item.model_dump(mode="json")
701+
702+
# Handle FeatureCollection (bulk insert)
703+
if item_dict["type"] == "FeatureCollection":
700704
bulk_client = BulkTransactionsClient(
701705
database=self.database, settings=self.settings
702706
)
707+
features = item_dict["features"]
703708
processed_items = [
704709
bulk_client.preprocess_item(
705-
item, base_url, BulkTransactionMethod.INSERT
710+
feature, base_url, BulkTransactionMethod.INSERT
706711
)
707-
for item in item["features"]
712+
for feature in features
708713
]
709-
710-
await self.database.bulk_async(
711-
collection_id, processed_items, refresh=kwargs.get("refresh", False)
714+
attempted = len(processed_items)
715+
success, errors = await self.database.bulk_async(
716+
collection_id,
717+
processed_items,
718+
refresh=kwargs.get("refresh", False),
712719
)
720+
if errors:
721+
logger.error(
722+
f"Bulk async operation encountered errors for collection {collection_id}: {errors} (attempted {attempted})"
723+
)
724+
else:
725+
logger.info(
726+
f"Bulk async operation succeeded with {success} actions for collection {collection_id}."
727+
)
728+
return f"Successfully added {success} Items. {attempted - success} errors occurred."
713729

714-
return None
715-
else:
716-
item = await self.database.prep_create_item(item=item, base_url=base_url)
717-
await self.database.create_item(item, refresh=kwargs.get("refresh", False))
718-
return ItemSerializer.db_to_stac(item, base_url)
730+
# Handle single item
731+
await self.database.create_item(
732+
item_dict,
733+
refresh=kwargs.get("refresh", False),
734+
base_url=base_url,
735+
exist_ok=False,
736+
)
737+
return ItemSerializer.db_to_stac(item_dict, base_url)
719738

720739
@overrides
721740
async def update_item(
@@ -741,9 +760,9 @@ async def update_item(
741760
now = datetime_type.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
742761
item["properties"]["updated"] = now
743762

744-
await self.database.check_collection_exists(collection_id)
745-
await self.delete_item(item_id=item_id, collection_id=collection_id)
746-
await self.create_item(collection_id=collection_id, item=Item(**item), **kwargs)
763+
await self.database.create_item(
764+
item, refresh=kwargs.get("refresh", False), base_url=base_url, exist_ok=True
765+
)
747766

748767
return ItemSerializer.db_to_stac(item, base_url)
749768

@@ -876,7 +895,7 @@ def preprocess_item(
876895
The preprocessed item.
877896
"""
878897
exist_ok = method == BulkTransactionMethod.UPSERT
879-
return self.database.sync_prep_create_item(
898+
return self.database.bulk_sync_prep_create_item(
880899
item=item, base_url=base_url, exist_ok=exist_ok
881900
)
882901

@@ -900,19 +919,32 @@ def bulk_item_insert(
900919
else:
901920
base_url = ""
902921

903-
processed_items = [
904-
self.preprocess_item(item, base_url, items.method)
905-
for item in items.items.values()
906-
]
922+
processed_items = []
923+
for item in items.items.values():
924+
try:
925+
validated = Item(**item) if not isinstance(item, Item) else item
926+
processed_items.append(
927+
self.preprocess_item(
928+
validated.model_dump(mode="json"), base_url, items.method
929+
)
930+
)
931+
except ValidationError:
932+
# Immediately raise on the first invalid item (strict mode)
933+
raise
907934

908-
# not a great way to get the collection_id-- should be part of the method signature
909935
collection_id = processed_items[0]["collection"]
910-
911-
self.database.bulk_sync(
912-
collection_id, processed_items, refresh=kwargs.get("refresh", False)
936+
attempted = len(processed_items)
937+
success, errors = self.database.bulk_sync(
938+
collection_id,
939+
processed_items,
940+
refresh=kwargs.get("refresh", False),
913941
)
942+
if errors:
943+
logger.error(f"Bulk sync operation encountered errors: {errors}")
944+
else:
945+
logger.info(f"Bulk sync operation succeeded with {success} actions.")
914946

915-
return f"Successfully added {len(processed_items)} Items."
947+
return f"Successfully added/updated {success} Items. {attempted - success} errors occurred."
916948

917949

918950
_DEFAULT_QUERYABLES: Dict[str, Dict[str, Any]] = {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""library version."""
2-
__version__ = "4.0.0"
2+
__version__ = "4.1.0"

0 commit comments

Comments
 (0)