eodag 2.12.1__py3-none-any.whl → 3.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.
- eodag/__init__.py +6 -8
- eodag/api/core.py +654 -538
- eodag/api/product/__init__.py +12 -2
- eodag/api/product/_assets.py +59 -16
- eodag/api/product/_product.py +100 -93
- eodag/api/product/drivers/__init__.py +7 -2
- eodag/api/product/drivers/base.py +0 -3
- eodag/api/product/metadata_mapping.py +192 -96
- eodag/api/search_result.py +69 -10
- eodag/cli.py +55 -25
- eodag/config.py +391 -116
- eodag/plugins/apis/base.py +11 -168
- eodag/plugins/apis/ecmwf.py +36 -25
- eodag/plugins/apis/usgs.py +80 -35
- eodag/plugins/authentication/aws_auth.py +13 -4
- eodag/plugins/authentication/base.py +10 -1
- eodag/plugins/authentication/generic.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +17 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +268 -49
- eodag/plugins/authentication/qsauth.py +4 -1
- eodag/plugins/authentication/sas_auth.py +9 -2
- eodag/plugins/authentication/token.py +98 -47
- eodag/plugins/authentication/token_exchange.py +122 -0
- eodag/plugins/crunch/base.py +3 -1
- eodag/plugins/crunch/filter_date.py +3 -9
- eodag/plugins/crunch/filter_latest_intersect.py +0 -3
- eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
- eodag/plugins/crunch/filter_overlap.py +4 -8
- eodag/plugins/crunch/filter_property.py +5 -11
- eodag/plugins/download/aws.py +149 -185
- eodag/plugins/download/base.py +88 -97
- eodag/plugins/download/creodias_s3.py +1 -1
- eodag/plugins/download/http.py +638 -310
- eodag/plugins/download/s3rest.py +47 -45
- eodag/plugins/manager.py +228 -88
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +239 -30
- eodag/plugins/search/build_search_result.py +382 -37
- eodag/plugins/search/cop_marine.py +441 -0
- eodag/plugins/search/creodias_s3.py +25 -20
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +61 -30
- eodag/plugins/search/qssearch.py +713 -255
- eodag/plugins/search/static_stac_search.py +106 -40
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1921 -34
- eodag/resources/providers.yml +4091 -3655
- eodag/resources/stac.yml +50 -216
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +89 -32
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +26 -0
- eodag/rest/core.py +735 -0
- eodag/rest/errors.py +178 -0
- eodag/rest/server.py +264 -431
- eodag/rest/stac.py +442 -836
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +238 -47
- eodag/rest/types/queryables.py +164 -0
- eodag/rest/types/stac_search.py +273 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +64 -0
- eodag/types/__init__.py +106 -10
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +40 -0
- eodag/types/search_args.py +57 -7
- eodag/types/whoosh.py +79 -0
- eodag/utils/__init__.py +110 -91
- eodag/utils/constraints.py +37 -45
- eodag/utils/exceptions.py +39 -22
- eodag/utils/import_system.py +0 -4
- eodag/utils/logging.py +37 -80
- eodag/utils/notebook.py +4 -4
- eodag/utils/repr.py +113 -0
- eodag/utils/requests.py +128 -0
- eodag/utils/rest.py +100 -0
- eodag/utils/stac_reader.py +93 -21
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
- eodag-3.0.0.dist-info/RECORD +109 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/types/stac_queryables.py +0 -134
- eodag/rest/utils.py +0 -1133
- eodag-2.12.1.dist-info/RECORD +0 -94
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0.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,13 @@ 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
|
+
from typing_extensions import Self
|
|
53
|
+
|
|
41
54
|
Geometry = Union[
|
|
42
55
|
Dict[str, Any],
|
|
43
56
|
Point,
|
|
@@ -61,17 +74,25 @@ class EODAGSearch(BaseModel):
|
|
|
61
74
|
productType: Optional[str] = Field(None, alias="collections", validate_default=True)
|
|
62
75
|
provider: Optional[str] = Field(None)
|
|
63
76
|
ids: Optional[List[str]] = Field(None)
|
|
64
|
-
id: Optional[List[str]] = Field(
|
|
77
|
+
id: Optional[List[str]] = Field(
|
|
78
|
+
None, alias="ids"
|
|
79
|
+
) # TODO: remove when updating queryables
|
|
65
80
|
geom: Optional[Geometry] = Field(None, alias="geometry")
|
|
66
81
|
start: Optional[str] = Field(None, alias="start_datetime")
|
|
67
82
|
end: Optional[str] = Field(None, alias="end_datetime")
|
|
83
|
+
startTimeFromAscendingNode: Optional[str] = Field(
|
|
84
|
+
None,
|
|
85
|
+
alias="start_datetime",
|
|
86
|
+
validation_alias=AliasChoices("start_datetime", "datetime"),
|
|
87
|
+
)
|
|
88
|
+
completionTimeFromAscendingNode: Optional[str] = Field(None, alias="end_datetime")
|
|
68
89
|
publicationDate: Optional[str] = Field(None, alias="published")
|
|
69
90
|
creationDate: Optional[str] = Field(None, alias="created")
|
|
70
91
|
modificationDate: Optional[str] = Field(None, alias="updated")
|
|
71
92
|
platformSerialIdentifier: Optional[str] = Field(None, alias="platform")
|
|
72
93
|
instrument: Optional[str] = Field(None, alias="instruments")
|
|
73
94
|
platform: Optional[str] = Field(None, alias="constellation")
|
|
74
|
-
resolution: Optional[int] = Field(None, alias="gsd")
|
|
95
|
+
resolution: Optional[Union[int, str]] = Field(None, alias="gsd")
|
|
75
96
|
cloudCover: Optional[int] = Field(None, alias="eo:cloud_cover")
|
|
76
97
|
snowCover: Optional[int] = Field(None, alias="eo:snow_cover")
|
|
77
98
|
processingLevel: Optional[str] = Field(None, alias="processing:level")
|
|
@@ -83,40 +104,198 @@ class EODAGSearch(BaseModel):
|
|
|
83
104
|
polarizationChannels: Optional[List[str]] = Field(None, alias="sar:polarizations")
|
|
84
105
|
dopplerFrequency: Optional[str] = Field(None, alias="sar:frequency_band")
|
|
85
106
|
doi: Optional[str] = Field(None, alias="sci:doi")
|
|
86
|
-
productVersion: Optional[str] = Field(None, alias="version")
|
|
87
107
|
illuminationElevationAngle: Optional[float] = Field(
|
|
88
108
|
None, alias="view:sun_elevation"
|
|
89
109
|
)
|
|
90
110
|
illuminationAzimuthAngle: Optional[float] = Field(None, alias="view:sun_azimuth")
|
|
91
111
|
page: Optional[int] = Field(1)
|
|
92
112
|
items_per_page: int = Field(DEFAULT_ITEMS_PER_PAGE, alias="limit")
|
|
93
|
-
|
|
113
|
+
sort_by: Optional[List[Tuple[str, str]]] = Field(None, alias="sortby")
|
|
114
|
+
raise_errors: bool = False
|
|
94
115
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
_to_eodag_map: Dict[str, str]
|
|
117
|
+
|
|
118
|
+
@model_validator(mode="after")
|
|
119
|
+
def remove_timeFromAscendingNode(self) -> Self: # pylint: disable=invalid-name
|
|
120
|
+
"""TimeFromAscendingNode are just used for translation and not for search"""
|
|
121
|
+
self.startTimeFromAscendingNode = None # pylint: disable=invalid-name
|
|
122
|
+
self.completionTimeFromAscendingNode = None # pylint: disable=invalid-name
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
@model_validator(mode="after")
|
|
126
|
+
def parse_extra_fields(self) -> Self:
|
|
98
127
|
"""process unknown and oseo EODAG custom extensions fields"""
|
|
99
128
|
# Transform EODAG custom extensions OSEO and UNK.
|
|
129
|
+
if not self.__pydantic_extra__:
|
|
130
|
+
return self
|
|
131
|
+
|
|
100
132
|
keys_to_update: Dict[str, str] = {}
|
|
101
|
-
for key in
|
|
133
|
+
for key in self.__pydantic_extra__.keys():
|
|
102
134
|
if key.startswith("unk:"):
|
|
103
135
|
keys_to_update[key] = key[len("unk:") :]
|
|
104
136
|
elif key.startswith("oseo:"):
|
|
105
137
|
keys_to_update[key] = key[len("oseo:") :]
|
|
106
138
|
|
|
107
139
|
for old_key, new_key in keys_to_update.items():
|
|
108
|
-
|
|
140
|
+
self.__pydantic_extra__[
|
|
141
|
+
to_camel(to_snake(new_key))
|
|
142
|
+
] = self.__pydantic_extra__.pop(old_key)
|
|
109
143
|
|
|
110
|
-
return
|
|
144
|
+
return self
|
|
111
145
|
|
|
112
146
|
@model_validator(mode="before")
|
|
113
147
|
@classmethod
|
|
114
148
|
def remove_keys(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
115
149
|
"""Remove 'datetime', 'crunch', 'intersects', and 'bbox' keys"""
|
|
116
|
-
for key in ["datetime", "crunch", "intersects", "bbox"]:
|
|
150
|
+
for key in ["datetime", "crunch", "intersects", "bbox", "filter_lang"]:
|
|
117
151
|
values.pop(key, None)
|
|
118
152
|
return values
|
|
119
153
|
|
|
154
|
+
@model_validator(mode="before")
|
|
155
|
+
@classmethod
|
|
156
|
+
def parse_collections(
|
|
157
|
+
cls, values: Dict[str, Any], info: ValidationInfo
|
|
158
|
+
) -> Dict[str, Any]:
|
|
159
|
+
"""convert collections to productType"""
|
|
160
|
+
|
|
161
|
+
if collections := values.pop("collections", None):
|
|
162
|
+
if len(collections) > 1:
|
|
163
|
+
raise ValueError("Only one collection is supported per search")
|
|
164
|
+
values["productType"] = collections[0]
|
|
165
|
+
else:
|
|
166
|
+
if not getattr(info, "context", None) or not info.context.get( # type: ignore
|
|
167
|
+
"isCatalog"
|
|
168
|
+
):
|
|
169
|
+
raise ValueError("A collection is required")
|
|
170
|
+
|
|
171
|
+
return values
|
|
172
|
+
|
|
173
|
+
@model_validator(mode="before")
|
|
174
|
+
@classmethod
|
|
175
|
+
def parse_query(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
176
|
+
"""
|
|
177
|
+
Convert a STAC query parameter filter with the "eq" operator to a dict.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def add_error(error_message: str, input: Any) -> None:
|
|
181
|
+
errors.append(
|
|
182
|
+
InitErrorDetails(
|
|
183
|
+
type=PydanticCustomError("invalid_query", error_message), # type: ignore
|
|
184
|
+
loc=("query",),
|
|
185
|
+
input=input,
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
query = values.pop("query", None)
|
|
190
|
+
if not query:
|
|
191
|
+
return values
|
|
192
|
+
|
|
193
|
+
query_props: Dict[str, Any] = {}
|
|
194
|
+
errors: List[InitErrorDetails] = []
|
|
195
|
+
for property_name, conditions in cast(Dict[str, Any], query).items():
|
|
196
|
+
# Remove the prefix "properties." if present
|
|
197
|
+
prop = property_name.replace("properties.", "", 1)
|
|
198
|
+
|
|
199
|
+
# Check if exactly one operator is specified per property
|
|
200
|
+
if not is_dict_str_any(conditions) or len(conditions) != 1: # type: ignore
|
|
201
|
+
add_error(
|
|
202
|
+
"Exactly 1 operator must be specified per property",
|
|
203
|
+
query[property_name],
|
|
204
|
+
)
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
# Retrieve the operator and its value
|
|
208
|
+
operator, value = next(iter(cast(Dict[str, Any], conditions).items()))
|
|
209
|
+
|
|
210
|
+
# Validate the operator
|
|
211
|
+
# only eq, in and lte are allowed
|
|
212
|
+
# lte is only supported with eo:cloud_cover
|
|
213
|
+
# eo:cloud_cover only accept lte operator
|
|
214
|
+
if (
|
|
215
|
+
operator not in ("eq", "lte", "in")
|
|
216
|
+
or (operator == "lte" and prop != "eo:cloud_cover")
|
|
217
|
+
or (prop == "eo:cloud_cover" and operator != "lte")
|
|
218
|
+
):
|
|
219
|
+
add_error(
|
|
220
|
+
f'operator "{operator}" is not supported for property "{prop}"',
|
|
221
|
+
query[property_name],
|
|
222
|
+
)
|
|
223
|
+
continue
|
|
224
|
+
if operator == "in" and not isinstance(value, list):
|
|
225
|
+
add_error(
|
|
226
|
+
f'operator "{operator}" requires a value of type list for property "{prop}"',
|
|
227
|
+
query[property_name],
|
|
228
|
+
)
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
query_props[prop] = value
|
|
232
|
+
|
|
233
|
+
if errors:
|
|
234
|
+
raise ValidationError.from_exception_data(
|
|
235
|
+
title=cls.__name__, line_errors=errors
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return {**values, **query_props}
|
|
239
|
+
|
|
240
|
+
@model_validator(mode="before")
|
|
241
|
+
@classmethod
|
|
242
|
+
def parse_cql(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
243
|
+
"""
|
|
244
|
+
Process cql2 filter
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
def add_error(error_message: str) -> None:
|
|
248
|
+
errors.append(
|
|
249
|
+
InitErrorDetails(
|
|
250
|
+
type=PydanticCustomError("invalid_filter", error_message), # type: ignore
|
|
251
|
+
loc=("filter",),
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
filter_ = values.pop("filter", None)
|
|
256
|
+
if not filter_:
|
|
257
|
+
return values
|
|
258
|
+
|
|
259
|
+
errors: List[InitErrorDetails] = []
|
|
260
|
+
try:
|
|
261
|
+
parsing_result = EodagEvaluator().evaluate(parse_json(filter_)) # type: ignore
|
|
262
|
+
except (ValueError, NotImplementedError) as e:
|
|
263
|
+
add_error(str(e))
|
|
264
|
+
raise ValidationError.from_exception_data(
|
|
265
|
+
title=cls.__name__, line_errors=errors
|
|
266
|
+
) from e
|
|
267
|
+
|
|
268
|
+
if not is_dict_str_any(parsing_result):
|
|
269
|
+
add_error("The parsed filter is not a proper dictionary")
|
|
270
|
+
raise ValidationError.from_exception_data(
|
|
271
|
+
title=cls.__name__, line_errors=errors
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
cql_args: Dict[str, Any] = cast(Dict[str, Any], parsing_result)
|
|
275
|
+
|
|
276
|
+
invalid_keys = {
|
|
277
|
+
"collections": 'Use "collection" instead of "collections"',
|
|
278
|
+
"ids": 'Use "id" instead of "ids"',
|
|
279
|
+
}
|
|
280
|
+
for k, m in invalid_keys.items():
|
|
281
|
+
if k in cql_args:
|
|
282
|
+
add_error(m)
|
|
283
|
+
|
|
284
|
+
if errors:
|
|
285
|
+
raise ValidationError.from_exception_data(
|
|
286
|
+
title=cls.__name__, line_errors=errors
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# convert collection to EODAG collections
|
|
290
|
+
if col := cql_args.pop("collection", None):
|
|
291
|
+
cql_args["collections"] = col if isinstance(col, list) else [col]
|
|
292
|
+
|
|
293
|
+
# convert id to EODAG ids
|
|
294
|
+
if id := cql_args.pop("id", None):
|
|
295
|
+
cql_args["ids"] = id if isinstance(id, list) else [id]
|
|
296
|
+
|
|
297
|
+
return {**values, **cql_args}
|
|
298
|
+
|
|
120
299
|
@field_validator("instrument", mode="before")
|
|
121
300
|
@classmethod
|
|
122
301
|
def join_instruments(cls, v: Union[str, List[str]]) -> str:
|
|
@@ -125,35 +304,29 @@ class EODAGSearch(BaseModel):
|
|
|
125
304
|
return ",".join(v)
|
|
126
305
|
return v
|
|
127
306
|
|
|
128
|
-
@field_validator("
|
|
307
|
+
@field_validator("sort_by", mode="before")
|
|
129
308
|
@classmethod
|
|
130
|
-
def
|
|
309
|
+
def parse_sortby(
|
|
131
310
|
cls,
|
|
132
311
|
sortby_post_params: List[Dict[str, str]],
|
|
133
312
|
) -> List[Tuple[str, str]]:
|
|
134
313
|
"""
|
|
135
|
-
Convert STAC POST sortby to EODAG
|
|
314
|
+
Convert STAC POST sortby to EODAG sort_by
|
|
136
315
|
"""
|
|
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
|
|
316
|
+
special_fields = {
|
|
317
|
+
"start": "startTimeFromAscendingNode",
|
|
318
|
+
"end": "completionTimeFromAscendingNode",
|
|
319
|
+
}
|
|
320
|
+
return [
|
|
321
|
+
(
|
|
322
|
+
special_fields.get(
|
|
323
|
+
to_camel(to_snake(cls.to_eodag(param["field"]))),
|
|
324
|
+
to_camel(to_snake(cls.to_eodag(param["field"]))),
|
|
325
|
+
),
|
|
326
|
+
param["direction"],
|
|
327
|
+
)
|
|
328
|
+
for param in sortby_post_params
|
|
329
|
+
]
|
|
157
330
|
|
|
158
331
|
@field_validator("start", "end")
|
|
159
332
|
@classmethod
|
|
@@ -164,26 +337,44 @@ class EODAGSearch(BaseModel):
|
|
|
164
337
|
return v
|
|
165
338
|
|
|
166
339
|
@classmethod
|
|
167
|
-
def
|
|
168
|
-
"""
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
340
|
+
def _create_to_eodag_map(cls) -> None:
|
|
341
|
+
"""Create mapping to convert fields from STAC to EODAG"""
|
|
342
|
+
cls._to_eodag_map = {}
|
|
343
|
+
for name, field_info in cls.model_fields.items():
|
|
344
|
+
if field_info.validation_alias:
|
|
345
|
+
if isinstance(field_info.validation_alias, (AliasChoices, AliasPath)):
|
|
346
|
+
for a in list_to_str_list(
|
|
347
|
+
flatten_list(field_info.validation_alias.convert_to_aliases())
|
|
348
|
+
):
|
|
349
|
+
cls._to_eodag_map[a] = name
|
|
350
|
+
else:
|
|
351
|
+
cls._to_eodag_map[field_info.validation_alias] = name
|
|
352
|
+
elif field_info.alias:
|
|
353
|
+
cls._to_eodag_map[field_info.alias] = name
|
|
172
354
|
|
|
173
355
|
@classmethod
|
|
174
356
|
def to_eodag(cls, value: str) -> str:
|
|
175
357
|
"""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)
|
|
358
|
+
if not isinstance(cls._to_eodag_map, dict) or not cls._to_eodag_map:
|
|
359
|
+
cls._create_to_eodag_map()
|
|
360
|
+
return cls._to_eodag_map.get(value, value)
|
|
182
361
|
|
|
183
362
|
@classmethod
|
|
184
|
-
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:
|
|
185
369
|
"""Get the alias of a field in a Pydantic model"""
|
|
186
370
|
field = cls.model_fields.get(field_name)
|
|
187
371
|
if field is not None and field.alias is not None:
|
|
188
372
|
return field.alias
|
|
373
|
+
if (
|
|
374
|
+
provider
|
|
375
|
+
and ":" not in field_name
|
|
376
|
+
and stac_item_properties
|
|
377
|
+
and field_name not in stac_item_properties
|
|
378
|
+
):
|
|
379
|
+
return f"{provider}:{field_name}"
|
|
189
380
|
return field_name
|
|
@@ -0,0 +1,164 @@
|
|
|
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 __future__ import annotations
|
|
19
|
+
|
|
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
|
|
34
|
+
from eodag.types import python_field_definition_to_json
|
|
35
|
+
from eodag.utils import Annotated
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from pydantic.fields import FieldInfo
|
|
39
|
+
|
|
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
|
+
# use [prop-decorator] mypy error code when mypy==1.12 is released
|
|
55
|
+
@computed_field # type: ignore[misc]
|
|
56
|
+
@property
|
|
57
|
+
def start_datetime(self) -> Optional[str]:
|
|
58
|
+
"""Extract start_datetime property from datetime"""
|
|
59
|
+
start = str_to_interval(self.datetime)[0]
|
|
60
|
+
return start.strftime("%Y-%m-%dT%H:%M:%SZ") if start else None
|
|
61
|
+
|
|
62
|
+
# use [prop-decorator] mypy error code when mypy==1.12 is released
|
|
63
|
+
@computed_field # type: ignore[misc]
|
|
64
|
+
@property
|
|
65
|
+
def end_datetime(self) -> Optional[str]:
|
|
66
|
+
"""Extract end_datetime property from datetime"""
|
|
67
|
+
end = str_to_interval(self.datetime)[1]
|
|
68
|
+
return end.strftime("%Y-%m-%dT%H:%M:%SZ") if end else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class StacQueryableProperty(BaseModel):
|
|
72
|
+
"""A class representing a queryable property.
|
|
73
|
+
|
|
74
|
+
:param description: The description of the queryables property
|
|
75
|
+
:param ref: (optional) A reference link to the schema of the property.
|
|
76
|
+
:param type: (optional) possible types of the property
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
description: str
|
|
80
|
+
ref: Optional[str] = Field(default=None, serialization_alias="$ref")
|
|
81
|
+
type: Optional[Union[str, List[str]]] = None
|
|
82
|
+
enum: Optional[List[Any]] = None
|
|
83
|
+
value: Optional[Any] = None
|
|
84
|
+
min: Optional[Union[int, List[Union[int, None]]]] = None
|
|
85
|
+
max: Optional[Union[int, List[Union[int, None]]]] = None
|
|
86
|
+
oneOf: Optional[List[Any]] = None
|
|
87
|
+
items: Optional[Any] = None
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_python_field_definition(
|
|
91
|
+
cls, id: str, python_field_definition: Annotated[Any, FieldInfo]
|
|
92
|
+
) -> StacQueryableProperty:
|
|
93
|
+
"""Build Model from python_field_definition"""
|
|
94
|
+
def_dict = python_field_definition_to_json(python_field_definition)
|
|
95
|
+
|
|
96
|
+
if not def_dict.get("description", None):
|
|
97
|
+
def_dict["description"] = def_dict.get("title", None) or id
|
|
98
|
+
|
|
99
|
+
return cls(**def_dict)
|
|
100
|
+
|
|
101
|
+
@model_serializer(mode="wrap")
|
|
102
|
+
def remove_none(
|
|
103
|
+
self,
|
|
104
|
+
handler: SerializerFunctionWrapHandler,
|
|
105
|
+
_: SerializationInfo,
|
|
106
|
+
):
|
|
107
|
+
"""Remove none value property fields during serialization"""
|
|
108
|
+
props: Dict[str, Any] = handler(self)
|
|
109
|
+
return {k: v for k, v in props.items() if v is not None}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class StacQueryables(BaseModel):
|
|
113
|
+
"""A class representing queryable properties for the STAC API.
|
|
114
|
+
|
|
115
|
+
:param json_schema: The URL of the JSON schema.
|
|
116
|
+
:param q_id: (optional) The identifier of the queryables.
|
|
117
|
+
:param q_type: The type of the object.
|
|
118
|
+
:param title: The title of the queryables.
|
|
119
|
+
:param description: The description of the queryables
|
|
120
|
+
:param properties: A dictionary of queryable properties.
|
|
121
|
+
:param additional_properties: Whether additional properties are allowed.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
json_schema: str = Field(
|
|
125
|
+
default="https://json-schema.org/draft/2019-09/schema",
|
|
126
|
+
serialization_alias="$schema",
|
|
127
|
+
)
|
|
128
|
+
q_id: Optional[str] = Field(default=None, serialization_alias="$id")
|
|
129
|
+
q_type: str = Field(default="object", serialization_alias="type")
|
|
130
|
+
title: str = Field(default="Queryables for EODAG STAC API")
|
|
131
|
+
description: str = Field(
|
|
132
|
+
default="Queryable names for the EODAG STAC API Item Search filter."
|
|
133
|
+
)
|
|
134
|
+
default_properties: ClassVar[Dict[str, StacQueryableProperty]] = {
|
|
135
|
+
"id": StacQueryableProperty(
|
|
136
|
+
description="ID",
|
|
137
|
+
ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/id",
|
|
138
|
+
),
|
|
139
|
+
"collection": StacQueryableProperty(
|
|
140
|
+
description="Collection",
|
|
141
|
+
ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection",
|
|
142
|
+
),
|
|
143
|
+
"geometry": StacQueryableProperty(
|
|
144
|
+
description="Geometry",
|
|
145
|
+
ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry",
|
|
146
|
+
),
|
|
147
|
+
"datetime": StacQueryableProperty(
|
|
148
|
+
description="Datetime - use parameters year, month, day, time instead if available",
|
|
149
|
+
ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
|
|
150
|
+
),
|
|
151
|
+
"bbox": StacQueryableProperty(
|
|
152
|
+
description="BBox",
|
|
153
|
+
type="array",
|
|
154
|
+
oneOf=[{"minItems": 4, "maxItems": 4}, {"minItems": 6, "maxItems": 6}],
|
|
155
|
+
items={"type": "number"},
|
|
156
|
+
),
|
|
157
|
+
}
|
|
158
|
+
properties: Dict[str, StacQueryableProperty] = Field()
|
|
159
|
+
additional_properties: bool = Field(
|
|
160
|
+
default=True, serialization_alias="additionalProperties"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def __contains__(self, name: str) -> bool:
|
|
164
|
+
return name in self.properties
|