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.
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/PKG-INFO +1 -1
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/setup.py +5 -5
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/core.py +167 -85
- stac_fastapi.core-4.0.0a0/stac_fastapi/core/database_logic.py +226 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/aggregation.py +8 -8
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/query.py +1 -3
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/models/links.py +1 -1
- stac_fastapi.core-4.0.0a0/stac_fastapi/core/version.py +2 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/PKG-INFO +1 -1
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/SOURCES.txt +1 -0
- stac_fastapi.core-4.0.0a0/stac_fastapi.core.egg-info/requires.txt +13 -0
- stac_fastapi.core-3.2.4/stac_fastapi/core/version.py +0 -2
- stac_fastapi.core-3.2.4/stac_fastapi.core.egg-info/requires.txt +0 -13
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/README.md +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/setup.cfg +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/__init__.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/base_database_logic.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/base_settings.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/basic_auth.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/datetime_utils.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/__init__.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/fields.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/filter.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/models/__init__.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/models/search.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/rate_limit.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/route_dependencies.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/serializers.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/session.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/utilities.py +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/dependency_links.txt +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/not-zip-safe +0 -0
- {stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/top_level.txt +0 -0
|
@@ -6,13 +6,13 @@ with open("README.md") as f:
|
|
|
6
6
|
desc = f.read()
|
|
7
7
|
|
|
8
8
|
install_requires = [
|
|
9
|
-
"fastapi
|
|
9
|
+
"fastapi",
|
|
10
10
|
"attrs>=23.2.0",
|
|
11
11
|
"pydantic",
|
|
12
|
-
"stac_pydantic
|
|
13
|
-
"stac-fastapi.
|
|
14
|
-
"stac-fastapi.
|
|
15
|
-
"stac-fastapi.
|
|
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[
|
|
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[
|
|
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,
|
|
429
|
+
def _format_datetime_range(self, date_str: str) -> str:
|
|
431
430
|
"""
|
|
432
|
-
Convert a
|
|
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
|
-
|
|
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:
|
|
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
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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[
|
|
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
|
-
|
|
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[
|
|
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
|
|
511
|
-
base_args["
|
|
520
|
+
if filter_expr:
|
|
521
|
+
base_args["filter_lang"] = "cql2-json"
|
|
512
522
|
base_args["filter"] = orjson.loads(
|
|
513
|
-
unquote_plus(
|
|
523
|
+
unquote_plus(filter_expr)
|
|
514
524
|
if filter_lang == "cql2-json"
|
|
515
|
-
else to_cql2(parse_cql2_text(
|
|
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, "
|
|
598
|
-
cql2_filter = getattr(search_request, "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
940
|
-
"description": "Queryable names for the
|
|
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
|
+
]
|
{stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/aggregation.py
RENAMED
|
@@ -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
|
-
|
|
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
|
|
384
|
-
base_args["filter"] = self.get_filter(
|
|
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.
|
|
393
|
-
aggregate_request.
|
|
394
|
-
aggregate_request.
|
|
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.
|
|
468
|
+
if aggregate_request.filter_expr:
|
|
469
469
|
try:
|
|
470
470
|
search = self.database.apply_cql2_filter(
|
|
471
|
-
search, aggregate_request.
|
|
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
|
|
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 =
|
|
15
|
+
INFERRED_LINK_RELS = {"self", "item", "parent", "collection", "root"}
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def merge_params(url: str, newparams: Dict) -> str:
|
{stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/SOURCES.txt
RENAMED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/base_database_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/__init__.py
RENAMED
|
File without changes
|
{stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/fields.py
RENAMED
|
File without changes
|
{stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/extensions/filter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi/core/route_dependencies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{stac_fastapi.core-3.2.4 → stac_fastapi.core-4.0.0a0}/stac_fastapi.core.egg-info/top_level.txt
RENAMED
|
File without changes
|