stac-fastapi-elasticsearch 4.0.0a2__tar.gz → 4.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (17) hide show
  1. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/PKG-INFO +3 -2
  2. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/README.md +2 -1
  3. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/setup.py +1 -1
  4. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/stac_fastapi/elasticsearch/app.py +15 -8
  5. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/stac_fastapi/elasticsearch/config.py +2 -0
  6. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/stac_fastapi/elasticsearch/database_logic.py +249 -57
  7. stac_fastapi_elasticsearch-4.1.0/stac_fastapi/elasticsearch/version.py +2 -0
  8. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/stac_fastapi_elasticsearch.egg-info/PKG-INFO +3 -2
  9. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/stac_fastapi_elasticsearch.egg-info/requires.txt +1 -1
  10. stac_fastapi_elasticsearch-4.0.0a2/stac_fastapi/elasticsearch/version.py +0 -2
  11. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/setup.cfg +0 -0
  12. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/stac_fastapi/elasticsearch/__init__.py +0 -0
  13. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/stac_fastapi_elasticsearch.egg-info/SOURCES.txt +0 -0
  14. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/stac_fastapi_elasticsearch.egg-info/dependency_links.txt +0 -0
  15. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/stac_fastapi_elasticsearch.egg-info/entry_points.txt +0 -0
  16. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/stac_fastapi_elasticsearch.egg-info/not-zip-safe +0 -0
  17. {stac_fastapi_elasticsearch-4.0.0a2 → stac_fastapi_elasticsearch-4.1.0}/stac_fastapi_elasticsearch.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac_fastapi_elasticsearch
3
- Version: 4.0.0a2
3
+ Version: 4.1.0
4
4
  Summary: An implementation of STAC API based on the FastAPI framework with both Elasticsearch and Opensearch.
5
5
  Home-page: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
6
6
  License: MIT
@@ -134,7 +134,8 @@ You can customize additional settings in your `.env` file:
134
134
  | `BACKEND` | Tests-related variable | `elasticsearch` or `opensearch` based on the backend | Optional |
135
135
  | `ELASTICSEARCH_VERSION` | Version of Elasticsearch to use. | `8.11.0` | Optional |
136
136
  | `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
137
- | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional |
137
+ | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional
138
+ | `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 |
138
139
 
139
140
  > [!NOTE]
140
141
  > 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.
@@ -113,7 +113,8 @@ You can customize additional settings in your `.env` file:
113
113
  | `BACKEND` | Tests-related variable | `elasticsearch` or `opensearch` based on the backend | Optional |
114
114
  | `ELASTICSEARCH_VERSION` | Version of Elasticsearch to use. | `8.11.0` | Optional |
115
115
  | `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
116
- | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional |
116
+ | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional
117
+ | `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 |
117
118
 
118
119
  > [!NOTE]
119
120
  > 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.
@@ -6,7 +6,7 @@ with open("README.md") as f:
6
6
  desc = f.read()
7
7
 
