stac-fastapi-opensearch 6.9.0__py3-none-any.whl → 6.10.0__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.
@@ -214,7 +214,7 @@ if ENABLE_CATALOGS_ROUTE:
214
214
  ),
215
215
  settings=settings,
216
216
  conformance_classes=[
217
- "https://api.stacspec.org/v1.0.0-beta.1/catalogs-endpoint",
217
+ "https://api.stacspec.org/v1.0.0-beta.1/multi-tenant-catalogs",
218
218
  ],
219
219
  )
220
220
  extensions.append(catalogs_extension)
@@ -243,7 +243,7 @@ items_get_request_model = create_request_model(
243
243
  app_config = {
244
244
  "title": os.getenv("STAC_FASTAPI_TITLE", "stac-fastapi-opensearch"),
245
245
  "description": os.getenv("STAC_FASTAPI_DESCRIPTION", "stac-fastapi-opensearch"),
246
- "api_version": os.getenv("STAC_FASTAPI_VERSION", "6.9.0"),
246
+ "api_version": os.getenv("STAC_FASTAPI_VERSION", "6.10.0"),
247
247
  "settings": settings,
248
248
  "extensions": extensions,
249
249
  "client": CoreClient(
@@ -49,6 +49,7 @@ from stac_fastapi.sfeos_helpers.database.query import (
49
49
  add_collections_to_body,
50
50
  )
51
51
  from stac_fastapi.sfeos_helpers.database.utils import (
52
+ add_hidden_filter,
52
53
  merge_to_operations,
53
54
  operations_to_script,
54
55
  )
@@ -64,6 +65,7 @@ from stac_fastapi.sfeos_helpers.mappings import (
64
65
  from stac_fastapi.sfeos_helpers.search_engine import (
65
66
  BaseIndexInserter,
66
67
  BaseIndexSelector,
68
+ DatetimeIndexInserter,
67
69
  IndexInsertionFactory,
68
70
  IndexSelectorFactory,
69
71
  )
@@ -406,12 +408,22 @@ class DatabaseLogic(BaseDatabaseLogic):
406
408
  Notes:
407
409
  The Item is retrieved from the Opensearch 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
  )
@@ -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:
@@ -846,7 +858,11 @@ class DatabaseLogic(BaseDatabaseLogic):
846
858
  index_param = ITEM_INDICES
847
859
  query = add_collections_to_body(collection_ids, query)
848
860
 
849
- if query:
861
+ HIDE_ITEM_PATH = os.getenv("HIDE_ITEM_PATH", None)
862
+
863
+ if HIDE_ITEM_PATH:
864
+ search_body["query"] = add_hidden_filter(query, HIDE_ITEM_PATH)
865
+ elif query:
850
866
  search_body["query"] = query
851
867
 
852
868
  search_after = None
@@ -871,11 +887,17 @@ class DatabaseLogic(BaseDatabaseLogic):
871
887
  )
872
888
  )
873
889
 
890
+ # Ensure hidden item is not counted
891
+ count_query = search.to_dict(count=True)
892
+ if HIDE_ITEM_PATH:
893
+ q = count_query.get("query")
894
+ count_query["query"] = add_hidden_filter(q, HIDE_ITEM_PATH)
895
+
874
896
  count_task = asyncio.create_task(
875
897
  self.client.count(
876
898
  index=index_param,
877
899
  ignore_unavailable=ignore_unavailable,
878
- body=search.to_dict(count=True),
900
+ body=count_query,
879
901
  )
880
902
  )
881
903
 
@@ -918,7 +940,7 @@ class DatabaseLogic(BaseDatabaseLogic):
918
940
  geometry_geohash_grid_precision: int,
919
941
  geometry_geotile_grid_precision: int,
920
942
  datetime_frequency_interval: str,
921
- datetime_search,
943
+ datetime_search: str,
922
944
  ignore_unavailable: Optional[bool] = True,
923
945
  ):
924
946
  """Return aggregations of STAC Items."""
@@ -1096,19 +1118,25 @@ class DatabaseLogic(BaseDatabaseLogic):
1096
1118
  raise NotFoundError(f"Collection {item['collection']} does not exist")
1097
1119
 
1098
1120
  # Check if the item already exists in the database
1099
- if not exist_ok and self.sync_client.exists(
1100
- index=index_alias_by_collection_id(item["collection"]),
1101
- id=mk_item_id(item["id"], item["collection"]),
1102
- ):
1103
- error_message = (
1104
- f"Item {item['id']} in collection {item['collection']} already exists."
1105
- )
1106
- if self.sync_settings.raise_on_bulk_error:
1107
- raise ConflictError(error_message)
1108
- else:
1109
- logger.warning(
1110
- f"{error_message} Continuing as `RAISE_ON_BULK_ERROR` is set to false."
1111
- )
1121
+ alias = index_alias_by_collection_id(item["collection"])
1122
+ doc_id = mk_item_id(item["id"], item["collection"])
1123
+
1124
+ if not exist_ok:
1125
+ alias_exists = self.sync_client.indices.exists_alias(name=alias)
1126
+
1127
+ if alias_exists:
1128
+ alias_info = self.sync_client.indices.get_alias(name=alias)
1129
+ indices = list(alias_info.keys())
1130
+
1131
+ for index in indices:
1132
+ if self.sync_client.exists(index=index, id=doc_id):
1133
+ error_message = f"Item {item['id']} in collection {item['collection']} already exists."
1134
+ if self.sync_settings.raise_on_bulk_error:
1135
+ raise ConflictError(error_message)
1136
+ else:
1137
+ logger.warning(
1138
+ f"{error_message} Continuing as `RAISE_ON_BULK_ERROR` is set to false."
1139
+ )
1112
1140
 
