stac-fastapi-elasticsearch 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
  import elasticsearch.helpers as helpers
@@ -14,29 +14,38 @@ from elasticsearch.exceptions import NotFoundError as ESNotFoundError
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 (
18
- COLLECTIONS_INDEX,
19
- DEFAULT_SORT,
20
- ES_COLLECTIONS_MAPPINGS,
21
- ES_ITEMS_MAPPINGS,
22
- ES_ITEMS_SETTINGS,
23
- ITEM_INDICES,
24
- ITEMS_INDEX_PREFIX,
25
- Geometry,
17
+ from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
18
+ from stac_fastapi.core.utilities import MAX_LIMIT, bbox2polygon
19
+ from stac_fastapi.elasticsearch.config import AsyncElasticsearchSettings
20
+ from stac_fastapi.elasticsearch.config import (
21
+ ElasticsearchSettings as SyncElasticsearchSettings,
22
+ )
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,
26
30
  index_alias_by_collection_id,
27
31
  index_by_collection_id,
28
32
  indices,
29
33
  mk_actions,
30
34
  mk_item_id,
35
+ populate_sort_shared,
36
+ return_date,
37
+ validate_refresh,
31
38
  )
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.elasticsearch.config import AsyncElasticsearchSettings
36
- from stac_fastapi.elasticsearch.config import (
37
- ElasticsearchSettings as SyncElasticsearchSettings,
39
+ from stac_fastapi.sfeos_helpers.mappings import (
40
+ AGGREGATION_MAPPING,
41
+ COLLECTIONS_INDEX,
42
+ DEFAULT_SORT,
43
+ ITEM_INDICES,
44
+ ITEMS_INDEX_PREFIX,
45
+ Geometry,
38
46
  )
39
47
  from stac_fastapi.types.errors import ConflictError, NotFoundError
48
+ from stac_fastapi.types.rfc3339 import DateTimeType
40
49
  from stac_fastapi.types.stac import Collection, Item
41
50
 
42
51
  logger = logging.getLogger(__name__)
@@ -50,22 +59,7 @@ async def create_index_templates() -> None:
50
59
  None
51
60
 
52
61
  """
53
- client = AsyncElasticsearchSettings().create_client
54
- await client.indices.put_index_template(
55
- name=f"template_{COLLECTIONS_INDEX}",
56
- body={
57
- "index_patterns": [f"{COLLECTIONS_INDEX}*"],
58
- "template": {"mappings": ES_COLLECTIONS_MAPPINGS},
59
- },
60
- )
61
- await client.indices.put_index_template(
62
- name=f"template_{ITEMS_INDEX_PREFIX}",
63
- body={
64
- "index_patterns": [f"{ITEMS_INDEX_PREFIX}*"],
65
- "template": {"settings": ES_ITEMS_SETTINGS, "mappings": ES_ITEMS_MAPPINGS},
66
- },
67
- )
68
- await client.close()
62
+ await create_index_templates_shared(settings=AsyncElasticsearchSettings())
69
63
 
70
64
 
71
65
  async def create_collection_index() -> None:
@@ -110,18 +104,13 @@ async def delete_item_index(collection_id: str):
110
104
 
111
105
  Args:
112
106
  collection_id (str): The ID of the collection whose items index will be deleted.
113
- """
114
- client = AsyncElasticsearchSettings().create_client
115
107
 
116
- name = index_alias_by_collection_id(collection_id)
117
- resolved = await client.indices.resolve_index(name=name)
118
- if "aliases" in resolved and resolved["aliases"]:
119
- [alias] = resolved["aliases"]
120
- await client.indices.delete_alias(index=alias["indices"], name=alias["name"])
121
- await client.indices.delete(index=alias["indices"])
122
- else:
123
- await client.indices.delete(index=name)
124
- await client.close()
108
+ Notes:
109
+ This function delegates to the shared implementation in delete_item_index_shared.
110
+ """
111
+ await delete_item_index_shared(
112
+ settings=AsyncElasticsearchSettings(), collection_id=collection_id
113
+ )
125
114
 
126
115
 
127
116
  @attr.s
@@ -150,76 +139,7 @@ class DatabaseLogic(BaseDatabaseLogic):
150
139
 
151
140
  extensions: List[str] = attr.ib(default=attr.Factory(list))
152
141
 
153
- aggregation_mapping: Dict[str, Dict[str, Any]] = {
154
- "total_count": {"value_count": {"field": "id"}},
155
- "collection_frequency": {"terms": {"field": "collection", "size": 100}},
156
- "platform_frequency": {"terms": {"field": "properties.platform", "size": 100}},
157
- "cloud_cover_frequency": {
158
- "range": {
159
- "field": "properties.eo:cloud_cover",
160
- "ranges": [
161
- {"to": 5},
162
- {"from": 5, "to": 15},
163
- {"from": 15, "to": 40},
164
- {"from": 40},
165
- ],
166
- }
167
- },
168
- "datetime_frequency": {
169
- "date_histogram": {
170
- "field": "properties.datetime",
171
- "calendar_interval": "month",
172
- }
173
- },
174
- "datetime_min": {"min": {"field": "properties.datetime"}},
175
- "datetime_max": {"max": {"field": "properties.datetime"}},
176
- "grid_code_frequency": {
177
- "terms": {
178
- "field": "properties.grid:code",
179
- "missing": "none",
180
- "size": 10000,
181
- }
182
- },
183
- "sun_elevation_frequency": {
184
- "histogram": {"field": "properties.view:sun_elevation", "interval": 5}
185
- },
186
- "sun_azimuth_frequency": {
187
- "histogram": {"field": "properties.view:sun_azimuth", "interval": 5}
188
- },
189
- "off_nadir_frequency": {
190
- "histogram": {"field": "properties.view:off_nadir", "interval": 5}
191
- },
192
- "centroid_geohash_grid_frequency": {
193
- "geohash_grid": {
194
- "field": "properties.proj:centroid",
195
- "precision": 1,
196
- }
197
- },
198
- "centroid_geohex_grid_frequency": {
199
- "geohex_grid": {
200
- "field": "properties.proj:centroid",
201
- "precision": 0,
202
- }
203
- },
204
- "centroid_geotile_grid_frequency": {
205
- "geotile_grid": {
206
- "field": "properties.proj:centroid",
207
- "precision": 0,
208
- }
209
- },
210
- "geometry_geohash_grid_frequency": {
211
- "geohash_grid": {
212
- "field": "geometry",
213
- "precision": 1,
214
- }
215
- },
216
- "geometry_geotile_grid_frequency": {
217
- "geotile_grid": {
218
- "field": "geometry",
219
- "precision": 0,
220
- }
221
- },
222
- }
142
+ aggregation_mapping: Dict[str, Dict[str, Any]] = AGGREGATION_MAPPING
223
143
 
224
144
  """CORE LOGIC"""
225
145
 
@@ -290,6 +210,23 @@ class DatabaseLogic(BaseDatabaseLogic):
290
210
  )
291
211
  return item["_source"]
292
212
 
213
+ async def get_queryables_mapping(self, collection_id: str = "*") -> dict:
214
+ """Retrieve mapping of Queryables for search.
215
+
216
+ Args:
217
+ collection_id (str, optional): The id of the Collection the Queryables
218
+ belongs to. Defaults to "*".
219
+
220
+ Returns:
221
+ dict: A dictionary containing the Queryables mappings.
222
+ """
223
+ mappings = await self.client.indices.get_mapping(
224
+ index=f"{ITEMS_INDEX_PREFIX}{collection_id}",
225
+ )
226
+ return await get_queryables_mapping_shared(
227
+ collection_id=collection_id, mappings=mappings
228
+ )
229
+
293
230
  @staticmethod
294
231
  def make_search():
295
232
  """Database logic to create a Search instance."""
@@ -306,17 +243,20 @@ class DatabaseLogic(BaseDatabaseLogic):
306
243
  return search.filter("terms", collection=collection_ids)
307
244
 
308
245
  @staticmethod
309
- def apply_datetime_filter(search: Search, datetime_search: dict):
246
+ def apply_datetime_filter(
247
+ search: Search, interval: Optional[Union[DateTimeType, str]]
248
+ ):
310
249
  """Apply a filter to search on datetime, start_datetime, and end_datetime fields.
311
250
 
312
251
  Args:
313
252
  search (Search): The search object to filter.
314
- datetime_search (dict): The datetime filter criteria.
253
+ interval: Optional[Union[DateTimeType, str]]
315
254
 
316
255
  Returns:
317
256
  Search: The filtered search object.
318
257
  """
319
258
  should = []
259
+ datetime_search = return_date(interval)
320
260
 
321
261
  # If the request is a single datetime return
322
262
  # items with datetimes equal to the requested datetime OR
@@ -469,21 +409,8 @@ class DatabaseLogic(BaseDatabaseLogic):
469
409
  Notes:
470
410
  A geo_shape filter is added to the search object, set to intersect with the specified geometry.
471
411
  """
472
- return search.filter(
473
- Q(
474
- {
475
- "geo_shape": {
476
- "geometry": {
477
- "shape": {
478
- "type": intersects.type.lower(),
479
- "coordinates": intersects.coordinates,
480
- },
481
- "relation": "intersects",
482
- }
483
- }
484
- }
485
- )
486
- )
412
+ filter = apply_intersects_filter_shared(intersects=intersects)
413
+ return search.filter(Q(filter))
487
414
 
488
415
  @staticmethod
489
416
  def apply_stacql_filter(search: Search, op: str, field: str, value: float):
@@ -509,17 +436,25 @@ class DatabaseLogic(BaseDatabaseLogic):
509
436
 
510
437
  @staticmethod
511
438
  def apply_free_text_filter(search: Search, free_text_queries: Optional[List[str]]):
512
- """Database logic to perform query for search endpoint."""
513
- if free_text_queries is not None:
514
- free_text_query_string = '" OR properties.\\*:"'.join(free_text_queries)
515
- search = search.query(
516
- "query_string", query=f'properties.\\*:"{free_text_query_string}"'
517
- )
439
+ """Create a free text query for Elasticsearch queries.
518
440
 
519
- return search
441
+ This method delegates to the shared implementation in apply_free_text_filter_shared.
520
442
 
521
- @staticmethod
522
- def apply_cql2_filter(search: Search, _filter: Optional[Dict[str, Any]]):
443
+ Args:
444
+ search (Search): The search object to apply the query to.
445
+ free_text_queries (Optional[List[str]]): A list of text strings to search for in the properties.
446
+
447
+ Returns:
448
+ Search: The search object with the free text query applied, or the original search
449
+ object if no free_text_queries were provided.
450
+ """
451
+ return apply_free_text_filter_shared(
452
+ search=search, free_text_queries=free_text_queries
453
+ )
454
+
455
+ async def apply_cql2_filter(
456
+ self, search: Search, _filter: Optional[Dict[str, Any]]
457
+ ):
523
458
  """
524
459
  Apply a CQL2 filter to an Elasticsearch Search object.
525
460
 
@@ -539,18 +474,25 @@ class DatabaseLogic(BaseDatabaseLogic):
539
474
  otherwise the original Search object.
540
475
  """
541
476
  if _filter is not None:
542
- es_query = filter.to_es(_filter)
477
+ es_query = filter.to_es(await self.get_queryables_mapping(), _filter)
543
478
  search = search.query(es_query)
544
479
 
545
480
  return search
546
481
 
547
482
  @staticmethod
548
483
  def populate_sort(sortby: List) -> Optional[Dict[str, Dict[str, str]]]:
549
- """Database logic to sort search instance."""
550
- if sortby:
551
- return {s.field: {"order": s.direction} for s in sortby}
552
- else:
553
- return None
484
+ """Create a sort configuration for Elasticsearch queries.
485
+
486
+ This method delegates to the shared implementation in populate_sort_shared.
487
+
488
+ Args:
489
+ sortby (List): A list of sort specifications, each containing a field and direction.
490
+
491
+ Returns:
492
+ Optional[Dict[str, Dict[str, str]]]: A dictionary mapping field names to sort direction
493
+ configurations, or None if no sort was specified.
494
+ """
495
+ return populate_sort_shared(sortby=sortby)
554
496
 
555
497
  async def execute_search(
556
498
  self,
@@ -845,15 +787,19 @@ class DatabaseLogic(BaseDatabaseLogic):
845
787
  async def create_item(
846
788
  self,
847
789
  item: Item,
848
- refresh: bool = False,
849
790
  base_url: str = "",
850
791
  exist_ok: bool = False,
792
+ **kwargs: Any,
851
793
  ):
852
794
  """Database logic for creating one item.
853
795
 
854
796
  Args:
855
797
  item (Item): The item to be created.
856
- refresh (bool, optional): Refresh the index after performing the operation. Defaults to False.
798
+ base_url (str, optional): The base URL for the item. Defaults to an empty string.
799
+ exist_ok (bool, optional): Whether to allow the item to exist already. Defaults to False.
800
+ **kwargs: Additional keyword arguments.
801
+ - refresh (str): Whether to refresh the index after the operation. Can be "true", "false", or "wait_for".
802
+ - refresh (bool): Whether to refresh the index after the operation. Defaults to the value in `self.async_settings.database_refresh`.
857
803
 
858
804
  Raises:
859
805
  ConflictError: If the item already exists in the database.
@@ -861,12 +807,28 @@ class DatabaseLogic(BaseDatabaseLogic):
861
807
  Returns:
862
808
  None
863
809
  """
864
- # todo: check if collection exists, but cache
810
+ # Extract item and collection IDs
865
811
  item_id = item["id"]
866
812
  collection_id = item["collection"]
813
+
814
+ # Ensure kwargs is a dictionary
815
+ kwargs = kwargs or {}
816
+
817
+ # Resolve the `refresh` parameter
818
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
819
+ refresh = validate_refresh(refresh)
820
+
821
+ # Log the creation attempt
822
+ logger.info(
823
+ f"Creating item {item_id} in collection {collection_id} with refresh={refresh}"
824
+ )
825
+
826
+ # Prepare the item for insertion
867
827
  item = await self.async_prep_create_item(
868
828
  item=item, base_url=base_url, exist_ok=exist_ok
869
829
  )
830
+
831
+ # Index the item in the database
870
832
  await self.client.index(
871
833
  index=index_alias_by_collection_id(collection_id),
872
834
  id=mk_item_id(item_id, collection_id),
@@ -874,26 +836,43 @@ class DatabaseLogic(BaseDatabaseLogic):
874
836
  refresh=refresh,
875
837
  )
876
838
 
877
- async def delete_item(
878
- self, item_id: str, collection_id: str, refresh: bool = False
879
- ):
839
+ async def delete_item(self, item_id: str, collection_id: str, **kwargs: Any):
880
840
  """Delete a single item from the database.
881
841
 
882
842
  Args:
883
843
  item_id (str): The id of the Item to be deleted.
884
844
  collection_id (str): The id of the Collection that the Item belongs to.
885
- refresh (bool, optional): Whether to refresh the index after the deletion. Default is False.
845
+ **kwargs: Additional keyword arguments.
846
+ - refresh (str): Whether to refresh the index after the operation. Can be "true", "false", or "wait_for".
847
+ - refresh (bool): Whether to refresh the index after the operation. Defaults to the value in `self.async_settings.database_refresh`.
886
848
 
887
849
  Raises:
888
850
  NotFoundError: If the Item does not exist in the database.
851
+
852
+ Returns:
853
+ None
889
854
  """
855
+ # Ensure kwargs is a dictionary
856
+ kwargs = kwargs or {}
857
+
858
+ # Resolve the `refresh` parameter
859
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
860
+ refresh = validate_refresh(refresh)
861
+
862
+ # Log the deletion attempt
863
+ logger.info(
864
+ f"Deleting item {item_id} from collection {collection_id} with refresh={refresh}"
865
+ )
866
+
890
867
  try:
868
+ # Perform the delete operation
891
869
  await self.client.delete(
892
870
  index=index_alias_by_collection_id(collection_id),
893
871
  id=mk_item_id(item_id, collection_id),
894
872
  refresh=refresh,
895
873
  )
896
874
  except ESNotFoundError:
875
+ # Raise a custom NotFoundError if the item does not exist
897
876
  raise NotFoundError(
898
877
  f"Item {item_id} in collection {collection_id} not found"
899
878
  )
@@ -916,24 +895,41 @@ class DatabaseLogic(BaseDatabaseLogic):
916
895
  except ESNotFoundError:
917
896
  raise NotFoundError(f"Mapping for index {index_name} not found")
918
897
 
919
- async def create_collection(self, collection: Collection, refresh: bool = False):
898
+ async def create_collection(self, collection: Collection, **kwargs: Any):
920
899
  """Create a single collection in the database.
921
900
 
922
901
  Args:
923
902
  collection (Collection): The Collection object to be created.
924
- refresh (bool, optional): Whether to refresh the index after the creation. Default is False.
903
+ **kwargs: Additional keyword arguments.
904
+ - refresh (str): Whether to refresh the index after the operation. Can be "true", "false", or "wait_for".
905
+ - refresh (bool): Whether to refresh the index after the operation. Defaults to the value in `self.async_settings.database_refresh`.
925
906
 
926
907
  Raises:
927
908
  ConflictError: If a Collection with the same id already exists in the database.
928
909
 
910
+ Returns:
911
+ None
912
+
929
913
  Notes:
930
914
  A new index is created for the items in the Collection using the `create_item_index` function.
931
915
  """
932
916
  collection_id = collection["id"]
933
917
 
918
+ # Ensure kwargs is a dictionary
919
+ kwargs = kwargs or {}
920
+
921
+ # Resolve the `refresh` parameter
922
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
923
+ refresh = validate_refresh(refresh)
924
+
925
+ # Log the creation attempt
926
+ logger.info(f"Creating collection {collection_id} with refresh={refresh}")
927
+
928
+ # Check if the collection already exists
934
929
  if await self.client.exists(index=COLLECTIONS_INDEX, id=collection_id):
935
930
  raise ConflictError(f"Collection {collection_id} already exists")
936
931
 
932
+ # Index the collection in the database
937
933
  await self.client.index(
938
934
  index=COLLECTIONS_INDEX,
939
935
  id=collection_id,
@@ -941,6 +937,7 @@ class DatabaseLogic(BaseDatabaseLogic):
941
937
  refresh=refresh,
942
938
  )
943
939
 
940
+ # Create the item index for the collection
944
941
  await create_item_index(collection_id)
945
942
 
946
943
  async def find_collection(self, collection_id: str) -> Collection:
@@ -970,29 +967,52 @@ class DatabaseLogic(BaseDatabaseLogic):
970
967
  return collection["_source"]
971
968
 
972
969
  async def update_collection(
973
- self, collection_id: str, collection: Collection, refresh: bool = False
970
+ self, collection_id: str, collection: Collection, **kwargs: Any
974
971
  ):
975
- """Update a collection from the database.
972
+ """Update a collection in the database.
976
973
 
977
974
  Args:
978
- self: The instance of the object calling this function.
979
975
  collection_id (str): The ID of the collection to be updated.
980
976
  collection (Collection): The Collection object to be used for the update.
977
+ **kwargs: Additional keyword arguments.
978
+ - refresh (str): Whether to refresh the index after the operation. Can be "true", "false", or "wait_for".
979
+ - refresh (bool): Whether to refresh the index after the operation. Defaults to the value in `self.async_settings.database_refresh`.
980
+ Returns:
981
+ None
981
982
 
982
983
  Raises:
983
- NotFoundError: If the collection with the given `collection_id` is not
984
- found in the database.
984
+ NotFoundError: If the collection with the given `collection_id` is not found in the database.
985
+ ConflictError: If a conflict occurs during the update.
985
986
 
986
987
  Notes:
987
988
  This function updates the collection in the database using the specified
988
- `collection_id` and with the collection specified in the `Collection` object.
989
- If the collection is not found, a `NotFoundError` is raised.
989
+ `collection_id` and the provided `Collection` object. If the collection ID
990
+ changes, the function creates a new collection, reindexes the items, and deletes
991
+ the old collection.
990
992
  """
993
+ # Ensure kwargs is a dictionary
994
+ kwargs = kwargs or {}
995
+
996
+ # Resolve the `refresh` parameter
997
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
998
+ refresh = validate_refresh(refresh)
999
+
1000
+ # Log the update attempt
1001
+ logger.info(f"Updating collection {collection_id} with refresh={refresh}")
1002
+
1003
+ # Ensure the collection exists
991
1004
  await self.find_collection(collection_id=collection_id)
992
1005
 
1006
+ # Handle collection ID change
993
1007
  if collection_id != collection["id"]:
1008
+ logger.info(
1009
+ f"Collection ID change detected: {collection_id} -> {collection['id']}"
1010
+ )
1011
+
1012
+ # Create the new collection
994
1013
  await self.create_collection(collection, refresh=refresh)
995
1014
 
1015
+ # Reindex items from the old collection to the new collection
996
1016
  await self.client.reindex(
997
1017
  body={
998
1018
  "dest": {"index": f"{ITEMS_INDEX_PREFIX}{collection['id']}"},
@@ -1006,9 +1026,11 @@ class DatabaseLogic(BaseDatabaseLogic):
1006
1026
  refresh=refresh,
1007
1027
  )
1008
1028
 
1029
+ # Delete the old collection
1009
1030
  await self.delete_collection(collection_id)
1010
1031
 
1011
1032
  else:
1033
+ # Update the existing collection
1012
1034
  await self.client.index(
1013
1035
  index=COLLECTIONS_INDEX,
1014
1036
  id=collection_id,
@@ -1016,33 +1038,57 @@ class DatabaseLogic(BaseDatabaseLogic):
1016
1038
  refresh=refresh,
1017
1039
  )
1018
1040
 
1019
- async def delete_collection(self, collection_id: str, refresh: bool = False):
1041
+ async def delete_collection(self, collection_id: str, **kwargs: Any):
1020
1042
  """Delete a collection from the database.
1021
1043
 
1022
1044
  Parameters:
1023
- self: The instance of the object calling this function.
1024
1045
  collection_id (str): The ID of the collection to be deleted.
1025
- refresh (bool): Whether to refresh the index after the deletion (default: False).
1046
+ kwargs (Any, optional): Additional keyword arguments, including `refresh`.
1047
+ - refresh (str): Whether to refresh the index after the operation. Can be "true", "false", or "wait_for".
1048
+ - refresh (bool): Whether to refresh the index after the operation. Defaults to the value in `self.async_settings.database_refresh`.
1026
1049
 
1027
1050
  Raises:
1028
1051
  NotFoundError: If the collection with the given `collection_id` is not found in the database.
1029
1052
 
1053
+ Returns:
1054
+ None
1055
+
1030
1056
  Notes:
1031
1057
  This function first verifies that the collection with the specified `collection_id` exists in the database, and then
1032
- deletes the collection. If `refresh` is set to True, the index is refreshed after the deletion. Additionally, this
1033
- function also calls `delete_item_index` to delete the index for the items in the collection.
1058
+ deletes the collection. If `refresh` is set to "true", "false", or "wait_for", the index is refreshed accordingly after
1059
+ the deletion. Additionally, this function also calls `delete_item_index` to delete the index for the items in the collection.
1034
1060
  """
1061
+ # Ensure kwargs is a dictionary
1062
+ kwargs = kwargs or {}
1063
+
1064
+ # Verify that the collection exists
1035
1065
  await self.find_collection(collection_id=collection_id)
1066
+
1067
+ # Resolve the `refresh` parameter
1068
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
1069
+ refresh = validate_refresh(refresh)
1070
+
1071
+ # Log the deletion attempt
1072
+ logger.info(f"Deleting collection {collection_id} with refresh={refresh}")
1073
+
1074
+ # Delete the collection from the database
1036
1075
  await self.client.delete(
1037
1076
  index=COLLECTIONS_INDEX, id=collection_id, refresh=refresh
1038
1077
  )
1039
- await delete_item_index(collection_id)
1078
+
1079
+ # Delete the item index for the collection
1080
+ try:
1081
+ await delete_item_index(collection_id)
1082
+ except Exception as e:
1083
+ logger.error(
1084
+ f"Failed to delete item index for collection {collection_id}: {e}"
1085
+ )
1040
1086
 
1041
1087
  async def bulk_async(
1042
1088
  self,
1043
1089
  collection_id: str,
1044
1090
  processed_items: List[Item],
1045
- refresh: bool = False,
1091
+ **kwargs: Any,
1046
1092
  ) -> Tuple[int, List[Dict[str, Any]]]:
1047
1093
  """
1048
1094
  Perform a bulk insert of items into the database asynchronously.
@@ -1050,7 +1096,12 @@ class DatabaseLogic(BaseDatabaseLogic):
1050
1096
  Args:
1051
1097
  collection_id (str): The ID of the collection to which the items belong.
1052
1098
  processed_items (List[Item]): A list of `Item` objects to be inserted into the database.
1053
- refresh (bool): Whether to refresh the index after the bulk insert (default: False).
1099
+ **kwargs (Any): Additional keyword arguments, including:
1100
+ - refresh (str, optional): Whether to refresh the index after the bulk insert.
1101
+ Can be "true", "false", or "wait_for". Defaults to the value of `self.sync_settings.database_refresh`.
1102
+ - refresh (bool, optional): Whether to refresh the index after the bulk insert.
1103
+ - raise_on_error (bool, optional): Whether to raise an error if any of the bulk operations fail.
1104
+ Defaults to the value of `self.async_settings.raise_on_bulk_error`.
1054
1105
 
1055
1106
  Returns:
1056
1107
  Tuple[int, List[Dict[str, Any]]]: A tuple containing:
@@ -1059,10 +1110,31 @@ class DatabaseLogic(BaseDatabaseLogic):
1059
1110
 
1060
1111
  Notes:
1061
1112
  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.
1113
+ The insert is performed synchronously and blocking, meaning that the function does not return until the insert has
1114
+ completed. The `mk_actions` function is called to generate a list of actions for the bulk insert. The `refresh`
1115
+ parameter determines whether the index is refreshed after the bulk insert:
1116
+ - "true": Forces an immediate refresh of the index.
1117
+ - "false": Does not refresh the index immediately (default behavior).
1118
+ - "wait_for": Waits for the next refresh cycle to make the changes visible.
1065
1119
  """
1120
+ # Ensure kwargs is a dictionary
1121
+ kwargs = kwargs or {}
1122
+
1123
+ # Resolve the `refresh` parameter
1124
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
1125
+ refresh = validate_refresh(refresh)
1126
+
1127
+ # Log the bulk insert attempt
1128
+ logger.info(
1129
+ f"Performing bulk insert for collection {collection_id} with refresh={refresh}"
1130
+ )
1131
+
1132
+ # Handle empty processed_items
1133
+ if not processed_items:
1134
+ logger.warning(f"No items to insert for collection {collection_id}")
1135
+ return 0, []
1136
+
1137
+ # Perform the bulk insert
1066
1138
  raise_on_error = self.async_settings.raise_on_bulk_error
1067
1139
  success, errors = await helpers.async_bulk(
1068
1140
  self.client,
@@ -1070,13 +1142,19 @@ class DatabaseLogic(BaseDatabaseLogic):
1070
1142
  refresh=refresh,
1071
1143
  raise_on_error=raise_on_error,
1072
1144
  )
1145
+
1146
+ # Log the result
1147
+ logger.info(
1148
+ f"Bulk insert completed for collection {collection_id}: {success} successes, {len(errors)} errors"
1149
+ )
1150
+
1073
1151
  return success, errors
1074
1152
 
1075
1153
  def bulk_sync(
1076
1154
  self,
1077
1155
  collection_id: str,
1078
1156
  processed_items: List[Item],
1079
- refresh: bool = False,
1157
+ **kwargs: Any,
1080
1158
  ) -> Tuple[int, List[Dict[str, Any]]]:
1081
1159
  """
1082
1160
  Perform a bulk insert of items into the database synchronously.
@@ -1084,7 +1162,12 @@ class DatabaseLogic(BaseDatabaseLogic):
1084
1162
  Args:
1085
1163
  collection_id (str): The ID of the collection to which the items belong.
1086
1164
  processed_items (List[Item]): A list of `Item` objects to be inserted into the database.
1087
- refresh (bool): Whether to refresh the index after the bulk insert (default: False).
1165
+ **kwargs (Any): Additional keyword arguments, including:
1166
+ - refresh (str, optional): Whether to refresh the index after the bulk insert.
1167
+ Can be "true", "false", or "wait_for". Defaults to the value of `self.sync_settings.database_refresh`.
1168
+ - refresh (bool, optional): Whether to refresh the index after the bulk insert.
1169
+ - raise_on_error (bool, optional): Whether to raise an error if any of the bulk operations fail.
1170
+ Defaults to the value of `self.async_settings.raise_on_bulk_error`.
1088
1171
 
1089
1172
  Returns:
1090
1173
  Tuple[int, List[Dict[str, Any]]]: A tuple containing:
@@ -1094,9 +1177,30 @@ class DatabaseLogic(BaseDatabaseLogic):
1094
1177
  Notes:
1095
1178
  This function performs a bulk insert of `processed_items` into the database using the specified `collection_id`.
1096
1179
  The insert is performed synchronously and blocking, meaning that the function does not return until the insert has
1097
- completed. The `mk_actions` function is called to generate a list of actions for the bulk insert. If `refresh` is set to
1098
- True, the index is refreshed after the bulk insert.
1180
+ completed. The `mk_actions` function is called to generate a list of actions for the bulk insert. The `refresh`
1181
+ parameter determines whether the index is refreshed after the bulk insert:
1182
+ - "true": Forces an immediate refresh of the index.
1183
+ - "false": Does not refresh the index immediately (default behavior).
1184
+ - "wait_for": Waits for the next refresh cycle to make the changes visible.
1099
1185
  """
1186
+ # Ensure kwargs is a dictionary
1187
+ kwargs = kwargs or {}
1188
+
1189
+ # Resolve the `refresh` parameter
1190
+ refresh = kwargs.get("refresh", self.async_settings.database_refresh)
1191
+ refresh = validate_refresh(refresh)
1192
+
1193
+ # Log the bulk insert attempt
1194
+ logger.info(
1195
+ f"Performing bulk insert for collection {collection_id} with refresh={refresh}"
1196
+ )
1197
+
1198
+ # Handle empty processed_items
1199
+ if not processed_items:
1200
+ logger.warning(f"No items to insert for collection {collection_id}")
1201
+ return 0, []
1202
+
1203
+ # Perform the bulk insert
1100
1204
  raise_on_error = self.sync_settings.raise_on_bulk_error
1101
1205
  success, errors = helpers.bulk(
1102
1206
  self.sync_client,
@@ -1104,6 +1208,12 @@ class DatabaseLogic(BaseDatabaseLogic):
1104
1208
  refresh=refresh,
1105
1209
  raise_on_error=raise_on_error,
1106
1210
  )
1211
+
1212
+ # Log the result
1213
+ logger.info(
1214
+ f"Bulk insert completed for collection {collection_id}: {success} successes, {len(errors)} errors"
1215
+ )
1216
+
1107
1217
  return success, errors
1108
1218
 
1109
1219
  # DANGER