8
8
  install_requires = [
9
- "stac-fastapi-core==4.0.0a2",
9
+ "stac-fastapi-core==4.1.0",
10
10
  "elasticsearch[async]~=8.18.0",
11
11
  "uvicorn~=0.23.0",
12
12
  "starlette>=0.35.0,<0.36.0",
@@ -1,6 +1,9 @@
1
1
  """FastAPI application."""
2
2
 
3
3
  import os
4
+ from contextlib import asynccontextmanager
5
+
6
+ from fastapi import FastAPI
4
7
 
5
8
  from stac_fastapi.api.app import StacApi
6
9
  from stac_fastapi.api.models import create_get_request_model, create_post_request_model
@@ -87,7 +90,7 @@ post_request_model = create_post_request_model(search_extensions)
87
90
  api = StacApi(
88
91
  title=os.getenv("STAC_FASTAPI_TITLE", "stac-fastapi-elasticsearch"),
89
92
  description=os.getenv("STAC_FASTAPI_DESCRIPTION", "stac-fastapi-elasticsearch"),
90
- api_version=os.getenv("STAC_FASTAPI_VERSION", "4.0.0a2"),
93
+ api_version=os.getenv("STAC_FASTAPI_VERSION", "4.1.0"),
91
94
  settings=settings,
92
95
  extensions=extensions,
93
96
  client=CoreClient(
@@ -97,17 +100,21 @@ api = StacApi(
97
100
  search_post_request_model=post_request_model,
98
101
  route_dependencies=get_route_dependencies(),
99
102
  )
100
- app = api.app
101
- app.root_path = os.getenv("STAC_FASTAPI_ROOT_PATH", "")
102
-
103
- # Add rate limit
104
- setup_rate_limit(app, rate_limit=os.getenv("STAC_FASTAPI_RATE_LIMIT"))
105
103
 
106
104
 
107
- @app.on_event("startup")
108
- async def _startup_event() -> None:
105
+ @asynccontextmanager
106
+ async def lifespan(app: FastAPI):
107
+ """Lifespan handler for FastAPI app. Initializes index templates and collections at startup."""
109
108
  await create_index_templates()
110
109
  await create_collection_index()
110
+ yield
111
+
112
+
113
+ app = api.app
114
+ app.router.lifespan_context = lifespan
115
+ app.root_path = os.getenv("STAC_FASTAPI_ROOT_PATH", "")
116
+ # Add rate limit
117
+ setup_rate_limit(app, rate_limit=os.getenv("STAC_FASTAPI_RATE_LIMIT"))
111
118
 
112
119
 
113
120
  def run() -> None:
@@ -86,6 +86,7 @@ class ElasticsearchSettings(ApiSettings, ApiBaseSettings):
86
86
  indexed_fields: Set[str] = {"datetime"}
87
87
  enable_response_models: bool = False
88
88
  enable_direct_response: bool = get_bool_env("ENABLE_DIRECT_RESPONSE", default=False)
89
+ raise_on_bulk_error: bool = get_bool_env("RAISE_ON_BULK_ERROR", default=False)
89
90
 
90
91
  @property
91
92
  def create_client(self):
@@ -106,6 +107,7 @@ class AsyncElasticsearchSettings(ApiSettings, ApiBaseSettings):
106
107
  indexed_fields: Set[str] = {"datetime"}
107
108
  enable_response_models: bool = False
108
109
  enable_direct_response: bool = get_bool_env("ENABLE_DIRECT_RESPONSE", default=False)
110
+ raise_on_bulk_error: bool = get_bool_env("RAISE_ON_BULK_ERROR", default=False)
109
111
 
110
112
  @property
111
113
  def create_client(self):
@@ -128,8 +128,20 @@ async def delete_item_index(collection_id: str):
128
128
  class DatabaseLogic(BaseDatabaseLogic):
129
129
  """Database logic."""
130
130
 
131
- client = AsyncElasticsearchSettings().create_client
132
- sync_client = SyncElasticsearchSettings().create_client
131
+ async_settings: AsyncElasticsearchSettings = attr.ib(
132
+ factory=AsyncElasticsearchSettings
133
+ )
134
+ sync_settings: SyncElasticsearchSettings = attr.ib(
135
+ factory=SyncElasticsearchSettings
136
+ )
137
+
138
+ client = attr.ib(init=False)
139
+ sync_client = attr.ib(init=False)
140
+
141
+ def __attrs_post_init__(self):
142
+ """Initialize clients after the class is instantiated."""
143
+ self.client = self.async_settings.create_client
144
+ self.sync_client = self.sync_settings.create_client
133
145
 
134
146
  item_serializer: Type[ItemSerializer] = attr.ib(default=ItemSerializer)
135
147
  collection_serializer: Type[CollectionSerializer] = attr.ib(
@@ -294,8 +306,8 @@ class DatabaseLogic(BaseDatabaseLogic):
294
306
  return search.filter("terms", collection=collection_ids)
295
307
 
296
308
  @staticmethod
297
- def apply_datetime_filter(search: Search, datetime_search):
298
- """Apply a filter to search based on datetime field.
309
+ def apply_datetime_filter(search: Search, datetime_search: dict):
310
+ """Apply a filter to search on datetime, start_datetime, and end_datetime fields.
299
311
 
300
312
  Args:
301
313
  search (Search): The search object to filter.
@@ -304,17 +316,109 @@ class DatabaseLogic(BaseDatabaseLogic):
304
316
  Returns:
305
317
  Search: The filtered search object.
306
318
  """
319
+ should = []
320
+
321
+ # If the request is a single datetime return
322
+ # items with datetimes equal to the requested datetime OR
323
+ # the requested datetime is between their start and end datetimes
307
324
  if "eq" in datetime_search:
308
- search = search.filter(
309
- "term", **{"properties__datetime": datetime_search["eq"]}
325
+ should.extend(
326
+ [
327
+ Q(
328
+ "bool",
329
+ filter=[
330
+ Q(
331
+ "term",
332
+ properties__datetime=datetime_search["eq"],
333
+ ),
334
+ ],
335
+ ),
336
+ Q(
337
+ "bool",
338
+ filter=[
339
+ Q(
340
+ "range",
341
+ properties__start_datetime={
342
+ "lte": datetime_search["eq"],
343
+ },
344
+ ),
345
+ Q(
346
+ "range",
347
+ properties__end_datetime={
348
+ "gte": datetime_search["eq"],
349
+ },
350
+ ),
351
+ ],
352
+ ),
353
+ ]
310
354
  )
355
+
356
+ # If the request is a date range return
357
+ # items with datetimes within the requested date range OR
358
+ # their startdatetime ithin the requested date range OR
359
+ # their enddatetime ithin the requested date range OR
360
+ # the requested daterange within their start and end datetimes
311
361
  else:
312
- search = search.filter(
313
- "range", properties__datetime={"lte": datetime_search["lte"]}
314
- )
315
- search = search.filter(
316
- "range", properties__datetime={"gte": datetime_search["gte"]}
362
+ should.extend(
363
+ [
364
+ Q(
365
+ "bool",
366
+ filter=[
367
+ Q(
368
+ "range",
369
+ properties__datetime={
370
+ "gte": datetime_search["gte"],
371
+ "lte": datetime_search["lte"],
372
+ },
373
+ ),
374
+ ],
375
+ ),
376
+ Q(
377
+ "bool",
378
+ filter=[
379
+ Q(
380
+ "range",
381
+ properties__start_datetime={
382
+ "gte": datetime_search["gte"],
383
+ "lte": datetime_search["lte"],
384
+ },
385
+ ),
386
+ ],
387
+ ),
388
+ Q(
389
+ "bool",
390
+ filter=[
391
+ Q(
392
+ "range",
393
+ properties__end_datetime={
394
+ "gte": datetime_search["gte"],
395
+ "lte": datetime_search["lte"],
396
+ },
397
+ ),
398
+ ],
399
+ ),
400
+ Q(
401
+ "bool",
402
+ filter=[
403
+ Q(
404
+ "range",
405
+ properties__start_datetime={
406
+ "lte": datetime_search["gte"]
407
+ },
408
+ ),
409
+ Q(
410
+ "range",
411
+ properties__end_datetime={
412
+ "gte": datetime_search["lte"]
413
+ },
414
+ ),
415
+ ],
416
+ ),
417
+ ]
317
418
  )
419
+
420
+ search = search.query(Q("bool", filter=[Q("bool", should=should)]))
421
+
318
422
  return search
319
423
 
320
424
  @staticmethod
@@ -607,7 +711,7 @@ class DatabaseLogic(BaseDatabaseLogic):
607
711
  if not await self.client.exists(index=COLLECTIONS_INDEX, id=collection_id):
608
712
  raise NotFoundError(f"Collection {collection_id} does not exist")
609
713
 
610
- async def prep_create_item(
714
+ async def async_prep_create_item(
611
715
  self, item: Item, base_url: str, exist_ok: bool = False
612
716
  ) -> Item:
613
717
  """
@@ -637,44 +741,114 @@ class DatabaseLogic(BaseDatabaseLogic):
637
741
 
638
742
  return self.item_serializer.stac_to_db(item, base_url)
639
743
 
640
- def sync_prep_create_item(
744
+ async def bulk_async_prep_create_item(
641
745
  self, item: Item, base_url: str, exist_ok: bool = False
642
746
  ) -> Item:
643
747
  """
644
748
  Prepare an item for insertion into the database.
645
749
 
646
- This method performs pre-insertion preparation on the given `item`,
647
- such as checking if the collection the item belongs to exists,
648
- and optionally verifying that an item with the same ID does not already exist in the database.
750
+ This method performs pre-insertion preparation on the given `item`, such as:
751
+ - Verifying that the collection the item belongs to exists.
752
+ - Optionally checking if an item with the same ID already exists in the database.
753
+ - Serializing the item into a database-compatible format.
649
754
 
650
755
  Args:
651
- item (Item): The item to be inserted into the database.
652
- base_url (str): The base URL used for constructing URLs for the item.
653
- exist_ok (bool): Indicates whether the item can exist already.
756
+ item (Item): The item to be prepared for insertion.
757
+ base_url (str): The base URL used to construct the item's self URL.
758
+ exist_ok (bool): Indicates whether the item can already exist in the database.
759
+ If False, a `ConflictError` is raised if the item exists.
654
760
 
655
761
  Returns:
656
- Item: The item after preparation is done.
762
+ Item: The prepared item, serialized into a database-compatible format.
657
763
 
658
764
  Raises:
659
765
  NotFoundError: If the collection that the item belongs to does not exist in the database.
660
- ConflictError: If an item with the same ID already exists in the collection.
766
+ ConflictError: If an item with the same ID already exists in the collection and `exist_ok` is False,
767
+ and `RAISE_ON_BULK_ERROR` is set to `true`.
661
768
  """
662
- item_id = item["id"]
663
- collection_id = item["collection"]
664
- if not self.sync_client.exists(index=COLLECTIONS_INDEX, id=collection_id):
665
- raise NotFoundError(f"Collection {collection_id} does not exist")
769
+ logger.debug(f"Preparing item {item['id']} in collection {item['collection']}.")
666
770
 
667
- if not exist_ok and self.sync_client.exists(
668
- index=index_alias_by_collection_id(collection_id),
669
- id=mk_item_id(item_id, collection_id),
771
+ # Check if the collection exists
772
+ await self.check_collection_exists(collection_id=item["collection"])
773
+
774
+ # Check if the item already exists in the database
775
+ if not exist_ok and await self.client.exists(
776
+ index=index_alias_by_collection_id(item["collection"]),
777
+ id=mk_item_id(item["id"], item["collection"]),
670
778
  ):
671
- raise ConflictError(
672
- f"Item {item_id} in collection {collection_id} already exists"
779
+ error_message = (
780
+ f"Item {item['id']} in collection {item['collection']} already exists."
673
781
  )
782
+ if self.async_settings.raise_on_bulk_error:
783
+ raise ConflictError(error_message)
784
+ else:
785
+ logger.warning(
786
+ f"{error_message} Continuing as `RAISE_ON_BULK_ERROR` is set to false."
787
+ )
788
+
789
+ # Serialize the item into a database-compatible format
790
+ prepped_item = self.item_serializer.stac_to_db(item, base_url)
791
+ logger.debug(f"Item {item['id']} prepared successfully.")
792
+ return prepped_item
793
+
794
+ def bulk_sync_prep_create_item(
795
+ self, item: Item, base_url: str, exist_ok: bool = False
796
+ ) -> Item:
797
+ """
798
+ Prepare an item for insertion into the database.
674
799
 
675
- return self.item_serializer.stac_to_db(item, base_url)
800
+ This method performs pre-insertion preparation on the given `item`, such as:
801
+ - Verifying that the collection the item belongs to exists.
802
+ - Optionally checking if an item with the same ID already exists in the database.
803
+ - Serializing the item into a database-compatible format.
804
+
805
+ Args:
806
+ item (Item): The item to be prepared for insertion.
807
+ base_url (str): The base URL used to construct the item's self URL.
808
+ exist_ok (bool): Indicates whether the item can already exist in the database.
809
+ If False, a `ConflictError` is raised if the item exists.
676
810
 
677
- async def create_item(self, item: Item, refresh: bool = False):
811
+ Returns:
812
+ Item: The prepared item, serialized into a database-compatible format.
813
+
814
+ Raises:
815
+ NotFoundError: If the collection that the item belongs to does not exist in the database.
816
+ ConflictError: If an item with the same ID already exists in the collection and `exist_ok` is False,
817
+ and `RAISE_ON_BULK_ERROR` is set to `true`.
818
+ """
819
+ logger.debug(f"Preparing item {item['id']} in collection {item['collection']}.")
820
+
821
+ # Check if the collection exists
822
+ if not self.sync_client.exists(index=COLLECTIONS_INDEX, id=item["collection"]):
823
+ raise NotFoundError(f"Collection {item['collection']} does not exist")
824
+
825
+ # Check if the item already exists in the database
826
+ if not exist_ok and self.sync_client.exists(
827
+ index=index_alias_by_collection_id(item["collection"]),
828
+ id=mk_item_id(item["id"], item["collection"]),
829
+ ):
830
+ error_message = (
831
+ f"Item {item['id']} in collection {item['collection']} already exists."
832
+ )
833
+ if self.sync_settings.raise_on_bulk_error:
834
+ raise ConflictError(error_message)
835
+ else:
836
+ logger.warning(
837
+ f"{error_message} Continuing as `RAISE_ON_BULK_ERROR` is set to false."
838
+ )
839
+
840
+ # Serialize the item into a database-compatible format
841
+ prepped_item = self.item_serializer.stac_to_db(item, base_url)
842
+ logger.debug(f"Item {item['id']} prepared successfully.")
843
+ return prepped_item
844
+
845
+ async def create_item(
846
+ self,
847
+ item: Item,
848
+ refresh: bool = False,
849
+ base_url: str = "",
850
+ exist_ok: bool = False,
851
+ ):
678
852
  """Database logic for creating one item.
679
853
 
680
854
  Args:
@@ -690,18 +864,16 @@ class DatabaseLogic(BaseDatabaseLogic):
690
864
  # todo: check if collection exists, but cache
691
865
  item_id = item["id"]
692
866
  collection_id = item["collection"]
693
- es_resp = await self.client.index(
867
+ item = await self.async_prep_create_item(
868
+ item=item, base_url=base_url, exist_ok=exist_ok
869
+ )
870
+ await self.client.index(
694
871
  index=index_alias_by_collection_id(collection_id),
695
872
  id=mk_item_id(item_id, collection_id),
696
873
  document=item,
697
874
  refresh=refresh,
698
875
  )
699
876
 
700
- if (meta := es_resp.get("meta")) and meta.get("status") == 409:
701
- raise ConflictError(
702
- f"Item {item_id} in collection {collection_id} already exists"
703
- )
704
-
705
877
  async def delete_item(
706
878
  self, item_id: str, collection_id: str, refresh: bool = False
707
879
  ):
@@ -867,52 +1039,72 @@ class DatabaseLogic(BaseDatabaseLogic):
867
1039
  await delete_item_index(collection_id)
868
1040
 
869
1041
  async def bulk_async(
870
- self, collection_id: str, processed_items: List[Item], refresh: bool = False
871
- ) -> None:
872
- """Perform a bulk insert of items into the database asynchronously.
1042
+ self,
1043
+ collection_id: str,
1044
+ processed_items: List[Item],
1045
+ refresh: bool = False,
1046
+ ) -> Tuple[int, List[Dict[str, Any]]]:
1047
+ """
1048
+ Perform a bulk insert of items into the database asynchronously.
873
1049
 
874
1050
  Args:
875
- self: The instance of the object calling this function.
876
1051
  collection_id (str): The ID of the collection to which the items belong.
877
1052
  processed_items (List[Item]): A list of `Item` objects to be inserted into the database.
878
1053
  refresh (bool): Whether to refresh the index after the bulk insert (default: False).
879
1054
 
1055
+ Returns:
1056
+ Tuple[int, List[Dict[str, Any]]]: A tuple containing:
1057
+ - The number of successfully processed actions (`success`).
1058
+ - A list of errors encountered during the bulk operation (`errors`).
1059
+
880
1060
  Notes:
881
- This function performs a bulk insert of `processed_items` into the database using the specified `collection_id`. The
882
- insert is performed asynchronously, and the event loop is used to run the operation in a separate executor. The
883
- `mk_actions` function is called to generate a list of actions for the bulk insert. If `refresh` is set to True, the
884
- index is refreshed after the bulk insert. The function does not return any value.
1061
+ This function performs a bulk insert of `processed_items` into the database using the specified `collection_id`.
1062
+ The insert is performed asynchronously, and the event loop is used to run the operation in a separate executor.
1063
+ The `mk_actions` function is called to generate a list of actions for the bulk insert. If `refresh` is set to True,
1064
+ the index is refreshed after the bulk insert.
885
1065
  """
886
- await helpers.async_bulk(
1066
+ raise_on_error = self.async_settings.raise_on_bulk_error
1067
+ success, errors = await helpers.async_bulk(
887
1068
  self.client,
888
1069
  mk_actions(collection_id, processed_items),
889
1070
  refresh=refresh,
890
- raise_on_error=False,
1071
+ raise_on_error=raise_on_error,
891
1072
  )
1073
+ return success, errors
892
1074
 
893
1075
  def bulk_sync(
894
- self, collection_id: str, processed_items: List[Item], refresh: bool = False
895
- ) -> None:
896
- """Perform a bulk insert of items into the database synchronously.
1076
+ self,
1077
+ collection_id: str,
1078
+ processed_items: List[Item],
1079
+ refresh: bool = False,
1080
+ ) -> Tuple[int, List[Dict[str, Any]]]:
1081
+ """
1082
+ Perform a bulk insert of items into the database synchronously.
897
1083
 
898
1084
  Args:
899
- self: The instance of the object calling this function.
900
1085
  collection_id (str): The ID of the collection to which the items belong.
901
1086
  processed_items (List[Item]): A list of `Item` objects to be inserted into the database.
902
1087
  refresh (bool): Whether to refresh the index after the bulk insert (default: False).
903
1088
 
1089
+ Returns:
1090
+ Tuple[int, List[Dict[str, Any]]]: A tuple containing:
1091
+ - The number of successfully processed actions (`success`).
1092
+ - A list of errors encountered during the bulk operation (`errors`).
1093
+
904
1094
  Notes:
905
- This function performs a bulk insert of `processed_items` into the database using the specified `collection_id`. The
906
- insert is performed synchronously and blocking, meaning that the function does not return until the insert has
1095
+ This function performs a bulk insert of `processed_items` into the database using the specified `collection_id`.
1096
+ The insert is performed synchronously and blocking, meaning that the function does not return until the insert has
907
1097
  completed. The `mk_actions` function is called to generate a list of actions for the bulk insert. If `refresh` is set to
908
- True, the index is refreshed after the bulk insert. The function does not return any value.
1098
+ True, the index is refreshed after the bulk insert.
909
1099
  """
910
- helpers.bulk(
1100
+ raise_on_error = self.sync_settings.raise_on_bulk_error
1101
+ success, errors = helpers.bulk(
911
1102
  self.sync_client,
912
1103
  mk_actions(collection_id, processed_items),
913
1104
  refresh=refresh,
914
- raise_on_error=False,
1105
+ raise_on_error=raise_on_error,
915
1106
  )
1107
+ return success, errors
916
1108
 
917
1109
  # DANGER
918
1110
  async def delete_items(self) -> None:
@@ -0,0 +1,2 @@
1
+ """library version."""
2
+ __version__ = "4.1.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac-fastapi-elasticsearch
3
- Version: 4.0.0a2
3
+ Version: 4.1.0
4
4
  Summary: An implementation of STAC API based on the FastAPI framework with both Elasticsearch and Opensearch.
5
5
  Home-page: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
6
6
  License: MIT
@@ -134,7 +134,8 @@ You can customize additional settings in your `.env` file:
134
134
  | `BACKEND` | Tests-related variable | `elasticsearch` or `opensearch` based on the backend | Optional |
135
135
  | `ELASTICSEARCH_VERSION` | Version of Elasticsearch to use. | `8.11.0` | Optional |
136
136
  | `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
137
- | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional |
137
+ | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional
138
+ | `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 |
138
139
 
139
140
  > [!NOTE]
140
141
  > 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.
@@ -1,4 +1,4 @@
1
- stac-fastapi-core==4.0.0a2
1
+ stac-fastapi-core==4.1.0
2
2
  elasticsearch[async]~=8.18.0
3
3
  uvicorn~=0.23.0
4
4
  starlette<0.36.0,>=0.35.0
@@ -1,2 +0,0 @@
1
- """library version."""
2
- __version__ = "4.0.0a2"