eodag 3.0.0b3__py3-none-any.whl → 3.1.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 +292 -198
- eodag/api/product/_assets.py +6 -6
- eodag/api/product/_product.py +18 -18
- eodag/api/product/metadata_mapping.py +51 -14
- eodag/api/search_result.py +29 -3
- eodag/cli.py +57 -20
- eodag/config.py +413 -117
- eodag/plugins/apis/base.py +10 -4
- eodag/plugins/apis/ecmwf.py +49 -16
- eodag/plugins/apis/usgs.py +30 -7
- eodag/plugins/authentication/aws_auth.py +14 -5
- eodag/plugins/authentication/base.py +10 -1
- eodag/plugins/authentication/generic.py +14 -3
- eodag/plugins/authentication/header.py +12 -4
- eodag/plugins/authentication/keycloak.py +41 -22
- eodag/plugins/authentication/oauth.py +11 -1
- eodag/plugins/authentication/openid_connect.py +178 -163
- eodag/plugins/authentication/qsauth.py +12 -4
- eodag/plugins/authentication/sas_auth.py +19 -2
- eodag/plugins/authentication/token.py +93 -15
- eodag/plugins/authentication/token_exchange.py +19 -19
- eodag/plugins/crunch/base.py +4 -1
- eodag/plugins/crunch/filter_date.py +5 -2
- eodag/plugins/crunch/filter_latest_intersect.py +5 -4
- eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
- eodag/plugins/crunch/filter_overlap.py +5 -7
- eodag/plugins/crunch/filter_property.py +6 -6
- eodag/plugins/download/aws.py +50 -34
- eodag/plugins/download/base.py +41 -50
- eodag/plugins/download/creodias_s3.py +40 -2
- eodag/plugins/download/http.py +221 -195
- eodag/plugins/download/s3rest.py +25 -25
- eodag/plugins/manager.py +168 -23
- eodag/plugins/search/base.py +106 -39
- eodag/plugins/search/build_search_result.py +1065 -324
- eodag/plugins/search/cop_marine.py +112 -29
- eodag/plugins/search/creodias_s3.py +45 -24
- eodag/plugins/search/csw.py +41 -1
- eodag/plugins/search/data_request_search.py +109 -9
- eodag/plugins/search/qssearch.py +549 -257
- eodag/plugins/search/static_stac_search.py +20 -21
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +577 -87
- eodag/resources/providers.yml +1619 -2776
- eodag/resources/stac.yml +3 -163
- eodag/resources/user_conf_template.yml +112 -97
- eodag/rest/config.py +1 -2
- eodag/rest/constants.py +0 -1
- eodag/rest/core.py +138 -98
- eodag/rest/errors.py +181 -0
- eodag/rest/server.py +55 -329
- eodag/rest/stac.py +93 -544
- eodag/rest/types/eodag_search.py +19 -8
- eodag/rest/types/queryables.py +6 -8
- eodag/rest/types/stac_search.py +11 -2
- eodag/rest/utils/__init__.py +3 -0
- eodag/types/__init__.py +71 -18
- eodag/types/download_args.py +3 -3
- eodag/types/queryables.py +180 -73
- eodag/types/search_args.py +3 -3
- eodag/types/whoosh.py +126 -0
- eodag/utils/__init__.py +147 -66
- eodag/utils/exceptions.py +47 -26
- eodag/utils/logging.py +37 -77
- eodag/utils/repr.py +65 -6
- eodag/utils/requests.py +11 -13
- eodag/utils/stac_reader.py +1 -1
- {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/METADATA +80 -81
- eodag-3.1.0b1.dist-info/RECORD +108 -0
- {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/WHEEL +1 -1
- {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/entry_points.txt +4 -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.0b1.dist-info}/LICENSE +0 -0
- {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/top_level.txt +0 -0
eodag/plugins/download/s3rest.py
CHANGED
|
@@ -62,23 +62,28 @@ logger = logging.getLogger("eodag.download.s3rest")
|
|
|
62
62
|
|
|
63
63
|
class S3RestDownload(Download):
|
|
64
64
|
"""Http download on S3-like object storage location
|
|
65
|
-
|
|
65
|
+
|
|
66
|
+
For example using Mundi REST API (free account)
|
|
66
67
|
https://mundiwebservices.com/keystoneapi/uploads/documents/CWS-DATA-MUT-087-EN-Mundi_Download_v1.1.pdf#page=13
|
|
67
68
|
|
|
68
|
-
Re-use AwsDownload bucket some handling methods
|
|
69
|
+
Re-use AwsDownload bucket and some handling methods
|
|
69
70
|
|
|
70
71
|
:param provider: provider name
|
|
71
72
|
:param config: Download plugin configuration:
|
|
72
73
|
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
74
|
+
* :attr:`~eodag.config.PluginConfig.base_uri` (``str``) (**mandatory**): default endpoint url
|
|
75
|
+
* :attr:`~eodag.config.PluginConfig.extract` (``bool``): extract downloaded archive or not
|
|
76
|
+
* :attr:`~eodag.config.PluginConfig.auth_error_code` (``int``): authentication error code
|
|
77
|
+
* :attr:`~eodag.config.PluginConfig.bucket_path_level` (``int``): bucket location index in ``path.split('/')``
|
|
78
|
+
* :attr:`~eodag.config.PluginConfig.order_enabled` (``bool``): whether order is enabled
|
|
79
|
+
or not if product is `OFFLINE`
|
|
80
|
+
* :attr:`~eodag.config.PluginConfig.order_method` (``str``) HTTP request method, ``GET`` (default) or ``POST``
|
|
81
|
+
* :attr:`~eodag.config.PluginConfig.order_headers` (``[Dict[str, str]]``): order request headers
|
|
82
|
+
* :attr:`~eodag.config.PluginConfig.order_on_response` (:class:`~eodag.config.PluginConfig.OrderOnResponse`):
|
|
83
|
+
a typed dictionary containing the key :attr:`~eodag.config.PluginConfig.OrderOnResponse.metadata_mapping`
|
|
84
|
+
which can be used to add new product properties based on the data in response to the order request
|
|
85
|
+
* :attr:`~eodag.config.PluginConfig.order_status` (:class:`~eodag.config.PluginConfig.OrderStatus`):
|
|
86
|
+
Order status handling
|
|
82
87
|
"""
|
|
83
88
|
|
|
84
89
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
@@ -90,8 +95,8 @@ class S3RestDownload(Download):
|
|
|
90
95
|
product: EOProduct,
|
|
91
96
|
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
92
97
|
progress_callback: Optional[ProgressCallback] = None,
|
|
93
|
-
wait:
|
|
94
|
-
timeout:
|
|
98
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
99
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
95
100
|
**kwargs: Unpack[DownloadConf],
|
|
96
101
|
) -> Optional[str]:
|
|
97
102
|
"""Download method for S3 REST API.
|
|
@@ -125,9 +130,9 @@ class S3RestDownload(Download):
|
|
|
125
130
|
and "storageStatus" in product.properties
|
|
126
131
|
and product.properties["storageStatus"] != ONLINE_STATUS
|
|
127
132
|
):
|
|
128
|
-
self.http_download_plugin.
|
|
133
|
+
self.http_download_plugin._order(product=product, auth=auth)
|
|
129
134
|
|
|
130
|
-
@self.
|
|
135
|
+
@self._order_download_retry(product, wait, timeout)
|
|
131
136
|
def download_request(
|
|
132
137
|
product: EOProduct,
|
|
133
138
|
auth: AuthBase,
|
|
@@ -137,9 +142,7 @@ class S3RestDownload(Download):
|
|
|
137
142
|
):
|
|
138
143
|
# check order status
|
|
139
144
|
if product.properties.get("orderStatusLink", None):
|
|
140
|
-
self.http_download_plugin.
|
|
141
|
-
product=product, auth=auth
|
|
142
|
-
)
|
|
145
|
+
self.http_download_plugin._order_status(product=product, auth=auth)
|
|
143
146
|
|
|
144
147
|
# get bucket urls
|
|
145
148
|
bucket_name, prefix = get_bucket_name_and_prefix(
|
|
@@ -189,12 +192,9 @@ class S3RestDownload(Download):
|
|
|
189
192
|
auth_errors = [auth_errors]
|
|
190
193
|
if err.response and err.response.status_code in auth_errors:
|
|
191
194
|
raise AuthenticationError(
|
|
192
|
-
"
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
err.response.text.strip(),
|
|
196
|
-
self.provider,
|
|
197
|
-
)
|
|
195
|
+
f"Please check your credentials for {self.provider}.",
|
|
196
|
+
f"HTTP Error {err.response.status_code} returned.",
|
|
197
|
+
err.response.text.strip(),
|
|
198
198
|
)
|
|
199
199
|
# product not available
|
|
200
200
|
elif (
|
|
@@ -225,7 +225,7 @@ class S3RestDownload(Download):
|
|
|
225
225
|
self.__class__.__name__,
|
|
226
226
|
bucket_contents.text,
|
|
227
227
|
)
|
|
228
|
-
raise RequestError(
|
|
228
|
+
raise RequestError.from_error(err) from err
|
|
229
229
|
try:
|
|
230
230
|
xmldoc = minidom.parseString(bucket_contents.text)
|
|
231
231
|
except ExpatError as err:
|
eodag/plugins/manager.py
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
+
import re
|
|
21
22
|
from operator import attrgetter
|
|
22
23
|
from pathlib import Path
|
|
23
24
|
from typing import (
|
|
@@ -35,17 +36,28 @@ from typing import (
|
|
|
35
36
|
|
|
36
37
|
import pkg_resources
|
|
37
38
|
|
|
38
|
-
from eodag.config import
|
|
39
|
+
from eodag.config import (
|
|
40
|
+
AUTH_TOPIC_KEYS,
|
|
41
|
+
PLUGINS_TOPICS_KEYS,
|
|
42
|
+
load_config,
|
|
43
|
+
merge_configs,
|
|
44
|
+
)
|
|
39
45
|
from eodag.plugins.apis.base import Api
|
|
40
46
|
from eodag.plugins.authentication.base import Authentication
|
|
41
47
|
from eodag.plugins.base import EODAGPluginMount
|
|
42
48
|
from eodag.plugins.crunch.base import Crunch
|
|
43
49
|
from eodag.plugins.download.base import Download
|
|
44
50
|
from eodag.plugins.search.base import Search
|
|
45
|
-
from eodag.utils import GENERIC_PRODUCT_TYPE
|
|
46
|
-
from eodag.utils.exceptions import
|
|
51
|
+
from eodag.utils import GENERIC_PRODUCT_TYPE, deepcopy, dict_md5sum
|
|
52
|
+
from eodag.utils.exceptions import (
|
|
53
|
+
AuthenticationError,
|
|
54
|
+
MisconfiguredError,
|
|
55
|
+
UnsupportedProvider,
|
|
56
|
+
)
|
|
47
57
|
|
|
48
58
|
if TYPE_CHECKING:
|
|
59
|
+
from requests.auth import AuthBase
|
|
60
|
+
|
|
49
61
|
from eodag.api.product import EOProduct
|
|
50
62
|
from eodag.config import PluginConfig, ProviderConfig
|
|
51
63
|
from eodag.plugins.base import PluginTopic
|
|
@@ -70,7 +82,7 @@ class PluginManager:
|
|
|
70
82
|
supported by ``eodag``
|
|
71
83
|
"""
|
|
72
84
|
|
|
73
|
-
supported_topics =
|
|
85
|
+
supported_topics = set(PLUGINS_TOPICS_KEYS)
|
|
74
86
|
|
|
75
87
|
product_type_to_provider_config_map: Dict[str, List[ProviderConfig]]
|
|
76
88
|
|
|
@@ -139,7 +151,7 @@ class PluginManager:
|
|
|
139
151
|
self.providers_config = providers_config
|
|
140
152
|
|
|
141
153
|
self.build_product_type_to_provider_config_map()
|
|
142
|
-
self._built_plugins_cache: Dict[Tuple[str, str], Any] = {}
|
|
154
|
+
self._built_plugins_cache: Dict[Tuple[str, str, str], Any] = {}
|
|
143
155
|
|
|
144
156
|
def build_product_type_to_provider_config_map(self) -> None:
|
|
145
157
|
"""Build mapping conf between product types and providers"""
|
|
@@ -249,25 +261,146 @@ class PluginManager:
|
|
|
249
261
|
)
|
|
250
262
|
return plugin
|
|
251
263
|
|
|
252
|
-
def get_auth_plugin(
|
|
264
|
+
def get_auth_plugin(
|
|
265
|
+
self, associated_plugin: PluginTopic, product: Optional[EOProduct] = None
|
|
266
|
+
) -> Optional[Authentication]:
|
|
267
|
+
"""Build and return the authentication plugin associated to the given
|
|
268
|
+
search/download plugin
|
|
269
|
+
|
|
270
|
+
.. versionchanged:: v3.0.0
|
|
271
|
+
``get_auth_plugin()`` now needs ``associated_plugin`` instead of ``provider``
|
|
272
|
+
as argument.
|
|
273
|
+
|
|
274
|
+
:param associated_plugin: The search/download plugin to which the authentication
|
|
275
|
+
plugin is linked
|
|
276
|
+
:param product: The product to download. ``None`` for search authentication
|
|
277
|
+
:returns: The Authentication plugin
|
|
278
|
+
"""
|
|
279
|
+
# matching url from product to download
|
|
280
|
+
if product is not None and len(product.assets) > 0:
|
|
281
|
+
matching_url = next(iter(product.assets.values()))["href"]
|
|
282
|
+
elif product is not None:
|
|
283
|
+
matching_url = product.properties.get(
|
|
284
|
+
"downloadLink"
|
|
285
|
+
) or product.properties.get("orderLink")
|
|
286
|
+
else:
|
|
287
|
+
# search auth
|
|
288
|
+
matching_url = getattr(associated_plugin.config, "api_endpoint", None)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
auth_plugin = next(
|
|
292
|
+
self.get_auth_plugins(
|
|
293
|
+
associated_plugin.provider,
|
|
294
|
+
matching_url=matching_url,
|
|
295
|
+
matching_conf=associated_plugin.config,
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
except StopIteration:
|
|
299
|
+
auth_plugin = None
|
|
300
|
+
return auth_plugin
|
|
301
|
+
|
|
302
|
+
def get_auth_plugins(
|
|
303
|
+
self,
|
|
304
|
+
provider: str,
|
|
305
|
+
matching_url: Optional[str] = None,
|
|
306
|
+
matching_conf: Optional[PluginConfig] = None,
|
|
307
|
+
) -> Iterator[Authentication]:
|
|
253
308
|
"""Build and return the authentication plugin for the given product_type and
|
|
254
309
|
provider
|
|
255
310
|
|
|
256
311
|
:param provider: The provider for which to get the authentication plugin
|
|
257
|
-
:
|
|
312
|
+
:param matching_url: url to compare with plugin matching_url pattern
|
|
313
|
+
:param matching_conf: configuration to compare with plugin matching_conf
|
|
314
|
+
:returns: All the Authentication plugins for the given criteria
|
|
258
315
|
"""
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
316
|
+
auth_conf: Optional[PluginConfig] = None
|
|
317
|
+
|
|
318
|
+
def _is_auth_plugin_matching(
|
|
319
|
+
auth_conf: PluginConfig,
|
|
320
|
+
matching_url: Optional[str],
|
|
321
|
+
matching_conf: Optional[PluginConfig],
|
|
322
|
+
) -> bool:
|
|
323
|
+
plugin_matching_conf = getattr(auth_conf, "matching_conf", {})
|
|
324
|
+
if matching_conf:
|
|
325
|
+
if (
|
|
326
|
+
plugin_matching_conf
|
|
327
|
+
and matching_conf.__dict__.items() >= plugin_matching_conf.items()
|
|
328
|
+
):
|
|
329
|
+
# conf matches
|
|
330
|
+
return True
|
|
331
|
+
plugin_matching_url = getattr(auth_conf, "matching_url", None)
|
|
332
|
+
if matching_url:
|
|
333
|
+
if plugin_matching_url and re.match(
|
|
334
|
+
rf"{plugin_matching_url}", matching_url
|
|
335
|
+
):
|
|
336
|
+
# url matches
|
|
337
|
+
return True
|
|
338
|
+
# no match
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
# providers configs with given provider at first
|
|
342
|
+
sorted_providers_config = deepcopy(self.providers_config)
|
|
343
|
+
sorted_providers_config = {
|
|
344
|
+
provider: sorted_providers_config.pop(provider),
|
|
345
|
+
**sorted_providers_config,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for plugin_provider, provider_conf in sorted_providers_config.items():
|
|
349
|
+
for key in AUTH_TOPIC_KEYS:
|
|
350
|
+
auth_conf = getattr(provider_conf, key, None)
|
|
351
|
+
if auth_conf is None:
|
|
352
|
+
continue
|
|
353
|
+
# plugin without configured match criteria: only works for given provider
|
|
354
|
+
unconfigured_match = (
|
|
355
|
+
True
|
|
356
|
+
if (
|
|
357
|
+
not getattr(auth_conf, "matching_conf", {})
|
|
358
|
+
and not getattr(auth_conf, "matching_url", None)
|
|
359
|
+
and provider == plugin_provider
|
|
360
|
+
)
|
|
361
|
+
else False
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if unconfigured_match or _is_auth_plugin_matching(
|
|
365
|
+
auth_conf, matching_url, matching_conf
|
|
366
|
+
):
|
|
367
|
+
auth_conf.priority = provider_conf.priority
|
|
368
|
+
plugin = cast(
|
|
369
|
+
Authentication,
|
|
370
|
+
self._build_plugin(plugin_provider, auth_conf, Authentication),
|
|
371
|
+
)
|
|
372
|
+
yield plugin
|
|
373
|
+
else:
|
|
374
|
+
continue
|
|
375
|
+
|
|
376
|
+
def get_auth(
|
|
377
|
+
self,
|
|
378
|
+
provider: str,
|
|
379
|
+
matching_url: Optional[str] = None,
|
|
380
|
+
matching_conf: Optional[PluginConfig] = None,
|
|
381
|
+
) -> Optional[Union[AuthBase, Dict[str, str]]]:
|
|
382
|
+
"""Authenticate and return the authenticated object for the first matching
|
|
383
|
+
authentication plugin
|
|
384
|
+
|
|
385
|
+
:param provider: The provider for which to get the authentication plugin
|
|
386
|
+
:param matching_url: url to compare with plugin matching_url pattern
|
|
387
|
+
:param matching_conf: configuration to compare with plugin matching_conf
|
|
388
|
+
:returns: All the Authentication plugins for the given criteria
|
|
389
|
+
"""
|
|
390
|
+
for auth_plugin in self.get_auth_plugins(provider, matching_url, matching_conf):
|
|
391
|
+
if auth_plugin and callable(getattr(auth_plugin, "authenticate", None)):
|
|
392
|
+
try:
|
|
393
|
+
auth = auth_plugin.authenticate()
|
|
394
|
+
return auth
|
|
395
|
+
except (AuthenticationError, MisconfiguredError) as e:
|
|
396
|
+
logger.debug(f"Could not authenticate on {provider}: {str(e)}")
|
|
397
|
+
continue
|
|
398
|
+
else:
|
|
399
|
+
logger.debug(
|
|
400
|
+
f"Could not authenticate on {provider} using {auth_plugin} plugin"
|
|
401
|
+
)
|
|
402
|
+
continue
|
|
403
|
+
return None
|
|
271
404
|
|
|
272
405
|
@staticmethod
|
|
273
406
|
def get_crunch_plugin(name: str, **options: Any) -> Crunch:
|
|
@@ -304,9 +437,11 @@ class PluginManager:
|
|
|
304
437
|
# Sort the provider configs, taking into account the new priority order
|
|
305
438
|
provider_configs.sort(key=attrgetter("priority"), reverse=True)
|
|
306
439
|
# Update the priority of already built plugins of the given provider
|
|
307
|
-
for provider_name, topic_class in self._built_plugins_cache:
|
|
440
|
+
for provider_name, topic_class, auth_match_md5 in self._built_plugins_cache:
|
|
308
441
|
if provider_name == provider:
|
|
309
|
-
self._built_plugins_cache[
|
|
442
|
+
self._built_plugins_cache[
|
|
443
|
+
(provider, topic_class, auth_match_md5)
|
|
444
|
+
].priority = priority
|
|
310
445
|
|
|
311
446
|
def _build_plugin(
|
|
312
447
|
self,
|
|
@@ -325,8 +460,16 @@ class PluginManager:
|
|
|
325
460
|
:class:`~eodag.plugin.authentication.Authentication` or
|
|
326
461
|
:class:`~eodag.plugin.crunch.Crunch`
|
|
327
462
|
"""
|
|
463
|
+
# md5 hash to helps identifying an auth plugin within several for a given provider
|
|
464
|
+
# (each has distinct matching settings)
|
|
465
|
+
auth_match_md5 = dict_md5sum(
|
|
466
|
+
{
|
|
467
|
+
"matching_url": getattr(plugin_conf, "matching_url", None),
|
|
468
|
+
"matching_conf": getattr(plugin_conf, "matching_conf", None),
|
|
469
|
+
}
|
|
470
|
+
)
|
|
328
471
|
cached_instance = self._built_plugins_cache.setdefault(
|
|
329
|
-
(provider, topic_class.__name__), None
|
|
472
|
+
(provider, topic_class.__name__, auth_match_md5), None
|
|
330
473
|
)
|
|
331
474
|
if cached_instance is not None:
|
|
332
475
|
return cached_instance
|
|
@@ -336,5 +479,7 @@ class PluginManager:
|
|
|
336
479
|
plugin: Union[Api, Search, Download, Authentication, Crunch] = plugin_class(
|
|
337
480
|
provider, plugin_conf
|
|
338
481
|
)
|
|
339
|
-
self._built_plugins_cache[
|
|
482
|
+
self._built_plugins_cache[
|
|
483
|
+
(provider, topic_class.__name__, auth_match_md5)
|
|
484
|
+
] = plugin
|
|
340
485
|
return plugin
|
eodag/plugins/search/base.py
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
-
from typing import TYPE_CHECKING
|
|
21
|
+
from typing import TYPE_CHECKING, Annotated, get_args
|
|
22
22
|
|
|
23
23
|
import orjson
|
|
24
24
|
from pydantic.fields import Field, FieldInfo
|
|
@@ -31,15 +31,13 @@ from eodag.api.product.metadata_mapping import (
|
|
|
31
31
|
from eodag.plugins.base import PluginTopic
|
|
32
32
|
from eodag.plugins.search import PreparedSearch
|
|
33
33
|
from eodag.types import model_fields_to_annotated
|
|
34
|
-
from eodag.types.queryables import Queryables
|
|
34
|
+
from eodag.types.queryables import Queryables, QueryablesDict
|
|
35
35
|
from eodag.types.search_args import SortByList
|
|
36
36
|
from eodag.utils import (
|
|
37
37
|
GENERIC_PRODUCT_TYPE,
|
|
38
|
-
Annotated,
|
|
39
38
|
copy_deepcopy,
|
|
40
39
|
deepcopy,
|
|
41
40
|
format_dict_items,
|
|
42
|
-
get_args,
|
|
43
41
|
update_nested_dict,
|
|
44
42
|
)
|
|
45
43
|
from eodag.utils.exceptions import ValidationError
|
|
@@ -95,9 +93,9 @@ class Search(PluginTopic):
|
|
|
95
93
|
) -> Tuple[List[EOProduct], Optional[int]]:
|
|
96
94
|
"""Implementation of how the products must be searched goes here.
|
|
97
95
|
|
|
98
|
-
This method must return a tuple with (1) a list of
|
|
99
|
-
which will be processed by a Download plugin (2) and the total number of
|
|
100
|
-
the search criteria. If ``prep.count`` is False, the second element returned must be ``None``.
|
|
96
|
+
This method must return a tuple with (1) a list of :class:`~eodag.api.product._product.EOProduct` instances
|
|
97
|
+
which will be processed by a :class:`~eodag.plugins.download.base.Download` plugin (2) and the total number of
|
|
98
|
+
products matching the search criteria. If ``prep.count`` is False, the second element returned must be ``None``.
|
|
101
99
|
"""
|
|
102
100
|
raise NotImplementedError("A Search plugin must implement a method named query")
|
|
103
101
|
|
|
@@ -108,9 +106,9 @@ class Search(PluginTopic):
|
|
|
108
106
|
def discover_queryables(
|
|
109
107
|
self, **kwargs: Any
|
|
110
108
|
) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
|
|
111
|
-
"""Fetch queryables list from provider using
|
|
109
|
+
"""Fetch queryables list from provider using :attr:`~eodag.config.PluginConfig.discover_queryables` conf
|
|
112
110
|
|
|
113
|
-
:param kwargs: additional filters for queryables (
|
|
111
|
+
:param kwargs: additional filters for queryables (``productType`` and other search
|
|
114
112
|
arguments)
|
|
115
113
|
:returns: fetched queryable parameters dict
|
|
116
114
|
"""
|
|
@@ -179,6 +177,27 @@ class Search(PluginTopic):
|
|
|
179
177
|
else:
|
|
180
178
|
return {}
|
|
181
179
|
|
|
180
|
+
def get_product_type_cfg_value(self, key: str, default: Any = None) -> Any:
|
|
181
|
+
"""
|
|
182
|
+
Get the value of a configuration option specific to the current product type.
|
|
183
|
+
|
|
184
|
+
This method retrieves the value of a configuration option from the
|
|
185
|
+
``product_type_config`` attribute. If the option is not found, the provided
|
|
186
|
+
default value is returned.
|
|
187
|
+
|
|
188
|
+
:param key: The configuration option key.
|
|
189
|
+
:type key: str
|
|
190
|
+
:param default: The default value to be returned if the option is not found (default is None).
|
|
191
|
+
:type default: Any
|
|
192
|
+
|
|
193
|
+
:return: The value of the specified configuration option or the default value.
|
|
194
|
+
:rtype: Any
|
|
195
|
+
"""
|
|
196
|
+
product_type_cfg = getattr(self.config, "product_type_config", {})
|
|
197
|
+
non_none_cfg = {k: v for k, v in product_type_cfg.items() if v}
|
|
198
|
+
|
|
199
|
+
return non_none_cfg.get(key, default)
|
|
200
|
+
|
|
182
201
|
def get_metadata_mapping(
|
|
183
202
|
self, product_type: Optional[str] = None
|
|
184
203
|
) -> Dict[str, Union[str, List[str]]]:
|
|
@@ -194,10 +213,10 @@ class Search(PluginTopic):
|
|
|
194
213
|
return self.config.metadata_mapping
|
|
195
214
|
|
|
196
215
|
def get_sort_by_arg(self, kwargs: Dict[str, Any]) -> Optional[SortByList]:
|
|
197
|
-
"""Extract the
|
|
216
|
+
"""Extract the ``sort_by`` argument from the kwargs or the provider default sort configuration
|
|
198
217
|
|
|
199
218
|
:param kwargs: Search arguments
|
|
200
|
-
:returns: The
|
|
219
|
+
:returns: The ``sort_by`` argument from the kwargs or the provider default sort configuration
|
|
201
220
|
"""
|
|
202
221
|
# remove "sort_by" from search args if exists because it is not part of metadata mapping,
|
|
203
222
|
# it will complete the query string or body once metadata mapping will be done
|
|
@@ -216,16 +235,16 @@ class Search(PluginTopic):
|
|
|
216
235
|
self, sort_by_arg: SortByList
|
|
217
236
|
) -> Tuple[str, Dict[str, List[Dict[str, str]]]]:
|
|
218
237
|
"""Build the sorting part of the query string or body by transforming
|
|
219
|
-
the
|
|
238
|
+
the ``sort_by`` argument into a provider-specific string or dictionary
|
|
220
239
|
|
|
221
|
-
:param sort_by_arg: the
|
|
222
|
-
:returns: The
|
|
240
|
+
:param sort_by_arg: the ``sort_by`` argument in EODAG format
|
|
241
|
+
:returns: The ``sort_by`` argument in provider-specific format
|
|
223
242
|
"""
|
|
224
243
|
if not hasattr(self.config, "sort"):
|
|
225
244
|
raise ValidationError(f"{self.provider} does not support sorting feature")
|
|
226
245
|
# TODO: remove this code block when search args model validation is embeded
|
|
227
246
|
# remove duplicates
|
|
228
|
-
sort_by_arg = list(
|
|
247
|
+
sort_by_arg = list(dict.fromkeys(sort_by_arg))
|
|
229
248
|
|
|
230
249
|
sort_by_qs: str = ""
|
|
231
250
|
sort_by_qp: Dict[str, Any] = {}
|
|
@@ -306,35 +325,93 @@ class Search(PluginTopic):
|
|
|
306
325
|
sort_by_qs += parsed_sort_by_tpl
|
|
307
326
|
return (sort_by_qs, sort_by_qp)
|
|
308
327
|
|
|
328
|
+
def _get_product_type_queryables(
|
|
329
|
+
self, product_type: Optional[str], alias: Optional[str], filters: Dict[str, Any]
|
|
330
|
+
) -> Dict[str, Annotated[Any, FieldInfo]]:
|
|
331
|
+
default_values: Dict[str, Any] = deepcopy(
|
|
332
|
+
getattr(self.config, "products", {}).get(product_type, {})
|
|
333
|
+
)
|
|
334
|
+
default_values.pop("metadata_mapping", None)
|
|
335
|
+
try:
|
|
336
|
+
filters["productType"] = product_type
|
|
337
|
+
return self.discover_queryables(**{**default_values, **filters}) or {}
|
|
338
|
+
except NotImplementedError:
|
|
339
|
+
return self.queryables_from_metadata_mapping(product_type, alias)
|
|
340
|
+
|
|
309
341
|
def list_queryables(
|
|
310
342
|
self,
|
|
311
343
|
filters: Dict[str, Any],
|
|
344
|
+
available_product_types: List[Any],
|
|
345
|
+
product_type_configs: Dict[str, Dict[str, Any]],
|
|
312
346
|
product_type: Optional[str] = None,
|
|
313
|
-
|
|
347
|
+
alias: Optional[str] = None,
|
|
348
|
+
) -> QueryablesDict:
|
|
314
349
|
"""
|
|
315
350
|
Get queryables
|
|
316
351
|
|
|
317
352
|
:param filters: Additional filters for queryables.
|
|
353
|
+
:param available_product_types: list of available product types
|
|
354
|
+
:param product_type_configs: dict containing the product type information for all used product types
|
|
318
355
|
:param product_type: (optional) The product type.
|
|
356
|
+
:param alias: (optional) alias of the product type
|
|
319
357
|
|
|
320
358
|
:return: A dictionary containing the queryable properties, associating parameters to their
|
|
321
359
|
annotated type.
|
|
322
360
|
"""
|
|
323
|
-
|
|
324
|
-
|
|
361
|
+
additional_info = (
|
|
362
|
+
"Please select a product type to get the possible values of the parameters!"
|
|
363
|
+
if not product_type
|
|
364
|
+
else ""
|
|
325
365
|
)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
366
|
+
if product_type or getattr(self.config, "discover_queryables", {}).get(
|
|
367
|
+
"fetch_url", ""
|
|
368
|
+
):
|
|
369
|
+
if product_type:
|
|
370
|
+
self.config.product_type_config = product_type_configs[product_type]
|
|
371
|
+
queryables = self._get_product_type_queryables(product_type, alias, filters)
|
|
372
|
+
if getattr(self.config, "discover_queryables", {}).get(
|
|
373
|
+
"constraints_url", ""
|
|
374
|
+
):
|
|
375
|
+
additional_properties = False
|
|
376
|
+
else:
|
|
377
|
+
additional_properties = True
|
|
378
|
+
return QueryablesDict(
|
|
379
|
+
additional_properties=additional_properties,
|
|
380
|
+
additional_information=additional_info,
|
|
381
|
+
**queryables,
|
|
382
|
+
)
|
|
383
|
+
else:
|
|
384
|
+
all_queryables: Dict[str, Any] = {}
|
|
385
|
+
for pt in available_product_types:
|
|
386
|
+
self.config.product_type_config = product_type_configs[pt]
|
|
387
|
+
pt_queryables = self._get_product_type_queryables(pt, None, filters)
|
|
388
|
+
# only use key and type because values and defaults will vary between product types
|
|
389
|
+
pt_queryables_neutral = {
|
|
390
|
+
k: Annotated[v.__args__[0], Field(default=None)]
|
|
391
|
+
for k, v in pt_queryables.items()
|
|
392
|
+
}
|
|
393
|
+
all_queryables.update(pt_queryables_neutral)
|
|
394
|
+
return QueryablesDict(
|
|
395
|
+
additional_properties=True,
|
|
396
|
+
additional_information=additional_info,
|
|
397
|
+
**all_queryables,
|
|
398
|
+
)
|
|
333
399
|
|
|
400
|
+
def queryables_from_metadata_mapping(
|
|
401
|
+
self, product_type: Optional[str] = None, alias: Optional[str] = None
|
|
402
|
+
) -> Dict[str, Annotated[Any, FieldInfo]]:
|
|
403
|
+
"""
|
|
404
|
+
Extract queryable parameters from product type metadata mapping.
|
|
405
|
+
:param product_type: product type id (optional)
|
|
406
|
+
:param alias: (optional) alias of the product type
|
|
407
|
+
:returns: dict of annotated queryables
|
|
408
|
+
"""
|
|
334
409
|
metadata_mapping: Dict[str, Any] = deepcopy(
|
|
335
410
|
self.get_metadata_mapping(product_type)
|
|
336
411
|
)
|
|
337
412
|
|
|
413
|
+
queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
|
|
414
|
+
|
|
338
415
|
for param in list(metadata_mapping.keys()):
|
|
339
416
|
if NOT_MAPPED in metadata_mapping[param] or not isinstance(
|
|
340
417
|
metadata_mapping[param], list
|
|
@@ -344,28 +421,18 @@ class Search(PluginTopic):
|
|
|
344
421
|
eodag_queryables = copy_deepcopy(
|
|
345
422
|
model_fields_to_annotated(Queryables.model_fields)
|
|
346
423
|
)
|
|
424
|
+
# add default value for product type
|
|
425
|
+
if alias:
|
|
426
|
+
eodag_queryables.pop("productType")
|
|
427
|
+
eodag_queryables["productType"] = Annotated[str, Field(default=alias)]
|
|
347
428
|
for k, v in eodag_queryables.items():
|
|
348
429
|
eodag_queryable_field_info = (
|
|
349
430
|
get_args(v)[1] if len(get_args(v)) > 1 else None
|
|
350
431
|
)
|
|
351
432
|
if not isinstance(eodag_queryable_field_info, FieldInfo):
|
|
352
433
|
continue
|
|
353
|
-
# keep default field info of eodag queryables
|
|
354
|
-
if k in filters and k in queryables:
|
|
355
|
-
queryable_field_info = (
|
|
356
|
-
get_args(queryables[k])[1]
|
|
357
|
-
if len(get_args(queryables[k])) > 1
|
|
358
|
-
else None
|
|
359
|
-
)
|
|
360
|
-
if not isinstance(queryable_field_info, FieldInfo):
|
|
361
|
-
continue
|
|
362
|
-
queryable_field_info.default = filters[k]
|
|
363
|
-
continue
|
|
364
|
-
if k in queryables:
|
|
365
|
-
continue
|
|
366
434
|
if eodag_queryable_field_info.is_required() or (
|
|
367
435
|
(eodag_queryable_field_info.alias or k) in metadata_mapping
|
|
368
436
|
):
|
|
369
437
|
queryables[k] = v
|
|
370
|
-
|
|
371
438
|
return queryables
|