stac-fastapi-core 4.2.0__py3-none-any.whl → 5.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stac_fastapi/core/core.py +5 -246
- stac_fastapi/core/datetime_utils.py +33 -1
- stac_fastapi/core/extensions/aggregation.py +2 -530
- stac_fastapi/core/extensions/filter.py +44 -157
- stac_fastapi/core/utilities.py +0 -40
- stac_fastapi/core/version.py +1 -1
- stac_fastapi_core-5.0.0a1.dist-info/METADATA +570 -0
- {stac_fastapi_core-4.2.0.dist-info → stac_fastapi_core-5.0.0a1.dist-info}/RECORD +10 -11
- stac_fastapi/core/database_logic.py +0 -232
- stac_fastapi_core-4.2.0.dist-info/METADATA +0 -377
- {stac_fastapi_core-4.2.0.dist-info → stac_fastapi_core-5.0.0a1.dist-info}/WHEEL +0 -0
- {stac_fastapi_core-4.2.0.dist-info → stac_fastapi_core-5.0.0a1.dist-info}/top_level.txt +0 -0
stac_fastapi/core/core.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""Core client."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from collections import deque
|
|
5
4
|
from datetime import datetime as datetime_type
|
|
6
5
|
from datetime import timezone
|
|
7
6
|
from enum import Enum
|
|
8
|
-
from typing import
|
|
7
|
+
from typing import List, Optional, Set, Type, Union
|
|
9
8
|
from urllib.parse import unquote_plus, urljoin
|
|
10
9
|
|
|
11
10
|
import attr
|
|
@@ -22,11 +21,11 @@ from stac_pydantic.version import STAC_VERSION
|
|
|
22
21
|
|
|
23
22
|
from stac_fastapi.core.base_database_logic import BaseDatabaseLogic
|
|
24
23
|
from stac_fastapi.core.base_settings import ApiBaseSettings
|
|
24
|
+
from stac_fastapi.core.datetime_utils import format_datetime_range
|
|
25
25
|
from stac_fastapi.core.models.links import PagingLinks
|
|
26
26
|
from stac_fastapi.core.serializers import CollectionSerializer, ItemSerializer
|
|
27
27
|
from stac_fastapi.core.session import Session
|
|
28
28
|
from stac_fastapi.core.utilities import filter_fields
|
|
29
|
-
from stac_fastapi.extensions.core.filter.client import AsyncBaseFiltersClient
|
|
30
29
|
from stac_fastapi.extensions.third_party.bulk_transactions import (
|
|
31
30
|
BaseBulkTransactionsClient,
|
|
32
31
|
BulkTransactionMethod,
|
|
@@ -37,7 +36,6 @@ from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES
|
|
|
37
36
|
from stac_fastapi.types.core import AsyncBaseCoreClient, AsyncBaseTransactionsClient
|
|
38
37
|
from stac_fastapi.types.extension import ApiExtension
|
|
39
38
|
from stac_fastapi.types.requests import get_base_url
|
|
40
|
-
from stac_fastapi.types.rfc3339 import DateTimeType, rfc3339_str_to_datetime
|
|
41
39
|
from stac_fastapi.types.search import BaseSearchPostRequest
|
|
42
40
|
|
|
43
41
|
logger = logging.getLogger(__name__)
|
|
@@ -318,9 +316,8 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
318
316
|
)
|
|
319
317
|
|
|
320
318
|
if datetime:
|
|
321
|
-
datetime_search = self._return_date(datetime)
|
|
322
319
|
search = self.database.apply_datetime_filter(
|
|
323
|
-
search=search,
|
|
320
|
+
search=search, interval=datetime
|
|
324
321
|
)
|
|
325
322
|
|
|
326
323
|
if bbox:
|
|
@@ -374,87 +371,6 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
374
371
|
)
|
|
375
372
|
return self.item_serializer.db_to_stac(item, base_url)
|
|
376
373
|
|
|
377
|
-
@staticmethod
|
|
378
|
-
def _return_date(
|
|
379
|
-
interval: Optional[Union[DateTimeType, str]]
|
|
380
|
-
) -> Dict[str, Optional[str]]:
|
|
381
|
-
"""
|
|
382
|
-
Convert a date interval.
|
|
383
|
-
|
|
384
|
-
(which may be a datetime, a tuple of one or two datetimes a string
|
|
385
|
-
representing a datetime or range, or None) into a dictionary for filtering
|
|
386
|
-
search results with Elasticsearch.
|
|
387
|
-
|
|
388
|
-
This function ensures the output dictionary contains 'gte' and 'lte' keys,
|
|
389
|
-
even if they are set to None, to prevent KeyError in the consuming logic.
|
|
390
|
-
|
|
391
|
-
Args:
|
|
392
|
-
interval (Optional[Union[DateTimeType, str]]): The date interval, which might be a single datetime,
|
|
393
|
-
a tuple with one or two datetimes, a string, or None.
|
|
394
|
-
|
|
395
|
-
Returns:
|
|
396
|
-
dict: A dictionary representing the date interval for use in filtering search results,
|
|
397
|
-
always containing 'gte' and 'lte' keys.
|
|
398
|
-
"""
|
|
399
|
-
result: Dict[str, Optional[str]] = {"gte": None, "lte": None}
|
|
400
|
-
|
|
401
|
-
if interval is None:
|
|
402
|
-
return result
|
|
403
|
-
|
|
404
|
-
if isinstance(interval, str):
|
|
405
|
-
if "/" in interval:
|
|
406
|
-
parts = interval.split("/")
|
|
407
|
-
result["gte"] = parts[0] if parts[0] != ".." else None
|
|
408
|
-
result["lte"] = (
|
|
409
|
-
parts[1] if len(parts) > 1 and parts[1] != ".." else None
|
|
410
|
-
)
|
|
411
|
-
else:
|
|
412
|
-
converted_time = interval if interval != ".." else None
|
|
413
|
-
result["gte"] = result["lte"] = converted_time
|
|
414
|
-
return result
|
|
415
|
-
|
|
416
|
-
if isinstance(interval, datetime_type):
|
|
417
|
-
datetime_iso = interval.isoformat()
|
|
418
|
-
result["gte"] = result["lte"] = datetime_iso
|
|
419
|
-
elif isinstance(interval, tuple):
|
|
420
|
-
start, end = interval
|
|
421
|
-
# Ensure datetimes are converted to UTC and formatted with 'Z'
|
|
422
|
-
if start:
|
|
423
|
-
result["gte"] = start.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
424
|
-
if end:
|
|
425
|
-
result["lte"] = end.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
426
|
-
|
|
427
|
-
return result
|
|
428
|
-
|
|
429
|
-
def _format_datetime_range(self, date_str: str) -> str:
|
|
430
|
-
"""
|
|
431
|
-
Convert a datetime range string into a normalized UTC string for API requests using rfc3339_str_to_datetime.
|
|
432
|
-
|
|
433
|
-
Args:
|
|
434
|
-
date_str (str): A string containing two datetime values separated by a '/'.
|
|
435
|
-
|
|
436
|
-
Returns:
|
|
437
|
-
str: A string formatted as 'YYYY-MM-DDTHH:MM:SSZ/YYYY-MM-DDTHH:MM:SSZ', with '..' used if any element is None.
|
|
438
|
-
"""
|
|
439
|
-
|
|
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)}"
|
|
457
|
-
|
|
458
374
|
async def get_search(
|
|
459
375
|
self,
|
|
460
376
|
request: Request,
|
|
@@ -506,7 +422,7 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
506
422
|
}
|
|
507
423
|
|
|
508
424
|
if datetime:
|
|
509
|
-
base_args["datetime"] =
|
|
425
|
+
base_args["datetime"] = format_datetime_range(date_str=datetime)
|
|
510
426
|
|
|
511
427
|
if intersects:
|
|
512
428
|
base_args["intersects"] = orjson.loads(unquote_plus(intersects))
|
|
@@ -576,9 +492,8 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
576
492
|
)
|
|
577
493
|
|
|
578
494
|
if search_request.datetime:
|
|
579
|
-
datetime_search = self._return_date(search_request.datetime)
|
|
580
495
|
search = self.database.apply_datetime_filter(
|
|
581
|
-
search=search,
|
|
496
|
+
search=search, interval=search_request.datetime
|
|
582
497
|
)
|
|
583
498
|
|
|
584
499
|
if search_request.bbox:
|
|
@@ -947,159 +862,3 @@ class BulkTransactionsClient(BaseBulkTransactionsClient):
|
|
|
947
862
|
logger.info(f"Bulk sync operation succeeded with {success} actions.")
|
|
948
863
|
|
|
949
864
|
return f"Successfully added/updated {success} Items. {attempted - success} errors occurred."
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
_DEFAULT_QUERYABLES: Dict[str, Dict[str, Any]] = {
|
|
953
|
-
"id": {
|
|
954
|
-
"description": "ID",
|
|
955
|
-
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id",
|
|
956
|
-
},
|
|
957
|
-
"collection": {
|
|
958
|
-
"description": "Collection",
|
|
959
|
-
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/then/properties/collection",
|
|
960
|
-
},
|
|
961
|
-
"geometry": {
|
|
962
|
-
"description": "Geometry",
|
|
963
|
-
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/1/oneOf/0/properties/geometry",
|
|
964
|
-
},
|
|
965
|
-
"datetime": {
|
|
966
|
-
"description": "Acquisition Timestamp",
|
|
967
|
-
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
|
|
968
|
-
},
|
|
969
|
-
"created": {
|
|
970
|
-
"description": "Creation Timestamp",
|
|
971
|
-
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/created",
|
|
972
|
-
},
|
|
973
|
-
"updated": {
|
|
974
|
-
"description": "Creation Timestamp",
|
|
975
|
-
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/updated",
|
|
976
|
-
},
|
|
977
|
-
"cloud_cover": {
|
|
978
|
-
"description": "Cloud Cover",
|
|
979
|
-
"$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fields/properties/eo:cloud_cover",
|
|
980
|
-
},
|
|
981
|
-
"cloud_shadow_percentage": {
|
|
982
|
-
"title": "Cloud Shadow Percentage",
|
|
983
|
-
"description": "Cloud Shadow Percentage",
|
|
984
|
-
"type": "number",
|
|
985
|
-
"minimum": 0,
|
|
986
|
-
"maximum": 100,
|
|
987
|
-
},
|
|
988
|
-
"nodata_pixel_percentage": {
|
|
989
|
-
"title": "No Data Pixel Percentage",
|
|
990
|
-
"description": "No Data Pixel Percentage",
|
|
991
|
-
"type": "number",
|
|
992
|
-
"minimum": 0,
|
|
993
|
-
"maximum": 100,
|
|
994
|
-
},
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
_ES_MAPPING_TYPE_TO_JSON: Dict[
|
|
998
|
-
str, Literal["string", "number", "boolean", "object", "array", "null"]
|
|
999
|
-
] = {
|
|
1000
|
-
"date": "string",
|
|
1001
|
-
"date_nanos": "string",
|
|
1002
|
-
"keyword": "string",
|
|
1003
|
-
"match_only_text": "string",
|
|
1004
|
-
"text": "string",
|
|
1005
|
-
"wildcard": "string",
|
|
1006
|
-
"byte": "number",
|
|
1007
|
-
"double": "number",
|
|
1008
|
-
"float": "number",
|
|
1009
|
-
"half_float": "number",
|
|
1010
|
-
"long": "number",
|
|
1011
|
-
"scaled_float": "number",
|
|
1012
|
-
"short": "number",
|
|
1013
|
-
"token_count": "number",
|
|
1014
|
-
"unsigned_long": "number",
|
|
1015
|
-
"geo_point": "object",
|
|
1016
|
-
"geo_shape": "object",
|
|
1017
|
-
"nested": "array",
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
@attr.s
|
|
1022
|
-
class EsAsyncBaseFiltersClient(AsyncBaseFiltersClient):
|
|
1023
|
-
"""Defines a pattern for implementing the STAC filter extension."""
|
|
1024
|
-
|
|
1025
|
-
database: BaseDatabaseLogic = attr.ib()
|
|
1026
|
-
|
|
1027
|
-
async def get_queryables(
|
|
1028
|
-
self, collection_id: Optional[str] = None, **kwargs
|
|
1029
|
-
) -> Dict[str, Any]:
|
|
1030
|
-
"""Get the queryables available for the given collection_id.
|
|
1031
|
-
|
|
1032
|
-
If collection_id is None, returns the intersection of all
|
|
1033
|
-
queryables over all collections.
|
|
1034
|
-
|
|
1035
|
-
This base implementation returns a blank queryable schema. This is not allowed
|
|
1036
|
-
under OGC CQL but it is allowed by the STAC API Filter Extension
|
|
1037
|
-
|
|
1038
|
-
https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter#queryables
|
|
1039
|
-
|
|
1040
|
-
Args:
|
|
1041
|
-
collection_id (str, optional): The id of the collection to get queryables for.
|
|
1042
|
-
**kwargs: additional keyword arguments
|
|
1043
|
-
|
|
1044
|
-
Returns:
|
|
1045
|
-
Dict[str, Any]: A dictionary containing the queryables for the given collection.
|
|
1046
|
-
"""
|
|
1047
|
-
queryables: Dict[str, Any] = {
|
|
1048
|
-
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
|
1049
|
-
"$id": "https://stac-api.example.com/queryables",
|
|
1050
|
-
"type": "object",
|
|
1051
|
-
"title": "Queryables for STAC API",
|
|
1052
|
-
"description": "Queryable names for the STAC API Item Search filter.",
|
|
1053
|
-
"properties": _DEFAULT_QUERYABLES,
|
|
1054
|
-
"additionalProperties": True,
|
|
1055
|
-
}
|
|
1056
|
-
if not collection_id:
|
|
1057
|
-
return queryables
|
|
1058
|
-
|
|
1059
|
-
properties: Dict[str, Any] = queryables["properties"]
|
|
1060
|
-
queryables.update(
|
|
1061
|
-
{
|
|
1062
|
-
"properties": properties,
|
|
1063
|
-
"additionalProperties": False,
|
|
1064
|
-
}
|
|
1065
|
-
)
|
|
1066
|
-
|
|
1067
|
-
mapping_data = await self.database.get_items_mapping(collection_id)
|
|
1068
|
-
mapping_properties = next(iter(mapping_data.values()))["mappings"]["properties"]
|
|
1069
|
-
stack = deque(mapping_properties.items())
|
|
1070
|
-
|
|
1071
|
-
while stack:
|
|
1072
|
-
field_name, field_def = stack.popleft()
|
|
1073
|
-
|
|
1074
|
-
# Iterate over nested fields
|
|
1075
|
-
field_properties = field_def.get("properties")
|
|
1076
|
-
if field_properties:
|
|
1077
|
-
# Fields in Item Properties should be exposed with their un-prefixed names,
|
|
1078
|
-
# and not require expressions to prefix them with properties,
|
|
1079
|
-
# e.g., eo:cloud_cover instead of properties.eo:cloud_cover.
|
|
1080
|
-
if field_name == "properties":
|
|
1081
|
-
stack.extend(field_properties.items())
|
|
1082
|
-
else:
|
|
1083
|
-
stack.extend(
|
|
1084
|
-
(f"{field_name}.{k}", v) for k, v in field_properties.items()
|
|
1085
|
-
)
|
|
1086
|
-
|
|
1087
|
-
# Skip non-indexed or disabled fields
|
|
1088
|
-
field_type = field_def.get("type")
|
|
1089
|
-
if not field_type or not field_def.get("enabled", True):
|
|
1090
|
-
continue
|
|
1091
|
-
|
|
1092
|
-
# Generate field properties
|
|
1093
|
-
field_result = _DEFAULT_QUERYABLES.get(field_name, {})
|
|
1094
|
-
properties[field_name] = field_result
|
|
1095
|
-
|
|
1096
|
-
field_name_human = field_name.replace("_", " ").title()
|
|
1097
|
-
field_result.setdefault("title", field_name_human)
|
|
1098
|
-
|
|
1099
|
-
field_type_json = _ES_MAPPING_TYPE_TO_JSON.get(field_type, field_type)
|
|
1100
|
-
field_result.setdefault("type", field_type_json)
|
|
1101
|
-
|
|
1102
|
-
if field_type in {"date", "date_nanos"}:
|
|
1103
|
-
field_result.setdefault("format", "date-time")
|
|
1104
|
-
|
|
1105
|
-
return queryables
|
|
@@ -1,6 +1,38 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Utility functions to handle datetime parsing."""
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
3
|
|
|
4
|
+
from stac_fastapi.types.rfc3339 import rfc3339_str_to_datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def format_datetime_range(date_str: str) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Convert a datetime range string into a normalized UTC string for API requests using rfc3339_str_to_datetime.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
date_str (str): A string containing two datetime values separated by a '/'.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
str: A string formatted as 'YYYY-MM-DDTHH:MM:SSZ/YYYY-MM-DDTHH:MM:SSZ', with '..' used if any element is None.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def normalize(dt):
|
|
19
|
+
dt = dt.strip()
|
|
20
|
+
if not dt or dt == "..":
|
|
21
|
+
return ".."
|
|
22
|
+
dt_obj = rfc3339_str_to_datetime(dt)
|
|
23
|
+
dt_utc = dt_obj.astimezone(timezone.utc)
|
|
24
|
+
return dt_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
25
|
+
|
|
26
|
+
if not isinstance(date_str, str):
|
|
27
|
+
return "../.."
|
|
28
|
+
if "/" not in date_str:
|
|
29
|
+
return f"{normalize(date_str)}/{normalize(date_str)}"
|
|
30
|
+
try:
|
|
31
|
+
start, end = date_str.split("/", 1)
|
|
32
|
+
except Exception:
|
|
33
|
+
return "../.."
|
|
34
|
+
return f"{normalize(start)}/{normalize(end)}"
|
|
35
|
+
|
|
4
36
|
|
|
5
37
|
# Borrowed from pystac - https://github.com/stac-utils/pystac/blob/f5e4cf4a29b62e9ef675d4a4dac7977b09f53c8f/pystac/utils.py#L370-L394
|
|
6
38
|
def datetime_to_str(dt: datetime, timespec: str = "auto") -> str:
|