1113
1141
  # Serialize the item into a database-compatible format
1114
1142
  prepped_item = self.item_serializer.stac_to_db(item, base_url)
@@ -1152,6 +1180,31 @@ class DatabaseLogic(BaseDatabaseLogic):
1152
1180
  f"Creating item {item_id} in collection {collection_id} with refresh={refresh}"
1153
1181
  )
1154
1182
 
1183
+ if exist_ok and isinstance(self.async_index_inserter, DatetimeIndexInserter):
1184
+ existing_item = await self.get_one_item(collection_id, item_id)
1185
+ primary_datetime_name = self.async_index_inserter.primary_datetime_name
1186
+
1187
+ existing_primary_datetime = existing_item.get("properties", {}).get(
1188
+ primary_datetime_name
1189
+ )
1190
+ new_primary_datetime = item.get("properties", {}).get(primary_datetime_name)
1191
+
1192
+ if existing_primary_datetime != new_primary_datetime:
1193
+ self.async_index_inserter.validate_datetime_field_update(
1194
+ f"properties/{primary_datetime_name}"
1195
+ )
1196
+
1197
+ if primary_datetime_name == "start_datetime":
1198
+ existing_end_datetime = existing_item.get("properties", {}).get(
1199
+ "end_datetime"
1200
+ )
1201
+ new_end_datetime = item.get("properties", {}).get("end_datetime")
1202
+
1203
+ if existing_end_datetime != new_end_datetime:
1204
+ self.async_index_inserter.validate_datetime_field_update(
1205
+ "properties/end_datetime"
1206
+ )
1207
+
1155
1208
  item = await self.async_prep_create_item(
1156
1209
  item=item, base_url=base_url, exist_ok=exist_ok
1157
1210
  )
@@ -1219,6 +1272,10 @@ class DatabaseLogic(BaseDatabaseLogic):
1219
1272
  Returns:
1220
1273
  patched item.
