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
eodag/plugins/search/base.py
CHANGED
|
@@ -18,24 +18,37 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
-
from typing import TYPE_CHECKING
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
22
|
|
|
23
|
+
import orjson
|
|
23
24
|
from pydantic.fields import Field, FieldInfo
|
|
24
25
|
|
|
25
26
|
from eodag.api.product.metadata_mapping import (
|
|
26
27
|
DEFAULT_METADATA_MAPPING,
|
|
28
|
+
NOT_MAPPED,
|
|
27
29
|
mtd_cfg_as_conversion_and_querypath,
|
|
28
30
|
)
|
|
29
31
|
from eodag.plugins.base import PluginTopic
|
|
32
|
+
from eodag.plugins.search import PreparedSearch
|
|
33
|
+
from eodag.types import model_fields_to_annotated
|
|
34
|
+
from eodag.types.queryables import Queryables
|
|
35
|
+
from eodag.types.search_args import SortByList
|
|
30
36
|
from eodag.utils import (
|
|
31
|
-
DEFAULT_ITEMS_PER_PAGE,
|
|
32
|
-
DEFAULT_PAGE,
|
|
33
37
|
GENERIC_PRODUCT_TYPE,
|
|
34
38
|
Annotated,
|
|
39
|
+
copy_deepcopy,
|
|
40
|
+
deepcopy,
|
|
35
41
|
format_dict_items,
|
|
42
|
+
get_args,
|
|
43
|
+
update_nested_dict,
|
|
36
44
|
)
|
|
45
|
+
from eodag.utils.exceptions import ValidationError
|
|
37
46
|
|
|
38
47
|
if TYPE_CHECKING:
|
|
48
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
49
|
+
|
|
50
|
+
from requests.auth import AuthBase
|
|
51
|
+
|
|
39
52
|
from eodag.api.product import EOProduct
|
|
40
53
|
from eodag.config import PluginConfig
|
|
41
54
|
|
|
@@ -51,11 +64,18 @@ class Search(PluginTopic):
|
|
|
51
64
|
:type config: :class:`~eodag.config.PluginConfig`
|
|
52
65
|
"""
|
|
53
66
|
|
|
67
|
+
auth: Union[AuthBase, Dict[str, str]]
|
|
68
|
+
next_page_url: Optional[str]
|
|
69
|
+
next_page_query_obj: Optional[Dict[str, Any]]
|
|
70
|
+
total_items_nb: int
|
|
71
|
+
need_count: bool
|
|
72
|
+
_request: Any # needed by deprecated load_stac_items
|
|
73
|
+
|
|
54
74
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
55
75
|
super(Search, self).__init__(provider, config)
|
|
56
76
|
# Prepare the metadata mapping
|
|
57
77
|
# Do a shallow copy, the structure is flat enough for this to be sufficient
|
|
58
|
-
metas = DEFAULT_METADATA_MAPPING.copy()
|
|
78
|
+
metas: Dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
|
|
59
79
|
# Update the defaults with the mapping value. This will add any new key
|
|
60
80
|
# added by the provider mapping that is not in the default metadata
|
|
61
81
|
if self.config.metadata_mapping:
|
|
@@ -72,21 +92,18 @@ class Search(PluginTopic):
|
|
|
72
92
|
|
|
73
93
|
def query(
|
|
74
94
|
self,
|
|
75
|
-
|
|
76
|
-
items_per_page: int = DEFAULT_ITEMS_PER_PAGE,
|
|
77
|
-
page: int = DEFAULT_PAGE,
|
|
78
|
-
count: bool = True,
|
|
95
|
+
prep: PreparedSearch = PreparedSearch(),
|
|
79
96
|
**kwargs: Any,
|
|
80
97
|
) -> Tuple[List[EOProduct], Optional[int]]:
|
|
81
98
|
"""Implementation of how the products must be searched goes here.
|
|
82
99
|
|
|
83
100
|
This method must return a tuple with (1) a list of EOProduct instances (see eodag.api.product module)
|
|
84
101
|
which will be processed by a Download plugin (2) and the total number of products matching
|
|
85
|
-
the search criteria. If ``count`` is False, the second element returned must be ``None``.
|
|
102
|
+
the search criteria. If ``prep.count`` is False, the second element returned must be ``None``.
|
|
86
103
|
"""
|
|
87
104
|
raise NotImplementedError("A Search plugin must implement a method named query")
|
|
88
105
|
|
|
89
|
-
def discover_product_types(self) -> Optional[Dict[str, Any]]:
|
|
106
|
+
def discover_product_types(self, **kwargs: Any) -> Optional[Dict[str, Any]]:
|
|
90
107
|
"""Fetch product types list from provider using `discover_product_types` conf"""
|
|
91
108
|
return None
|
|
92
109
|
|
|
@@ -101,21 +118,25 @@ class Search(PluginTopic):
|
|
|
101
118
|
:returns: fetched queryable parameters dict
|
|
102
119
|
:rtype: Optional[Dict[str, Annotated[Any, FieldInfo]]]
|
|
103
120
|
"""
|
|
104
|
-
|
|
121
|
+
raise NotImplementedError(
|
|
122
|
+
f"discover_queryables is not implemeted for plugin {self.__class__.__name__}"
|
|
123
|
+
)
|
|
105
124
|
|
|
106
|
-
def
|
|
125
|
+
def _get_defaults_as_queryables(
|
|
107
126
|
self, product_type: str
|
|
108
127
|
) -> Dict[str, Annotated[Any, FieldInfo]]:
|
|
109
128
|
"""
|
|
110
|
-
Return given product type
|
|
129
|
+
Return given product type default settings as queryables
|
|
111
130
|
|
|
112
131
|
:param product_type: given product type
|
|
113
132
|
:type product_type: str
|
|
114
133
|
:returns: queryable parameters dict
|
|
115
134
|
:rtype: Dict[str, Annotated[Any, FieldInfo]]
|
|
116
135
|
"""
|
|
117
|
-
defaults = self.config.products.get(product_type, {})
|
|
118
|
-
|
|
136
|
+
defaults = deepcopy(self.config.products.get(product_type, {}))
|
|
137
|
+
defaults.pop("metadata_mapping", None)
|
|
138
|
+
|
|
139
|
+
queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
|
|
119
140
|
for parameter, value in defaults.items():
|
|
120
141
|
queryables[parameter] = Annotated[type(value), Field(default=value)]
|
|
121
142
|
return queryables
|
|
@@ -170,7 +191,7 @@ class Search(PluginTopic):
|
|
|
170
191
|
|
|
171
192
|
def get_metadata_mapping(
|
|
172
193
|
self, product_type: Optional[str] = None
|
|
173
|
-
) -> Dict[str, str]:
|
|
194
|
+
) -> Dict[str, Union[str, List[str]]]:
|
|
174
195
|
"""Get the plugin metadata mapping configuration (product type specific if exists)
|
|
175
196
|
|
|
176
197
|
:param product_type: the desired product type
|
|
@@ -178,6 +199,192 @@ class Search(PluginTopic):
|
|
|
178
199
|
:returns: The product type specific metadata-mapping
|
|
179
200
|
:rtype: dict
|
|
180
201
|
"""
|
|
181
|
-
|
|
182
|
-
|
|
202
|
+
if product_type:
|
|
203
|
+
return self.config.products.get(product_type, {}).get(
|
|
204
|
+
"metadata_mapping", self.config.metadata_mapping
|
|
205
|
+
)
|
|
206
|
+
return self.config.metadata_mapping
|
|
207
|
+
|
|
208
|
+
def get_sort_by_arg(self, kwargs: Dict[str, Any]) -> Optional[SortByList]:
|
|
209
|
+
"""Extract the "sortBy" argument from the kwargs or the provider default sort configuration
|
|
210
|
+
|
|
211
|
+
:param kwargs: Search arguments
|
|
212
|
+
:type kwargs: Dict[str, Any]
|
|
213
|
+
:returns: The "sortBy" argument from the kwargs or the provider default sort configuration
|
|
214
|
+
:rtype: :class:`~eodag.types.search_args.SortByList`
|
|
215
|
+
"""
|
|
216
|
+
# remove "sortBy" from search args if exists because it is not part of metadata mapping,
|
|
217
|
+
# it will complete the query string or body once metadata mapping will be done
|
|
218
|
+
sort_by_arg_tmp = kwargs.pop("sortBy", None)
|
|
219
|
+
sort_by_arg = sort_by_arg_tmp or getattr(self.config, "sort", {}).get(
|
|
220
|
+
"sort_by_default", None
|
|
221
|
+
)
|
|
222
|
+
if not sort_by_arg_tmp and sort_by_arg:
|
|
223
|
+
logger.info(
|
|
224
|
+
f"{self.provider} is configured with default sorting by '{sort_by_arg[0][0]}' "
|
|
225
|
+
f"in {'ascending' if sort_by_arg[0][1] == 'ASC' else 'descending'} order"
|
|
226
|
+
)
|
|
227
|
+
return sort_by_arg
|
|
228
|
+
|
|
229
|
+
def build_sort_by(
|
|
230
|
+
self, sort_by_arg: SortByList
|
|
231
|
+
) -> Tuple[str, Dict[str, List[Dict[str, str]]]]:
|
|
232
|
+
"""Build the sorting part of the query string or body by transforming
|
|
233
|
+
the "sortBy" argument into a provider-specific string or dictionnary
|
|
234
|
+
|
|
235
|
+
:param sort_by_arg: the "sortBy" argument in EODAG format
|
|
236
|
+
:type sort_by_arg: :class:`~eodag.types.search_args.SortByList`
|
|
237
|
+
:returns: The "sortBy" argument in provider-specific format
|
|
238
|
+
:rtype: Union[str, Dict[str, List[Dict[str, str]]]]
|
|
239
|
+
"""
|
|
240
|
+
if not hasattr(self.config, "sort"):
|
|
241
|
+
raise ValidationError(f"{self.provider} does not support sorting feature")
|
|
242
|
+
# TODO: remove this code block when search args model validation is embeded
|
|
243
|
+
# remove duplicates
|
|
244
|
+
sort_by_arg = list(set(sort_by_arg))
|
|
245
|
+
|
|
246
|
+
sort_by_qs: str = ""
|
|
247
|
+
sort_by_qp: Dict[str, Any] = {}
|
|
248
|
+
|
|
249
|
+
provider_sort_by_tuples_used: List[Tuple[str, str]] = []
|
|
250
|
+
for eodag_sort_by_tuple in sort_by_arg:
|
|
251
|
+
eodag_sort_param = eodag_sort_by_tuple[0]
|
|
252
|
+
provider_sort_param = self.config.sort["sort_param_mapping"].get(
|
|
253
|
+
eodag_sort_param, None
|
|
254
|
+
)
|
|
255
|
+
if not provider_sort_param:
|
|
256
|
+
joined_eodag_params_to_map = ", ".join(
|
|
257
|
+
k for k in self.config.sort["sort_param_mapping"].keys()
|
|
258
|
+
)
|
|
259
|
+
params = set(self.config.sort["sort_param_mapping"].keys())
|
|
260
|
+
params.add(eodag_sort_param)
|
|
261
|
+
raise ValidationError(
|
|
262
|
+
f"'{eodag_sort_param}' parameter is not sortable with {self.provider}. "
|
|
263
|
+
f"Here is the list of sortable parameter(s) with {self.provider}: {joined_eodag_params_to_map}",
|
|
264
|
+
params,
|
|
265
|
+
)
|
|
266
|
+
eodag_sort_order = eodag_sort_by_tuple[1]
|
|
267
|
+
# TODO: remove this code block when search args model validation is embeded
|
|
268
|
+
# Remove leading and trailing whitespace(s) if exist
|
|
269
|
+
eodag_sort_order = eodag_sort_order.strip().upper()
|
|
270
|
+
if eodag_sort_order[:3] != "ASC" and eodag_sort_order[:3] != "DES":
|
|
271
|
+
raise ValidationError(
|
|
272
|
+
"Sorting order is invalid: it must be set to 'ASC' (ASCENDING) or "
|
|
273
|
+
f"'DESC' (DESCENDING), got '{eodag_sort_order}' with '{eodag_sort_param}' instead"
|
|
274
|
+
)
|
|
275
|
+
eodag_sort_order = eodag_sort_order[:3]
|
|
276
|
+
|
|
277
|
+
provider_sort_order = (
|
|
278
|
+
self.config.sort["sort_order_mapping"]["ascending"]
|
|
279
|
+
if eodag_sort_order == "ASC"
|
|
280
|
+
else self.config.sort["sort_order_mapping"]["descending"]
|
|
281
|
+
)
|
|
282
|
+
provider_sort_by_tuple: Tuple[str, str] = (
|
|
283
|
+
provider_sort_param,
|
|
284
|
+
provider_sort_order,
|
|
285
|
+
)
|
|
286
|
+
# TODO: remove this code block when search args model validation is embeded
|
|
287
|
+
for provider_sort_by_tuple_used in provider_sort_by_tuples_used:
|
|
288
|
+
# since duplicated tuples or dictionnaries have been removed, if two sorting parameters are equal,
|
|
289
|
+
# then their sorting order is different and there is a contradiction that would raise an error
|
|
290
|
+
if provider_sort_by_tuple[0] == provider_sort_by_tuple_used[0]:
|
|
291
|
+
raise ValidationError(
|
|
292
|
+
f"'{eodag_sort_param}' parameter is called several times to sort results with different "
|
|
293
|
+
"sorting orders. Please set it to only one ('ASC' (ASCENDING) or 'DESC' (DESCENDING))",
|
|
294
|
+
set([eodag_sort_param]),
|
|
295
|
+
)
|
|
296
|
+
provider_sort_by_tuples_used.append(provider_sort_by_tuple)
|
|
297
|
+
|
|
298
|
+
# TODO: move this code block to the top of this method when search args model validation is embeded
|
|
299
|
+
# check if the limit number of sorting parameter(s) is respected with this sorting parameter
|
|
300
|
+
if (
|
|
301
|
+
self.config.sort.get("max_sort_params", None)
|
|
302
|
+
and len(provider_sort_by_tuples_used)
|
|
303
|
+
> self.config.sort["max_sort_params"]
|
|
304
|
+
):
|
|
305
|
+
raise ValidationError(
|
|
306
|
+
f"Search results can be sorted by only {self.config.sort['max_sort_params']} "
|
|
307
|
+
f"parameter(s) with {self.provider}"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
parsed_sort_by_tpl: str = self.config.sort["sort_by_tpl"].format(
|
|
311
|
+
sort_param=provider_sort_by_tuple[0],
|
|
312
|
+
sort_order=provider_sort_by_tuple[1],
|
|
313
|
+
)
|
|
314
|
+
try:
|
|
315
|
+
parsed_sort_by_tpl_dict: Dict[str, Any] = orjson.loads(
|
|
316
|
+
parsed_sort_by_tpl
|
|
317
|
+
)
|
|
318
|
+
sort_by_qp = update_nested_dict(
|
|
319
|
+
sort_by_qp, parsed_sort_by_tpl_dict, extend_list_values=True
|
|
320
|
+
)
|
|
321
|
+
except orjson.JSONDecodeError:
|
|
322
|
+
sort_by_qs += parsed_sort_by_tpl
|
|
323
|
+
return (sort_by_qs, sort_by_qp)
|
|
324
|
+
|
|
325
|
+
def list_queryables(
|
|
326
|
+
self,
|
|
327
|
+
filters: Dict[str, Any],
|
|
328
|
+
product_type: Optional[str] = None,
|
|
329
|
+
) -> Dict[str, Annotated[Any, FieldInfo]]:
|
|
330
|
+
"""
|
|
331
|
+
Get queryables
|
|
332
|
+
|
|
333
|
+
:param filters: Additional filters for queryables.
|
|
334
|
+
:type filters: Dict[str, Any]
|
|
335
|
+
:param product_type: (optional) The product type.
|
|
336
|
+
:type product_type: Optional[str]
|
|
337
|
+
|
|
338
|
+
:return: A dictionary containing the queryable properties, associating parameters to their
|
|
339
|
+
annotated type.
|
|
340
|
+
:rtype: Dict[str, Annotated[Any, FieldInfo]]
|
|
341
|
+
"""
|
|
342
|
+
default_values: Dict[str, Any] = deepcopy(
|
|
343
|
+
getattr(self.config, "products", {}).get(product_type, {})
|
|
344
|
+
)
|
|
345
|
+
default_values.pop("metadata_mapping", None)
|
|
346
|
+
|
|
347
|
+
queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
|
|
348
|
+
try:
|
|
349
|
+
queryables = self.discover_queryables(**{**default_values, **filters}) or {}
|
|
350
|
+
except NotImplementedError:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
metadata_mapping: Dict[str, Any] = deepcopy(
|
|
354
|
+
self.get_metadata_mapping(product_type)
|
|
183
355
|
)
|
|
356
|
+
|
|
357
|
+
for param in list(metadata_mapping.keys()):
|
|
358
|
+
if NOT_MAPPED in metadata_mapping[param] or not isinstance(
|
|
359
|
+
metadata_mapping[param], list
|
|
360
|
+
):
|
|
361
|
+
del metadata_mapping[param]
|
|
362
|
+
|
|
363
|
+
eodag_queryables = copy_deepcopy(
|
|
364
|
+
model_fields_to_annotated(Queryables.model_fields)
|
|
365
|
+
)
|
|
366
|
+
for k, v in eodag_queryables.items():
|
|
367
|
+
eodag_queryable_field_info = (
|
|
368
|
+
get_args(v)[1] if len(get_args(v)) > 1 else None
|
|
369
|
+
)
|
|
370
|
+
if not isinstance(eodag_queryable_field_info, FieldInfo):
|
|
371
|
+
continue
|
|
372
|
+
# keep default field info of eodag queryables
|
|
373
|
+
if k in filters and k in queryables:
|
|
374
|
+
queryable_field_info = (
|
|
375
|
+
get_args(queryables[k])[1]
|
|
376
|
+
if len(get_args(queryables[k])) > 1
|
|
377
|
+
else None
|
|
378
|
+
)
|
|
379
|
+
if not isinstance(queryable_field_info, FieldInfo):
|
|
380
|
+
continue
|
|
381
|
+
queryable_field_info.default = filters[k]
|
|
382
|
+
continue
|
|
383
|
+
if k in queryables:
|
|
384
|
+
continue
|
|
385
|
+
if eodag_queryable_field_info.is_required() or (
|
|
386
|
+
(eodag_queryable_field_info.alias or k) in metadata_mapping
|
|
387
|
+
):
|
|
388
|
+
queryables[k] = v
|
|
389
|
+
|
|
390
|
+
return queryables
|