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.
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/PKG-INFO +8 -4
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/README.md +7 -3
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/core.py +75 -41
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/database_logic.py +7 -1
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/aggregation.py +1 -1
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/filter.py +37 -27
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/utilities.py +60 -5
- stac_fastapi_core-4.2.0/stac_fastapi/core/version.py +2 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/PKG-INFO +8 -4
- stac_fastapi_core-4.0.0a2/stac_fastapi/core/version.py +0 -2
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/setup.cfg +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/setup.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/__init__.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/base_database_logic.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/base_settings.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/basic_auth.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/datetime_utils.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/__init__.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/fields.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/query.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/models/__init__.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/models/links.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/models/search.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/rate_limit.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/route_dependencies.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/serializers.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/session.py +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/SOURCES.txt +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/dependency_links.txt +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/not-zip-safe +0 -0
- {stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/requires.txt +0 -0
- {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
|
|
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
|
-
| `
|
|
134
|
-
| `
|
|
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
|
-
| `
|
|
116
|
-
| `
|
|
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
|
-
) ->
|
|
680
|
-
"""
|
|
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
|
|
684
|
-
item (
|
|
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
|
-
|
|
692
|
-
ConflictError: If
|
|
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
|
-
|
|
696
|
-
base_url = str(
|
|
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
|
-
#
|
|
699
|
-
if
|
|
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
|
-
|
|
710
|
+
feature, base_url, BulkTransactionMethod.INSERT
|
|
706
711
|
)
|
|
707
|
-
for
|
|
712
|
+
for feature in features
|
|
708
713
|
]
|
|
714
|
+
attempted = len(processed_items)
|
|
709
715
|
|
|
710
|
-
await self.database.bulk_async(
|
|
711
|
-
collection_id
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
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.
|
|
745
|
-
|
|
746
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
905
|
-
|
|
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,
|
|
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 {
|
|
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
|
-
{"
|
|
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 = {
|
{stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/aggregation.py
RENAMED
|
@@ -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:
|
{stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/filter.py
RENAMED
|
@@ -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
|
|
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
|
|
86
|
-
"""Enumeration for spatial
|
|
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 {
|
|
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"]
|
|
198
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
92
|
+
f"Expected one of {true_values + false_values}. Using default: {default_str}"
|
|
38
93
|
)
|
|
39
|
-
return
|
|
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]]]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stac-fastapi-core
|
|
3
|
-
Version: 4.0
|
|
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
|
-
| `
|
|
134
|
-
| `
|
|
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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/base_database_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/__init__.py
RENAMED
|
File without changes
|
{stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/extensions/fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi/core/route_dependencies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/requires.txt
RENAMED
|
File without changes
|
{stac_fastapi_core-4.0.0a2 → stac_fastapi_core-4.2.0}/stac_fastapi_core.egg-info/top_level.txt
RENAMED
|
File without changes
|