eodag 3.0.0b3__py3-none-any.whl → 3.0.1__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 (71) hide show
  1. eodag/api/core.py +189 -125
  2. eodag/api/product/metadata_mapping.py +12 -3
  3. eodag/api/search_result.py +29 -3
  4. eodag/cli.py +35 -19
  5. eodag/config.py +412 -116
  6. eodag/plugins/apis/base.py +10 -4
  7. eodag/plugins/apis/ecmwf.py +14 -4
  8. eodag/plugins/apis/usgs.py +25 -2
  9. eodag/plugins/authentication/aws_auth.py +14 -5
  10. eodag/plugins/authentication/base.py +10 -1
  11. eodag/plugins/authentication/generic.py +14 -3
  12. eodag/plugins/authentication/header.py +12 -4
  13. eodag/plugins/authentication/keycloak.py +41 -22
  14. eodag/plugins/authentication/oauth.py +11 -1
  15. eodag/plugins/authentication/openid_connect.py +178 -163
  16. eodag/plugins/authentication/qsauth.py +12 -4
  17. eodag/plugins/authentication/sas_auth.py +19 -2
  18. eodag/plugins/authentication/token.py +57 -10
  19. eodag/plugins/authentication/token_exchange.py +19 -19
  20. eodag/plugins/crunch/base.py +4 -1
  21. eodag/plugins/crunch/filter_date.py +5 -2
  22. eodag/plugins/crunch/filter_latest_intersect.py +5 -4
  23. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
  24. eodag/plugins/crunch/filter_overlap.py +5 -7
  25. eodag/plugins/crunch/filter_property.py +4 -3
  26. eodag/plugins/download/aws.py +39 -22
  27. eodag/plugins/download/base.py +11 -11
  28. eodag/plugins/download/creodias_s3.py +11 -2
  29. eodag/plugins/download/http.py +86 -52
  30. eodag/plugins/download/s3rest.py +20 -18
  31. eodag/plugins/manager.py +168 -23
  32. eodag/plugins/search/base.py +33 -14
  33. eodag/plugins/search/build_search_result.py +55 -51
  34. eodag/plugins/search/cop_marine.py +112 -29
  35. eodag/plugins/search/creodias_s3.py +20 -5
  36. eodag/plugins/search/csw.py +41 -1
  37. eodag/plugins/search/data_request_search.py +109 -9
  38. eodag/plugins/search/qssearch.py +532 -152
  39. eodag/plugins/search/static_stac_search.py +20 -21
  40. eodag/resources/ext_product_types.json +1 -1
  41. eodag/resources/product_types.yml +187 -56
  42. eodag/resources/providers.yml +1610 -1701
  43. eodag/resources/stac.yml +3 -163
  44. eodag/resources/user_conf_template.yml +112 -97
  45. eodag/rest/config.py +1 -2
  46. eodag/rest/constants.py +0 -1
  47. eodag/rest/core.py +61 -51
  48. eodag/rest/errors.py +181 -0
  49. eodag/rest/server.py +24 -325
  50. eodag/rest/stac.py +93 -544
  51. eodag/rest/types/eodag_search.py +13 -8
  52. eodag/rest/types/queryables.py +1 -2
  53. eodag/rest/types/stac_search.py +11 -2
  54. eodag/types/__init__.py +15 -3
  55. eodag/types/download_args.py +1 -1
  56. eodag/types/queryables.py +1 -2
  57. eodag/types/search_args.py +3 -3
  58. eodag/utils/__init__.py +77 -57
  59. eodag/utils/exceptions.py +23 -9
  60. eodag/utils/logging.py +37 -77
  61. eodag/utils/requests.py +1 -3
  62. eodag/utils/stac_reader.py +1 -1
  63. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/METADATA +11 -12
  64. eodag-3.0.1.dist-info/RECORD +109 -0
  65. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/WHEEL +1 -1
  66. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/entry_points.txt +1 -0
  67. eodag/resources/constraints/climate-dt.json +0 -13
  68. eodag/resources/constraints/extremes-dt.json +0 -8
  69. eodag-3.0.0b3.dist-info/RECORD +0 -110
  70. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/LICENSE +0 -0
  71. {eodag-3.0.0b3.dist-info → eodag-3.0.1.dist-info}/top_level.txt +0 -0
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import logging
21
21
  import os
22
+ import re
22
23
  import shutil
23
24
  import tarfile
24
25
  import zipfile
@@ -79,6 +80,7 @@ from eodag.utils.exceptions import (
79
80
  DownloadError,
80
81
  MisconfiguredError,
81
82
  NotAvailableError,
83
+ RequestError,
82
84
  TimeOutError,
83
85
  )
