stac-fastapi-core 4.2.0__py3-none-any.whl → 5.0.0__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/base_database_logic.py +13 -1
- 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 +53 -155
- stac_fastapi/core/route_dependencies.py +6 -6
- stac_fastapi/core/utilities.py +0 -40
- stac_fastapi/core/version.py +1 -1
- stac_fastapi_core-5.0.0.dist-info/METADATA +570 -0
- {stac_fastapi_core-4.2.0.dist-info → stac_fastapi_core-5.0.0.dist-info}/RECORD +12 -13
- 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.0.dist-info}/WHEEL +0 -0
- {stac_fastapi_core-4.2.0.dist-info → stac_fastapi_core-5.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Filter extension logic for
|
|
1
|
+
"""Filter extension logic for conversion."""
|
|
2
2
|
|
|
3
3
|
# """
|
|
4
4
|
# Implements Filter Extension.
|
|
@@ -13,45 +13,64 @@
|
|
|
13
13
|
# defines spatial operators (S_INTERSECTS, S_CONTAINS, S_WITHIN, S_DISJOINT).
|
|
14
14
|
# """
|
|
15
15
|
|
|
16
|
-
import re
|
|
17
16
|
from enum import Enum
|
|
18
17
|
from typing import Any, Dict
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
19
|
+
DEFAULT_QUERYABLES: Dict[str, Dict[str, Any]] = {
|
|
20
|
+
"id": {
|
|
21
|
+
"description": "ID",
|
|
22
|
+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id",
|
|
23
|
+
},
|
|
24
|
+
"collection": {
|
|
25
|
+
"description": "Collection",
|
|
26
|
+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/then/properties/collection",
|
|
27
|
+
},
|
|
28
|
+
"geometry": {
|
|
29
|
+
"description": "Geometry",
|
|
30
|
+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/1/oneOf/0/properties/geometry",
|
|
31
|
+
},
|
|
32
|
+
"datetime": {
|
|
33
|
+
"description": "Acquisition Timestamp",
|
|
34
|
+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
|
|
35
|
+
},
|
|
36
|
+
"created": {
|
|
37
|
+
"description": "Creation Timestamp",
|
|
38
|
+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/created",
|
|
39
|
+
},
|
|
40
|
+
"updated": {
|
|
41
|
+
"description": "Creation Timestamp",
|
|
42
|
+
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/updated",
|
|
43
|
+
},
|
|
44
|
+
"cloud_cover": {
|
|
45
|
+
"description": "Cloud Cover",
|
|
46
|
+
"$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fields/properties/eo:cloud_cover",
|
|
47
|
+
},
|
|
48
|
+
"cloud_shadow_percentage": {
|
|
49
|
+
"title": "Cloud Shadow Percentage",
|
|
50
|
+
"description": "Cloud Shadow Percentage",
|
|
51
|
+
"type": "number",
|
|
52
|
+
"minimum": 0,
|
|
53
|
+
"maximum": 100,
|
|
54
|
+
},
|
|
55
|
+
"nodata_pixel_percentage": {
|
|
56
|
+
"title": "No Data Pixel Percentage",
|
|
57
|
+
"description": "No Data Pixel Percentage",
|
|
58
|
+
"type": "number",
|
|
59
|
+
"minimum": 0,
|
|
60
|
+
"maximum": 100,
|
|
61
|
+
},
|
|
27
62
|
}
|
|
63
|
+
"""Queryables that are present in all collections."""
|
|
28
64
|
|
|
65
|
+
OPTIONAL_QUERYABLES: Dict[str, Dict[str, Any]] = {
|
|
66
|
+
"platform": {
|
|
67
|
+
"$enum": True,
|
|
68
|
+
"description": "Satellite platform identifier",
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
"""Queryables that are present in some collections."""
|
|
29
72
|
|
|
30
|
-
|
|
31
|
-
pattern = match.group()
|
|
32
|
-
try:
|
|
33
|
-
return _valid_like_substitutions[pattern]
|
|
34
|
-
except KeyError:
|
|
35
|
-
raise ValueError(f"'{pattern}' is not a valid escape sequence")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def cql2_like_to_es(string: str) -> str:
|
|
39
|
-
"""
|
|
40
|
-
Convert CQL2 "LIKE" characters to Elasticsearch "wildcard" characters.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
string (str): The string containing CQL2 wildcard characters.
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
str: The converted string with Elasticsearch compatible wildcards.
|
|
47
|
-
|
|
48
|
-
Raises:
|
|
49
|
-
ValueError: If an invalid escape sequence is encountered.
|
|
50
|
-
"""
|
|
51
|
-
return _cql2_like_patterns.sub(
|
|
52
|
-
repl=_replace_like_patterns,
|
|
53
|
-
string=string,
|
|
54
|
-
)
|
|
73
|
+
ALL_QUERYABLES: Dict[str, Dict[str, Any]] = DEFAULT_QUERYABLES | OPTIONAL_QUERYABLES
|
|
55
74
|
|
|
56
75
|
|
|
57
76
|
class LogicalOp(str, Enum):
|
|
@@ -89,124 +108,3 @@ class SpatialOp(str, Enum):
|
|
|
89
108
|
S_CONTAINS = "s_contains"
|
|
90
109
|
S_WITHIN = "s_within"
|
|
91
110
|
S_DISJOINT = "s_disjoint"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def to_es_field(queryables_mapping: Dict[str, Any], field: str) -> str:
|
|
95
|
-
"""
|
|
96
|
-
Map a given field to its corresponding Elasticsearch field according to a predefined mapping.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
field (str): The field name from a user query or filter.
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
str: The mapped field name suitable for Elasticsearch queries.
|
|
103
|
-
"""
|
|
104
|
-
return queryables_mapping.get(field, field)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def to_es(queryables_mapping: Dict[str, Any], query: Dict[str, Any]) -> Dict[str, Any]:
|
|
108
|
-
"""
|
|
109
|
-
Transform a simplified CQL2 query structure to an Elasticsearch compatible query DSL.
|
|
110
|
-
|
|
111
|
-
Args:
|
|
112
|
-
query (Dict[str, Any]): The query dictionary containing 'op' and 'args'.
|
|
113
|
-
|
|
114
|
-
Returns:
|
|
115
|
-
Dict[str, Any]: The corresponding Elasticsearch query in the form of a dictionary.
|
|
116
|
-
"""
|
|
117
|
-
if query["op"] in [LogicalOp.AND, LogicalOp.OR, LogicalOp.NOT]:
|
|
118
|
-
bool_type = {
|
|
119
|
-
LogicalOp.AND: "must",
|
|
120
|
-
LogicalOp.OR: "should",
|
|
121
|
-
LogicalOp.NOT: "must_not",
|
|
122
|
-
}[query["op"]]
|
|
123
|
-
return {
|
|
124
|
-
"bool": {
|
|
125
|
-
bool_type: [
|
|
126
|
-
to_es(queryables_mapping, sub_query) for sub_query in query["args"]
|
|
127
|
-
]
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
elif query["op"] in [
|
|
132
|
-
ComparisonOp.EQ,
|
|
133
|
-
ComparisonOp.NEQ,
|
|
134
|
-
ComparisonOp.LT,
|
|
135
|
-
ComparisonOp.LTE,
|
|
136
|
-
ComparisonOp.GT,
|
|
137
|
-
ComparisonOp.GTE,
|
|
138
|
-
]:
|
|
139
|
-
range_op = {
|
|
140
|
-
ComparisonOp.LT: "lt",
|
|
141
|
-
ComparisonOp.LTE: "lte",
|
|
142
|
-
ComparisonOp.GT: "gt",
|
|
143
|
-
ComparisonOp.GTE: "gte",
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
field = to_es_field(queryables_mapping, query["args"][0]["property"])
|
|
147
|
-
value = query["args"][1]
|
|
148
|
-
if isinstance(value, dict) and "timestamp" in value:
|
|
149
|
-
value = value["timestamp"]
|
|
150
|
-
if query["op"] == ComparisonOp.EQ:
|
|
151
|
-
return {"range": {field: {"gte": value, "lte": value}}}
|
|
152
|
-
elif query["op"] == ComparisonOp.NEQ:
|
|
153
|
-
return {
|
|
154
|
-
"bool": {
|
|
155
|
-
"must_not": [{"range": {field: {"gte": value, "lte": value}}}]
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
else:
|
|
159
|
-
return {"range": {field: {range_op[query["op"]]: value}}}
|
|
160
|
-
else:
|
|
161
|
-
if query["op"] == ComparisonOp.EQ:
|
|
162
|
-
return {"term": {field: value}}
|
|
163
|
-
elif query["op"] == ComparisonOp.NEQ:
|
|
164
|
-
return {"bool": {"must_not": [{"term": {field: value}}]}}
|
|
165
|
-
else:
|
|
166
|
-
return {"range": {field: {range_op[query["op"]]: value}}}
|
|
167
|
-
|
|
168
|
-
elif query["op"] == ComparisonOp.IS_NULL:
|
|
169
|
-
field = to_es_field(queryables_mapping, query["args"][0]["property"])
|
|
170
|
-
return {"bool": {"must_not": {"exists": {"field": field}}}}
|
|
171
|
-
|
|
172
|
-
elif query["op"] == AdvancedComparisonOp.BETWEEN:
|
|
173
|
-
field = to_es_field(queryables_mapping, query["args"][0]["property"])
|
|
174
|
-
gte, lte = query["args"][1], query["args"][2]
|
|
175
|
-
if isinstance(gte, dict) and "timestamp" in gte:
|
|
176
|
-
gte = gte["timestamp"]
|
|
177
|
-
if isinstance(lte, dict) and "timestamp" in lte:
|
|
178
|
-
lte = lte["timestamp"]
|
|
179
|
-
return {"range": {field: {"gte": gte, "lte": lte}}}
|
|
180
|
-
|
|
181
|
-
elif query["op"] == AdvancedComparisonOp.IN:
|
|
182
|
-
field = to_es_field(queryables_mapping, query["args"][0]["property"])
|
|
183
|
-
values = query["args"][1]
|
|
184
|
-
if not isinstance(values, list):
|
|
185
|
-
raise ValueError(f"Arg {values} is not a list")
|
|
186
|
-
return {"terms": {field: values}}
|
|
187
|
-
|
|
188
|
-
elif query["op"] == AdvancedComparisonOp.LIKE:
|
|
189
|
-
field = to_es_field(queryables_mapping, query["args"][0]["property"])
|
|
190
|
-
pattern = cql2_like_to_es(query["args"][1])
|
|
191
|
-
return {"wildcard": {field: {"value": pattern, "case_insensitive": True}}}
|
|
192
|
-
|
|
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"])
|
|
200
|
-
geometry = query["args"][1]
|
|
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}}}
|
|
211
|
-
|
|
212
|
-
return {}
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import importlib
|
|
4
4
|
import inspect
|
|
5
|
-
import json
|
|
6
5
|
import logging
|
|
7
6
|
import os
|
|
8
7
|
from typing import List
|
|
9
8
|
|
|
9
|
+
import orjson
|
|
10
10
|
from fastapi import Depends
|
|
11
11
|
from jsonschema import validate
|
|
12
12
|
|
|
@@ -84,14 +84,14 @@ route_dependencies_schema = {
|
|
|
84
84
|
|
|
85
85
|
def get_route_dependencies_conf(route_dependencies_env: str) -> list:
|
|
86
86
|
"""Get Route dependencies configuration from file or environment variable."""
|
|
87
|
-
if os.path.
|
|
88
|
-
with open(route_dependencies_env,
|
|
89
|
-
route_dependencies_conf =
|
|
87
|
+
if os.path.isfile(route_dependencies_env):
|
|
88
|
+
with open(route_dependencies_env, "rb") as f:
|
|
89
|
+
route_dependencies_conf = orjson.loads(f.read())
|
|
90
90
|
|
|
91
91
|
else:
|
|
92
92
|
try:
|
|
93
|
-
route_dependencies_conf =
|
|
94
|
-
except
|
|
93
|
+
route_dependencies_conf = orjson.loads(route_dependencies_env)
|
|
94
|
+
except orjson.JSONDecodeError as exception:
|
|
95
95
|
_LOGGER.error("Invalid JSON format for route dependencies. %s", exception)
|
|
96
96
|
raise
|
|
97
97
|
|
stac_fastapi/core/utilities.py
CHANGED
|
@@ -12,46 +12,6 @@ from stac_fastapi.types.stac import Item
|
|
|
12
12
|
MAX_LIMIT = 10000
|
|
13
13
|
|
|
14
14
|
|
|
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
15
|
def get_bool_env(name: str, default: Union[bool, str] = False) -> bool:
|
|
56
16
|
"""
|
|
57
17
|
Retrieve a boolean value from an environment variable.
|
stac_fastapi/core/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""library version."""
|
|
2
|
-
__version__ = "
|
|
2
|
+
__version__ = "5.0.0"
|