stac-fastapi.core 3.2.4__tar.gz → 4.0.0a0__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 (33) hide show
  1. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/PKG-INFO +1 -1
  2. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/setup.py +5 -5
  3. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/core.py +167 -85
  4. stac_fastapi.core-4.0.0a0/stac_fastapi/core/database_logic.py +226 -0
  5. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/aggregation.py +8 -8
  6. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/query.py +1 -3
  7. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/models/links.py +1 -1
  8. stac_fastapi.core-4.0.0a0/stac_fastapi/core/version.py +2 -0
  9. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/PKG-INFO +1 -1
  10. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/SOURCES.txt +1 -0
  11. stac_fastapi.core-4.0.0a0/stac_fastapi.core.egg-info/requires.txt +13 -0
  12. stac_fastapi.core-3.2.4/stac_fastapi/core/version.py +0 -2
  13. stac_fastapi.core-3.2.4/stac_fastapi.core.egg-info/requires.txt +0 -13
  14. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/README.md +0 -0
  15. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/setup.cfg +0 -0
  16. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/__init__.py +0 -0
  17. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/base_database_logic.py +0 -0
  18. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/base_settings.py +0 -0
  19. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/basic_auth.py +0 -0
  20. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/datetime_utils.py +0 -0
  21. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/__init__.py +0 -0
  22. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/fields.py +0 -0
  23. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/filter.py +0 -0
  24. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/models/__init__.py +0 -0
  25. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/models/search.py +0 -0
  26. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/rate_limit.py +0 -0
  27. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/route_dependencies.py +0 -0
  28. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/serializers.py +0 -0
  29. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/session.py +0 -0
  30. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/utilities.py +0 -0
  31. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/dependency_links.txt +0 -0
  32. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/not-zip-safe +0 -0
  33. {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac_fastapi.core
3
- Version: 3.2.4
3
+ Version: 4.0.0a0
4
4
  Summary: Core library for the Elasticsearch and Opensearch stac-fastapi backends.
5
5
  Home-page: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
6
6
  License: MIT
@@ -6,13 +6,13 @@ with open("README.md") as f:
6
6
  desc = f.read()
7
7
 
8
8
  install_requires = [
9
- "fastapi-slim",
9
+ "fastapi",
10
10
  "attrs>=23.2.0",
11
11
  "pydantic",
12
- "stac_pydantic>=3",
13
- "stac-fastapi.types==3.0.0",
14
- "stac-fastapi.api==3.0.0",
15
- "stac-fastapi.extensions==3.0.0",
12
+ "stac_pydantic==3.1.*",
13
+ "stac-fastapi.api==5.1.1",
14
+ "stac-fastapi.extensions==5.1.1",
15
+ "stac-fastapi.types==5.1.1",
16
16
  "orjson",
17
17
  "overrides",
18
18
  "geojson-pydantic",
@@ -1,10 +1,11 @@
1
1
  """Core client."""
2
2
 
3
3
  import logging
4
+ from collections import deque
4
5
  from datetime import datetime as datetime_type
5
6
  from datetime import timezone
6
7
  from enum import Enum
7
- from typing import Any, Dict, List, Optional, Set, Type, Union
8
+ from typing import Any, Dict, List, Literal, Optional, Set, Type, Union
8
9
  from urllib.parse import unquote_plus, urljoin
9
10
 
10
11
  import attr
@@ -36,13 +37,11 @@ from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES
36
37
  from stac_fastapi.types.core import AsyncBaseCoreClient, AsyncBaseTransactionsClient
37
38
  from stac_fastapi.types.extension import ApiExtension
38
39
  from stac_fastapi.types.requests import get_base_url
39
- from stac_fastapi.types.rfc3339 import DateTimeType
40
+ from stac_fastapi.types.rfc3339 import DateTimeType, rfc3339_str_to_datetime
40
41
  from stac_fastapi.types.search import BaseSearchPostRequest
41
42
 
42
43
  logger = logging.getLogger(__name__)
43
44
 
44
- NumType = Union[float, int]
45
-
46
45
 
47
46
  @attr.s
48
47
  class CoreClient(AsyncBaseCoreClient):
@@ -278,7 +277,7 @@ class CoreClient(AsyncBaseCoreClient):
278
277
  self,
279
278
  collection_id: str,
280
279
  bbox: Optional[BBox] = None,
281
- datetime: Optional[DateTimeType] = None,
280
+ datetime: Optional[str] = None,
282
281
  limit: Optional[int] = 10,
283
282
  token: Optional[str] = None,
284
283
  **kwargs,
@@ -288,7 +287,7 @@ class CoreClient(AsyncBaseCoreClient):
288
287
  Args:
289
288
  collection_id (str): The identifier of the collection to read items from.
290
289
  bbox (Optional[BBox]): The bounding box to filter items by.
291
- datetime (Optional[DateTimeType]): The datetime range to filter items by.
290
+ datetime (Optional[str]): The datetime range to filter items by.
292
291
  limit (int): The maximum number of items to return. The default value is 10.
293
292
  token (str): A token used for pagination.
294
293
  request (Request): The incoming request.
@@ -427,23 +426,34 @@ class CoreClient(AsyncBaseCoreClient):
427
426
 
428
427
  return result
429
428
 
430
- def _format_datetime_range(self, date_tuple: DateTimeType) -> str:
429
+ def _format_datetime_range(self, date_str: str) -> str:
431
430
  """
432
- Convert a tuple of datetime objects or None into a formatted string for API requests.
431
+ Convert a datetime range string into a normalized UTC string for API requests using rfc3339_str_to_datetime.
433
432
 
434
433
  Args:
435
- date_tuple (tuple): A tuple containing two elements, each can be a datetime object or None.
434
+ date_str (str): A string containing two datetime values separated by a '/'.
436
435
 
437
436
  Returns:
438
- str: A string formatted as 'YYYY-MM-DDTHH:MM:SS.sssZ/YYYY-MM-DDTHH:MM:SS.sssZ', with '..' used if any element is None.
437
+ str: A string formatted as 'YYYY-MM-DDTHH:MM:SSZ/YYYY-MM-DDTHH:MM:SSZ', with '..' used if any element is None.
439
438
  """
440
439
 
441
- def format_datetime(dt):
442
- """Format a single datetime object to the ISO8601 extended format with 'Z'."""
443
- return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" if dt else ".."
444
-
445
- start, end = date_tuple
446
- return f"{format_datetime(start)}/{format_datetime(end)}"
440
+ def normalize(dt):
441
+ dt = dt.strip()
442
+ if not dt or dt == "..":
443
+ return ".."
444
+ dt_obj = rfc3339_str_to_datetime(dt)
445
+ dt_utc = dt_obj.astimezone(timezone.utc)
446
+ return dt_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
447
+
448
+ if not isinstance(date_str, str):
449
+ return "../.."
450
+ if "/" not in date_str:
451
+ return f"{normalize(date_str)}/{normalize(date_str)}"
452
+ try:
453
+ start, end = date_str.split("/", 1)
454
+ except Exception:
455
+ return "../.."
456
+ return f"{normalize(start)}/{normalize(end)}"
447
457
 
448
458
  async def get_search(
449
459
  self,
@@ -451,7 +461,7 @@ class CoreClient(AsyncBaseCoreClient):
451
461
  collections: Optional[List[str]] = None,
452
462
  ids: Optional[List[str]] = None,
453
463
  bbox: Optional[BBox] = None,
454
- datetime: Optional[DateTimeType] = None,
464
+ datetime: Optional[str] = None,
455
465
  limit: Optional[int] = 10,
456
466
  query: Optional[str] = None,
457
467
  token: Optional[str] = None,
@@ -459,7 +469,7 @@ class CoreClient(AsyncBaseCoreClient):
459
469
  sortby: Optional[str] = None,
460
470
  q: Optional[List[str]] = None,
461
471
  intersects: Optional[str] = None,
462
- filter: Optional[str] = None,
472
+ filter_expr: Optional[str] = None,
463
473
  filter_lang: Optional[str] = None,
464
474
  **kwargs,
465
475
  ) -> stac_types.ItemCollection:
@@ -469,7 +479,7 @@ class CoreClient(AsyncBaseCoreClient):
469
479
  collections (Optional[List[str]]): List of collection IDs to search in.
470
480
  ids (Optional[List[str]]): List of item IDs to search for.
471
481
  bbox (Optional[BBox]): Bounding box to search in.
472
- datetime (Optional[DateTimeType]): Filter items based on the datetime field.
482
+ datetime (Optional[str]): Filter items based on the datetime field.
473
483
  limit (Optional[int]): Maximum number of results to return.
474
484
  query (Optional[str]): Query string to filter the results.
475
485
  token (Optional[str]): Access token to use when searching the catalog.
@@ -496,7 +506,7 @@ class CoreClient(AsyncBaseCoreClient):
496
506
  }
497
507
 
498
508
  if datetime:
499
- base_args["datetime"] = self._format_datetime_range(datetime)
509
+ base_args["datetime"] = self._format_datetime_range(date_str=datetime)
500
510
 
501
511
  if intersects:
502
512
  base_args["intersects"] = orjson.loads(unquote_plus(intersects))
@@ -507,12 +517,12 @@ class CoreClient(AsyncBaseCoreClient):
507
517
  for sort in sortby
508
518
  ]