84
86
 
@@ -100,18 +102,47 @@ class HTTPDownload(Download):
100
102
  :param provider: provider name
101
103
  :param config: Download plugin configuration:
102
104
 
103
- * ``config.base_uri`` (str) - (optional) default endpoint url
104
- * ``config.extract`` (bool) - (optional) extract downloaded archive or not
105
- * ``config.auth_error_code`` (int) - (optional) authentication error code
106
- * ``config.dl_url_params`` (dict) - (optional) attitional parameters to send in the request
107
- * ``config.archive_depth`` (int) - (optional) level in extracted path tree where to find data
108
- * ``config.flatten_top_dirs`` (bool) - (optional) flatten directory structure
109
- * ``config.ignore_assets`` (bool) - (optional) ignore assets and download using downloadLink
110
- * ``config.order_enabled`` (bool) - (optional) wether order is enabled or not if product is `OFFLINE`
111
- * ``config.order_method`` (str) - (optional) HTTP request method, GET (default) or POST
112
- * ``config.order_headers`` (dict) - (optional) order request headers
113
- * ``config.order_on_response`` (dict) - (optional) edit or add new product properties
114
- * ``config.order_status`` (:class:`~eodag.config.PluginConfig.OrderStatus`) - (optional) Order status handling
105
+ * :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): ``HTTPDownload``
106
+ * :attr:`~eodag.config.PluginConfig.base_uri` (``str``): default endpoint url
107
+ * :attr:`~eodag.config.PluginConfig.method` (``str``): HTTP request method for the download request (``GET`` or
108
+ ``POST``); default: ``GET``
109
+ * :attr:`~eodag.config.PluginConfig.extract` (``bool``): if the content of the downloaded file should be
110
+ extracted; default: ``True``
111
+ * :attr:`~eodag.config.PluginConfig.auth_error_code` (``int``): which error code is returned in case of an
112
+ authentication error
113
+ * :attr:`~eodag.config.PluginConfig.dl_url_params` (``Dict[str, Any]``): parameters to be
114
+ added to the query params of the request
115
+ * :attr:`~eodag.config.PluginConfig.archive_depth` (``int``): level in extracted path tree where to find data;
116
+ default: ``1``
117
+ * :attr:`~eodag.config.PluginConfig.flatten_top_dirs` (``bool``): if the directory structure should be
118
+ flattened; default: ``True``
119
+ * :attr:`~eodag.config.PluginConfig.ignore_assets` (``bool``): ignore assets and download using downloadLink;
120
+ default: ``False``
121
+ * :attr:`~eodag.config.PluginConfig.timeout` (``int``): time to wait until request timeout in seconds;
122
+ default: ``5``
123
+ * :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates should be verified in
124
+ requests; default: ``True``
125
+ * :attr:`~eodag.config.PluginConfig.output_extension` (``str``): which extension should be used for the
126
+ downloaded file
127
+ * :attr:`~eodag.config.PluginConfig.no_auth_download` (``bool``): if the download should be done without
128
+ authentication; default: ``True``
129
+ * :attr:`~eodag.config.PluginConfig.order_enabled` (``bool``): if the product has to be ordered to download it;
130
+ if this parameter is set to true, a mapping for the orderLink has to be added to the metadata mapping of
131
+ the search plugin used for the provider; default: ``False``
132
+ * :attr:`~eodag.config.PluginConfig.order_method` (``str``): HTTP request method for the order request (``GET``
133
+ or ``POST``); default: ``GET``
134
+ * :attr:`~eodag.config.PluginConfig.order_headers` (``[Dict[str, str]]``): headers to be added to the order
135
+ request
136
+ * :attr:`~eodag.config.PluginConfig.order_on_response` (:class:`~eodag.config.PluginConfig.OrderOnResponse`):
137
+ a typed dictionary containing the key ``metadata_mapping`` which can be used to add new product properties
138
+ based on the data in response to the order request
139
+ * :attr:`~eodag.config.PluginConfig.order_status` (:class:`~eodag.config.PluginConfig.OrderStatus`):
140
+ configuration to handle the order status; contains information which method to use, how the response data is
141
+ interpreted, which status corresponds to success, ordered and error and what should be done on success.
142
+ * :attr:`~eodag.config.PluginConfig.products` (``Dict[str, Dict[str, Any]``): product type specific config; the
143
+ keys are the product types, the values are dictionaries which can contain the keys
144
+ :attr:`~eodag.config.PluginConfig.output_extension` and :attr:`~eodag.config.PluginConfig.extract` to
145
+ overwrite the provider config for a specific product type
115
146
 
116
147
  """
117
148
 
@@ -130,12 +161,13 @@ class HTTPDownload(Download):
130
161
  and has `orderLink` in its properties.
131
162
  Product ordering can be configured using the following download plugin parameters:
132
163
 
133
- - **order_enabled**: Wether order is enabled or not (may not use this method
164
+ - :attr:`~eodag.config.PluginConfig.order_enabled`: Wether order is enabled or not (may not use this method
134
165
  if no `orderLink` exists)
135
166
 
136
- - **order_method**: (optional) HTTP request method, GET (default) or POST
167
+ - :attr:`~eodag.config.PluginConfig.order_method`: (optional) HTTP request method, GET (default) or POST
137
168
 
138
- - **order_on_response**: (optional) things to do with obtained order response:
169
+ - :attr:`~eodag.config.PluginConfig.order_on_response`: (optional) things to do with obtained order
170
+ response:
139
171
 
140
172
  - *metadata_mapping*: edit or add new product propoerties properties
141
173
 
@@ -193,18 +225,11 @@ class HTTPDownload(Download):
193
225
  logger.debug(ordered_message)
194
226
  product.properties["storageStatus"] = STAGING_STATUS
195
227
  except RequestException as e:
196
- if hasattr(e, "response") and (
197
- content := getattr(e.response, "content", None)
198
- ):
199
- error_message = f"{content.decode('utf-8')} - {e}"
200
- else:
201
- error_message = str(e)
202
- logger.warning(
203
- "%s could not be ordered, request returned %s",
204
- product.properties["title"],
205
- error_message,
206
- )
207
228
  self._check_auth_exception(e)
229
+ title = product.properties["title"]
230
+ message = f"{title} could not be ordered"
231
+ raise RequestError.from_error(e, message) from e
232
+
208
233
  return self.order_response_process(response, product)
209
234
  except requests.exceptions.Timeout as exc:
210
235
  raise TimeOutError(exc, timeout=timeout) from exc
@@ -256,7 +281,7 @@ class HTTPDownload(Download):
256
281
  It will be executed before each download retry.
257
282
  Product order status request can be configured using the following download plugin parameters:
258
283
 
259
- - **order_status**: :class:`~eodag.config.PluginConfig.OrderStatus`
284
+ - :attr:`~eodag.config.PluginConfig.order_status`: :class:`~eodag.config.PluginConfig.OrderStatus`
260
285
 
261
286
  Product properties used for order status:
262
287
 
@@ -739,7 +764,7 @@ class HTTPDownload(Download):
739
764
  **kwargs: Unpack[DownloadConf],
740
765
  ) -> StreamResponse:
741
766
  r"""
742
- Returns dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
767
+ Returns dictionary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
743
768
  It contains a generator to streamed download chunks and the response headers.
744
769
 
745
770
  :param product: The EO product to download
@@ -752,7 +777,7 @@ class HTTPDownload(Download):
752
777
  and `dl_url_params` (dict) can be provided as additional kwargs
753
778
  and will override any other values defined in a configuration
754
779
  file or with environment variables.
755
- :returns: Dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments
780
+ :returns: Dictionary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments
756
781
  """
757
782
  if auth is not None and not isinstance(auth, AuthBase):
758
783
  raise MisconfiguredError(f"Incompatible auth plugin: {type(auth)}")
@@ -837,12 +862,9 @@ class HTTPDownload(Download):
837
862
  and e.response.status_code in auth_errors
838
863
  ):
839
864
  raise AuthenticationError(
840
- "HTTP Error %s returned, %s\nPlease check your credentials for %s"
841
- % (
842
- e.response.status_code,
843
- response_text,
844
- self.provider,
845
- )
865
+ f"Please check your credentials for {self.provider}.",
866
+ f"HTTP Error {e.response.status_code} returned.",
867
+ response_text,
846
868
  )
847
869
 
848
870
  def _process_exception(
@@ -903,6 +925,8 @@ class HTTPDownload(Download):
903
925
  logger.info("Progress bar unavailable, please call product.download()")
904
926
  progress_callback = ProgressCallback(disable=True)
905
927
 
928
+ ssl_verify = getattr(self.config, "ssl_verify", True)
929
+
906
930
  ordered_message = ""
907
931
  if (
908
932
  "orderLink" in product.properties
@@ -953,6 +977,7 @@ class HTTPDownload(Download):
953
977
  params=params,
954
978
  headers=USER_AGENT,
955
979
  timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
980
+ verify=ssl_verify,
956
981
  **req_kwargs,
957
982
  ) as self.stream:
958
983
  try:
@@ -1055,6 +1080,16 @@ class HTTPDownload(Download):
1055
1080
  "flatten_top_dirs", getattr(self.config, "flatten_top_dirs", True)
1056
1081
  )
1057
1082
  ssl_verify = getattr(self.config, "ssl_verify", True)
1083
+ matching_url = (
1084
+ getattr(product.downloader_auth.config, "matching_url", "")
1085
+ if product.downloader_auth
1086
+ else ""
1087
+ )
1088
+ matching_conf = (
1089
+ getattr(product.downloader_auth.config, "matching_conf", None)
1090
+ if product.downloader_auth
1091
+ else None
1092
+ )
1058
1093
 
1059
1094
  # loop for assets download
1060
1095
  for asset in assets_values:
@@ -1063,11 +1098,16 @@ class HTTPDownload(Download):
1063
1098
  f"Local asset detected. Download skipped for {asset['href']}"
1064
1099
  )
1065
1100
  continue
1066
-
1101
+ if matching_conf or (
1102
+ matching_url and re.match(matching_url, asset["href"])
1103
+ ):
1104
+ auth_object = auth
1105
+ else:
1106
+ auth_object = None
1067
1107
  with requests.get(
1068
1108
  asset["href"],
1069
1109
  stream=True,
1070
- auth=auth,
1110
+ auth=auth_object,
1071
1111
  params=params,
1072
1112
  headers=USER_AGENT,
1073
1113
  timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
@@ -1080,8 +1120,7 @@ class HTTPDownload(Download):
1080
1120
  exc, timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT
1081
1121
  ) from exc
1082
1122
  except RequestException as e:
1083
- raise_errors = True if len(assets_values) == 1 else False
1084
- self._handle_asset_exception(e, asset, raise_errors=raise_errors)
1123
+ self._handle_asset_exception(e, asset)
1085
1124
  else:
1086
1125
  asset_rel_path = (
1087
1126
  asset.rel_path.replace(assets_common_subdir, "").strip(os.sep)
@@ -1239,27 +1278,22 @@ class HTTPDownload(Download):
1239
1278
 
1240
1279
  return fs_dir_path
1241
1280
 
1242
- def _handle_asset_exception(
1243
- self, e: RequestException, asset: Asset, raise_errors: bool = False
1244
- ) -> None:
1281
+ def _handle_asset_exception(self, e: RequestException, asset: Asset) -> None:
1245
1282
  # check if error is identified as auth_error in provider conf
1246
1283
  auth_errors = getattr(self.config, "auth_error_code", [None])
1247
1284
  if not isinstance(auth_errors, list):
1248
1285
  auth_errors = [auth_errors]
1249
1286
  if e.response is not None and e.response.status_code in auth_errors:
1250
1287
  raise AuthenticationError(
1251
- "HTTP Error %s returned, %s\nPlease check your credentials for %s"
1252
- % (
1253
- e.response.status_code,
1254
- e.response.text.strip(),
1255
- self.provider,
1256
- )
1288
+ f"Please check your credentials for {self.provider}.",
1289
+ f"HTTP Error {e.response.status_code} returned.",
1290
+ e.response.text.strip(),
1257
1291
  )
1258
- elif raise_errors:
1259
- raise DownloadError(e)
1260
1292
  else:
1261
- logger.warning("Unexpected error: %s" % e)
1262
- logger.warning("Skipping %s" % asset["href"])
1293
+ logger.error(
1294
+ "Unexpected error at download of asset %s: %s", asset["href"], e
1295
+ )
1296
+ raise DownloadError(e)
1263
1297
 
1264
1298
  def _get_asset_sizes(
1265
1299
  self,
@@ -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:
@@ -189,12 +194,9 @@ class S3RestDownload(Download):
189
194
  auth_errors = [auth_errors]
190
195
  if err.response and err.response.status_code in auth_errors:
191
196
  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
- )
197
+ f"Please check your credentials for {self.provider}.",
198
+ f"HTTP Error {err.response.status_code} returned.",
199
+ err.response.text.strip(),
198
200
  )
199
201
  # product not available
200
202
  elif (
@@ -225,7 +227,7 @@ class S3RestDownload(Download):
225
227
  self.__class__.__name__,
226
228
  bucket_contents.text,
227
229
  )
228
- raise RequestError(str(err))
230
+ raise RequestError.from_error(err) from err
229
231
  try:
230
232
  xmldoc = minidom.parseString(bucket_contents.text)
231
233
  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