eodag 3.0.0b3__py3-none-any.whl → 3.1.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.
- eodag/api/core.py +347 -247
- eodag/api/product/_assets.py +44 -15
- eodag/api/product/_product.py +58 -47
- eodag/api/product/drivers/__init__.py +81 -4
- eodag/api/product/drivers/base.py +65 -4
- eodag/api/product/drivers/generic.py +65 -0
- eodag/api/product/drivers/sentinel1.py +97 -0
- eodag/api/product/drivers/sentinel2.py +95 -0
- eodag/api/product/metadata_mapping.py +129 -93
- eodag/api/search_result.py +28 -12
- eodag/cli.py +61 -24
- eodag/config.py +457 -167
- eodag/plugins/apis/base.py +10 -4
- eodag/plugins/apis/ecmwf.py +53 -23
- eodag/plugins/apis/usgs.py +41 -17
- eodag/plugins/authentication/aws_auth.py +30 -18
- eodag/plugins/authentication/base.py +14 -3
- eodag/plugins/authentication/generic.py +14 -3
- eodag/plugins/authentication/header.py +14 -6
- eodag/plugins/authentication/keycloak.py +44 -25
- eodag/plugins/authentication/oauth.py +18 -4
- eodag/plugins/authentication/openid_connect.py +192 -171
- eodag/plugins/authentication/qsauth.py +12 -4
- eodag/plugins/authentication/sas_auth.py +22 -5
- eodag/plugins/authentication/token.py +95 -17
- eodag/plugins/authentication/token_exchange.py +19 -19
- eodag/plugins/base.py +4 -4
- eodag/plugins/crunch/base.py +8 -5
- eodag/plugins/crunch/filter_date.py +9 -6
- eodag/plugins/crunch/filter_latest_intersect.py +9 -8
- eodag/plugins/crunch/filter_latest_tpl_name.py +8 -8
- eodag/plugins/crunch/filter_overlap.py +9 -11
- eodag/plugins/crunch/filter_property.py +10 -10
- eodag/plugins/download/aws.py +181 -105
- eodag/plugins/download/base.py +49 -67
- eodag/plugins/download/creodias_s3.py +40 -2
- eodag/plugins/download/http.py +247 -223
- eodag/plugins/download/s3rest.py +29 -28
- eodag/plugins/manager.py +176 -41
- eodag/plugins/search/__init__.py +6 -5
- eodag/plugins/search/base.py +123 -60
- eodag/plugins/search/build_search_result.py +1046 -355
- eodag/plugins/search/cop_marine.py +132 -39
- eodag/plugins/search/creodias_s3.py +19 -68
- eodag/plugins/search/csw.py +48 -8
- eodag/plugins/search/data_request_search.py +124 -23
- eodag/plugins/search/qssearch.py +531 -310
- eodag/plugins/search/stac_list_assets.py +85 -0
- eodag/plugins/search/static_stac_search.py +23 -24
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1295 -355
- eodag/resources/providers.yml +1819 -3010
- eodag/resources/stac.yml +3 -163
- eodag/resources/stac_api.yml +2 -2
- eodag/resources/user_conf_template.yml +115 -99
- eodag/rest/cache.py +2 -2
- eodag/rest/config.py +3 -4
- eodag/rest/constants.py +0 -1
- eodag/rest/core.py +157 -117
- eodag/rest/errors.py +181 -0
- eodag/rest/server.py +57 -339
- eodag/rest/stac.py +133 -581
- eodag/rest/types/collections_search.py +3 -3
- eodag/rest/types/eodag_search.py +41 -30
- eodag/rest/types/queryables.py +42 -32
- eodag/rest/types/stac_search.py +15 -16
- eodag/rest/utils/__init__.py +14 -21
- eodag/rest/utils/cql_evaluate.py +6 -6
- eodag/rest/utils/rfc3339.py +2 -2
- eodag/types/__init__.py +153 -32
- eodag/types/bbox.py +2 -2
- eodag/types/download_args.py +4 -4
- eodag/types/queryables.py +183 -73
- eodag/types/search_args.py +6 -6
- eodag/types/whoosh.py +127 -3
- eodag/utils/__init__.py +228 -106
- eodag/utils/exceptions.py +47 -26
- eodag/utils/import_system.py +2 -2
- eodag/utils/logging.py +37 -77
- eodag/utils/repr.py +65 -6
- eodag/utils/requests.py +13 -15
- eodag/utils/rest.py +2 -2
- eodag/utils/s3.py +231 -0
- eodag/utils/stac_reader.py +11 -11
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/METADATA +81 -81
- eodag-3.1.0.dist-info/RECORD +113 -0
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +5 -2
- eodag/resources/constraints/climate-dt.json +0 -13
- eodag/resources/constraints/extremes-dt.json +0 -8
- eodag/utils/constraints.py +0 -244
- eodag-3.0.0b3.dist-info/RECORD +0 -110
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
- {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/top_level.txt +0 -0
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
16
|
# See the License for the specific language governing permissions and
|
|
17
17
|
# limitations under the License.
|
|
18
|
-
from typing import Any,
|
|
18
|
+
from typing import Any, Optional
|
|
19
19
|
|
|
20
20
|
from pydantic import (
|
|
21
21
|
BaseModel,
|
|
@@ -39,6 +39,6 @@ class CollectionsSearchRequest(BaseModel):
|
|
|
39
39
|
constellation: Optional[str] = Field(default=None)
|
|
40
40
|
|
|
41
41
|
@model_serializer(mode="wrap")
|
|
42
|
-
def _serialize(self, handler: SerializerFunctionWrapHandler) ->
|
|
43
|
-
dumped:
|
|
42
|
+
def _serialize(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
|
|
43
|
+
dumped: dict[str, Any] = handler(self)
|
|
44
44
|
return {EODAGSearch.to_eodag(k): v for k, v in dumped.items()}
|
eodag/rest/types/eodag_search.py
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
from typing import TYPE_CHECKING, Any,
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
21
21
|
|
|
22
22
|
from pydantic import (
|
|
23
23
|
AliasChoices,
|
|
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
|
|
|
52
52
|
from typing_extensions import Self
|
|
53
53
|
|
|
54
54
|
Geometry = Union[
|
|
55
|
-
|
|
55
|
+
dict[str, Any],
|
|
56
56
|
Point,
|
|
57
57
|
MultiPoint,
|
|
58
58
|
LineString,
|
|
@@ -73,8 +73,8 @@ class EODAGSearch(BaseModel):
|
|
|
73
73
|
|
|
74
74
|
productType: Optional[str] = Field(None, alias="collections", validate_default=True)
|
|
75
75
|
provider: Optional[str] = Field(None)
|
|
76
|
-
ids: Optional[
|
|
77
|
-
id: Optional[
|
|
76
|
+
ids: Optional[list[str]] = Field(None)
|
|
77
|
+
id: Optional[list[str]] = Field(
|
|
78
78
|
None, alias="ids"
|
|
79
79
|
) # TODO: remove when updating queryables
|
|
80
80
|
geom: Optional[Geometry] = Field(None, alias="geometry")
|
|
@@ -101,7 +101,7 @@ class EODAGSearch(BaseModel):
|
|
|
101
101
|
orbitNumber: Optional[int] = Field(None, alias="sat:absolute_orbit")
|
|
102
102
|
# TODO: colision in property name. Need to handle "sar:product_type"
|
|
103
103
|
sensorMode: Optional[str] = Field(None, alias="sar:instrument_mode")
|
|
104
|
-
polarizationChannels: Optional[
|
|
104
|
+
polarizationChannels: Optional[list[str]] = Field(None, alias="sar:polarizations")
|
|
105
105
|
dopplerFrequency: Optional[str] = Field(None, alias="sar:frequency_band")
|
|
106
106
|
doi: Optional[str] = Field(None, alias="sci:doi")
|
|
107
107
|
illuminationElevationAngle: Optional[float] = Field(
|
|
@@ -110,17 +110,10 @@ class EODAGSearch(BaseModel):
|
|
|
110
110
|
illuminationAzimuthAngle: Optional[float] = Field(None, alias="view:sun_azimuth")
|
|
111
111
|
page: Optional[int] = Field(1)
|
|
112
112
|
items_per_page: int = Field(DEFAULT_ITEMS_PER_PAGE, alias="limit")
|
|
113
|
-
sort_by: Optional[
|
|
113
|
+
sort_by: Optional[list[tuple[str, str]]] = Field(None, alias="sortby")
|
|
114
114
|
raise_errors: bool = False
|
|
115
115
|
|
|
116
|
-
_to_eodag_map:
|
|
117
|
-
|
|
118
|
-
@model_validator(mode="after")
|
|
119
|
-
def set_raise_errors(self) -> Self:
|
|
120
|
-
"""Set raise_errors to True if provider is set"""
|
|
121
|
-
if self.provider:
|
|
122
|
-
self.raise_errors = True
|
|
123
|
-
return self
|
|
116
|
+
_to_eodag_map: dict[str, str]
|
|
124
117
|
|
|
125
118
|
@model_validator(mode="after")
|
|
126
119
|
def remove_timeFromAscendingNode(self) -> Self: # pylint: disable=invalid-name
|
|
@@ -136,7 +129,7 @@ class EODAGSearch(BaseModel):
|
|
|
136
129
|
if not self.__pydantic_extra__:
|
|
137
130
|
return self
|
|
138
131
|
|
|
139
|
-
keys_to_update:
|
|
132
|
+
keys_to_update: dict[str, str] = {}
|
|
140
133
|
for key in self.__pydantic_extra__.keys():
|
|
141
134
|
if key.startswith("unk:"):
|
|
142
135
|
keys_to_update[key] = key[len("unk:") :]
|
|
@@ -152,7 +145,7 @@ class EODAGSearch(BaseModel):
|
|
|
152
145
|
|
|
153
146
|
@model_validator(mode="before")
|
|
154
147
|
@classmethod
|
|
155
|
-
def remove_keys(cls, values:
|
|
148
|
+
def remove_keys(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
156
149
|
"""Remove 'datetime', 'crunch', 'intersects', and 'bbox' keys"""
|
|
157
150
|
for key in ["datetime", "crunch", "intersects", "bbox", "filter_lang"]:
|
|
158
151
|
values.pop(key, None)
|
|
@@ -161,8 +154,8 @@ class EODAGSearch(BaseModel):
|
|
|
161
154
|
@model_validator(mode="before")
|
|
162
155
|
@classmethod
|
|
163
156
|
def parse_collections(
|
|
164
|
-
cls, values:
|
|
165
|
-
) ->
|
|
157
|
+
cls, values: dict[str, Any], info: ValidationInfo
|
|
158
|
+
) -> dict[str, Any]:
|
|
166
159
|
"""convert collections to productType"""
|
|
167
160
|
|
|
168
161
|
if collections := values.pop("collections", None):
|
|
@@ -179,7 +172,7 @@ class EODAGSearch(BaseModel):
|
|
|
179
172
|
|
|
180
173
|
@model_validator(mode="before")
|
|
181
174
|
@classmethod
|
|
182
|
-
def parse_query(cls, values:
|
|
175
|
+
def parse_query(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
183
176
|
"""
|
|
184
177
|
Convert a STAC query parameter filter with the "eq" operator to a dict.
|
|
185
178
|
"""
|
|
@@ -197,9 +190,9 @@ class EODAGSearch(BaseModel):
|
|
|
197
190
|
if not query:
|
|
198
191
|
return values
|
|
199
192
|
|
|
200
|
-
query_props:
|
|
201
|
-
errors:
|
|
202
|
-
for property_name, conditions in cast(
|
|
193
|
+
query_props: dict[str, Any] = {}
|
|
194
|
+
errors: list[InitErrorDetails] = []
|
|
195
|
+
for property_name, conditions in cast(dict[str, Any], query).items():
|
|
203
196
|
# Remove the prefix "properties." if present
|
|
204
197
|
prop = property_name.replace("properties.", "", 1)
|
|
205
198
|
|
|
@@ -212,7 +205,7 @@ class EODAGSearch(BaseModel):
|
|
|
212
205
|
continue
|
|
213
206
|
|
|
214
207
|
# Retrieve the operator and its value
|
|
215
|
-
operator, value = next(iter(cast(
|
|
208
|
+
operator, value = next(iter(cast(dict[str, Any], conditions).items()))
|
|
216
209
|
|
|
217
210
|
# Validate the operator
|
|
218
211
|
# only eq, in and lte are allowed
|
|
@@ -246,7 +239,7 @@ class EODAGSearch(BaseModel):
|
|
|
246
239
|
|
|
247
240
|
@model_validator(mode="before")
|
|
248
241
|
@classmethod
|
|
249
|
-
def parse_cql(cls, values:
|
|
242
|
+
def parse_cql(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
250
243
|
"""
|
|
251
244
|
Process cql2 filter
|
|
252
245
|
"""
|
|
@@ -263,7 +256,7 @@ class EODAGSearch(BaseModel):
|
|
|
263
256
|
if not filter_:
|
|
264
257
|
return values
|
|
265
258
|
|
|
266
|
-
errors:
|
|
259
|
+
errors: list[InitErrorDetails] = []
|
|
267
260
|
try:
|
|
268
261
|
parsing_result = EodagEvaluator().evaluate(parse_json(filter_)) # type: ignore
|
|
269
262
|
except (ValueError, NotImplementedError) as e:
|
|
@@ -278,7 +271,7 @@ class EODAGSearch(BaseModel):
|
|
|
278
271
|
title=cls.__name__, line_errors=errors
|
|
279
272
|
)
|
|
280
273
|
|
|
281
|
-
cql_args:
|
|
274
|
+
cql_args: dict[str, Any] = cast(dict[str, Any], parsing_result)
|
|
282
275
|
|
|
283
276
|
invalid_keys = {
|
|
284
277
|
"collections": 'Use "collection" instead of "collections"',
|
|
@@ -305,7 +298,7 @@ class EODAGSearch(BaseModel):
|
|
|
305
298
|
|
|
306
299
|
@field_validator("instrument", mode="before")
|
|
307
300
|
@classmethod
|
|
308
|
-
def join_instruments(cls, v: Union[str,
|
|
301
|
+
def join_instruments(cls, v: Union[str, list[str]]) -> str:
|
|
309
302
|
"""convert instruments to instrument"""
|
|
310
303
|
if isinstance(v, list):
|
|
311
304
|
return ",".join(v)
|
|
@@ -315,8 +308,8 @@ class EODAGSearch(BaseModel):
|
|
|
315
308
|
@classmethod
|
|
316
309
|
def parse_sortby(
|
|
317
310
|
cls,
|
|
318
|
-
sortby_post_params:
|
|
319
|
-
) ->
|
|
311
|
+
sortby_post_params: list[dict[str, str]],
|
|
312
|
+
) -> list[tuple[str, str]]:
|
|
320
313
|
"""
|
|
321
314
|
Convert STAC POST sortby to EODAG sort_by
|
|
322
315
|
"""
|
|
@@ -367,9 +360,27 @@ class EODAGSearch(BaseModel):
|
|
|
367
360
|
return cls._to_eodag_map.get(value, value)
|
|
368
361
|
|
|
369
362
|
@classmethod
|
|
370
|
-
def to_stac(
|
|
363
|
+
def to_stac(
|
|
364
|
+
cls,
|
|
365
|
+
field_name: str,
|
|
366
|
+
stac_item_properties: Optional[list[str]] = None,
|
|
367
|
+
provider: Optional[str] = None,
|
|
368
|
+
) -> str:
|
|
371
369
|
"""Get the alias of a field in a Pydantic model"""
|
|
370
|
+
# quick fix. TODO: refactor of EODAGSearch.
|
|
371
|
+
if field_name in ("productType", "id", "start_datetime", "end_datetime"):
|
|
372
|
+
return field_name
|
|
373
|
+
# another quick fix to handle different names of geometry
|
|
374
|
+
if field_name == "geometry":
|
|
375
|
+
field_name = "geom"
|
|
372
376
|
field = cls.model_fields.get(field_name)
|
|
373
377
|
if field is not None and field.alias is not None:
|
|
374
378
|
return field.alias
|
|
379
|
+
if (
|
|
380
|
+
provider
|
|
381
|
+
and ":" not in field_name
|
|
382
|
+
and stac_item_properties
|
|
383
|
+
and field_name not in stac_item_properties
|
|
384
|
+
):
|
|
385
|
+
return f"{provider}:{field_name}"
|
|
375
386
|
return field_name
|
eodag/rest/types/queryables.py
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
from typing import TYPE_CHECKING, Any, ClassVar,
|
|
20
|
+
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Optional, Union
|
|
21
21
|
|
|
22
22
|
from pydantic import (
|
|
23
23
|
BaseModel,
|
|
@@ -25,17 +25,18 @@ from pydantic import (
|
|
|
25
25
|
Field,
|
|
26
26
|
SerializationInfo,
|
|
27
27
|
SerializerFunctionWrapHandler,
|
|
28
|
-
|
|
28
|
+
field_validator,
|
|
29
29
|
model_serializer,
|
|
30
|
+
model_validator,
|
|
30
31
|
)
|
|
31
32
|
|
|
32
33
|
from eodag.rest.types.eodag_search import EODAGSearch
|
|
33
34
|
from eodag.rest.utils.rfc3339 import str_to_interval
|
|
34
35
|
from eodag.types import python_field_definition_to_json
|
|
35
|
-
from eodag.utils import Annotated
|
|
36
36
|
|
|
37
37
|
if TYPE_CHECKING:
|
|
38
38
|
from pydantic.fields import FieldInfo
|
|
39
|
+
from typing_extensions import Self
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
class QueryablesGetParams(BaseModel):
|
|
@@ -43,29 +44,39 @@ class QueryablesGetParams(BaseModel):
|
|
|
43
44
|
|
|
44
45
|
collection: Optional[str] = Field(default=None, serialization_alias="productType")
|
|
45
46
|
datetime: Optional[str] = Field(default=None)
|
|
47
|
+
start_datetime: Optional[str] = Field(default=None)
|
|
48
|
+
end_datetime: Optional[str] = Field(default=None)
|
|
46
49
|
|
|
47
50
|
model_config = ConfigDict(extra="allow", frozen=True)
|
|
48
51
|
|
|
49
52
|
@model_serializer(mode="wrap")
|
|
50
|
-
def _serialize(self, handler: SerializerFunctionWrapHandler) ->
|
|
51
|
-
dumped:
|
|
53
|
+
def _serialize(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
|
|
54
|
+
dumped: dict[str, Any] = handler(self)
|
|
52
55
|
return {EODAGSearch.to_eodag(k): v for k, v in dumped.items()}
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
start = str_to_interval(self.datetime)[0]
|
|
60
|
-
return start.strftime("%Y-%m-%dT%H:%M:%SZ") if start else None
|
|
57
|
+
@field_validator("datetime", "start_datetime", "end_datetime", mode="before")
|
|
58
|
+
def validate_datetime(cls, value: Any) -> Optional[str]:
|
|
59
|
+
"""datetime, start_datetime and end_datetime must be a string"""
|
|
60
|
+
if isinstance(value, list):
|
|
61
|
+
return value[0]
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@
|
|
65
|
-
def
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
@model_validator(mode="after")
|
|
66
|
+
def compute_datetimes(self: Self) -> Self:
|
|
67
|
+
"""Start datetime must be a string"""
|
|
68
|
+
if not self.datetime:
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
start, end = str_to_interval(self.datetime)
|
|
72
|
+
|
|
73
|
+
if not self.start_datetime and start:
|
|
74
|
+
self.start_datetime = start.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
75
|
+
|
|
76
|
+
if not self.end_datetime and end:
|
|
77
|
+
self.end_datetime = end.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
78
|
+
|
|
79
|
+
return self
|
|
69
80
|
|
|
70
81
|
|
|
71
82
|
class StacQueryableProperty(BaseModel):
|
|
@@ -78,12 +89,12 @@ class StacQueryableProperty(BaseModel):
|
|
|
78
89
|
|
|
79
90
|
description: str
|
|
80
91
|
ref: Optional[str] = Field(default=None, serialization_alias="$ref")
|
|
81
|
-
type: Optional[Union[str,
|
|
82
|
-
enum: Optional[
|
|
92
|
+
type: Optional[Union[str, list[str]]] = None
|
|
93
|
+
enum: Optional[list[Any]] = None
|
|
83
94
|
value: Optional[Any] = None
|
|
84
|
-
min: Optional[Union[int,
|
|
85
|
-
max: Optional[Union[int,
|
|
86
|
-
oneOf: Optional[
|
|
95
|
+
min: Optional[Union[int, list[Union[int, None]]]] = None
|
|
96
|
+
max: Optional[Union[int, list[Union[int, None]]]] = None
|
|
97
|
+
oneOf: Optional[list[Any]] = None
|
|
87
98
|
items: Optional[Any] = None
|
|
88
99
|
|
|
89
100
|
@classmethod
|
|
@@ -105,7 +116,7 @@ class StacQueryableProperty(BaseModel):
|
|
|
105
116
|
_: SerializationInfo,
|
|
106
117
|
):
|
|
107
118
|
"""Remove none value property fields during serialization"""
|
|
108
|
-
props:
|
|
119
|
+
props: dict[str, Any] = handler(self)
|
|
109
120
|
return {k: v for k, v in props.items() if v is not None}
|
|
110
121
|
|
|
111
122
|
|
|
@@ -131,15 +142,13 @@ class StacQueryables(BaseModel):
|
|
|
131
142
|
description: str = Field(
|
|
132
143
|
default="Queryable names for the EODAG STAC API Item Search filter."
|
|
133
144
|
)
|
|
134
|
-
default_properties: ClassVar[
|
|
135
|
-
"id": StacQueryableProperty(
|
|
136
|
-
description="ID",
|
|
137
|
-
ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/id",
|
|
138
|
-
),
|
|
145
|
+
default_properties: ClassVar[dict[str, StacQueryableProperty]] = {
|
|
139
146
|
"collection": StacQueryableProperty(
|
|
140
147
|
description="Collection",
|
|
141
148
|
ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection",
|
|
142
|
-
)
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
possible_properties: ClassVar[dict[str, StacQueryableProperty]] = {
|
|
143
152
|
"geometry": StacQueryableProperty(
|
|
144
153
|
description="Geometry",
|
|
145
154
|
ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry",
|
|
@@ -155,7 +164,8 @@ class StacQueryables(BaseModel):
|
|
|
155
164
|
items={"type": "number"},
|
|
156
165
|
),
|
|
157
166
|
}
|
|
158
|
-
properties:
|
|
167
|
+
properties: dict[str, Any] = Field()
|
|
168
|
+
required: Optional[list[str]] = Field(None)
|
|
159
169
|
additional_properties: bool = Field(
|
|
160
170
|
default=True, serialization_alias="additionalProperties"
|
|
161
171
|
)
|
eodag/rest/types/stac_search.py
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
-
from typing import TYPE_CHECKING,
|
|
22
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional, Union
|
|
23
23
|
|
|
24
24
|
import geojson
|
|
25
25
|
from pydantic import (
|
|
@@ -43,7 +43,6 @@ from shapely.geometry import (
|
|
|
43
43
|
shape,
|
|
44
44
|
)
|
|
45
45
|
from shapely.geometry.base import GEOMETRY_TYPES, BaseGeometry
|
|
46
|
-
from typing_extensions import Annotated
|
|
47
46
|
|
|
48
47
|
from eodag.rest.utils.rfc3339 import rfc3339_str_to_datetime, str_to_interval
|
|
49
48
|
from eodag.utils.exceptions import ValidationError
|
|
@@ -54,8 +53,8 @@ if TYPE_CHECKING:
|
|
|
54
53
|
NumType = Union[float, int]
|
|
55
54
|
|
|
56
55
|
BBox = Union[
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
tuple[NumType, NumType, NumType, NumType],
|
|
57
|
+
tuple[NumType, NumType, NumType, NumType, NumType, NumType],
|
|
59
58
|
]
|
|
60
59
|
|
|
61
60
|
Geometry = Union[
|
|
@@ -97,8 +96,8 @@ class SearchPostRequest(BaseModel):
|
|
|
97
96
|
model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
|
|
98
97
|
|
|
99
98
|
provider: Optional[str] = None
|
|
100
|
-
collections: Optional[
|
|
101
|
-
ids: Optional[
|
|
99
|
+
collections: Optional[list[str]] = None
|
|
100
|
+
ids: Optional[list[str]] = None
|
|
102
101
|
bbox: Optional[BBox] = None
|
|
103
102
|
intersects: Optional[Geometry] = None
|
|
104
103
|
datetime: Optional[str] = None
|
|
@@ -108,21 +107,21 @@ class SearchPostRequest(BaseModel):
|
|
|
108
107
|
page: Optional[PositiveInt] = Field( # type: ignore
|
|
109
108
|
default=None, description="Page number, must be a positive integer."
|
|
110
109
|
)
|
|
111
|
-
query: Optional[
|
|
112
|
-
filter: Optional[
|
|
110
|
+
query: Optional[dict[str, Any]] = None
|
|
111
|
+
filter: Optional[dict[str, Any]] = None
|
|
113
112
|
filter_lang: Optional[str] = Field(
|
|
114
113
|
default=None,
|
|
115
114
|
alias="filter-lang",
|
|
116
115
|
description="The language used for filtering.",
|
|
117
116
|
validate_default=True,
|
|
118
117
|
)
|
|
119
|
-
sortby: Optional[
|
|
118
|
+
sortby: Optional[list[SortBy]] = None
|
|
120
119
|
crunch: Optional[str] = None
|
|
121
120
|
|
|
122
121
|
@field_serializer("intersects")
|
|
123
122
|
def serialize_intersects(
|
|
124
123
|
self, intersects: Optional[Geometry]
|
|
125
|
-
) -> Optional[
|
|
124
|
+
) -> Optional[dict[str, Any]]:
|
|
126
125
|
"""Serialize intersects from shapely to a proper dict"""
|
|
127
126
|
if intersects:
|
|
128
127
|
return geojson.loads(geojson.dumps(intersects)) # type: ignore
|
|
@@ -141,7 +140,7 @@ class SearchPostRequest(BaseModel):
|
|
|
141
140
|
|
|
142
141
|
@model_validator(mode="before")
|
|
143
142
|
@classmethod
|
|
144
|
-
def only_one_spatial(cls, values:
|
|
143
|
+
def only_one_spatial(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
145
144
|
"""Check bbox and intersects are not both supplied."""
|
|
146
145
|
if "intersects" in values and "bbox" in values:
|
|
147
146
|
raise ValueError("intersects and bbox parameters are mutually exclusive")
|
|
@@ -161,7 +160,7 @@ class SearchPostRequest(BaseModel):
|
|
|
161
160
|
|
|
162
161
|
@field_validator("ids", "collections", mode="before")
|
|
163
162
|
@classmethod
|
|
164
|
-
def str_to_str_list(cls, v: Union[str,
|
|
163
|
+
def str_to_str_list(cls, v: Union[str, list[str]]) -> list[str]:
|
|
165
164
|
"""Convert ids and collections strings to list of strings"""
|
|
166
165
|
if isinstance(v, str):
|
|
167
166
|
return [i.strip() for i in v.split(",")]
|
|
@@ -169,7 +168,7 @@ class SearchPostRequest(BaseModel):
|
|
|
169
168
|
|
|
170
169
|
@field_validator("intersects", mode="before")
|
|
171
170
|
@classmethod
|
|
172
|
-
def validate_intersects(cls, v: Union[
|
|
171
|
+
def validate_intersects(cls, v: Union[dict[str, Any], Geometry]) -> Geometry:
|
|
173
172
|
"""Verify format of intersects"""
|
|
174
173
|
if isinstance(v, BaseGeometry):
|
|
175
174
|
return v
|
|
@@ -215,7 +214,7 @@ class SearchPostRequest(BaseModel):
|
|
|
215
214
|
# Single date is interpreted as end date
|
|
216
215
|
values = ["..", v]
|
|
217
216
|
|
|
218
|
-
dates:
|
|
217
|
+
dates: list[str] = []
|
|
219
218
|
for value in values:
|
|
220
219
|
if value == ".." or value == "":
|
|
221
220
|
dates.append("..")
|
|
@@ -258,13 +257,13 @@ class SearchPostRequest(BaseModel):
|
|
|
258
257
|
|
|
259
258
|
def sortby2list(
|
|
260
259
|
v: Optional[str],
|
|
261
|
-
) -> Optional[
|
|
260
|
+
) -> Optional[list[SortBy]]:
|
|
262
261
|
"""
|
|
263
262
|
Convert sortby filter parameter GET syntax to POST syntax
|
|
264
263
|
"""
|
|
265
264
|
if not v:
|
|
266
265
|
return None
|
|
267
|
-
sortby:
|
|
266
|
+
sortby: list[SortBy] = []
|
|
268
267
|
for sortby_param in v.split(","):
|
|
269
268
|
sortby_param = sortby_param.strip()
|
|
270
269
|
direction: Direction = "desc" if sortby_param.startswith("-") else "asc"
|
eodag/rest/utils/__init__.py
CHANGED
|
@@ -23,17 +23,7 @@ import logging
|
|
|
23
23
|
import os
|
|
24
24
|
from io import BufferedReader
|
|
25
25
|
from shutil import make_archive, rmtree
|
|
26
|
-
from typing import
|
|
27
|
-
TYPE_CHECKING,
|
|
28
|
-
Any,
|
|
29
|
-
Callable,
|
|
30
|
-
Dict,
|
|
31
|
-
Iterator,
|
|
32
|
-
List,
|
|
33
|
-
NamedTuple,
|
|
34
|
-
Optional,
|
|
35
|
-
Union,
|
|
36
|
-
)
|
|
26
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterator, NamedTuple, Optional, Union
|
|
37
27
|
from urllib.parse import unquote_plus, urlencode
|
|
38
28
|
|
|
39
29
|
import orjson
|
|
@@ -55,12 +45,15 @@ __all__ = ["get_date", "get_datetime"]
|
|
|
55
45
|
|
|
56
46
|
logger = logging.getLogger("eodag.rest.utils")
|
|
57
47
|
|
|
48
|
+
# Path of the liveness endpoint
|
|
49
|
+
LIVENESS_PROBE_PATH = "/_mgmt/ping"
|
|
50
|
+
|
|
58
51
|
|
|
59
52
|
class Cruncher(NamedTuple):
|
|
60
53
|
"""Type hinted Cruncher namedTuple"""
|
|
61
54
|
|
|
62
55
|
clazz: Callable[..., Any]
|
|
63
|
-
config_params:
|
|
56
|
+
config_params: list[str]
|
|
64
57
|
|
|
65
58
|
|
|
66
59
|
crunchers = {
|
|
@@ -87,19 +80,19 @@ def format_pydantic_error(e: pydanticValidationError) -> str:
|
|
|
87
80
|
|
|
88
81
|
def is_dict_str_any(var: Any) -> bool:
|
|
89
82
|
"""Verify whether the variable is of type dict[str, Any]"""
|
|
90
|
-
if isinstance(var,
|
|
83
|
+
if isinstance(var, dict):
|
|
91
84
|
return all(isinstance(k, str) for k in var.keys()) # type: ignore
|
|
92
85
|
return False
|
|
93
86
|
|
|
94
87
|
|
|
95
|
-
def str2list(v: Optional[str]) -> Optional[
|
|
88
|
+
def str2list(v: Optional[str]) -> Optional[list[str]]:
|
|
96
89
|
"""Convert string to list base on , delimiter."""
|
|
97
90
|
if v:
|
|
98
91
|
return v.split(",")
|
|
99
92
|
return None
|
|
100
93
|
|
|
101
94
|
|
|
102
|
-
def str2json(k: str, v: Optional[str] = None) -> Optional[
|
|
95
|
+
def str2json(k: str, v: Optional[str] = None) -> Optional[dict[str, Any]]:
|
|
103
96
|
"""decoding a URL parameter and then parsing it as JSON."""
|
|
104
97
|
if not v:
|
|
105
98
|
return None
|
|
@@ -109,25 +102,25 @@ def str2json(k: str, v: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
|
109
102
|
raise ValidationError(f"{k}: Incorrect JSON object") from e
|
|
110
103
|
|
|
111
104
|
|
|
112
|
-
def flatten_list(nested_list: Union[Any,
|
|
105
|
+
def flatten_list(nested_list: Union[Any, list[Any]]) -> list[Any]:
|
|
113
106
|
"""Flatten a nested list structure into a single list."""
|
|
114
107
|
if not isinstance(nested_list, list):
|
|
115
108
|
return [nested_list]
|
|
116
109
|
else:
|
|
117
|
-
flattened:
|
|
110
|
+
flattened: list[Any] = []
|
|
118
111
|
for element in nested_list:
|
|
119
112
|
flattened.extend(flatten_list(element))
|
|
120
113
|
return flattened
|
|
121
114
|
|
|
122
115
|
|
|
123
|
-
def list_to_str_list(input_list:
|
|
116
|
+
def list_to_str_list(input_list: list[Any]) -> list[str]:
|
|
124
117
|
"""Attempt to convert a list of any type to a list of strings."""
|
|
125
118
|
try:
|
|
126
119
|
# Try to convert each element to a string
|
|
127
120
|
return [str(element) for element in input_list]
|
|
128
121
|
except Exception as e:
|
|
129
122
|
# Raise an exception if any element cannot be converted
|
|
130
|
-
raise TypeError(f"Failed to convert to
|
|
123
|
+
raise TypeError(f"Failed to convert to list[str]: {e}") from e
|
|
131
124
|
|
|
132
125
|
|
|
133
126
|
def get_next_link(
|
|
@@ -135,7 +128,7 @@ def get_next_link(
|
|
|
135
128
|
search_request: SearchPostRequest,
|
|
136
129
|
total_results: Optional[int],
|
|
137
130
|
items_per_page: int,
|
|
138
|
-
) -> Optional[
|
|
131
|
+
) -> Optional[dict[str, Any]]:
|
|
139
132
|
"""Generate next link URL and body"""
|
|
140
133
|
body = search_request.model_dump(exclude_none=True)
|
|
141
134
|
if "bbox" in body:
|
|
@@ -156,7 +149,7 @@ def get_next_link(
|
|
|
156
149
|
params["page"] = str(page + 1)
|
|
157
150
|
url += f"?{urlencode(params)}"
|
|
158
151
|
|
|
159
|
-
next:
|
|
152
|
+
next: dict[str, Any] = {
|
|
160
153
|
"rel": "next",
|
|
161
154
|
"href": url,
|
|
162
155
|
"title": "Next page",
|
eodag/rest/utils/cql_evaluate.py
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
# See the License for the specific language governing permissions and
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
from datetime import datetime as dt
|
|
19
|
-
from typing import Any,
|
|
19
|
+
from typing import Any, Optional, Union
|
|
20
20
|
|
|
21
21
|
from pygeofilter import ast
|
|
22
22
|
from pygeofilter.backends.evaluator import Evaluator, handle
|
|
23
23
|
from pygeofilter.values import Geometry, Interval
|
|
24
24
|
|
|
25
|
-
simpleNode = Union[ast.Attribute, str, int, complex, float,
|
|
25
|
+
simpleNode = Union[ast.Attribute, str, int, complex, float, list[Any], tuple[Any, ...]]
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class EodagEvaluator(Evaluator):
|
|
@@ -36,7 +36,7 @@ class EodagEvaluator(Evaluator):
|
|
|
36
36
|
return node
|
|
37
37
|
|
|
38
38
|
@handle(Geometry)
|
|
39
|
-
def spatial(self, node: Geometry) ->
|
|
39
|
+
def spatial(self, node: Geometry) -> dict[str, Any]:
|
|
40
40
|
"""handle geometry"""
|
|
41
41
|
return node.geometry
|
|
42
42
|
|
|
@@ -46,7 +46,7 @@ class EodagEvaluator(Evaluator):
|
|
|
46
46
|
return node.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
47
47
|
|
|
48
48
|
@handle(Interval)
|
|
49
|
-
def interval(self, _, *interval: Any) ->
|
|
49
|
+
def interval(self, _, *interval: Any) -> list[Any]:
|
|
50
50
|
"""handle datetime interval"""
|
|
51
51
|
return list(interval)
|
|
52
52
|
|
|
@@ -60,7 +60,7 @@ class EodagEvaluator(Evaluator):
|
|
|
60
60
|
)
|
|
61
61
|
def predicate(
|
|
62
62
|
self, node: ast.Predicate, lhs: Any, rhs: Any
|
|
63
|
-
) -> Optional[
|
|
63
|
+
) -> Optional[dict[str, Any]]:
|
|
64
64
|
"""
|
|
65
65
|
Handle predicates
|
|
66
66
|
Verify the property is first attribute in each predicate
|
|
@@ -114,6 +114,6 @@ class EodagEvaluator(Evaluator):
|
|
|
114
114
|
return {lhs.name: list(rhs)}
|
|
115
115
|
|
|
116
116
|
@handle(ast.And)
|
|
117
|
-
def combination(self, _, lhs:
|
|
117
|
+
def combination(self, _, lhs: dict[str, str], rhs: dict[str, str]):
|
|
118
118
|
"""handle combinations"""
|
|
119
119
|
return {**lhs, **rhs}
|
eodag/rest/utils/rfc3339.py
CHANGED
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
# See the License for the specific language governing permissions and
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
import datetime
|
|
19
|
-
from typing import Optional
|
|
19
|
+
from typing import Optional
|
|
20
20
|
|
|
21
21
|
from eodag.utils.rest import rfc3339_str_to_datetime
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def str_to_interval(
|
|
25
25
|
interval: Optional[str],
|
|
26
|
-
) ->
|
|
26
|
+
) -> tuple[Optional[datetime.datetime], Optional[datetime.datetime]]:
|
|
27
27
|
"""Extract a tuple of datetimes from an interval string.
|
|
28
28
|
|
|
29
29
|
Interval strings are defined by
|