eodag 2.12.0__py3-none-any.whl → 3.0.0b1__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 +434 -319
- eodag/api/product/__init__.py +5 -1
- eodag/api/product/_assets.py +7 -2
- eodag/api/product/_product.py +46 -68
- eodag/api/product/metadata_mapping.py +181 -66
- eodag/api/search_result.py +21 -1
- eodag/cli.py +20 -6
- eodag/config.py +95 -6
- eodag/plugins/apis/base.py +8 -162
- eodag/plugins/apis/ecmwf.py +36 -24
- eodag/plugins/apis/usgs.py +40 -24
- eodag/plugins/authentication/aws_auth.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +13 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +256 -46
- eodag/plugins/authentication/qsauth.py +3 -0
- eodag/plugins/authentication/sas_auth.py +8 -1
- eodag/plugins/authentication/token.py +92 -46
- eodag/plugins/authentication/token_exchange.py +120 -0
- eodag/plugins/download/aws.py +86 -91
- eodag/plugins/download/base.py +72 -40
- eodag/plugins/download/http.py +607 -264
- eodag/plugins/download/s3rest.py +28 -15
- eodag/plugins/manager.py +73 -57
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +225 -18
- eodag/plugins/search/build_search_result.py +389 -32
- eodag/plugins/search/cop_marine.py +378 -0
- eodag/plugins/search/creodias_s3.py +15 -14
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +44 -20
- eodag/plugins/search/qssearch.py +508 -203
- eodag/plugins/search/static_stac_search.py +99 -36
- eodag/resources/constraints/climate-dt.json +13 -0
- eodag/resources/constraints/extremes-dt.json +8 -0
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1897 -34
- eodag/resources/providers.yml +3539 -3277
- eodag/resources/stac.yml +48 -54
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +51 -3
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +27 -0
- eodag/rest/core.py +757 -0
- eodag/rest/server.py +397 -258
- eodag/rest/stac.py +438 -307
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +232 -43
- eodag/rest/types/{stac_queryables.py → queryables.py} +81 -43
- eodag/rest/types/stac_search.py +277 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +65 -0
- eodag/types/__init__.py +99 -9
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +31 -0
- eodag/types/search_args.py +58 -7
- eodag/types/whoosh.py +81 -0
- eodag/utils/__init__.py +72 -9
- eodag/utils/constraints.py +37 -37
- eodag/utils/exceptions.py +23 -17
- eodag/utils/requests.py +138 -0
- eodag/utils/rest.py +104 -0
- eodag/utils/stac_reader.py +100 -16
- {eodag-2.12.0.dist-info → eodag-3.0.0b1.dist-info}/METADATA +64 -44
- eodag-3.0.0b1.dist-info/RECORD +109 -0
- {eodag-2.12.0.dist-info → eodag-3.0.0b1.dist-info}/WHEEL +1 -1
- {eodag-2.12.0.dist-info → eodag-3.0.0b1.dist-info}/entry_points.txt +6 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/utils.py +0 -1133
- eodag-2.12.0.dist-info/RECORD +0 -94
- {eodag-2.12.0.dist-info → eodag-3.0.0b1.dist-info}/LICENSE +0 -0
- {eodag-2.12.0.dist-info → eodag-3.0.0b1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2023, CS GROUP - France, https://www.csgroup.eu/
|
|
3
|
+
#
|
|
4
|
+
# This file is part of EODAG project
|
|
5
|
+
# https://www.github.com/CS-SI/EODAG
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
from typing import Any, Dict, Optional
|
|
19
|
+
|
|
20
|
+
from pydantic import (
|
|
21
|
+
BaseModel,
|
|
22
|
+
ConfigDict,
|
|
23
|
+
Field,
|
|
24
|
+
SerializerFunctionWrapHandler,
|
|
25
|
+
model_serializer,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from eodag.rest.types.eodag_search import EODAGSearch
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CollectionsSearchRequest(BaseModel):
|
|
32
|
+
"""Search args for GET collections"""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(frozen=True)
|
|
35
|
+
|
|
36
|
+
q: Optional[str] = Field(default=None, serialization_alias="free_text")
|
|
37
|
+
platform: Optional[str] = Field(default=None)
|
|
38
|
+
instrument: Optional[str] = Field(default=None)
|
|
39
|
+
constellation: Optional[str] = Field(default=None)
|
|
40
|
+
|
|
41
|
+
@model_serializer(mode="wrap")
|
|
42
|
+
def _serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]:
|
|
43
|
+
dumped: Dict[str, Any] = handler(self)
|
|
44
|
+
return {EODAGSearch.to_eodag(k): v for k, v in dumped.items()}
|
eodag/rest/types/eodag_search.py
CHANGED
|
@@ -15,16 +15,24 @@
|
|
|
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
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
|
|
19
21
|
|
|
20
22
|
from pydantic import (
|
|
23
|
+
AliasChoices,
|
|
24
|
+
AliasPath,
|
|
21
25
|
BaseModel,
|
|
22
26
|
ConfigDict,
|
|
23
27
|
Field,
|
|
28
|
+
ValidationError,
|
|
24
29
|
ValidationInfo,
|
|
25
30
|
field_validator,
|
|
26
31
|
model_validator,
|
|
27
32
|
)
|
|
33
|
+
from pydantic.alias_generators import to_camel, to_snake
|
|
34
|
+
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
35
|
+
from pygeofilter.parsers.cql2_json import parse as parse_json
|
|
28
36
|
from shapely.geometry import (
|
|
29
37
|
GeometryCollection,
|
|
30
38
|
LinearRing,
|
|
@@ -36,8 +44,16 @@ from shapely.geometry import (
|
|
|
36
44
|
Polygon,
|
|
37
45
|
)
|
|
38
46
|
|
|
47
|
+
from eodag.rest.utils import flatten_list, is_dict_str_any, list_to_str_list
|
|
48
|
+
from eodag.rest.utils.cql_evaluate import EodagEvaluator
|
|
39
49
|
from eodag.utils import DEFAULT_ITEMS_PER_PAGE
|
|
40
50
|
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
try:
|
|
53
|
+
from typing import Self
|
|
54
|
+
except ImportError:
|
|
55
|
+
from _typeshed import Self
|
|
56
|
+
|
|
41
57
|
Geometry = Union[
|
|
42
58
|
Dict[str, Any],
|
|
43
59
|
Point,
|
|
@@ -61,17 +77,25 @@ class EODAGSearch(BaseModel):
|
|
|
61
77
|
productType: Optional[str] = Field(None, alias="collections", validate_default=True)
|
|
62
78
|
provider: Optional[str] = Field(None)
|
|
63
79
|
ids: Optional[List[str]] = Field(None)
|
|
64
|
-
id: Optional[List[str]] = Field(
|
|
80
|
+
id: Optional[List[str]] = Field(
|
|
81
|
+
None, alias="ids"
|
|
82
|
+
) # TODO: remove when updating queryables
|
|
65
83
|
geom: Optional[Geometry] = Field(None, alias="geometry")
|
|
66
84
|
start: Optional[str] = Field(None, alias="start_datetime")
|
|
67
85
|
end: Optional[str] = Field(None, alias="end_datetime")
|
|
86
|
+
startTimeFromAscendingNode: Optional[str] = Field(
|
|
87
|
+
None,
|
|
88
|
+
alias="start_datetime",
|
|
89
|
+
validation_alias=AliasChoices("start_datetime", "datetime"),
|
|
90
|
+
)
|
|
91
|
+
completionTimeFromAscendingNode: Optional[str] = Field(None, alias="end_datetime")
|
|
68
92
|
publicationDate: Optional[str] = Field(None, alias="published")
|
|
69
93
|
creationDate: Optional[str] = Field(None, alias="created")
|
|
70
94
|
modificationDate: Optional[str] = Field(None, alias="updated")
|
|
71
95
|
platformSerialIdentifier: Optional[str] = Field(None, alias="platform")
|
|
72
96
|
instrument: Optional[str] = Field(None, alias="instruments")
|
|
73
97
|
platform: Optional[str] = Field(None, alias="constellation")
|
|
74
|
-
resolution: Optional[int] = Field(None, alias="gsd")
|
|
98
|
+
resolution: Optional[Union[int, str]] = Field(None, alias="gsd")
|
|
75
99
|
cloudCover: Optional[int] = Field(None, alias="eo:cloud_cover")
|
|
76
100
|
snowCover: Optional[int] = Field(None, alias="eo:snow_cover")
|
|
77
101
|
processingLevel: Optional[str] = Field(None, alias="processing:level")
|
|
@@ -83,7 +107,6 @@ class EODAGSearch(BaseModel):
|
|
|
83
107
|
polarizationChannels: Optional[List[str]] = Field(None, alias="sar:polarizations")
|
|
84
108
|
dopplerFrequency: Optional[str] = Field(None, alias="sar:frequency_band")
|
|
85
109
|
doi: Optional[str] = Field(None, alias="sci:doi")
|
|
86
|
-
productVersion: Optional[str] = Field(None, alias="version")
|
|
87
110
|
illuminationElevationAngle: Optional[float] = Field(
|
|
88
111
|
None, alias="view:sun_elevation"
|
|
89
112
|
)
|
|
@@ -91,32 +114,198 @@ class EODAGSearch(BaseModel):
|
|
|
91
114
|
page: Optional[int] = Field(1)
|
|
92
115
|
items_per_page: int = Field(DEFAULT_ITEMS_PER_PAGE, alias="limit")
|
|
93
116
|
sortBy: Optional[List[Tuple[str, str]]] = Field(None, alias="sortby")
|
|
117
|
+
raise_errors: bool = False
|
|
94
118
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
119
|
+
_to_eodag_map: Dict[str, str]
|
|
120
|
+
|
|
121
|
+
@model_validator(mode="after")
|
|
122
|
+
def set_raise_errors(self) -> Self:
|
|
123
|
+
"""Set raise_errors to True if provider is set"""
|
|
124
|
+
if self.provider:
|
|
125
|
+
self.raise_errors = True
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
@model_validator(mode="after")
|
|
129
|
+
def remove_timeFromAscendingNode(self) -> Self: # pylint: disable=invalid-name
|
|
130
|
+
"""TimeFromAscendingNode are just used for translation and not for search"""
|
|
131
|
+
self.startTimeFromAscendingNode = None # pylint: disable=invalid-name
|
|
132
|
+
self.completionTimeFromAscendingNode = None # pylint: disable=invalid-name
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
@model_validator(mode="after")
|
|
136
|
+
def parse_extra_fields(self) -> Self:
|
|
98
137
|
"""process unknown and oseo EODAG custom extensions fields"""
|
|
99
138
|
# Transform EODAG custom extensions OSEO and UNK.
|
|
139
|
+
if not self.__pydantic_extra__:
|
|
140
|
+
return self
|
|
141
|
+
|
|
100
142
|
keys_to_update: Dict[str, str] = {}
|
|
101
|
-
for key in
|
|
143
|
+
for key in self.__pydantic_extra__.keys():
|
|
102
144
|
if key.startswith("unk:"):
|
|
103
145
|
keys_to_update[key] = key[len("unk:") :]
|
|
104
146
|
elif key.startswith("oseo:"):
|
|
105
147
|
keys_to_update[key] = key[len("oseo:") :]
|
|
106
148
|
|
|
107
149
|
for old_key, new_key in keys_to_update.items():
|
|
108
|
-
|
|
150
|
+
self.__pydantic_extra__[
|
|
151
|
+
to_camel(to_snake(new_key))
|
|
152
|
+
] = self.__pydantic_extra__.pop(old_key)
|
|
109
153
|
|
|
110
|
-
return
|
|
154
|
+
return self
|
|
111
155
|
|
|
112
156
|
@model_validator(mode="before")
|
|
113
157
|
@classmethod
|
|
114
158
|
def remove_keys(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
115
159
|
"""Remove 'datetime', 'crunch', 'intersects', and 'bbox' keys"""
|
|
116
|
-
for key in ["datetime", "crunch", "intersects", "bbox"]:
|
|
160
|
+
for key in ["datetime", "crunch", "intersects", "bbox", "filter_lang"]:
|
|
117
161
|
values.pop(key, None)
|
|
118
162
|
return values
|
|
119
163
|
|
|
164
|
+
@model_validator(mode="before")
|
|
165
|
+
@classmethod
|
|
166
|
+
def parse_collections(
|
|
167
|
+
cls, values: Dict[str, Any], info: ValidationInfo
|
|
168
|
+
) -> Dict[str, Any]:
|
|
169
|
+
"""convert collections to productType"""
|
|
170
|
+
|
|
171
|
+
if collections := values.pop("collections", None):
|
|
172
|
+
if len(collections) > 1:
|
|
173
|
+
raise ValueError("Only one collection is supported per search")
|
|
174
|
+
values["productType"] = collections[0]
|
|
175
|
+
else:
|
|
176
|
+
if not getattr(info, "context", None) or not info.context.get( # type: ignore
|
|
177
|
+
"isCatalog"
|
|
178
|
+
):
|
|
179
|
+
raise ValueError("A collection is required")
|
|
180
|
+
|
|
181
|
+
return values
|
|
182
|
+
|
|
183
|
+
@model_validator(mode="before")
|
|
184
|
+
@classmethod
|
|
185
|
+
def parse_query(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
186
|
+
"""
|
|
187
|
+
Convert a STAC query parameter filter with the "eq" operator to a dict.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def add_error(error_message: str, input: Any) -> None:
|
|
191
|
+
errors.append(
|
|
192
|
+
InitErrorDetails(
|
|
193
|
+
type=PydanticCustomError("invalid_query", error_message), # type: ignore
|
|
194
|
+
loc=("query",),
|
|
195
|
+
input=input,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
query = values.pop("query", None)
|
|
200
|
+
if not query:
|
|
201
|
+
return values
|
|
202
|
+
|
|
203
|
+
query_props: Dict[str, Any] = {}
|
|
204
|
+
errors: List[InitErrorDetails] = []
|
|
205
|
+
for property_name, conditions in cast(Dict[str, Any], query).items():
|
|
206
|
+
# Remove the prefix "properties." if present
|
|
207
|
+
prop = property_name.replace("properties.", "", 1)
|
|
208
|
+
|
|
209
|
+
# Check if exactly one operator is specified per property
|
|
210
|
+
if not is_dict_str_any(conditions) or len(conditions) != 1: # type: ignore
|
|
211
|
+
add_error(
|
|
212
|
+
"Exactly 1 operator must be specified per property",
|
|
213
|
+
query[property_name],
|
|
214
|
+
)
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
# Retrieve the operator and its value
|
|
218
|
+
operator, value = next(iter(cast(Dict[str, Any], conditions).items()))
|
|
219
|
+
|
|
220
|
+
# Validate the operator
|
|
221
|
+
# only eq, in and lte are allowed
|
|
222
|
+
# lte is only supported with eo:cloud_cover
|
|
223
|
+
# eo:cloud_cover only accept lte operator
|
|
224
|
+
if (
|
|
225
|
+
operator not in ("eq", "lte", "in")
|
|
226
|
+
or (operator == "lte" and prop != "eo:cloud_cover")
|
|
227
|
+
or (prop == "eo:cloud_cover" and operator != "lte")
|
|
228
|
+
):
|
|
229
|
+
add_error(
|
|
230
|
+
f'operator "{operator}" is not supported for property "{prop}"',
|
|
231
|
+
query[property_name],
|
|
232
|
+
)
|
|
233
|
+
continue
|
|
234
|
+
if operator == "in" and not isinstance(value, list):
|
|
235
|
+
add_error(
|
|
236
|
+
f'operator "{operator}" requires a value of type list for property "{prop}"',
|
|
237
|
+
query[property_name],
|
|
238
|
+
)
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
query_props[prop] = value
|
|
242
|
+
|
|
243
|
+
if errors:
|
|
244
|
+
raise ValidationError.from_exception_data(
|
|
245
|
+
title=cls.__name__, line_errors=errors
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return {**values, **query_props}
|
|
249
|
+
|
|
250
|
+
@model_validator(mode="before")
|
|
251
|
+
@classmethod
|
|
252
|
+
def parse_cql(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
253
|
+
"""
|
|
254
|
+
Process cql2 filter
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def add_error(error_message: str) -> None:
|
|
258
|
+
errors.append(
|
|
259
|
+
InitErrorDetails(
|
|
260
|
+
type=PydanticCustomError("invalid_filter", error_message), # type: ignore
|
|
261
|
+
loc=("filter",),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
filter_ = values.pop("filter", None)
|
|
266
|
+
if not filter_:
|
|
267
|
+
return values
|
|
268
|
+
|
|
269
|
+
errors: List[InitErrorDetails] = []
|
|
270
|
+
try:
|
|
271
|
+
parsing_result = EodagEvaluator().evaluate(parse_json(filter_)) # type: ignore
|
|
272
|
+
except (ValueError, NotImplementedError) as e:
|
|
273
|
+
add_error(str(e))
|
|
274
|
+
raise ValidationError.from_exception_data(
|
|
275
|
+
title=cls.__name__, line_errors=errors
|
|
276
|
+
) from e
|
|
277
|
+
|
|
278
|
+
if not is_dict_str_any(parsing_result):
|
|
279
|
+
add_error("The parsed filter is not a proper dictionary")
|
|
280
|
+
raise ValidationError.from_exception_data(
|
|
281
|
+
title=cls.__name__, line_errors=errors
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
cql_args: Dict[str, Any] = cast(Dict[str, Any], parsing_result)
|
|
285
|
+
|
|
286
|
+
invalid_keys = {
|
|
287
|
+
"collections": 'Use "collection" instead of "collections"',
|
|
288
|
+
"ids": 'Use "id" instead of "ids"',
|
|
289
|
+
}
|
|
290
|
+
for k, m in invalid_keys.items():
|
|
291
|
+
if k in cql_args:
|
|
292
|
+
add_error(m)
|
|
293
|
+
|
|
294
|
+
if errors:
|
|
295
|
+
raise ValidationError.from_exception_data(
|
|
296
|
+
title=cls.__name__, line_errors=errors
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# convert collection to EODAG collections
|
|
300
|
+
if col := cql_args.pop("collection", None):
|
|
301
|
+
cql_args["collections"] = col if isinstance(col, list) else [col]
|
|
302
|
+
|
|
303
|
+
# convert id to EODAG ids
|
|
304
|
+
if id := cql_args.pop("id", None):
|
|
305
|
+
cql_args["ids"] = id if isinstance(id, list) else [id]
|
|
306
|
+
|
|
307
|
+
return {**values, **cql_args}
|
|
308
|
+
|
|
120
309
|
@field_validator("instrument", mode="before")
|
|
121
310
|
@classmethod
|
|
122
311
|
def join_instruments(cls, v: Union[str, List[str]]) -> str:
|
|
@@ -127,33 +316,27 @@ class EODAGSearch(BaseModel):
|
|
|
127
316
|
|
|
128
317
|
@field_validator("sortBy", mode="before")
|
|
129
318
|
@classmethod
|
|
130
|
-
def
|
|
319
|
+
def parse_sortby(
|
|
131
320
|
cls,
|
|
132
321
|
sortby_post_params: List[Dict[str, str]],
|
|
133
322
|
) -> List[Tuple[str, str]]:
|
|
134
323
|
"""
|
|
135
324
|
Convert STAC POST sortby to EODAG sortby
|
|
136
325
|
"""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
or not getattr(info, "context", None)
|
|
152
|
-
or not info.context.get("isCatalog") # type: ignore
|
|
153
|
-
):
|
|
154
|
-
raise ValueError("A collection is required")
|
|
155
|
-
|
|
156
|
-
return v
|
|
326
|
+
special_fields = {
|
|
327
|
+
"start": "startTimeFromAscendingNode",
|
|
328
|
+
"end": "completionTimeFromAscendingNode",
|
|
329
|
+
}
|
|
330
|
+
return [
|
|
331
|
+
(
|
|
332
|
+
special_fields.get(
|
|
333
|
+
to_camel(to_snake(cls.to_eodag(param["field"]))),
|
|
334
|
+
to_camel(to_snake(cls.to_eodag(param["field"]))),
|
|
335
|
+
),
|
|
336
|
+
param["direction"],
|
|
337
|
+
)
|
|
338
|
+
for param in sortby_post_params
|
|
339
|
+
]
|
|
157
340
|
|
|
158
341
|
@field_validator("start", "end")
|
|
159
342
|
@classmethod
|
|
@@ -164,21 +347,27 @@ class EODAGSearch(BaseModel):
|
|
|
164
347
|
return v
|
|
165
348
|
|
|
166
349
|
@classmethod
|
|
167
|
-
def
|
|
168
|
-
"""
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
350
|
+
def _create_to_eodag_map(cls) -> None:
|
|
351
|
+
"""Create mapping to convert fields from STAC to EODAG"""
|
|
352
|
+
cls._to_eodag_map = {}
|
|
353
|
+
for name, field_info in cls.model_fields.items():
|
|
354
|
+
if field_info.validation_alias:
|
|
355
|
+
if isinstance(field_info.validation_alias, (AliasChoices, AliasPath)):
|
|
356
|
+
for a in list_to_str_list(
|
|
357
|
+
flatten_list(field_info.validation_alias.convert_to_aliases())
|
|
358
|
+
):
|
|
359
|
+
cls._to_eodag_map[a] = name
|
|
360
|
+
else:
|
|
361
|
+
cls._to_eodag_map[field_info.validation_alias] = name
|
|
362
|
+
elif field_info.alias:
|
|
363
|
+
cls._to_eodag_map[field_info.alias] = name
|
|
172
364
|
|
|
173
365
|
@classmethod
|
|
174
366
|
def to_eodag(cls, value: str) -> str:
|
|
175
367
|
"""Convert a STAC parameter to its matching EODAG name"""
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if field_info.alias
|
|
180
|
-
}
|
|
181
|
-
return alias_map.get(value, value)
|
|
368
|
+
if not isinstance(cls._to_eodag_map, dict) or not cls._to_eodag_map:
|
|
369
|
+
cls._create_to_eodag_map()
|
|
370
|
+
return cls._to_eodag_map.get(value, value)
|
|
182
371
|
|
|
183
372
|
@classmethod
|
|
184
373
|
def to_stac(cls, field_name: str) -> str:
|
|
@@ -17,10 +17,20 @@
|
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
21
|
-
|
|
22
|
-
from pydantic import
|
|
23
|
-
|
|
20
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Union
|
|
21
|
+
|
|
22
|
+
from pydantic import (
|
|
23
|
+
BaseModel,
|
|
24
|
+
ConfigDict,
|
|
25
|
+
Field,
|
|
26
|
+
SerializationInfo,
|
|
27
|
+
SerializerFunctionWrapHandler,
|
|
28
|
+
computed_field,
|
|
29
|
+
model_serializer,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from eodag.rest.types.eodag_search import EODAGSearch
|
|
33
|
+
from eodag.rest.utils.rfc3339 import str_to_interval
|
|
24
34
|
from eodag.types import python_field_definition_to_json
|
|
25
35
|
from eodag.utils import Annotated
|
|
26
36
|
|
|
@@ -28,6 +38,34 @@ if TYPE_CHECKING:
|
|
|
28
38
|
from pydantic.fields import FieldInfo
|
|
29
39
|
|
|
30
40
|
|
|
41
|
+
class QueryablesGetParams(BaseModel):
|
|
42
|
+
"""Store GET Queryables query params"""
|
|
43
|
+
|
|
44
|
+
collection: Optional[str] = Field(default=None, serialization_alias="productType")
|
|
45
|
+
datetime: Optional[str] = Field(default=None)
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(extra="allow", frozen=True)
|
|
48
|
+
|
|
49
|
+
@model_serializer(mode="wrap")
|
|
50
|
+
def _serialize(self, handler: SerializerFunctionWrapHandler) -> Dict[str, Any]:
|
|
51
|
+
dumped: Dict[str, Any] = handler(self)
|
|
52
|
+
return {EODAGSearch.to_eodag(k): v for k, v in dumped.items()}
|
|
53
|
+
|
|
54
|
+
@computed_field
|
|
55
|
+
@property
|
|
56
|
+
def start_datetime(self) -> Optional[str]:
|
|
57
|
+
"""Extract start_datetime property from datetime"""
|
|
58
|
+
start = str_to_interval(self.datetime)[0]
|
|
59
|
+
return start.strftime("%Y-%m-%dT%H:%M:%SZ") if start else None
|
|
60
|
+
|
|
61
|
+
@computed_field
|
|
62
|
+
@property
|
|
63
|
+
def end_datetime(self) -> Optional[str]:
|
|
64
|
+
"""Extract end_datetime property from datetime"""
|
|
65
|
+
end = str_to_interval(self.datetime)[1]
|
|
66
|
+
return end.strftime("%Y-%m-%dT%H:%M:%SZ") if end else None
|
|
67
|
+
|
|
68
|
+
|
|
31
69
|
class StacQueryableProperty(BaseModel):
|
|
32
70
|
"""A class representing a queryable property.
|
|
33
71
|
|
|
@@ -44,6 +82,10 @@ class StacQueryableProperty(BaseModel):
|
|
|
44
82
|
type: Optional[Union[str, List[str]]] = None
|
|
45
83
|
enum: Optional[List[Any]] = None
|
|
46
84
|
value: Optional[Any] = None
|
|
85
|
+
min: Optional[Union[int, List[Union[int, None]]]] = None
|
|
86
|
+
max: Optional[Union[int, List[Union[int, None]]]] = None
|
|
87
|
+
oneOf: Optional[List[Any]] = None
|
|
88
|
+
items: Optional[Any] = None
|
|
47
89
|
|
|
48
90
|
@classmethod
|
|
49
91
|
def from_python_field_definition(
|
|
@@ -57,6 +99,16 @@ class StacQueryableProperty(BaseModel):
|
|
|
57
99
|
|
|
58
100
|
return cls(**def_dict)
|
|
59
101
|
|
|
102
|
+
@model_serializer(mode="wrap")
|
|
103
|
+
def remove_none(
|
|
104
|
+
self,
|
|
105
|
+
handler: SerializerFunctionWrapHandler,
|
|
106
|
+
_: SerializationInfo,
|
|
107
|
+
):
|
|
108
|
+
"""Remove none value property fields during serialization"""
|
|
109
|
+
props: Dict[str, Any] = handler(self)
|
|
110
|
+
return {k: v for k, v in props.items() if v is not None}
|
|
111
|
+
|
|
60
112
|
|
|
61
113
|
class StacQueryables(BaseModel):
|
|
62
114
|
"""A class representing queryable properties for the STAC API.
|
|
@@ -87,48 +139,34 @@ class StacQueryables(BaseModel):
|
|
|
87
139
|
description: str = Field(
|
|
88
140
|
default="Queryable names for the EODAG STAC API Item Search filter."
|
|
89
141
|
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
142
|
+
default_properties: ClassVar[Dict[str, StacQueryableProperty]] = {
|
|
143
|
+
"id": StacQueryableProperty(
|
|
144
|
+
description="ID",
|
|
145
|
+
ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/id",
|
|
146
|
+
),
|
|
147
|
+
"collection": StacQueryableProperty(
|
|
148
|
+
description="Collection",
|
|
149
|
+
ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection",
|
|
150
|
+
),
|
|
151
|
+
"geometry": StacQueryableProperty(
|
|
152
|
+
description="Geometry",
|
|
153
|
+
ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry",
|
|
154
|
+
),
|
|
155
|
+
"datetime": StacQueryableProperty(
|
|
156
|
+
description="Datetime - use parameters year, month, day, time instead if available",
|
|
157
|
+
ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
|
|
158
|
+
),
|
|
159
|
+
"bbox": StacQueryableProperty(
|
|
160
|
+
description="BBox",
|
|
161
|
+
type="array",
|
|
162
|
+
oneOf=[{"minItems": 4, "maxItems": 4}, {"minItems": 6, "maxItems": 6}],
|
|
163
|
+
items={"type": "number"},
|
|
164
|
+
),
|
|
165
|
+
}
|
|
166
|
+
properties: Dict[str, StacQueryableProperty] = Field()
|
|
110
167
|
additional_properties: bool = Field(
|
|
111
168
|
default=True, serialization_alias="additionalProperties"
|
|
112
169
|
)
|
|
113
170
|
|
|
114
|
-
def get_properties(self) -> Dict[str, StacQueryableProperty]:
|
|
115
|
-
"""Get the queryable properties.
|
|
116
|
-
|
|
117
|
-
:returns: A dictionary containing queryable properties.
|
|
118
|
-
:rtype: typing.Dict[str, StacQueryableProperty]
|
|
119
|
-
"""
|
|
120
|
-
properties = {}
|
|
121
|
-
for key, property in self.properties.items():
|
|
122
|
-
property = StacQueryableProperty(
|
|
123
|
-
description=property.description, type=property.type
|
|
124
|
-
)
|
|
125
|
-
properties[key] = property
|
|
126
|
-
return properties
|
|
127
|
-
|
|
128
171
|
def __contains__(self, name: str) -> bool:
|
|
129
172
|
return name in self.properties
|
|
130
|
-
|
|
131
|
-
def __setitem__(self, name: str, qprop: StacQueryableProperty) -> None:
|
|
132
|
-
# only keep "datetime" queryable for dates
|
|
133
|
-
if name not in ("start_datetime", "end_datetime"):
|
|
134
|
-
self.properties[name] = qprop
|