stac-fastapi-opensearch 4.1.0__py3-none-any.whl → 5.0.0a0__py3-none-any.whl

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.
@@ -5,7 +5,7 @@ import json
5
5
  import logging
6
6
  from base64 import urlsafe_b64decode, urlsafe_b64encode
7
7
  from copy import deepcopy
8
- from typing import Any, Dict, Iterable, List, Optional, Tuple, Type
8
+ from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union
9
9
 
10
10
  import attr
11
11
  from opensearchpy import exceptions, helpers
@@ -14,7 +14,30 @@ from opensearchpy.helpers.search import Search
14
14
  from starlette.requests import Request
15
15
 
16
16
  from stac_fastapi.core.base_database_logic import BaseDatabaseLogic
17
- from stac_fastapi.core.database_logic import (
17
+ from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
18
+ from stac_fastapi.core.utilities import MAX_LIMIT, bbox2polygon
19
+ from stac_fastapi.opensearch.config import (
20
+ AsyncOpensearchSettings as AsyncSearchSettings,
21
+ )
22
+ from stac_fastapi.opensearch.config import OpensearchSettings as SyncSearchSettings
23
+ from stac_fastapi.sfeos_helpers import filter
24
+ from stac_fastapi.sfeos_helpers.database import (
25
+ apply_free_text_filter_shared,
26
+ apply_intersects_filter_shared,
27
+ create_index_templates_shared,
28
+ delete_item_index_shared,
29
+ get_queryables_mapping_shared,
30
+ index_alias_by_collection_id,
31
+ index_by_collection_id,
32
+ indices,
33
+ mk_actions,
34
+ mk_item_id,
35
+ populate_sort_shared,
36
+ return_date,
37
+ validate_refresh,
38
+ )
39
+ from stac_fastapi.sfeos_helpers.mappings import (
40
+ AGGREGATION_MAPPING,
18
41
  COLLECTIONS_INDEX,
19
42
  DEFAULT_SORT,
20
43
  ES_COLLECTIONS_MAPPINGS,
@@ -23,20 +46,9 @@ from stac_fastapi.core.database_logic import (
23
46
  ITEM_INDICES,
24
47
  ITEMS_INDEX_PREFIX,
25
48
  Geometry,
26
- index_alias_by_collection_id,
27
- index_by_collection_id,
28
- indices,
29
- mk_actions,
30
- mk_item_id,
31
- )
32
- from stac_fastapi.core.extensions import filter
33
- from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
34
- from stac_fastapi.core.utilities import MAX_LIMIT, bbox2polygon
35
- from stac_fastapi.opensearch.config import (
36
- AsyncOpensearchSettings as AsyncSearchSettings,
37
49
  )
38
- from stac_fastapi.opensearch.config import OpensearchSettings as SyncSearchSettings
39
50
  from stac_fastapi.types.errors import ConflictError, NotFoundError
51
+ from stac_fastapi.types.rfc3339 import DateTimeType
40
52
  from stac_fastapi.types.stac import Collection, Item
41
53
 
42
54
  logger = logging.getLogger(__name__)
@@ -50,23 +62,7 @@ async def create_index_templates() -> None:
50
62
  None
51
63
 
52
64
  """
53
- client = AsyncSearchSettings().create_client
54
- await client.indices.put_template(
55
- name=f"template_{COLLECTIONS_INDEX}",
56
- body={
57
- "index_patterns": [f"{COLLECTIONS_INDEX}*"],
58
- "mappings": ES_COLLECTIONS_MAPPINGS,
59
- },
60
- )
61
- await client.indices.put_template(
62
- name=f"template_{ITEMS_INDEX_PREFIX}",
63
- body={
64
- "index_patterns": [f"{ITEMS_INDEX_PREFIX}*"],
65
- "settings": ES_ITEMS_SETTINGS,
66
- "mappings": ES_ITEMS_MAPPINGS,
67
- },
68
- )
69
- await client.close()
65
+ await create_index_templates_shared(settings=AsyncSearchSettings())
70
66
 
71
67
 
72
68
  async def create_collection_index() -> None:
@@ -125,18 +121,13 @@ async def delete_item_index(collection_id: str) -> None:
125
121
 
126
122
  Args:
127
123
  collection_id (str): The ID of the collection whose items index will be deleted.
128
- """
129
- client = AsyncSearchSettings().create_client
130
124
 
131
- name = index_alias_by_collection_id(collection_id)
132
- resolved = await client.indices.resolve_index(name=name)
133
- if "aliases" in resolved and resolved["aliases"]:
134
- [alias] = resolved["aliases"]
135
- await client.indices.delete_alias(index=alias["indices"], name=alias["name"])
136
- await client.indices.delete(index=alias["indices"])
137
- else:
138
- await client.indices.delete(index=name)
139
- await client.close()
125
+ Notes:
126
+ This function delegates to the shared implementation in delete_item_index_shared.
127
+ """
128
+ await delete_item_index_shared(
129
+ settings=AsyncSearchSettings(), collection_id=collection_id
130
+ )
140
131
 
141
132
 
142
133
  @attr.s
@@ -161,76 +152,7 @@ class DatabaseLogic(BaseDatabaseLogic):
161
152
 
162
153
  extensions: List[str] = attr.ib(default=attr.Factory(list))
163
154
 
164
- aggregation_mapping: Dict[str, Dict[str, Any]] = {
165
- "total_count": {"value_count": {"field": "id"}},
166
- "collection_frequency": {"terms": {"field": "collection", "size": 100}},
167
- "platform_frequency": {"terms": {"field": "properties.platform", "size": 100}},
168
- "cloud_cover_frequency": {
169
- "range": {
170
- "field": "properties.eo:cloud_cover",
171
- "ranges": [
172
- {"to": 5},
173
- {"from": 5, "to": 15},
174
- {"from": 15, "to": 40},
175
- {"from": 40},
176
- ],
177
- }
178
- },
179
- "datetime_frequency": {
180
- "date_histogram": {
181
- "field": "properties.datetime",
182
- "calendar_interval": "month",
183
- }
184
- },
185
- "datetime_min": {"min": {"field": "properties.datetime"}},
186
- "datetime_max": {"max": {"field": "properties.datetime"}},
187
- "grid_code_frequency": {
188
- "terms": {
189
- "field": "properties.grid:code",
190
- "missing": "none",
191
- "size": 10000,
192
- }
193
- },
194
- "sun_elevation_frequency": {
195
- "histogram": {"field": "properties.view:sun_elevation", "interval": 5}
196
- },
197
- "sun_azimuth_frequency": {
198
- "histogram": {"field": "properties.view:sun_azimuth", "interval": 5}
199
- },
200
- "off_nadir_frequency": {
201
- "histogram": {"field": "properties.view:off_nadir", "interval": 5}
202
- },
203
- "centroid_geohash_grid_frequency": {
204
- "geohash_grid": {
205
- "field": "properties.proj:centroid",
206
- "precision": 1,
207
- }
208
- },
209
- "centroid_geohex_grid_frequency": {
210
- "geohex_grid": {
211
- "field": "properties.proj:centroid",
212
- "precision": 0,
213
- }
214
- },
215
- "centroid_geotile_grid_frequency": {
216
- "geotile_grid": {
217
- "field": "properties.proj:centroid",
218
- "precision": 0,
219
- }
220
- },
221
- "geometry_geohash_grid_frequency": {
222
- "geohash_grid": {
223
- "field": "geometry",
224
- "precision": 1,
225
- }
226
- },
227
- "geometry_geotile_grid_frequency": {
228
- "geotile_grid": {
229
- "field": "geometry",
230
- "precision": 0,
231
- }
232
- },
233
- }
155
+ aggregation_mapping: Dict[str, Dict[str, Any]] = AGGREGATION_MAPPING
234
156
 
235
157
  """CORE LOGIC"""
236
158
 
@@ -307,6 +229,23 @@ class DatabaseLogic(BaseDatabaseLogic):
307
229
  )
308
230
  return item["_source"]
309
231
 
232
+ async def get_queryables_mapping(self, collection_id: str = "*") -> dict:
233
+ """Retrieve mapping of Queryables for search.
234
+
235
+ Args:
236
+ collection_id (str, optional): The id of the Collection the Queryables
237
+ belongs to. Defaults to "*".
238
+
239
+ Returns:
240
+ dict: A dictionary containing the Queryables mappings.
241
+ """
242
+ mappings = await self.client.indices.get_mapping(
243
+ index=f"{ITEMS_INDEX_PREFIX}{collection_id}",
244
+ )
245
+ return await get_queryables_mapping_shared(
246
+ collection_id=collection_id, mappings=mappings
247
+ )
248
+
310
249
  @staticmethod
311
250
  def make_search():
312
251
  """Database logic to create a Search instance."""
@@ -324,27 +263,37 @@ class DatabaseLogic(BaseDatabaseLogic):
324
263
 
325
264
  @staticmethod
326
265
  def apply_free_text_filter(search: Search, free_text_queries: Optional[List[str]]):
327
- """Database logic to perform query for search endpoint."""
328
- if free_text_queries is not None:
329
- free_text_query_string = '" OR properties.\\*:"'.join(free_text_queries)
330
- search = search.query(
331
- "query_string", query=f'properties.\\*:"{free_text_query_string}"'
332
- )
266
+ """Create a free text query for OpenSearch queries.
333
267
 
334
- return search
268
+ This method delegates to the shared implementation in apply_free_text_filter_shared.
269
+
270
+ Args:
271
+ search (Search): The search object to apply the query to.
272
+ free_text_queries (Optional[List[str]]): A list of text strings to search for in the properties.
273
+
274
+ Returns:
275
+ Search: The search object with the free text query applied, or the original search
276
+ object if no free_text_queries were provided.
277
+ """
278
+ return apply_free_text_filter_shared(
279
+ search=search, free_text_queries=free_text_queries
280
+ )
335
281
 
336
282
  @staticmethod
337
- def apply_datetime_filter(search: Search, datetime_search):
283
+ def apply_datetime_filter(
284
+ search: Search, interval: Optional[Union[DateTimeType, str]]
285
+ ):
338
286
  """Apply a filter to search based on datetime field, start_datetime, and end_datetime fields.
339
287
 
340
288
  Args:
341
289
  search (Search): The search object to filter.
342
- datetime_search (dict): The datetime filter criteria.
290
+ interval: Optional[Union[DateTimeType, str]]
343
291
 
344
292
  Returns:
345
293
  Search: The filtered search object.
346
294
  """
347
295
  should = []
296
+ datetime_search = return_date(interval)
348
297
 
349
298
  # If the request is a single datetime return
350
299
  # items with datetimes equal to the requested datetime OR
@@ -497,21 +446,8 @@ class DatabaseLogic(BaseDatabaseLogic):
497
446
  Notes:
498
447
  A geo_shape filter is added to the search object, set to intersect with the specified geometry.
499
448
  """
500
- return search.filter(
501
- Q(
502
- {
503
- "geo_shape": {
504
- "geometry": {
505
- "shape": {
506
- "type": intersects.type.lower(),
507
- "coordinates": intersects.coordinates,
508
- },
509
- "relation": "intersects",
510
- }
511
- }
512
- }
513
- )
514
- )
449
+ filter = apply_intersects_filter_shared(intersects=intersects)
450
+ return search.filter(Q(filter))
515
451
 
516
452
  @staticmethod
517
453
  def apply_stacql_filter(search: Search, op: str, field: str, value: float):
@@ -535,8 +471,9 @@ class DatabaseLogic(BaseDatabaseLogic):
535
471
 
536
472
  return search
537
473
 
538
- @staticmethod
539
- def apply_cql2_filter(search: Search, _filter: Optional[Dict[str, Any]]):
474
+ async def apply_cql2_filter(
475
+ self, search: Search, _filter: Optional[Dict[str, Any]]
476
+ ):
540
477
  """
541
478
  Apply a CQL2 filter to an Opensearch Search object.
542
479
 
@@ -556,18 +493,25 @@ class DatabaseLogic(BaseDatabaseLogic):
556
493
  otherwise the original Search object.
557
494
  """
558
495
  if _filter is not None:
559
- es_query = filter.to_es(_filter)
496
+ es_query = filter.to_es(await self.get_queryables_mapping(), _filter)
560
497
  search = search.filter(es_query)
561
498
 
562
499
  return search
563
500
 
564
501
  @staticmethod
565
502
  def populate_sort(sortby: List) -> Optional[Dict[str, Dict[str, str]]]:
566
- """Database logic to sort search instance."""
567
- if sortby:
568
- return {s.field: {"order": s.direction} for s in sortby}
569
- else:
570
- return None
503
+ """Create a sort configuration for OpenSearch queries.
504
+
505
+ This method delegates to the shared implementation in populate_sort_shared.
506
+
507
+ Args:
508
+ sortby (List): A list of sort specifications, each containing a field and direction.
509
+
510
+ Returns:
511
+ Optional[Dict[str, Dict[str, str]]]: A dictionary mapping field names to sort direction
512
+ configurations, or None if no sort was specified.
513
+ """
514
+ return populate_sort_shared(sortby=sortby)
571
515
 
572
516
  async def execute_search(
573
517
  self,
@@ -864,15 +808,17 @@ class DatabaseLogic(BaseDatabaseLogic):
864
808
  async def create_item(
865
809
  self,
866
810
  item: Item,
867
- refresh: bool = False,
868
811
  base_url: str = "",
869
812
  exist_ok: bool = False,
813
+ **kwargs: Any,
870
814
  ):
871
815
  """Database logic for creating one item.
872
816
 
873
817
  Args:
874
818
  item (Item): The item to be created.
875
- refresh (bool, optional): Refresh the index after performing the operation. Defaults to False.
819
+ base_url (str, optional): The base URL for the item. Defaults to an empty string.
820
+ exist_ok (bool, optional): Whether to allow the item to exist already. Defaults to False.
821
+ **kwargs: Additional keyword arguments like refresh.
876
822
 
877
823
  Raises:
878
824
  ConflictError: If the item already exists in the database.
@@ -883,6 +829,19 @@ class DatabaseLogic(BaseDatabaseLogic):
883
829
  # todo: check if collection exists, but cache
884
830
  item_id = item["id"]
885
831
  collection_id = item["collection"]
832
+
833
+ # Ensure kwargs is a dictionary
834
+ kwargs = kwargs or {}
835
+
836
+ # Resolve the `refresh` parameter
837
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
838
+ refresh = validate_refresh(refresh)
839
+
840
+ # Log the creation attempt
841
+ logger.info(
842
+ f"Creating item {item_id} in collection {collection_id} with refresh={refresh}"
843
+ )
844
+
886
845
  item = await self.async_prep_create_item(
887
846
  item=item, base_url=base_url, exist_ok=exist_ok
888
847
  )
@@ -893,19 +852,29 @@ class DatabaseLogic(BaseDatabaseLogic):
893
852
  refresh=refresh,
894
853
  )
895
854
 
896
- async def delete_item(
897
- self, item_id: str, collection_id: str, refresh: bool = False
898
- ):
855
+ async def delete_item(self, item_id: str, collection_id: str, **kwargs: Any):
899
856
  """Delete a single item from the database.
900
857
 
901
858
  Args:
902
859
  item_id (str): The id of the Item to be deleted.
903
860
  collection_id (str): The id of the Collection that the Item belongs to.
904
- refresh (bool, optional): Whether to refresh the index after the deletion. Default is False.
861
+ **kwargs: Additional keyword arguments like refresh.
905
862
 
906
863
  Raises:
907
864
  NotFoundError: If the Item does not exist in the database.
908
865
  """
866
+ # Ensure kwargs is a dictionary
867
+ kwargs = kwargs or {}
868
+
869
+ # Resolve the `refresh` parameter
870
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
871
+ refresh = validate_refresh(refresh)
872
+
873
+ # Log the deletion attempt
874
+ logger.info(
875
+ f"Deleting item {item_id} from collection {collection_id} with refresh={refresh}"
876
+ )
877
+
909
878
  try:
910
879
  await self.client.delete(
911
880
  index=index_alias_by_collection_id(collection_id),
@@ -935,12 +904,12 @@ class DatabaseLogic(BaseDatabaseLogic):
935
904
  except exceptions.NotFoundError:
936
905
  raise NotFoundError(f"Mapping for index {index_name} not found")
937
906
 
938
- async def create_collection(self, collection: Collection, refresh: bool = False):
907
+ async def create_collection(self, collection: Collection, **kwargs: Any):
939
908
  """Create a single collection in the database.
940
909
 
941
910
  Args:
942
911
  collection (Collection): The Collection object to be created.
943
- refresh (bool, optional): Whether to refresh the index after the creation. Default is False.
912
+ **kwargs: Additional keyword arguments like refresh.
944
913
 
945
914
  Raises:
946
915
  ConflictError: If a Collection with the same id already exists in the database.
@@ -950,6 +919,16 @@ class DatabaseLogic(BaseDatabaseLogic):
950
919
  """
951
920
  collection_id = collection["id"]
952
921
 
922
+ # Ensure kwargs is a dictionary
923
+ kwargs = kwargs or {}
924
+
925
+ # Resolve the `refresh` parameter
926
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
927
+ refresh = validate_refresh(refresh)
928
+
929
+ # Log the creation attempt
930
+ logger.info(f"Creating collection {collection_id} with refresh={refresh}")
931
+
953
932
  if await self.client.exists(index=COLLECTIONS_INDEX, id=collection_id):
954
933
  raise ConflictError(f"Collection {collection_id} already exists")
955
934
 
@@ -989,14 +968,14 @@ class DatabaseLogic(BaseDatabaseLogic):
989
968
  return collection["_source"]
990
969
 
991
970
  async def update_collection(
992
- self, collection_id: str, collection: Collection, refresh: bool = False
971
+ self, collection_id: str, collection: Collection, **kwargs: Any
993
972
  ):
994
973
  """Update a collection from the database.
995
974
 
996
975
  Args:
997
- self: The instance of the object calling this function.
998
976
  collection_id (str): The ID of the collection to be updated.
999
977
  collection (Collection): The Collection object to be used for the update.
978
+ **kwargs: Additional keyword arguments like refresh.
1000
979
 
1001
980
  Raises:
1002
981
  NotFoundError: If the collection with the given `collection_id` is not
@@ -1007,9 +986,23 @@ class DatabaseLogic(BaseDatabaseLogic):
1007
986
  `collection_id` and with the collection specified in the `Collection` object.
1008
987
  If the collection is not found, a `NotFoundError` is raised.
1009
988
  """
989
+ # Ensure kwargs is a dictionary
990
+ kwargs = kwargs or {}
991
+
992
+ # Resolve the `refresh` parameter
993
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
994
+ refresh = validate_refresh(refresh)
995
+
996
+ # Log the update attempt
997
+ logger.info(f"Updating collection {collection_id} with refresh={refresh}")
998
+
1010
999
  await self.find_collection(collection_id=collection_id)
1011
1000
 
1012
1001
  if collection_id != collection["id"]:
1002
+ logger.info(
1003
+ f"Collection ID change detected: {collection_id} -> {collection['id']}"
1004
+ )
1005
+
1013
1006
  await self.create_collection(collection, refresh=refresh)
1014
1007
 
1015
1008
  await self.client.reindex(
@@ -1025,7 +1018,7 @@ class DatabaseLogic(BaseDatabaseLogic):
1025
1018
  refresh=refresh,
1026
1019
  )
1027
1020
 
1028
- await self.delete_collection(collection_id)
1021
+ await self.delete_collection(collection_id=collection_id, **kwargs)
1029
1022
 
1030
1023
  else:
1031
1024
  await self.client.index(
@@ -1035,23 +1028,34 @@ class DatabaseLogic(BaseDatabaseLogic):
1035
1028
  refresh=refresh,
1036
1029
  )
1037
1030
 
1038
- async def delete_collection(self, collection_id: str, refresh: bool = False):
1031
+ async def delete_collection(self, collection_id: str, **kwargs: Any):
1039
1032
  """Delete a collection from the database.
1040
1033
 
1041
1034
  Parameters:
1042
1035
  self: The instance of the object calling this function.
1043
1036
  collection_id (str): The ID of the collection to be deleted.
1044
- refresh (bool): Whether to refresh the index after the deletion (default: False).
1037
+ **kwargs: Additional keyword arguments like refresh.
1045
1038
 
1046
1039
  Raises:
1047
1040
  NotFoundError: If the collection with the given `collection_id` is not found in the database.
1048
1041
 
1049
1042
  Notes:
1050
1043
  This function first verifies that the collection with the specified `collection_id` exists in the database, and then
1051
- deletes the collection. If `refresh` is set to True, the index is refreshed after the deletion. Additionally, this
1052
- function also calls `delete_item_index` to delete the index for the items in the collection.
1044
+ deletes the collection. If `refresh` is set to "true", "false", or "wait_for", the index is refreshed accordingly after
1045
+ the deletion. Additionally, this function also calls `delete_item_index` to delete the index for the items in the collection.
1053
1046
  """
1047
+ # Ensure kwargs is a dictionary
1048
+ kwargs = kwargs or {}
1049
+
1054
1050
  await self.find_collection(collection_id=collection_id)
1051
+
1052
+ # Resolve the `refresh` parameter
1053
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
1054
+ refresh = validate_refresh(refresh)
1055
+
1056
+ # Log the deletion attempt
1057
+ logger.info(f"Deleting collection {collection_id} with refresh={refresh}")
1058
+
1055
1059
  await self.client.delete(
1056
1060
  index=COLLECTIONS_INDEX, id=collection_id, refresh=refresh
1057
1061
  )
@@ -1061,7 +1065,7 @@ class DatabaseLogic(BaseDatabaseLogic):
1061
1065
  self,
1062
1066
  collection_id: str,
1063
1067
  processed_items: List[Item],
1064
- refresh: bool = False,
1068
+ **kwargs: Any,
1065
1069
  ) -> Tuple[int, List[Dict[str, Any]]]:
1066
1070
  """
1067
1071
  Perform a bulk insert of items into the database asynchronously.
@@ -1069,7 +1073,12 @@ class DatabaseLogic(BaseDatabaseLogic):
1069
1073
  Args:
1070
1074
  collection_id (str): The ID of the collection to which the items belong.
1071
1075
  processed_items (List[Item]): A list of `Item` objects to be inserted into the database.
1072
- refresh (bool): Whether to refresh the index after the bulk insert (default: False).
1076
+ **kwargs (Any): Additional keyword arguments, including:
1077
+ - refresh (str, optional): Whether to refresh the index after the bulk insert.
1078
+ Can be "true", "false", or "wait_for". Defaults to the value of `self.sync_settings.database_refresh`.
1079
+ - refresh (bool, optional): Whether to refresh the index after the bulk insert.
1080
+ - raise_on_error (bool, optional): Whether to raise an error if any of the bulk operations fail.
1081
+ Defaults to the value of `self.async_settings.raise_on_bulk_error`.
1073
1082
 
1074
1083
  Returns:
1075
1084
  Tuple[int, List[Dict[str, Any]]]: A tuple containing:
@@ -1078,10 +1087,30 @@ class DatabaseLogic(BaseDatabaseLogic):
1078
1087
 
1079
1088
  Notes:
1080
1089
  This function performs a bulk insert of `processed_items` into the database using the specified `collection_id`.
1081
- The insert is performed asynchronously, and the event loop is used to run the operation in a separate executor.
1082
- The `mk_actions` function is called to generate a list of actions for the bulk insert. If `refresh` is set to True,
1083
- the index is refreshed after the bulk insert.
1090
+ The insert is performed synchronously and blocking, meaning that the function does not return until the insert has
1091
+ completed. The `mk_actions` function is called to generate a list of actions for the bulk insert. The `refresh`
1092
+ parameter determines whether the index is refreshed after the bulk insert:
1093
+ - "true": Forces an immediate refresh of the index.
1094
+ - "false": Does not refresh the index immediately (default behavior).
1095
+ - "wait_for": Waits for the next refresh cycle to make the changes visible.
1084
1096
  """
1097
+ # Ensure kwargs is a dictionary
1098
+ kwargs = kwargs or {}
1099
+
1100
+ # Resolve the `refresh` parameter
1101
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
1102
+ refresh = validate_refresh(refresh)
1103
+
1104
+ # Log the bulk insert attempt
1105
+ logger.info(
1106
+ f"Performing bulk insert for collection {collection_id} with refresh={refresh}"
1107
+ )
1108
+
1109
+ # Handle empty processed_items
1110
+ if not processed_items:
1111
+ logger.warning(f"No items to insert for collection {collection_id}")
1112
+ return 0, []
1113
+
1085
1114
  raise_on_error = self.async_settings.raise_on_bulk_error
1086
1115
  success, errors = await helpers.async_bulk(
1087
1116
  self.client,
@@ -1089,21 +1118,30 @@ class DatabaseLogic(BaseDatabaseLogic):
1089
1118
  refresh=refresh,
1090
1119
  raise_on_error=raise_on_error,
1091
1120
  )
1121
+ # Log the result
1122
+ logger.info(
1123
+ f"Bulk insert completed for collection {collection_id}: {success} successes, {len(errors)} errors"
1124
+ )
1092
1125
  return success, errors
1093
1126
 
1094
1127
  def bulk_sync(
1095
1128
  self,
1096
1129
  collection_id: str,
1097
1130
  processed_items: List[Item],
1098
- refresh: bool = False,
1131
+ **kwargs: Any,
1099
1132
  ) -> Tuple[int, List[Dict[str, Any]]]:
1100
1133
  """
1101
- Perform a bulk insert of items into the database synchronously.
1134
+ Perform a bulk insert of items into the database asynchronously.
1102
1135
 
1103
1136
  Args:
1104
1137
  collection_id (str): The ID of the collection to which the items belong.
1105
1138
  processed_items (List[Item]): A list of `Item` objects to be inserted into the database.
1106
- refresh (bool): Whether to refresh the index after the bulk insert (default: False).
1139
+ **kwargs (Any): Additional keyword arguments, including:
1140
+ - refresh (str, optional): Whether to refresh the index after the bulk insert.
1141
+ Can be "true", "false", or "wait_for". Defaults to the value of `self.sync_settings.database_refresh`.
1142
+ - refresh (bool, optional): Whether to refresh the index after the bulk insert.
1143
+ - raise_on_error (bool, optional): Whether to raise an error if any of the bulk operations fail.
1144
+ Defaults to the value of `self.async_settings.raise_on_bulk_error`.
1107
1145
 
1108
1146
  Returns:
1109
1147
  Tuple[int, List[Dict[str, Any]]]: A tuple containing:
@@ -1113,9 +1151,29 @@ class DatabaseLogic(BaseDatabaseLogic):
1113
1151
  Notes:
1114
1152
  This function performs a bulk insert of `processed_items` into the database using the specified `collection_id`.
1115
1153
  The insert is performed synchronously and blocking, meaning that the function does not return until the insert has
1116
- completed. The `mk_actions` function is called to generate a list of actions for the bulk insert. If `refresh` is set to
1117
- True, the index is refreshed after the bulk insert.
1154
+ completed. The `mk_actions` function is called to generate a list of actions for the bulk insert. The `refresh`
1155
+ parameter determines whether the index is refreshed after the bulk insert:
1156
+ - "true": Forces an immediate refresh of the index.
1157
+ - "false": Does not refresh the index immediately (default behavior).
1158
+ - "wait_for": Waits for the next refresh cycle to make the changes visible.
1118
1159
  """
1160
+ # Ensure kwargs is a dictionary
1161
+ kwargs = kwargs or {}
1162
+
1163
+ # Resolve the `refresh` parameter
1164
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
1165
+ refresh = validate_refresh(refresh)
1166
+
1167
+ # Log the bulk insert attempt
1168
+ logger.info(
1169
+ f"Performing bulk insert for collection {collection_id} with refresh={refresh}"
1170
+ )
1171
+
1172
+ # Handle empty processed_items
1173
+ if not processed_items:
1174
+ logger.warning(f"No items to insert for collection {collection_id}")
1175
+ return 0, []
1176
+
1119
1177
  raise_on_error = self.sync_settings.raise_on_bulk_error
1120
1178
  success, errors = helpers.bulk(
1121
1179
  self.sync_client,
@@ -1,2 +1,2 @@
1
1
  """library version."""
2
- __version__ = "4.1.0"
2
+ __version__ = "5.0.0a0"