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.
Files changed (77) hide show
  1. eodag/api/core.py +292 -198
  2. eodag/api/product/_assets.py +6 -6
  3. eodag/api/product/_product.py +18 -18
  4. eodag/api/product/metadata_mapping.py +51 -14
  5. eodag/api/search_result.py +29 -3
  6. eodag/cli.py +57 -20
  7. eodag/config.py +413 -117
  8. eodag/plugins/apis/base.py +10 -4
  9. eodag/plugins/apis/ecmwf.py +49 -16
  10. eodag/plugins/apis/usgs.py +30 -7
  11. eodag/plugins/authentication/aws_auth.py +14 -5
  12. eodag/plugins/authentication/base.py +10 -1
  13. eodag/plugins/authentication/generic.py +14 -3
  14. eodag/plugins/authentication/header.py +12 -4
  15. eodag/plugins/authentication/keycloak.py +41 -22
  16. eodag/plugins/authentication/oauth.py +11 -1
  17. eodag/plugins/authentication/openid_connect.py +178 -163
  18. eodag/plugins/authentication/qsauth.py +12 -4
  19. eodag/plugins/authentication/sas_auth.py +19 -2
  20. eodag/plugins/authentication/token.py +93 -15
  21. eodag/plugins/authentication/token_exchange.py +19 -19
  22. eodag/plugins/crunch/base.py +4 -1
  23. eodag/plugins/crunch/filter_date.py +5 -2
  24. eodag/plugins/crunch/filter_latest_intersect.py +5 -4
  25. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
  26. eodag/plugins/crunch/filter_overlap.py +5 -7
  27. eodag/plugins/crunch/filter_property.py +6 -6
  28. eodag/plugins/download/aws.py +50 -34
  29. eodag/plugins/download/base.py +41 -50
  30. eodag/plugins/download/creodias_s3.py +40 -2
  31. eodag/plugins/download/http.py +221 -195
  32. eodag/plugins/download/s3rest.py +25 -25
  33. eodag/plugins/manager.py +168 -23
  34. eodag/plugins/search/base.py +106 -39
  35. eodag/plugins/search/build_search_result.py +1065 -324
  36. eodag/plugins/search/cop_marine.py +112 -29
  37. eodag/plugins/search/creodias_s3.py +45 -24
  38. eodag/plugins/search/csw.py +41 -1
  39. eodag/plugins/search/data_request_search.py +109 -9
  40. eodag/plugins/search/qssearch.py +549 -257
  41. eodag/plugins/search/static_stac_search.py +20 -21
  42. eodag/resources/ext_product_types.json +1 -1
  43. eodag/resources/product_types.yml +577 -87
  44. eodag/resources/providers.yml +1619 -2776
  45. eodag/resources/stac.yml +3 -163
  46. eodag/resources/user_conf_template.yml +112 -97
  47. eodag/rest/config.py +1 -2
  48. eodag/rest/constants.py +0 -1
  49. eodag/rest/core.py +138 -98
  50. eodag/rest/errors.py +181 -0
  51. eodag/rest/server.py +55 -329
  52. eodag/rest/stac.py +93 -544
  53. eodag/rest/types/eodag_search.py +19 -8
  54. eodag/rest/types/queryables.py +6 -8
  55. eodag/rest/types/stac_search.py +11 -2
  56. eodag/rest/utils/__init__.py +3 -0
  57. eodag/types/__init__.py +71 -18
  58. eodag/types/download_args.py +3 -3
  59. eodag/types/queryables.py +180 -73
  60. eodag/types/search_args.py +3 -3
  61. eodag/types/whoosh.py +126 -0
  62. eodag/utils/__init__.py +147 -66
  63. eodag/utils/exceptions.py +47 -26
  64. eodag/utils/logging.py +37 -77
  65. eodag/utils/repr.py +65 -6
  66. eodag/utils/requests.py +11 -13
  67. eodag/utils/stac_reader.py +1 -1
  68. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/METADATA +80 -81
  69. eodag-3.1.0b1.dist-info/RECORD +108 -0
  70. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/WHEEL +1 -1
  71. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/entry_points.txt +4 -2
  72. eodag/resources/constraints/climate-dt.json +0 -13
  73. eodag/resources/constraints/extremes-dt.json +0 -8
  74. eodag/utils/constraints.py +0 -244
  75. eodag-3.0.0b3.dist-info/RECORD +0 -110
  76. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/LICENSE +0 -0
  77. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/top_level.txt +0 -0