509
519
 
510
- if filter:
511
- base_args["filter-lang"] = "cql2-json"
520
+ if filter_expr:
521
+ base_args["filter_lang"] = "cql2-json"
512
522
  base_args["filter"] = orjson.loads(
513
- unquote_plus(filter)
523
+ unquote_plus(filter_expr)
514
524
  if filter_lang == "cql2-json"
515
- else to_cql2(parse_cql2_text(filter))
525
+ else to_cql2(parse_cql2_text(filter_expr))
516
526
  )
517
527
 
518
528
  if fields:
@@ -594,8 +604,8 @@ class CoreClient(AsyncBaseCoreClient):
594
604
  )
595
605
 
596
606
  # only cql2_json is supported here
597
- if hasattr(search_request, "filter"):
598
- cql2_filter = getattr(search_request, "filter", None)
607
+ if hasattr(search_request, "filter_expr"):
608
+ cql2_filter = getattr(search_request, "filter_expr", None)
599
609
  try:
600
610
  search = self.database.apply_cql2_filter(search, cql2_filter)
601
611
  except Exception as e:
@@ -735,9 +745,7 @@ class TransactionsClient(AsyncBaseTransactionsClient):
735
745
  return ItemSerializer.db_to_stac(item, base_url)
736
746
 
737
747
  @overrides