1221
1274
  """
1275
+ for operation in operations:
1276
+ if operation.op in ["add", "replace", "remove"]:
1277
+ self.async_index_inserter.validate_datetime_field_update(operation.path)
1278
+
1222
1279
  new_item_id = None
1223
1280
  new_collection_id = None
1224
1281
  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
- body={"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
+ body={"script": script},
1320
+ refresh=True,
1321
+ )
1262
1322
  except exceptions.NotFoundError:
1263
1323
  raise NotFoundError(
1264
1324
  f"Item {item_id} does not exist inside Collection {collection_id}"
@@ -1271,24 +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": {"index": f"{ITEMS_INDEX_PREFIX}{new_collection_id}"},
1277
- "source": {
1278
- "index": f"{ITEMS_INDEX_PREFIX}{collection_id}",
1279
- "query": {"term": {"id": {"value": item_id}}},
1280
- },
1281
- "script": {
1282
- "lang": "painless",
1283
- "source": (
1284
- f"""ctx._id = ctx._id.replace('{collection_id}', '{new_collection_id}');"""
1285
- f"""ctx._source.collection = '{new_collection_id}';"""
1286
- ),
1287
- },
1288
- },
1289
- wait_for_completion=True,
1290
- refresh=True,
1291
- )
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)
1292
1337
 
1293
1338
  await self.delete_item(
1294
1339
  item_id=item_id,
@@ -1296,7 +1341,6 @@ class DatabaseLogic(BaseDatabaseLogic):
1296
1341
  refresh=refresh,
1297
1342
  )
1298
1343
 
1299
- item["collection"] = new_collection_id
1300
1344
  collection_id = new_collection_id
1301
1345
 
1302
1346
  if new_item_id:
@@ -1657,6 +1701,7 @@ class DatabaseLogic(BaseDatabaseLogic):
1657
1701
  )
1658
1702
  # Delete the item index for the collection
1659
1703
  await delete_item_index(collection_id)
1704
+ await self.async_index_inserter.refresh_cache()
1660
1705
 
1661
1706
  async def bulk_async(
1662
1707
  self,
@@ -1,2 +1,2 @@
1
1
  """library version."""
2
- __version__ = "6.9.0"
2
+ __version__ = "6.10.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stac_fastapi_opensearch
3
- Version: 6.9.0
3
+ Version: 6.10.0
4
4
  Summary: An implementation of STAC API based on the FastAPI framework with Opensearch.
5
5
  Project-URL: Homepage, https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
6
6
  License: MIT
@@ -16,8 +16,8 @@ Classifier: Programming Language :: Python :: 3.14
16
16
  Requires-Python: >=3.11
17
17
  Requires-Dist: opensearch-py[async]~=2.8.0
18
18
  Requires-Dist: opensearch-py~=2.8.0
19
- Requires-Dist: sfeos-helpers==6.9.0
20
- Requires-Dist: stac-fastapi-core==6.9.0
19
+ Requires-Dist: sfeos-helpers==6.10.0
20
+ Requires-Dist: stac-fastapi-core==6.10.0
21
21
  Requires-Dist: starlette<0.36.0,>=0.35.0
22
22
  Requires-Dist: uvicorn~=0.23.0
23
23
  Provides-Extra: dev
@@ -29,13 +29,13 @@ Requires-Dist: pytest-cov~=4.0.0; extra == 'dev'
29
29
  Requires-Dist: pytest~=8.0; extra == 'dev'
30
30
  Requires-Dist: redis~=6.4.0; extra == 'dev'
31
31
  Requires-Dist: retry~=0.9.2; extra == 'dev'
32
- Requires-Dist: stac-fastapi-core[redis]==6.9.0; extra == 'dev'
32
+ Requires-Dist: stac-fastapi-core[redis]==6.10.0; extra == 'dev'
33
33
  Provides-Extra: docs
34
34
  Requires-Dist: mkdocs-material~=9.0.0; extra == 'docs'
35
35
  Requires-Dist: mkdocs~=1.4.0; extra == 'docs'
36
36
  Requires-Dist: pdocs~=1.2.0; extra == 'docs'
37
37
  Provides-Extra: redis
38
- Requires-Dist: stac-fastapi-core[redis]==6.9.0; extra == 'redis'
38
+ Requires-Dist: stac-fastapi-core[redis]==6.10.0; extra == 'redis'
39
39
  Provides-Extra: server
40
40
  Requires-Dist: uvicorn[standard]~=0.23.0; extra == 'server'
41
41
  Description-Content-Type: text/markdown
@@ -0,0 +1,9 @@
1
+ stac_fastapi/opensearch/__init__.py,sha256=iJWMUgn7mUvmuPQSO_FlyhJ5eDdbbfmGv1qnFOX5-qk,28
2
+ stac_fastapi/opensearch/app.py,sha256=INHCe4KN3ld9tZtN1_5M2yVjk4GKbup15PQlee4kPaw,10768
3
+ stac_fastapi/opensearch/config.py,sha256=yICEk31TydUoogsi6q1TOJHMNOHxHQBneZE9hm7UW7U,5184
4
+ stac_fastapi/opensearch/database_logic.py,sha256=_MQkPCOlFWmO6D0Ky07eCqSf3JuGdV6KodkIGAylvNs,76383
5
+ stac_fastapi/opensearch/version.py,sha256=bQiD_D-FuZl4YJZxrz9LK-THtzxnF7e-QdjJAiGBoSY,46
6
+ stac_fastapi_opensearch-6.10.0.dist-info/METADATA,sha256=361KXJDFzSAjvsvYxbblRxsPNNgw2b2yOhGfeJDTMvw,3998
7
+ stac_fastapi_opensearch-6.10.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
+ stac_fastapi_opensearch-6.10.0.dist-info/entry_points.txt,sha256=zjZ0Xsr9BUNJqMkdPpl6zEIUykv1uFdJtNELFRChp0w,76
9
+ stac_fastapi_opensearch-6.10.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- stac_fastapi/opensearch/__init__.py,sha256=iJWMUgn7mUvmuPQSO_FlyhJ5eDdbbfmGv1qnFOX5-qk,28
2
- stac_fastapi/opensearch/app.py,sha256=rm5O1iTymVwzQSsHIfP8mZBra8mANeMRojKLou7wXVo,10763
3
- stac_fastapi/opensearch/config.py,sha256=yICEk31TydUoogsi6q1TOJHMNOHxHQBneZE9hm7UW7U,5184
4
- stac_fastapi/opensearch/database_logic.py,sha256=882ubxKjwBkI7qLMFgDdm8bNSoFAm3N2ZCHsKMEMbhs,74441
5
- stac_fastapi/opensearch/version.py,sha256=Zec8murh7xqMR7l6fB74XJcq2QdyNqRgOdhLMwBYPAI,45
6
- stac_fastapi_opensearch-6.9.0.dist-info/METADATA,sha256=spuY6OzXt0Roz-GjuJapqe0-G5gF0lIfDWtM1OjDSEU,3993
7
- stac_fastapi_opensearch-6.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
8
- stac_fastapi_opensearch-6.9.0.dist-info/entry_points.txt,sha256=zjZ0Xsr9BUNJqMkdPpl6zEIUykv1uFdJtNELFRChp0w,76
9
- stac_fastapi_opensearch-6.9.0.dist-info/RECORD,,