stac-fastapi-core 4.0.0a2__tar.gz → 4.2.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.
Files changed (32) hide show
  1. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/PKG-INFO +8 -4
  2. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/README.md +7 -3
  3. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/core.py +75 -41
  4. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/database_logic.py +7 -1
  5. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/aggregation.py +1 -1
  6. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/filter.py +37 -27
  7. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/utilities.py +60 -5
  8. stac_fastapi_core-4.2.0/stac_fastapi/core/version.py +2 -0
  9. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/PKG-INFO +8 -4
  10. stac_fastapi_core-4.0.0a2/stac_fastapi/core/version.py +0 -2
  11. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/setup.cfg +0 -0
  12. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/setup.py +0 -0
  13. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/__init__.py +0 -0
  14. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/base_database_logic.py +0 -0
  15. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/base_settings.py +0 -0
  16. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/basic_auth.py +0 -0
  17. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/datetime_utils.py +0 -0
  18. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/__init__.py +0 -0
  19. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/fields.py +0 -0
  20. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/query.py +0 -0
  21. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/models/__init__.py +0 -0
  22. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/models/links.py +0 -0
  23. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/models/search.py +0 -0
  24. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/rate_limit.py +0 -0
  25. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/route_dependencies.py +0 -0
  26. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/serializers.py +0 -0
  27. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/session.py +0 -0
  28. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/SOURCES.txt +0 -0
  29. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/dependency_links.txt +0 -0
  30. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/not-zip-safe +0 -0
  31. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/requires.txt +0 -0
  32. {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/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: 4.0.0a2
3
+ Version: 4.2.0
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
@@ -122,6 +122,7 @@ You can customize additional settings in your `.env` file:
122
122
  | `STAC_FASTAPI_TITLE` | Title of the API in the documentation. | `stac-fastapi-elasticsearch` or `stac-fastapi-opensearch` | Optional |
123
123
  | `STAC_FASTAPI_DESCRIPTION` | Description of the API in the documentation. | N/A | Optional |
124
124
  | `STAC_FASTAPI_VERSION` | API version. | `2.1` | Optional |
125
+ | `STAC_FASTAPI_LANDING_PAGE_ID` | Landing page ID | `stac-fastapi` | Optional |
125
126
  | `APP_HOST` | Server bind address. | `0.0.0.0` | Optional |
126
127
  | `APP_PORT` | Server port. | `8080` | Optional |
127
128
  | `ENVIRONMENT` | Runtime environment. | `local` | Optional |
@@ -129,9 +130,12 @@ You can customize additional settings in your `.env` file:
129
130
  | `RELOAD` | Enable auto-reload for development. | `true` | Optional |
130
131
  | `STAC_FASTAPI_RATE_LIMIT` | API rate limit per client. | `200/minute` | Optional |
131
132
  | `BACKEND` | Tests-related variable | `elasticsearch` or `opensearch` based on the backend | Optional |
132
- | `ELASTICSEARCH_VERSION` | Version of Elasticsearch to use. | `8.11.0` | Optional |
133
- | `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
134
- | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional |
133
+ | `ELASTICSEARCH_VERSION` | Version of Elasticsearch to use. | `8.11.0` | Optional | |
134
+ | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional
135
+ | `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional
136
+ | `RAISE_ON_BULK_ERROR` | Controls whether bulk insert operations raise exceptions on errors. If set to `true`, the operation will stop and raise an exception when an error occurs. If set to `false`, errors will be logged, and the operation will continue. **Note:** STAC Item and ItemCollection validation errors will always raise, regardless of this flag. | `false` Optional |
137
+ | `DATABASE_REFRESH` | Controls whether database operations refresh the index immediately after changes. If set to `true`, changes will be immediately searchable. If set to `false`, changes may not be immediately visible but can improve performance for bulk operations. If set to `wait_for`, changes will wait for the next refresh cycle to become visible. | `false` | Optional |
138
+ | `ENABLE_TRANSACTIONS_EXTENSIONS` | Enables or disables the Transactions and Bulk Transactions API extensions. If set to `false`, the POST `/collections` route and related transaction endpoints (including bulk transaction operations) will be unavailable in the API. This is useful for deployments where mutating the catalog via the API should be prevented. | `true` | Optional |
135
139
 
136
140
  > [!NOTE]
137
141
  > The variables `ES_HOST`, `ES_PORT`, `ES_USE_SSL`, and `ES_VERIFY_CERTS` apply to both Elasticsearch and OpenSearch backends, so there is no need to rename the key names to `OS_` even if you're using OpenSearch.
@@ -104,6 +104,7 @@ You can customize additional settings in your `.env` file:
104
104
  | `STAC_FASTAPI_TITLE` | Title of the API in the documentation. | `stac-fastapi-elasticsearch` or `stac-fastapi-opensearch` | Optional |
105
105
  | `STAC_FASTAPI_DESCRIPTION` | Description of the API in the documentation. | N/A | Optional |
106
106
  | `STAC_FASTAPI_VERSION` | API version. | `2.1` | Optional |
107
+ | `STAC_FASTAPI_LANDING_PAGE_ID` | Landing page ID | `stac-fastapi` | Optional |
107
108
  | `APP_HOST` | Server bind address. | `0.0.0.0` | Optional |
108
109
  | `APP_PORT` | Server port. | `8080` | Optional |
109
110
  | `ENVIRONMENT` | Runtime environment. | `local` | Optional |
@@ -111,9 +112,12 @@ You can customize additional settings in your `.env` file:
111
112
  | `RELOAD` | Enable auto-reload for development. | `true` | Optional |
112
113
  | `STAC_FASTAPI_RATE_LIMIT` | API rate limit per client. | `200/minute` | Optional |
113
114
  | `BACKEND` | Tests-related variable | `elasticsearch` or `opensearch` based on the backend | Optional |
114
- | `ELASTICSEARCH_VERSION` | Version of Elasticsearch to use. | `8.11.0` | Optional |
115
- | `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
116
- | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional |
115
+ | `ELASTICSEARCH_VERSION` | Version of Elasticsearch to use. | `8.11.0` | Optional | |
116
+ | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional
117
+ | `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional
118
+ | `RAISE_ON_BULK_ERROR` | Controls whether bulk insert operations raise exceptions on errors. If set to `true`, the operation will stop and raise an exception when an error occurs. If set to `false`, errors will be logged, and the operation will continue. **Note:** STAC Item and ItemCollection validation errors will always raise, regardless of this flag. | `false` Optional |
119
+ | `DATABASE_REFRESH` | Controls whether database operations refresh the index immediately after changes. If set to `true`, changes will be immediately searchable. If set to `false`, changes may not be immediately visible but can improve performance for bulk operations. If set to `wait_for`, changes will wait for the next refresh cycle to become visible. | `false` | Optional |
120
+ | `ENABLE_TRANSACTIONS_EXTENSIONS` | Enables or disables the Transactions and Bulk Transactions API extensions. If set to `false`, the POST `/collections` route and related transaction endpoints (including bulk transaction operations) will be unavailable in the API. This is useful for deployments where mutating the catalog via the API should be prevented. | `true` | Optional |
117
121
 
118
122
  > [!NOTE]
119
123
  > The variables `ES_HOST`, `ES_PORT`, `ES_USE_SSL`, and `ES_VERIFY_CERTS` apply to both Elasticsearch and OpenSearch backends, so there is no need to rename the key names to `OS_` even if you're using OpenSearch.
@@ -607,7 +607,7 @@ class CoreClient(AsyncBaseCoreClient):
607
607
  if hasattr(search_request, "filter_expr"):
608
608
  cql2_filter = getattr(search_request, "filter_expr", None)
609
609
  try:
610
- search = self.database.apply_cql2_filter(search, cql2_filter)
610
+ search = await self.database.apply_cql2_filter(search, cql2_filter)
611
611
  except Exception as e:
612
612
  raise HTTPException(
613
613
  status_code=400, detail=f"Error with cql2_json filter: {e}"
@@ -676,46 +676,63 @@ class TransactionsClient(AsyncBaseTransactionsClient):
676
676
  @overrides
677
677
  async def create_item(
678
678
  self, collection_id: str, item: Union[Item, ItemCollection], **kwargs
679
- ) -> Optional[stac_types.Item]:
680
- """Create an item in the collection.
679
+ ) -> Union[stac_types.Item, str]:
680
+ """
681
+ Create an item or a feature collection of items in the specified collection.
681
682
 
682
683
  Args:
683
- collection_id (str): The id of the collection to add the item to.
684
- item (stac_types.Item): The item to be added to the collection.
685
- kwargs: Additional keyword arguments.
684
+ collection_id (str): The ID of the collection to add the item(s) to.
685
+ item (Union[Item, ItemCollection]): A single item or a collection of items to be added.
686
+ **kwargs: Additional keyword arguments, such as `request` and `refresh`.
686
687
 
687
688
  Returns:
688
- stac_types.Item: The created item.
689
+ Union[stac_types.Item, str]: The created item if a single item is added, or a summary string
690
+ indicating the number of items successfully added and errors if a collection of items is added.
689
691
 
690
692
  Raises:
691
- NotFound: If the specified collection is not found in the database.
692
- ConflictError: If the item in the specified collection already exists.
693
-
693
+ NotFoundError: If the specified collection is not found in the database.
694
+ ConflictError: If an item with the same ID already exists in the collection.
694
695
  """
695
- item = item.model_dump(mode="json")
696
- base_url = str(kwargs["request"].base_url)
696
+ request = kwargs.get("request")
697
+ base_url = str(request.base_url)
698
+
699
+ # Convert Pydantic model to dict for uniform processing
700
+ item_dict = item.model_dump(mode="json")
697
701
 
698
- # If a feature collection is posted
699
- if item["type"] == "FeatureCollection":
702
+ # Handle FeatureCollection (bulk insert)
703
+ if item_dict["type"] == "FeatureCollection":
700
704
  bulk_client = BulkTransactionsClient(
701
705
  database=self.database, settings=self.settings
702
706
  )
707
+ features = item_dict["features"]
703
708
  processed_items = [
704
709
  bulk_client.preprocess_item(
705
- item, base_url, BulkTransactionMethod.INSERT
710
+ feature, base_url, BulkTransactionMethod.INSERT
706
711
  )
707
- for item in item["features"]
712
+ for feature in features
708
713
  ]
714
+ attempted = len(processed_items)
709
715
 
710
- await self.database.bulk_async(
711
- collection_id, processed_items, refresh=kwargs.get("refresh", False)
716
+ success, errors = await self.database.bulk_async(
717
+ collection_id=collection_id,
718
+ processed_items=processed_items,
719
+ **kwargs,
712
720
  )
721
+ if errors:
722
+ logger.error(
723
+ f"Bulk async operation encountered errors for collection {collection_id}: {errors} (attempted {attempted})"
724
+ )
725
+ else:
726
+ logger.info(
727
+ f"Bulk async operation succeeded with {success} actions for collection {collection_id}."
728
+ )
729
+ return f"Successfully added {success} Items. {attempted - success} errors occurred."
713
730
 
714
- return None
715
- else:
716
- item = await self.database.prep_create_item(item=item, base_url=base_url)
717
- await self.database.create_item(item, refresh=kwargs.get("refresh", False))
718
- return ItemSerializer.db_to_stac(item, base_url)
731
+ # Handle single item
732
+ await self.database.create_item(
733
+ item_dict, base_url=base_url, exist_ok=False, **kwargs
734
+ )
735
+ return ItemSerializer.db_to_stac(item_dict, base_url)
719
736
 
720
737
  @overrides
721
738
  async def update_item(
@@ -738,12 +755,13 @@ class TransactionsClient(AsyncBaseTransactionsClient):
738
755
  """
739
756
  item = item.model_dump(mode="json")
740
757
  base_url = str(kwargs["request"].base_url)
741
- now = datetime_type.now(timezone.utc).isoformat().replace("+00:00", "Z")
758
+
759
+ now = datetime_type.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
742
760
  item["properties"]["updated"] = now
743
761
 
744
- await self.database.check_collection_exists(collection_id)
745
- await self.delete_item(item_id=item_id, collection_id=collection_id)
746
- await self.create_item(collection_id=collection_id, item=Item(**item), **kwargs)
762
+ await self.database.create_item(
763
+ item, base_url=base_url, exist_ok=True, **kwargs
764
+ )
747
765
 
748
766
  return ItemSerializer.db_to_stac(item, base_url)
749
767
 
@@ -758,7 +776,9 @@ class TransactionsClient(AsyncBaseTransactionsClient):
758
776
  Returns:
759
777
  None: Returns 204 No Content on successful deletion
760
778
  """
761
- await self.database.delete_item(item_id=item_id, collection_id=collection_id)
779
+ await self.database.delete_item(
780
+ item_id=item_id, collection_id=collection_id, **kwargs
781
+ )
762
782
  return None
763
783
 
764
784
  @overrides
@@ -779,8 +799,9 @@ class TransactionsClient(AsyncBaseTransactionsClient):
779
799
  """
780
800
  collection = collection.model_dump(mode="json")
781
801
  request = kwargs["request"]
802
+
782
803
  collection = self.database.collection_serializer.stac_to_db(collection, request)
783
- await self.database.create_collection(collection=collection)
804
+ await self.database.create_collection(collection=collection, **kwargs)
784
805
  return CollectionSerializer.db_to_stac(
785
806
  collection,
786
807
  request,
@@ -816,7 +837,7 @@ class TransactionsClient(AsyncBaseTransactionsClient):
816
837
 
817
838
  collection = self.database.collection_serializer.stac_to_db(collection, request)
818
839
  await self.database.update_collection(
819
- collection_id=collection_id, collection=collection
840
+ collection_id=collection_id, collection=collection, **kwargs
820
841
  )
821
842
 
822
843
  return CollectionSerializer.db_to_stac(
@@ -841,7 +862,7 @@ class TransactionsClient(AsyncBaseTransactionsClient):
841
862
  Raises:
842
863
  NotFoundError: If the collection doesn't exist
843
864
  """
844
- await self.database.delete_collection(collection_id=collection_id)
865
+ await self.database.delete_collection(collection_id=collection_id, **kwargs)
845
866
  return None
846
867
 
847
868
 
@@ -876,7 +897,7 @@ class BulkTransactionsClient(BaseBulkTransactionsClient):
876
897
  The preprocessed item.
877
898
  """
878
899
  exist_ok = method == BulkTransactionMethod.UPSERT
879
- return self.database.sync_prep_create_item(
900
+ return self.database.bulk_sync_prep_create_item(
880
901
  item=item, base_url=base_url, exist_ok=exist_ok
881
902
  )
882
903
 
@@ -900,19 +921,32 @@ class BulkTransactionsClient(BaseBulkTransactionsClient):
900
921
  else:
901
922
  base_url = ""
902
923
 
903
- processed_items = [
904
- self.preprocess_item(item, base_url, items.method)
905
- for item in items.items.values()
906
- ]
924
+ processed_items = []
925
+ for item in items.items.values():
926
+ try:
927
+ validated = Item(**item) if not isinstance(item, Item) else item
928
+ processed_items.append(
929
+ self.preprocess_item(
930
+ validated.model_dump(mode="json"), base_url, items.method
931
+ )
932
+ )
933
+ except ValidationError:
934
+ # Immediately raise on the first invalid item (strict mode)
935
+ raise
907
936
 
908
- # not a great way to get the collection_id-- should be part of the method signature
909
937
  collection_id = processed_items[0]["collection"]
910
-
911
- self.database.bulk_sync(
912
- collection_id, processed_items, refresh=kwargs.get("refresh", False)
938
+ attempted = len(processed_items)
939
+ success, errors = self.database.bulk_sync(
940
+ collection_id,
941
+ processed_items,
942
+ **kwargs,
913
943
  )
944
+ if errors:
945
+ logger.error(f"Bulk sync operation encountered errors: {errors}")
946
+ else:
947
+ logger.info(f"Bulk sync operation succeeded with {success} actions.")
914
948
 
915
- return f"Successfully added {len(processed_items)} Items."
949
+ return f"Successfully added/updated {success} Items. {attempted - success} errors occurred."
916
950
 
917
951
 
918
952
  _DEFAULT_QUERYABLES: Dict[str, Dict[str, Any]] = {
@@ -96,7 +96,13 @@ ES_MAPPINGS_DYNAMIC_TEMPLATES = [
96
96
  },
97
97
  # Default all other strings not otherwise specified to keyword
98
98
  {"strings": {"match_mapping_type": "string", "mapping": {"type": "keyword"}}},
99
- {"numerics": {"match_mapping_type": "long", "mapping": {"type": "float"}}},
99
+ {"long_to_double": {"match_mapping_type": "long", "mapping": {"type": "double"}}},
100
+ {
101
+ "double_to_double": {
102
+ "match_mapping_type": "double",
103
+ "mapping": {"type": "double"},
104
+ }
105
+ },
100
106
  ]
101
107
 
102
108
  ES_ITEMS_MAPPINGS = {
@@ -467,7 +467,7 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
467
467
 
468
468
  if aggregate_request.filter_expr:
469
469
  try:
470
- search = self.database.apply_cql2_filter(
470
+ search = await self.database.apply_cql2_filter(
471
471
  search, aggregate_request.filter_expr
472
472
  )
473
473
  except Exception as e:
@@ -10,7 +10,7 @@
10
10
  # defines the LIKE, IN, and BETWEEN operators.
11
11
 
12
12
  # Basic Spatial Operators (http://www.opengis.net/spec/cql2/1.0/conf/basic-spatial-operators)
13
- # defines the intersects operator (S_INTERSECTS).
13
+ # defines spatial operators (S_INTERSECTS, S_CONTAINS, S_WITHIN, S_DISJOINT).
14
14
  # """
15
15
 
16
16
  import re
@@ -82,26 +82,16 @@ class AdvancedComparisonOp(str, Enum):
82
82
  IN = "in"
83
83
 
84
84
 
85
- class SpatialIntersectsOp(str, Enum):
86
- """Enumeration for spatial intersection operator as per CQL2 standards."""
85
+ class SpatialOp(str, Enum):
86
+ """Enumeration for spatial operators as per CQL2 standards."""
87
87
 
88
88
  S_INTERSECTS = "s_intersects"
89
+ S_CONTAINS = "s_contains"
90
+ S_WITHIN = "s_within"
91
+ S_DISJOINT = "s_disjoint"
89
92
 
90
93
 
91
- queryables_mapping = {
92
- "id": "id",
93
- "collection": "collection",
94
- "geometry": "geometry",
95
- "datetime": "properties.datetime",
96
- "created": "properties.created",
97
- "updated": "properties.updated",
98
- "cloud_cover": "properties.eo:cloud_cover",
99
- "cloud_shadow_percentage": "properties.s2:cloud_shadow_percentage",
100
- "nodata_pixel_percentage": "properties.s2:nodata_pixel_percentage",
101
- }
102
-
103
-
104
- def to_es_field(field: str) -> str:
94
+ def to_es_field(queryables_mapping: Dict[str, Any], field: str) -> str:
105
95
  """
106
96
  Map a given field to its corresponding Elasticsearch field according to a predefined mapping.
107
97
 
@@ -114,7 +104,7 @@ def to_es_field(field: str) -> str:
114
104
  return queryables_mapping.get(field, field)
115
105
 
116
106
 
117
- def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
107
+ def to_es(queryables_mapping: Dict[str, Any], query: Dict[str, Any]) -> Dict[str, Any]:
118
108
  """
119
109
  Transform a simplified CQL2 query structure to an Elasticsearch compatible query DSL.
120
110
 
@@ -130,7 +120,13 @@ def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
130
120
  LogicalOp.OR: "should",
131
121
  LogicalOp.NOT: "must_not",
132
122
  }[query["op"]]
133
- return {"bool": {bool_type: [to_es(sub_query) for sub_query in query["args"]]}}
123
+ return {
124
+ "bool": {
125
+ bool_type: [
126
+ to_es(queryables_mapping, sub_query) for sub_query in query["args"]
127
+ ]
128
+ }
129
+ }
134
130
 
135
131
  elif query["op"] in [
136
132
  ComparisonOp.EQ,
@@ -147,7 +143,7 @@ def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
147
143
  ComparisonOp.GTE: "gte",
148
144
  }
149
145
 
150
- field = to_es_field(query["args"][0]["property"])
146
+ field = to_es_field(queryables_mapping, query["args"][0]["property"])
151
147
  value = query["args"][1]
152
148
  if isinstance(value, dict) and "timestamp" in value:
153
149
  value = value["timestamp"]
@@ -170,11 +166,11 @@ def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
170
166
  return {"range": {field: {range_op[query["op"]]: value}}}
171
167
 
172
168
  elif query["op"] == ComparisonOp.IS_NULL:
173
- field = to_es_field(query["args"][0]["property"])
169
+ field = to_es_field(queryables_mapping, query["args"][0]["property"])
174
170
  return {"bool": {"must_not": {"exists": {"field": field}}}}
175
171
 
176
172
  elif query["op"] == AdvancedComparisonOp.BETWEEN:
177
- field = to_es_field(query["args"][0]["property"])
173
+ field = to_es_field(queryables_mapping, query["args"][0]["property"])
178
174
  gte, lte = query["args"][1], query["args"][2]
179
175
  if isinstance(gte, dict) and "timestamp" in gte:
180
176
  gte = gte["timestamp"]
@@ -183,20 +179,34 @@ def to_es(query: Dict[str, Any]) -> Dict[str, Any]:
183
179
  return {"range": {field: {"gte": gte, "lte": lte}}}
184
180
 
185
181
  elif query["op"] == AdvancedComparisonOp.IN:
186
- field = to_es_field(query["args"][0]["property"])
182
+ field = to_es_field(queryables_mapping, query["args"][0]["property"])
187
183
  values = query["args"][1]
188
184
  if not isinstance(values, list):
189
185
  raise ValueError(f"Arg {values} is not a list")
190
186
  return {"terms": {field: values}}
191
187
 
192
188
  elif query["op"] == AdvancedComparisonOp.LIKE:
193
- field = to_es_field(query["args"][0]["property"])
189
+ field = to_es_field(queryables_mapping, query["args"][0]["property"])
194
190
  pattern = cql2_like_to_es(query["args"][1])
195
191
  return {"wildcard": {field: {"value": pattern, "case_insensitive": True}}}
196
192
 
197
- elif query["op"] == SpatialIntersectsOp.S_INTERSECTS:
198
- field = to_es_field(query["args"][0]["property"])
193
+ elif query["op"] in [
194
+ SpatialOp.S_INTERSECTS,
195
+ SpatialOp.S_CONTAINS,
196
+ SpatialOp.S_WITHIN,
197
+ SpatialOp.S_DISJOINT,
198
+ ]:
199
+ field = to_es_field(queryables_mapping, query["args"][0]["property"])
199
200
  geometry = query["args"][1]
200
- return {"geo_shape": {field: {"shape": geometry, "relation": "intersects"}}}
201
+
202
+ relation_mapping = {
203
+ SpatialOp.S_INTERSECTS: "intersects",
204
+ SpatialOp.S_CONTAINS: "contains",
205
+ SpatialOp.S_WITHIN: "within",
206
+ SpatialOp.S_DISJOINT: "disjoint",
207
+ }
208
+
209
+ relation = relation_mapping[query["op"]]
210
+ return {"geo_shape": {field: {"shape": geometry, "relation": relation}}}
201
211
 
202
212
  return {}
@@ -12,20 +12,75 @@ from stac_fastapi.types.stac import Item
12
12
  MAX_LIMIT = 10000
13
13
 
14
14
 
15
- def get_bool_env(name: str, default: bool = False) -> bool:
15
+ def validate_refresh(value: Union[str, bool]) -> str:
16
+ """
17
+ Validate the `refresh` parameter value.
18
+
19
+ Args:
20
+ value (Union[str, bool]): The `refresh` parameter value, which can be a string or a boolean.
21
+
22
+ Returns:
23
+ str: The validated value of the `refresh` parameter, which can be "true", "false", or "wait_for".
24
+ """
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Handle boolean-like values using get_bool_env
28
+ if isinstance(value, bool) or value in {
29
+ "true",
30
+ "false",
31
+ "1",
32
+ "0",
33
+ "yes",
34
+ "no",
35
+ "y",
36
+ "n",
37
+ }:
38
+ is_true = get_bool_env("DATABASE_REFRESH", default=value)
39
+ return "true" if is_true else "false"
40
+
41
+ # Normalize to lowercase for case-insensitivity
42
+ value = value.lower()
43
+
44
+ # Handle "wait_for" explicitly
45
+ if value == "wait_for":
46
+ return "wait_for"
47
+
48
+ # Log a warning for invalid values and default to "false"
49
+ logger.warning(
50
+ f"Invalid value for `refresh`: '{value}'. Expected 'true', 'false', or 'wait_for'. Defaulting to 'false'."
51
+ )
52
+ return "false"
53
+
54
+
55
+ def get_bool_env(name: str, default: Union[bool, str] = False) -> bool:
16
56
  """
17
57
  Retrieve a boolean value from an environment variable.
18
58
 
19
59
  Args:
20
60
  name (str): The name of the environment variable.
21
- default (bool, optional): The default value to use if the variable is not set or unrecognized. Defaults to False.
61
+ default (Union[bool, str], optional): The default value to use if the variable is not set or unrecognized. Defaults to False.
22
62
 
23
63
  Returns:
24
64
  bool: The boolean value parsed from the environment variable.
25
65
  """
26
- value = os.getenv(name, str(default).lower())
27
66
  true_values = ("true", "1", "yes", "y")
28
67
  false_values = ("false", "0", "no", "n")
68
+
69
+ # Normalize the default value
70
+ if isinstance(default, bool):
71
+ default_str = "true" if default else "false"
72
+ elif isinstance(default, str):
73
+ default_str = default.lower()
74
+ else:
75
+ logger = logging.getLogger(__name__)
76
+ logger.warning(
77
+ f"The `default` parameter must be a boolean or string, got {type(default).__name__}. "
78
+ f"Falling back to `False`."
79
+ )
80
+ default_str = "false"
81
+
82
+ # Retrieve and normalize the environment variable value
83
+ value = os.getenv(name, default_str)
29
84
  if value.lower() in true_values:
30
85
  return True
31
86
  elif value.lower() in false_values:
@@ -34,9 +89,9 @@ def get_bool_env(name: str, default: bool = False) -> bool:
34
89
  logger = logging.getLogger(__name__)
35
90
  logger.warning(
36
91
  f"Environment variable '{name}' has unrecognized value '{value}'. "
37
- f"Expected one of {true_values + false_values}. Using default: {default}"
92
+ f"Expected one of {true_values + false_values}. Using default: {default_str}"
38
93
  )
39
- return default
94
+ return default_str in true_values
40
95
 
41
96
 
42
97
  def bbox2polygon(b0: float, b1: float, b2: float, b3: float) -> List[List[List[float]]]:
@@ -0,0 +1,2 @@
1
+ """library version."""
2
+ __version__ = "4.2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stac-fastapi-core
3
- Version: 4.0.0a2
3
+ Version: 4.2.0
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
@@ -122,6 +122,7 @@ You can customize additional settings in your `.env` file:
122
122
  | `STAC_FASTAPI_TITLE` | Title of the API in the documentation. | `stac-fastapi-elasticsearch` or `stac-fastapi-opensearch` | Optional |
123
123
  | `STAC_FASTAPI_DESCRIPTION` | Description of the API in the documentation. | N/A | Optional |
124
124
  | `STAC_FASTAPI_VERSION` | API version. | `2.1` | Optional |
125
+ | `STAC_FASTAPI_LANDING_PAGE_ID` | Landing page ID | `stac-fastapi` | Optional |
125
126
  | `APP_HOST` | Server bind address. | `0.0.0.0` | Optional |
126
127
  | `APP_PORT` | Server port. | `8080` | Optional |
127
128
  | `ENVIRONMENT` | Runtime environment. | `local` | Optional |
@@ -129,9 +130,12 @@ You can customize additional settings in your `.env` file:
129
130
  | `RELOAD` | Enable auto-reload for development. | `true` | Optional |
130
131
  | `STAC_FASTAPI_RATE_LIMIT` | API rate limit per client. | `200/minute` | Optional |
131
132
  | `BACKEND` | Tests-related variable | `elasticsearch` or `opensearch` based on the backend | Optional |
132
- | `ELASTICSEARCH_VERSION` | Version of Elasticsearch to use. | `8.11.0` | Optional |
133
- | `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
134
- | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional |
133
+ | `ELASTICSEARCH_VERSION` | Version of Elasticsearch to use. | `8.11.0` | Optional | |
134
+ | `OPENSEARCH_VERSION` | OpenSearch version | `2.11.1` | Optional
135
+ | `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional
136
+ | `RAISE_ON_BULK_ERROR` | Controls whether bulk insert operations raise exceptions on errors. If set to `true`, the operation will stop and raise an exception when an error occurs. If set to `false`, errors will be logged, and the operation will continue. **Note:** STAC Item and ItemCollection validation errors will always raise, regardless of this flag. | `false` Optional |
137
+ | `DATABASE_REFRESH` | Controls whether database operations refresh the index immediately after changes. If set to `true`, changes will be immediately searchable. If set to `false`, changes may not be immediately visible but can improve performance for bulk operations. If set to `wait_for`, changes will wait for the next refresh cycle to become visible. | `false` | Optional |
138
+ | `ENABLE_TRANSACTIONS_EXTENSIONS` | Enables or disables the Transactions and Bulk Transactions API extensions. If set to `false`, the POST `/collections` route and related transaction endpoints (including bulk transaction operations) will be unavailable in the API. This is useful for deployments where mutating the catalog via the API should be prevented. | `true` | Optional |
135
139
 
136
140
  > [!NOTE]
137
141
  > The variables `ES_HOST`, `ES_PORT`, `ES_USE_SSL`, and `ES_VERIFY_CERTS` apply to both Elasticsearch and OpenSearch backends, so there is no need to rename the key names to `OS_` even if you're using OpenSearch.
@@ -1,2 +0,0 @@
1
- """library version."""
2
- __version__ = "4.0.0a2"