738
- async def delete_item(
739
- self, item_id: str, collection_id: str, **kwargs
740
- ) -> Optional[stac_types.Item]:
748
+ async def delete_item(self, item_id: str, collection_id: str, **kwargs) -> None:
741
749
  """Delete an item from a collection.
742
750
 
743
751
  Args:
@@ -745,7 +753,7 @@ class TransactionsClient(AsyncBaseTransactionsClient):
745
753
  collection_id (str): The identifier of the collection that contains the item.
746
754
 
747
755
  Returns:
748
- Optional[stac_types.Item]: The deleted item, or `None` if the item was successfully deleted.
756
+ None: Returns 204 No Content on successful deletion
749
757
  """
750
758
  await self.database.delete_item(item_id=item_id, collection_id=collection_id)
751
759
  return None
@@ -815,23 +823,20 @@ class TransactionsClient(AsyncBaseTransactionsClient):
815
823
  )
816
824
 
817
825
  @overrides
818
- async def delete_collection(
819
- self, collection_id: str, **kwargs
820
- ) -> Optional[stac_types.Collection]:
826
+ async def delete_collection(self, collection_id: str, **kwargs) -> None:
821
827
  """
822
828
  Delete a collection.
823
829
 
824
830
  This method deletes an existing collection in the database.
825
831
 
826
832
  Args:
827
- collection_id (str): The identifier of the collection that contains the item.
828
- kwargs: Additional keyword arguments.
833
+ collection_id (str): The identifier of the collection to delete
829
834
 
830
835
  Returns:
831
- None.
836
+ None: Returns 204 No Content on successful deletion
832
837
 
833
838
  Raises:
834
- NotFoundError: If the collection doesn't exist.
839
+ NotFoundError: If the collection doesn't exist
835
840
  """
836
841
  await self.database.delete_collection(collection_id=collection_id)
837
842
  return None