@@ -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
- for example using Mundi REST API (free account)
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
- * ``config.base_uri`` (str) - default endpoint url
74
- * ``config.extract`` (bool) - (optional) extract downloaded archive or not
75
- * ``config.auth_error_code`` (int) - (optional) authentication error code
76
- * ``config.bucket_path_level`` (int) - (optional) bucket location index in path.split('/')
77
- * ``config.order_enabled`` (bool) - (optional) wether order is enabled or not if product is `OFFLINE`
78
- * ``config.order_method`` (str) - (optional) HTTP request method, GET (default) or POST
79
- * ``config.order_headers`` (dict) - (optional) order request headers
80
- * ``config.order_on_response`` (dict) - (optional) edit or add new product properties
81
- * ``config.order_status`` (:class:`~eodag.config.PluginConfig.OrderStatus`) - Order status handling
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: int = DEFAULT_DOWNLOAD_WAIT,
94
- timeout: int = DEFAULT_DOWNLOAD_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.order_download(product=product, auth=auth)
133
+ self.http_download_plugin._order(product=product, auth=auth)
129
134
 
130
- @self._download_retry(product, wait, timeout)
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.order_download_status(
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
- "HTTP Error %s returned, %s\nPlease check your credentials for %s"
193
- % (
194
- err.response.status_code,
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(str(err))
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 load_config, merge_configs
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 MisconfiguredError, UnsupportedProvider
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 = {"search", "download", "crunch", "auth", "api"}
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(self, provider: str) -> Optional[Authentication]:
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
- :returns: The Authentication plugin for the provider
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
- plugin_conf = self.providers_config[provider]
260
- auth: Optional[PluginConfig] = getattr(plugin_conf, "auth", None)
261
- if not auth:
262
- # We guess the plugin being built is of type Api, therefore no need
263
- # for an Auth plugin.
264
- return None
265
- auth.priority = plugin_conf.priority
266
- plugin = cast(
267
- Authentication,
268
- self._build_plugin(provider, auth, Authentication),
269
- )
270
- return plugin
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[(provider, topic_class)].priority = priority
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[(provider, topic_class.__name__)] = plugin
482
+ self._built_plugins_cache[
483
+ (provider, topic_class.__name__, auth_match_md5)
484
+ ] = plugin
340
485
  return plugin
@@ -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 EOProduct instances (see eodag.api.product module)
99
- which will be processed by a Download plugin (2) and the total number of products matching
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 `discover_queryables` conf
109
+ """Fetch queryables list from provider using :attr:`~eodag.config.PluginConfig.discover_queryables` conf
112
110
 
113
- :param kwargs: additional filters for queryables (`productType` and other search
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 "sort_by" argument from the kwargs or the provider default sort configuration
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 "sort_by" argument from the kwargs or the provider default sort configuration
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 "sort_by" argument into a provider-specific string or dictionnary
238
+ the ``sort_by`` argument into a provider-specific string or dictionary
220
239
 
221
- :param sort_by_arg: the "sort_by" argument in EODAG format
222
- :returns: The "sort_by" argument in provider-specific format
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(set(sort_by_arg))
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
- ) -> Dict[str, Annotated[Any, FieldInfo]]:
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
- default_values: Dict[str, Any] = deepcopy(
324
- getattr(self.config, "products", {}).get(product_type, {})
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
- default_values.pop("metadata_mapping", None)
327
-
328
- queryables: Dict[str, Annotated[Any, FieldInfo]] = {}
329
- try:
330
- queryables = self.discover_queryables(**{**default_values, **filters}) or {}
331
- except NotImplementedError:
332
- pass
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