stac-fastapi-elasticsearch 6.8.1__tar.gz → 6.10.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.
@@ -141,3 +141,14 @@ venv
141
141
  /docs/src/api/*
142
142
 
143
143
  .DS_Store
144
+
145
+ # Helm
146
+ *.tgz
147
+ charts/*/charts/
148
+ charts/*/requirements.lock
149
+ charts/*/Chart.lock
150
+ helm-chart/stac-fastapi/charts/
151
+ helm-chart/stac-fastapi/Chart.lock
152
+ helm-chart/stac-fastapi/*.tgz
153
+ helm-chart/test-results/
154
+ helm-chart/tmp/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stac_fastapi_elasticsearch
3
- Version: 6.8.1
3
+ Version: 6.10.0
4
4
  Summary: An implementation of STAC API based on the FastAPI framework with Elasticsearch.
5
5
  Project-URL: Homepage, https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
6
6
  License: MIT
@@ -15,8 +15,8 @@ Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Programming Language :: Python :: 3.14
16
16
  Requires-Python: >=3.11
17
17
  Requires-Dist: elasticsearch[async]~=8.19.1
18
- Requires-Dist: sfeos-helpers==6.8.1
19
- Requires-Dist: stac-fastapi-core==6.8.1
18
+ Requires-Dist: sfeos-helpers==6.10.0
19
+ Requires-Dist: stac-fastapi-core==6.10.0
20
20
  Requires-Dist: starlette<0.36.0,>=0.35.0
21
21
  Requires-Dist: uvicorn~=0.23.0
22
22
  Provides-Extra: dev
@@ -28,7 +28,7 @@ Requires-Dist: pytest-cov~=4.0.0; extra == 'dev'
28
28
  Requires-Dist: pytest~=8.0; extra == 'dev'
29
29
  Requires-Dist: redis~=6.4.0; extra == 'dev'
30
30
  Requires-Dist: retry~=0.9.2; extra == 'dev'
31
- Requires-Dist: stac-fastapi-core[redis]==6.8.1; extra == 'dev'
31
+ Requires-Dist: stac-fastapi-core[redis]==6.10.0; extra == 'dev'
32
32
  Provides-Extra: docs
33
33
  Requires-Dist: mkdocs-material~=9.0.0; extra == 'docs'
34
34
  Requires-Dist: mkdocs~=1.4.0; extra == 'docs'
@@ -36,7 +36,7 @@ Requires-Dist: pdocs~=1.2.0; extra == 'docs'
36
36
  Requires-Dist: redis~=6.4.0; extra == 'docs'
37
37
  Requires-Dist: retry~=0.9.2; extra == 'docs'
38
38
  Provides-Extra: redis
39
- Requires-Dist: stac-fastapi-core[redis]==6.8.1; extra == 'redis'
39
+ Requires-Dist: stac-fastapi-core[redis]==6.10.0; extra == 'redis'
40
40
  Provides-Extra: server
41
41
  Requires-Dist: uvicorn[standard]~=0.23.0; extra == 'server'
42
42
  Description-Content-Type: text/markdown
@@ -28,8 +28,8 @@ keywords = [
28
28
  ]
29
29
  dynamic = ["version"]
30
30
  dependencies = [
31
- "stac-fastapi-core==6.8.1",
32
- "sfeos-helpers==6.8.1",
31
+ "stac-fastapi-core==6.10.0",
32
+ "sfeos-helpers==6.10.0",
33
33
  "elasticsearch[async]~=8.19.1",
34
34
  "uvicorn~=0.23.0",
35
35
  "starlette>=0.35.0,<0.36.0",
@@ -48,7 +48,7 @@ dev = [
48
48
  "httpx>=0.24.0,<0.28.0",
49
49
  "redis~=6.4.0",
50
50
  "retry~=0.9.2",
51
- "stac-fastapi-core[redis]==6.8.1",
51
+ "stac-fastapi-core[redis]==6.10.0",
52
52
  ]
53
53
  docs = [
54
54
  "mkdocs~=1.4.0",
@@ -58,7 +58,7 @@ docs = [
58
58
  "retry~=0.9.2",
59
59
  ]
60
60
  redis = [
61
- "stac-fastapi-core[redis]==6.8.1",
61
+ "stac-fastapi-core[redis]==6.10.0",
62
62
  ]
63
63
  server = [
64
64
  "uvicorn[standard]~=0.23.0",
@@ -215,7 +215,7 @@ if ENABLE_CATALOGS_ROUTE:
215
215
  ),
216
216
  settings=settings,
217
217
  conformance_classes=[
218
- "https://api.stacspec.org/v1.0.0-beta.1/catalogs-endpoint",
218
+ "https://api.stacspec.org/v1.0.0-beta.1/multi-tenant-catalogs",
219
219
  ],
220
220
  )
221
221
  extensions.append(catalogs_extension)
@@ -244,7 +244,7 @@ items_get_request_model = create_request_model(
244
244
  app_config = {
245
245
  "title": os.getenv("STAC_FASTAPI_TITLE", "stac-fastapi-elasticsearch"),
246
246
  "description": os.getenv("STAC_FASTAPI_DESCRIPTION", "stac-fastapi-elasticsearch"),
247
- "api_version": os.getenv("STAC_FASTAPI_VERSION", "6.8.1"),
247
+ "api_version": os.getenv("STAC_FASTAPI_VERSION", "6.10.0"),
248
248
  "settings": settings,
249
249
  "extensions": extensions,
250
250
  "client": CoreClient(
@@ -53,6 +53,7 @@ from stac_fastapi.sfeos_helpers.database.query import (
53
53
  add_collections_to_body,
54
54
  )
55
55
  from stac_fastapi.sfeos_helpers.database.utils import (
56
+ add_hidden_filter,
56
57
  merge_to_operations,
57
58
  operations_to_script,
58
59
  )
@@ -67,6 +68,7 @@ from stac_fastapi.sfeos_helpers.mappings import (
67
68
  from stac_fastapi.sfeos_helpers.search_engine import (
68
69
  BaseIndexInserter,
69
70
  BaseIndexSelector,
71
+ DatetimeIndexInserter,
70
72
  IndexInsertionFactory,
71
73
  IndexSelectorFactory,
72
74
  )
@@ -406,12 +408,22 @@ class DatabaseLogic(BaseDatabaseLogic):
406
408
  Notes:
407
409
  The Item is retrieved from the Elasticsearch database using the `client.get` method,
408
410
  with the index for the Collection as the target index and the combined `mk_item_id` as the document id.
411
+ Item is hidden if hide_item_path is configured via env var.
409
412
  """
410
413
  try:
414
+ base_query = {"term": {"_id": mk_item_id(item_id, collection_id)}}
415
+
416
+ HIDE_ITEM_PATH = os.getenv("HIDE_ITEM_PATH", None)
417
+
418
+ if HIDE_ITEM_PATH:
419
+ query = add_hidden_filter(base_query, HIDE_ITEM_PATH)
420
+ else:
421
+ query = base_query
422
+
411
423
  response = await self.client.search(
412
424
  index=index_alias_by_collection_id(collection_id),
413
425
  body={
414
- "query": {"term": {"_id": mk_item_id(item_id, collection_id)}},
426
+ "query": query,
415
427
  "size": 1,
416
428
  },
417
429
  )
@@ -656,7 +668,7 @@ class DatabaseLogic(BaseDatabaseLogic):
656
668
  ),
657
669
  ],
658
670
  )
659
- return search.query(filter_query), datetime_search
671
+ return search.query(filter_query), datetime_search
660
672
 
661
673
  @staticmethod
662
674
  def apply_bbox_filter(search: Search, bbox: List):
@@ -811,7 +823,7 @@ class DatabaseLogic(BaseDatabaseLogic):
811
823
  token: Optional[str],
812
824
  sort: Optional[Dict[str, Dict[str, str]]],
813
825
  collection_ids: Optional[List[str]],
814
- datetime_search: Dict[str, Optional[str]],
826
+ datetime_search: str,
815
827
  ignore_unavailable: bool = True,
816
828
  ) -> Tuple[Iterable[Dict[str, Any]], Optional[int], Optional[str]]:
817
829
  """Execute a search query with limit and other optional parameters.
@@ -822,7 +834,7 @@ class DatabaseLogic(BaseDatabaseLogic):
822
834
  token (Optional[str]): The token used to return the next set of results.
823
835
  sort (Optional[Dict[str, Dict[str, str]]]): Specifies how the results should be sorted.
824
836
  collection_ids (Optional[List[str]]): The collection ids to search.
825
- datetime_search (Dict[str, Optional[str]]): Datetime range used for index selection.
837
+ datetime_search (str): Datetime used for index selection.
826
838
  ignore_unavailable (bool, optional): Whether to ignore unavailable collections. Defaults to True.
827
839
 
828
840
  Returns:
@@ -854,6 +866,10 @@ class DatabaseLogic(BaseDatabaseLogic):
854
866
 
855
867
  size_limit = min(limit + 1, max_result_window)
856
868
 
869
+ HIDE_ITEM_PATH = os.getenv("HIDE_ITEM_PATH", None)
870
+ if HIDE_ITEM_PATH:
871
+ query = add_hidden_filter(query, HIDE_ITEM_PATH)
872
+
857
873
  search_task = asyncio.create_task(
858
874
  self.client.search(
859
875
  index=index_param,
@@ -865,11 +881,17 @@ class DatabaseLogic(BaseDatabaseLogic):
865
881
  )
866
882
  )
867
883
 
884
+ # Apply hidden filter to count query as well
885
+ count_query = search.to_dict(count=True)
886
+ if HIDE_ITEM_PATH:
887
+ q = count_query.get("query")
888
+ count_query["query"] = add_hidden_filter(q, HIDE_ITEM_PATH)
889
+
868
890
  count_task = asyncio.create_task(
869
891
  self.client.count(
870
892
  index=index_param,
871
893
  ignore_unavailable=ignore_unavailable,
872
- body=search.to_dict(count=True),
894
+ body=count_query,
873
895
  )
874
896
  )
875
897
 
@@ -912,7 +934,7 @@ class DatabaseLogic(BaseDatabaseLogic):
912
934
  geometry_geohash_grid_precision: int,
913
935
  geometry_geotile_grid_precision: int,
914
936
  datetime_frequency_interval: str,
915
- datetime_search,
937
+ datetime_search: str,
916
938
  ignore_unavailable: Optional[bool] = True,
917
939
  ):
918
940
  """Return aggregations of STAC Items."""
@@ -1093,19 +1115,25 @@ class DatabaseLogic(BaseDatabaseLogic):
1093
1115
  raise NotFoundError(f"Collection {item['collection']} does not exist")
1094
1116
 
1095
1117
  # Check if the item already exists in the database
1096
- if not exist_ok and self.sync_client.exists(
1097
- index=index_alias_by_collection_id(item["collection"]),
1098
- id=mk_item_id(item["id"], item["collection"]),
1099
- ):
1100
- error_message = (
1101
- f"Item {item['id']} in collection {item['collection']} already exists."
1102
- )
1103
- if self.sync_settings.raise_on_bulk_error:
1104
- raise ConflictError(error_message)
1105
- else:
1106
- logger.warning(
1107
- f"{error_message} Continuing as `RAISE_ON_BULK_ERROR` is set to false."
1108
- )
1118
+ alias = index_alias_by_collection_id(item["collection"])
1119
+ doc_id = mk_item_id(item["id"], item["collection"])
1120
+
1121
+ if not exist_ok:
1122
+ alias_exists = self.sync_client.indices.exists_alias(name=alias)
1123
+
1124
+ if alias_exists:
1125
+ alias_info = self.sync_client.indices.get_alias(name=alias)
1126
+ indices = list(alias_info.keys())
1127
+
1128
+ for index in indices:
1129
+ if self.sync_client.exists(index=index, id=doc_id):
1130
+ error_message = f"Item {item['id']} in collection {item['collection']} already exists."
1131
+ if self.sync_settings.raise_on_bulk_error:
1132
+ raise ConflictError(error_message)
1133
+ else:
1134
+ logger.warning(
1135
+ f"{error_message} Continuing as `RAISE_ON_BULK_ERROR` is set to false."
1136
+ )
1109
1137
 
1110
1138
  # Serialize the item into a database-compatible format
1111
1139
  prepped_item = self.item_serializer.stac_to_db(item, base_url)
@@ -1150,6 +1178,31 @@ class DatabaseLogic(BaseDatabaseLogic):
1150
1178
  f"Creating item {item_id} in collection {collection_id} with refresh={refresh}"
1151
1179
  )
1152
1180
 
1181
+ if exist_ok and isinstance(self.async_index_inserter, DatetimeIndexInserter):
1182
+ existing_item = await self.get_one_item(collection_id, item_id)
1183
+ primary_datetime_name = self.async_index_inserter.primary_datetime_name
1184
+
1185
+ existing_primary_datetime = existing_item.get("properties", {}).get(
1186
+ primary_datetime_name
1187
+ )
1188
+ new_primary_datetime = item.get("properties", {}).get(primary_datetime_name)
1189
+
1190
+ if existing_primary_datetime != new_primary_datetime:
1191
+ self.async_index_inserter.validate_datetime_field_update(
1192
+ f"properties/{primary_datetime_name}"
1193
+ )
1194
+
1195
+ if primary_datetime_name == "start_datetime":
1196
+ existing_end_datetime = existing_item.get("properties", {}).get(
1197
+ "end_datetime"
1198
+ )
1199
+ new_end_datetime = item.get("properties", {}).get("end_datetime")
1200
+
1201
+ if existing_end_datetime != new_end_datetime:
1202
+ self.async_index_inserter.validate_datetime_field_update(
1203
+ "properties/end_datetime"
1204
+ )
1205
+
1153
1206
  # Prepare the item for insertion
1154
1207
  item = await self.async_prep_create_item(
1155
1208
  item=item, base_url=base_url, exist_ok=exist_ok
@@ -1218,6 +1271,10 @@ class DatabaseLogic(BaseDatabaseLogic):
1218
1271
  Returns:
1219
1272
  patched item.
1220
1273
  """
1274
+ for operation in operations:
1275
+ if operation.op in ["add", "replace", "remove"]:
1276
+ self.async_index_inserter.validate_datetime_field_update(operation.path)
1277
+
1221
1278
  new_item_id = None
1222
1279
  new_collection_id = None
1223
1280
  script_operations = []
@@ -1238,8 +1295,6 @@ class DatabaseLogic(BaseDatabaseLogic):
1238
1295
  else:
1239
1296
  script_operations.append(operation)
1240
1297
 
1241
- script = operations_to_script(script_operations, create_nest=create_nest)
1242
-
1243
1298
  try:
1244
1299
  search_response = await self.client.search(
1245
1300
  index=index_alias_by_collection_id(collection_id),
@@ -1252,13 +1307,18 @@ class DatabaseLogic(BaseDatabaseLogic):
1252
1307
  raise NotFoundError(
1253
1308
  f"Item {item_id} does not exist inside Collection {collection_id}"
1254
1309
  )
1255
- document_index = search_response["hits"]["hits"][0]["_index"]
1256
- await self.client.update(
1257
- index=document_index,
1258
- id=mk_item_id(item_id, collection_id),
1259
- script=script,
1260
- refresh=True,
1261
- )
1310
+
1311
+ if script_operations:
1312
+ script = operations_to_script(
1313
+ script_operations, create_nest=create_nest
1314
+ )
1315
+ document_index = search_response["hits"]["hits"][0]["_index"]
1316
+ await self.client.update(
1317
+ index=document_index,
1318
+ id=mk_item_id(item_id, collection_id),
1319
+ script=script,
1320
+ refresh=True,
1321
+ )
1262
1322
  except ESNotFoundError:
1263
1323
  raise NotFoundError(
1264
1324
  f"Item {item_id} does not exist inside Collection {collection_id}"
@@ -1271,26 +1331,9 @@ class DatabaseLogic(BaseDatabaseLogic):
1271
1331
  item = await self.get_one_item(collection_id, item_id)
1272
1332
 
1273
1333
  if new_collection_id:
1274
- await self.client.reindex(
1275
- body={
1276
- "dest": {
1277
- "index": f"{ITEMS_INDEX_PREFIX}{new_collection_id}"
1278
- }, # # noqa
1279
- "source": {
1280
- "index": f"{ITEMS_INDEX_PREFIX}{collection_id}",
1281
- "query": {"term": {"id": {"value": item_id}}},
1282
- },
1283
- "script": {
1284
- "lang": "painless",
1285
- "source": (
1286
- f"""ctx._id = ctx._id.replace('{collection_id}', '{new_collection_id}');""" # noqa
1287
- f"""ctx._source.collection = '{new_collection_id}';""" # noqa
1288
- ),
1289
- },
1290
- },
1291
- wait_for_completion=True,
1292
- refresh=True,
1293
- )
1334
+ item["collection"] = new_collection_id
1335
+ item = await self.async_prep_create_item(item=item, base_url=base_url)
1336
+ await self.create_item(item=item, refresh=True)
1294
1337
 
1295
1338
  await self.delete_item(
1296
1339
  item_id=item_id,
@@ -1298,7 +1341,6 @@ class DatabaseLogic(BaseDatabaseLogic):
1298
1341
  refresh=refresh,
1299
1342
  )
1300
1343
 
1301
- item["collection"] = new_collection_id
1302
1344
  collection_id = new_collection_id
1303
1345
 
1304
1346
  if new_item_id:
@@ -1684,6 +1726,7 @@ class DatabaseLogic(BaseDatabaseLogic):
1684
1726
  index=COLLECTIONS_INDEX, id=collection_id, refresh=refresh
1685
1727
  )
1686
1728
  await delete_item_index(collection_id)
1729
+ await self.async_index_inserter.refresh_cache()
1687
1730
 
1688
1731
  async def bulk_async(
1689
1732
  self,
@@ -1,2 +1,2 @@
1
1
  """library version."""
2
- __version__ = "6.8.1"
2
+ __version__ = "6.10.0"