@@ -907,11 +912,81 @@ class BulkTransactionsClient(BaseBulkTransactionsClient):
907
912
  return f"Successfully added {len(processed_items)} Items."
908
913
 
909
914
 
915
+ _DEFAULT_QUERYABLES: Dict[str, Dict[str, Any]] = {
916
+ "id": {
917
+ "description": "ID",
918
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id",
919
+ },
920
+ "collection": {
921
+ "description": "Collection",
922
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/then/properties/collection",
923
+ },
924
+ "geometry": {
925
+ "description": "Geometry",
926
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/1/oneOf/0/properties/geometry",
927
+ },
928
+ "datetime": {
929
+ "description": "Acquisition Timestamp",
930
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
931
+ },
932
+ "created": {
933
+ "description": "Creation Timestamp",
934
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/created",
935
+ },
936
+ "updated": {
937
+ "description": "Creation Timestamp",
938
+ "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/updated",
939
+ },
940
+ "cloud_cover": {
941
+ "description": "Cloud Cover",
942
+ "$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fields/properties/eo:cloud_cover",
943
+ },
944
+ "cloud_shadow_percentage": {
945
+ "title": "Cloud Shadow Percentage",
946
+ "description": "Cloud Shadow Percentage",
947
+ "type": "number",
948
+ "minimum": 0,
949
+ "maximum": 100,
950
+ },
951
+ "nodata_pixel_percentage": {
952
+ "title": "No Data Pixel Percentage",
953
+ "description": "No Data Pixel Percentage",
954
+ "type": "number",
955
+ "minimum": 0,
956
+ "maximum": 100,
957
+ },
958
+ }
959
+
960
+ _ES_MAPPING_TYPE_TO_JSON: Dict[
961
+ str, Literal["string", "number", "boolean", "object", "array", "null"]
962
+ ] = {
963
+ "date": "string",
964
+ "date_nanos": "string",
965
+ "keyword": "string",
966
+ "match_only_text": "string",
967
+ "text": "string",
968
+ "wildcard": "string",
969
+ "byte": "number",
970
+ "double": "number",
971
+ "float": "number",
972
+ "half_float": "number",
973
+ "long": "number",
974
+ "scaled_float": "number",
975
+ "short": "number",
976
+ "token_count": "number",
977
+ "unsigned_long": "number",
978
+ "geo_point": "object",
979
+ "geo_shape": "object",
980
+ "nested": "array",
981
+ }
982
+
983
+
910
984
  @attr.s
911
985
  class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
912
986
  """Defines a pattern for implementing the STAC filter extension."""
913
987
 
914
- # todo: use the ES _mapping endpoint to dynamically find what fields exist
988
+ database: BaseDatabaseLogic = attr.ib()
989
+
915
990
  async def get_queryables(
916
991
  self, collection_id: Optional[str] = None, **kwargs
917
992
  ) -> Dict[str, Any]:
@@ -932,55 +1007,62 @@ class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
932
1007
  Returns:
933
1008
  Dict[str, Any]: A dictionary containing the queryables for the given collection.
934
1009
  """
935
- return {
1010
+ queryables: Dict[str, Any] = {
936
1011
  "$schema": "https://json-schema.org/draft/2019-09/schema",
937
1012
  "$id": "https://stac-api.example.com/queryables",
938
1013
  "type": "object",
939
- "title": "Queryables for Example STAC API",
940
- "description": "Queryable names for the example STAC API Item Search filter.",
941
- "properties": {
942
- "id": {
943
- "description": "ID",
944
- "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id",
945
- },
946
- "collection": {
947
- "description": "Collection",
948
- "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/then/properties/collection",
949
- },
950
- "geometry": {
951
- "description": "Geometry",
952
- "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/1/oneOf/0/properties/geometry",
953
- },
954
- "datetime": {
955
- "description": "Acquisition Timestamp",
956
- "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
957
- },
958
- "created": {
959
- "description": "Creation Timestamp",
960
- "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/created",
961
- },
962
- "updated": {
963
- "description": "Creation Timestamp",
964
- "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/updated",
965
- },
966
- "cloud_cover": {
967
- "description": "Cloud Cover",
968
- "$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fields/properties/eo:cloud_cover",
969
- },
970
- "cloud_shadow_percentage": {
971
- "description": "Cloud Shadow Percentage",
972
- "title": "Cloud Shadow Percentage",
973
- "type": "number",
974
- "minimum": 0,
975
- "maximum": 100,
976
- },
977
- "nodata_pixel_percentage": {
978
- "description": "No Data Pixel Percentage",
979
- "title": "No Data Pixel Percentage",
980
- "type": "number",
981
- "minimum": 0,
982
- "maximum": 100,
983
- },
984
- },
1014
+ "title": "Queryables for STAC API",
1015
+ "description": "Queryable names for the STAC API Item Search filter.",
1016
+ "properties": _DEFAULT_QUERYABLES,
985
1017
  "additionalProperties": True,
986
1018
  }
1019
+ if not collection_id:
1020
+ return queryables
1021
+
1022
+ properties: Dict[str, Any] = queryables["properties"]
1023
+ queryables.update(
1024
+ {
1025
+ "properties": properties,
1026
+ "additionalProperties": False,
1027
+ }
1028
+ )
1029
+
1030
+ mapping_data = await self.database.get_items_mapping(collection_id)
1031
+ mapping_properties = next(iter(mapping_data.values()))["mappings"]["properties"]
1032
+ stack = deque(mapping_properties.items())
1033
+
1034
+ while stack:
1035
+ field_name, field_def = stack.popleft()
1036
+
1037
+ # Iterate over nested fields
1038
+ field_properties = field_def.get("properties")
1039
+ if field_properties:
1040
+ # Fields in Item Properties should be exposed with their un-prefixed names,
1041
+ # and not require expressions to prefix them with properties,
1042
+ # e.g., eo:cloud_cover instead of properties.eo:cloud_cover.
1043
+ if field_name == "properties":
1044
+ stack.extend(field_properties.items())
1045
+ else:
1046
+ stack.extend(
1047
+ (f"{field_name}.{k}", v) for k, v in field_properties.items()
1048
+ )
1049
+
1050
+ # Skip non-indexed or disabled fields
1051
+ field_type = field_def.get("type")
1052
+ if not field_type or not field_def.get("enabled", True):
1053
+ continue
1054
+
1055
+ # Generate field properties
1056
+ field_result = _DEFAULT_QUERYABLES.get(field_name, {})
1057
+ properties[field_name] = field_result
1058
+
1059
+ field_name_human = field_name.replace("_", " ").title()
1060
+ field_result.setdefault("title", field_name_human)
1061
+
1062
+ field_type_json = _ES_MAPPING_TYPE_TO_JSON.get(field_type, field_type)
1063
+ field_result.setdefault("type", field_type_json)
1064
+
1065
+ if field_type in {"date", "date_nanos"}:
1066
+ field_result.setdefault("format", "date-time")
1067
+
1068
+ return queryables
@@ -0,0 +1,226 @@
1
+ """Database logic core."""
2
+
3
+ import os
4
+ from functools import lru_cache
5
+ from typing import Any, Dict, List, Optional, Protocol
6
+
7
+ from stac_fastapi.types.stac import Item
8
+
9
+
10
+ # stac_pydantic classes extend _GeometryBase, which doesn't have a type field,
11
+ # So create our own Protocol for typing
12
+ # Union[ Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection]
13
+ class Geometry(Protocol): # noqa
14
+ type: str
15
+ coordinates: Any
16
+
17
+
18
+ COLLECTIONS_INDEX = os.getenv("STAC_COLLECTIONS_INDEX", "collections")
19
+ ITEMS_INDEX_PREFIX = os.getenv("STAC_ITEMS_INDEX_PREFIX", "items_")
20
+
21
+ ES_INDEX_NAME_UNSUPPORTED_CHARS = {
22
+ "\\",
23
+ "/",
24
+ "*",
25
+ "?",
26
+ '"',
27
+ "<",
28
+ ">",
29
+ "|",
30
+ " ",
31
+ ",",
32
+ "#",
33
+ ":",
34
+ }
35
+
36
+ _ES_INDEX_NAME_UNSUPPORTED_CHARS_TABLE = str.maketrans(
37
+ "", "", "".join(ES_INDEX_NAME_UNSUPPORTED_CHARS)
38
+ )
39
+
40
+ ITEM_INDICES = f"{ITEMS_INDEX_PREFIX}*,-*kibana*,-{COLLECTIONS_INDEX}*"
41
+
42
+ DEFAULT_SORT = {
43
+ "properties.datetime": {"order": "desc"},
44
+ "id": {"order": "desc"},
45
+ "collection": {"order": "desc"},
46
+ }
47
+
48
+ ES_ITEMS_SETTINGS = {
49
+ "index": {
50
+ "sort.field": list(DEFAULT_SORT.keys()),
51
+ "sort.order": [v["order"] for v in DEFAULT_SORT.values()],
52
+ }
53
+ }
54
+
55
+ ES_MAPPINGS_DYNAMIC_TEMPLATES = [
56
+ # Common https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md
57
+ {
58
+ "descriptions": {
59
+ "match_mapping_type": "string",
60
+ "match": "description",
61
+ "mapping": {"type": "text"},
62
+ }
63
+ },
64
+ {
65
+ "titles": {
66
+ "match_mapping_type": "string",
67
+ "match": "title",
68
+ "mapping": {"type": "text"},
69
+ }
70
+ },
71
+ # Projection Extension https://github.com/stac-extensions/projection
72
+ {"proj_epsg": {"match": "proj:epsg", "mapping": {"type": "integer"}}},
73
+ {
74
+ "proj_projjson": {
75
+ "match": "proj:projjson",
76
+ "mapping": {"type": "object", "enabled": False},
77
+ }
78
+ },
79
+ {
80
+ "proj_centroid": {
81
+ "match": "proj:centroid",
82
+ "mapping": {"type": "geo_point"},
83
+ }
84
+ },
85
+ {
86
+ "proj_geometry": {
87
+ "match": "proj:geometry",
88
+ "mapping": {"type": "object", "enabled": False},
89
+ }
90
+ },
91
+ {
92
+ "no_index_href": {
93
+ "match": "href",
94
+ "mapping": {"type": "text", "index": False},
95
+ }
96
+ },
97
+ # Default all other strings not otherwise specified to keyword
98
+ {"strings": {"match_mapping_type": "string", "mapping": {"type": "keyword"}}},
99
+ {"numerics": {"match_mapping_type": "long", "mapping": {"type": "float"}}},
100
+ ]
101
+
102
+ ES_ITEMS_MAPPINGS = {
103
+ "numeric_detection": False,
104
+ "dynamic_templates": ES_MAPPINGS_DYNAMIC_TEMPLATES,
105
+ "properties": {
106
+ "id": {"type": "keyword"},
107
+ "collection": {"type": "keyword"},
108
+ "geometry": {"type": "geo_shape"},
109
+ "assets": {"type": "object", "enabled": False},
110
+ "links": {"type": "object", "enabled": False},
111
+ "properties": {
112
+ "type": "object",
113
+ "properties": {
114
+ # Common https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md
115
+ "datetime": {"type": "date"},
116
+ "start_datetime": {"type": "date"},
117
+ "end_datetime": {"type": "date"},
118
+ "created": {"type": "date"},
119
+ "updated": {"type": "date"},
120
+ # Satellite Extension https://github.com/stac-extensions/sat
121
+ "sat:absolute_orbit": {"type": "integer"},
122
+ "sat:relative_orbit": {"type": "integer"},
123
+ },
124
+ },
125
+ },
126
+ }
127
+
128
+ ES_COLLECTIONS_MAPPINGS = {
129
+ "numeric_detection": False,
130
+ "dynamic_templates": ES_MAPPINGS_DYNAMIC_TEMPLATES,
131
+ "properties": {
132
+ "id": {"type": "keyword"},
133
+ "extent.spatial.bbox": {"type": "long"},
134
+ "extent.temporal.interval": {"type": "date"},
135
+ "providers": {"type": "object", "enabled": False},
136
+ "links": {"type": "object", "enabled": False},
137
+ "item_assets": {"type": "object", "enabled": False},
138
+ },
139
+ }
140
+
141
+
142
+ @lru_cache(256)
143
+ def index_by_collection_id(collection_id: str) -> str:
144
+ """
145
+ Translate a collection id into an Elasticsearch index name.
146
+
147
+ Args:
148
+ collection_id (str): The collection id to translate into an index name.
149
+
150
+ Returns:
151
+ str: The index name derived from the collection id.
152
+ """
153
+ cleaned = collection_id.translate(_ES_INDEX_NAME_UNSUPPORTED_CHARS_TABLE)
154
+ return (
155
+ f"{ITEMS_INDEX_PREFIX}{cleaned.lower()}_{collection_id.encode('utf-8').hex()}"
156
+ )
157
+
158
+
159
+ @lru_cache(256)
160
+ def index_alias_by_collection_id(collection_id: str) -> str:
161
+ """
162
+ Translate a collection id into an Elasticsearch index alias.
163
+
164
+ Args:
165
+ collection_id (str): The collection id to translate into an index alias.
166
+
167
+ Returns:
168
+ str: The index alias derived from the collection id.
169
+ """
170
+ cleaned = collection_id.translate(_ES_INDEX_NAME_UNSUPPORTED_CHARS_TABLE)
171
+ return f"{ITEMS_INDEX_PREFIX}{cleaned}"
172
+
173
+
174
+ def indices(collection_ids: Optional[List[str]]) -> str:
175
+ """
176
+ Get a comma-separated string of index names for a given list of collection ids.
177
+
178
+ Args:
179
+ collection_ids: A list of collection ids.
180
+
181
+ Returns:
182
+ A string of comma-separated index names. If `collection_ids` is empty, returns the default indices.
183
+ """
184
+ return (
185
+ ",".join(map(index_alias_by_collection_id, collection_ids))
186
+ if collection_ids
187
+ else ITEM_INDICES
188
+ )
189
+
190
+
191
+ def mk_item_id(item_id: str, collection_id: str) -> str:
192
+ """Create the document id for an Item in Elasticsearch.
193
+
194
+ Args:
195
+ item_id (str): The id of the Item.
196
+ collection_id (str): The id of the Collection that the Item belongs to.
197
+
198
+ Returns:
199
+ str: The document id for the Item, combining the Item id and the Collection id, separated by a `|` character.
200
+ """
201
+ return f"{item_id}|{collection_id}"
202
+
203
+
204
+ def mk_actions(collection_id: str, processed_items: List[Item]) -> List[Dict[str, Any]]:
205
+ """Create Elasticsearch bulk actions for a list of processed items.
206
+
207
+ Args:
208
+ collection_id (str): The identifier for the collection the items belong to.
209
+ processed_items (List[Item]): The list of processed items to be bulk indexed.
210
+
211
+ Returns:
212
+ List[Dict[str, Union[str, Dict]]]: The list of bulk actions to be executed,
213
+ each action being a dictionary with the following keys:
214
+ - `_index`: the index to store the document in.
215
+ - `_id`: the document's identifier.
216
+ - `_source`: the source of the document.
217
+ """
218
+ index_alias = index_alias_by_collection_id(collection_id)
219
+ return [
220
+ {
221
+ "_index": index_alias,
222
+ "_id": mk_item_id(item["id"], item["collection"]),
223
+ "_source": item,
224
+ }
225
+ for item in processed_items
226
+ ]
@@ -338,7 +338,7 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
338
338
  datetime: Optional[DateTimeType] = None,
339
339
  intersects: Optional[str] = None,
340
340
  filter_lang: Optional[str] = None,
341
- filter: Optional[str] = None,
341
+ filter_expr: Optional[str] = None,
342
342
  aggregations: Optional[str] = None,
343
343
  ids: Optional[List[str]] = None,
344
344
  bbox: Optional[BBox] = None,
@@ -380,8 +380,8 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
380
380
  if datetime:
381
381
  base_args["datetime"] = self._format_datetime_range(datetime)
382
382
 
383
- if filter:
384
- base_args["filter"] = self.get_filter(filter, filter_lang)
383
+ if filter_expr:
384
+ base_args["filter"] = self.get_filter(filter_expr, filter_lang)
385
385
  aggregate_request = EsAggregationExtensionPostRequest(**base_args)
386
386
  else:
387
387
  # Workaround for optional path param in POST requests
@@ -389,9 +389,9 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
389
389
  collection_id = path.split("/")[2]
390
390
 
391
391
  filter_lang = "cql2-json"
392
- if aggregate_request.filter:
393
- aggregate_request.filter = self.get_filter(
394
- aggregate_request.filter, filter_lang
392
+ if aggregate_request.filter_expr:
393
+ aggregate_request.filter_expr = self.get_filter(
394
+ aggregate_request.filter_expr, filter_lang
395
395
  )
396
396
 
397
397
  if collection_id:
@@ -465,10 +465,10 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
465
465
  detail=f"Aggregation {agg_name} not supported at catalog level",
466
466
  )
467
467
 
468
- if aggregate_request.filter:
468
+ if aggregate_request.filter_expr:
469
469
  try:
470
470
  search = self.database.apply_cql2_filter(
471
- search, aggregate_request.filter
471
+ search, aggregate_request.filter_expr
472
472
  )
473
473
  except Exception as e:
474
474
  raise HTTPException(
@@ -8,7 +8,7 @@ import operator
8
8
  from dataclasses import dataclass
9
9
  from enum import auto
10
10
  from types import DynamicClassAttribute
11
- from typing import Any, Callable, Dict, Optional, Union
11
+ from typing import Any, Callable, Dict, Optional
12
12
 
13
13
  from pydantic import BaseModel, root_validator
14
14
  from stac_pydantic.utils import AutoValueEnum
@@ -17,8 +17,6 @@ from stac_fastapi.extensions.core.query import QueryExtension as QueryExtensionB
17
17
 
18
18
  logger = logging.getLogger("uvicorn")
19
19
  logger.setLevel(logging.INFO)
20
- # Be careful: https://github.com/samuelcolvin/pydantic/issues/1423#issuecomment-642797287
21
- NumType = Union[float, int]
22
20
 
23
21
 
24
22
  class Operator(str, AutoValueEnum):
@@ -12,7 +12,7 @@ from starlette.requests import Request
12
12
 
13
13
  # These can be inferred from the item/collection, so they aren't included in the database
14
14
  # Instead they are dynamically generated when querying the database using the classes defined below
15
- INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "root"]
15
+ INFERRED_LINK_RELS = {"self", "item", "parent", "collection", "root"}
16
16
 
17
17
 
18
18
  def merge_params(url: str, newparams: Dict) -> str:
@@ -0,0 +1,2 @@
1
+ """library version."""
2
+ __version__ = "4.0.0a0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac-fastapi.core
3
- Version: 3.2.4
3
+ Version: 4.0.0a0
4
4
  Summary: Core library for the Elasticsearch and Opensearch stac-fastapi backends.
5
5
  Home-page: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
6
6
  License: MIT
@@ -12,6 +12,7 @@ stac_fastapi/core/base_database_logic.py
12
12
  stac_fastapi/core/base_settings.py
13
13
  stac_fastapi/core/basic_auth.py
14
14
  stac_fastapi/core/core.py
15
+ stac_fastapi/core/database_logic.py
15
16
  stac_fastapi/core/datetime_utils.py
16
17
  stac_fastapi/core/rate_limit.py
17
18
  stac_fastapi/core/route_dependencies.py
@@ -0,0 +1,13 @@
1
+ fastapi
2
+ attrs>=23.2.0
3
+ pydantic
4
+ stac_pydantic==3.1.*
5
+ stac-fastapi.api==5.1.1
6
+ stac-fastapi.extensions==5.1.1
7
+ stac-fastapi.types==5.1.1
8
+ orjson
9
+ overrides
10
+ geojson-pydantic
11
+ pygeofilter==0.3.1
12
+ jsonschema
13
+ slowapi==0.1.9
@@ -1,2 +0,0 @@
1
- """library version."""
2
- __version__ = "3.2.4"
@@ -1,13 +0,0 @@
1
- fastapi-slim
2
- attrs>=23.2.0
3
- pydantic
4
- stac_pydantic>=3
5
- stac-fastapi.types==3.0.0
6
- stac-fastapi.api==3.0.0
7
- stac-fastapi.extensions==3.0.0
8
- orjson
9
- overrides
10
- geojson-pydantic
11
- pygeofilter==0.3.1
12
- jsonschema
13
- slowapi==0.1.9