eodag 3.0.1__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 (44) hide show
  1. eodag/api/core.py +116 -86
  2. eodag/api/product/_assets.py +6 -6
  3. eodag/api/product/_product.py +18 -18
  4. eodag/api/product/metadata_mapping.py +39 -11
  5. eodag/cli.py +22 -1
  6. eodag/config.py +14 -14
  7. eodag/plugins/apis/ecmwf.py +37 -14
  8. eodag/plugins/apis/usgs.py +5 -5
  9. eodag/plugins/authentication/openid_connect.py +2 -2
  10. eodag/plugins/authentication/token.py +37 -6
  11. eodag/plugins/crunch/filter_property.py +2 -3
  12. eodag/plugins/download/aws.py +11 -12
  13. eodag/plugins/download/base.py +30 -39
  14. eodag/plugins/download/creodias_s3.py +29 -0
  15. eodag/plugins/download/http.py +144 -152
  16. eodag/plugins/download/s3rest.py +5 -7
  17. eodag/plugins/search/base.py +73 -25
  18. eodag/plugins/search/build_search_result.py +1047 -310
  19. eodag/plugins/search/creodias_s3.py +25 -19
  20. eodag/plugins/search/data_request_search.py +1 -1
  21. eodag/plugins/search/qssearch.py +51 -139
  22. eodag/resources/ext_product_types.json +1 -1
  23. eodag/resources/product_types.yml +391 -32
  24. eodag/resources/providers.yml +678 -1744
  25. eodag/rest/core.py +92 -62
  26. eodag/rest/server.py +31 -4
  27. eodag/rest/types/eodag_search.py +6 -0
  28. eodag/rest/types/queryables.py +5 -6
  29. eodag/rest/utils/__init__.py +3 -0
  30. eodag/types/__init__.py +56 -15
  31. eodag/types/download_args.py +2 -2
  32. eodag/types/queryables.py +180 -72
  33. eodag/types/whoosh.py +126 -0
  34. eodag/utils/__init__.py +71 -10
  35. eodag/utils/exceptions.py +27 -20
  36. eodag/utils/repr.py +65 -6
  37. eodag/utils/requests.py +11 -11
  38. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/METADATA +76 -76
  39. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/RECORD +43 -44
  40. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/WHEEL +1 -1
  41. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/entry_points.txt +3 -2
  42. eodag/utils/constraints.py +0 -244
  43. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/LICENSE +0 -0
  44. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/top_level.txt +0 -0
@@ -20,16 +20,21 @@ from __future__ import annotations
20
20
  import logging
21
21
  import os
22
22
  from datetime import datetime, timezone
23
- from typing import TYPE_CHECKING
23
+ from typing import TYPE_CHECKING, Annotated
24
24
 
25
25
  import geojson
26
26
  from ecmwfapi import ECMWFDataServer, ECMWFService
27
27
  from ecmwfapi.api import APIException, Connection, get_apikey_values
28
+ from pydantic.fields import FieldInfo
28
29
 
29
30
  from eodag.plugins.apis.base import Api
30
31
  from eodag.plugins.search import PreparedSearch
31
32
  from eodag.plugins.search.base import Search
32
- from eodag.plugins.search.build_search_result import BuildPostSearchResult
33
+ from eodag.plugins.search.build_search_result import (
34
+ ECMWF_KEYWORDS,
35
+ ECMWFSearch,
36
+ keywords_to_mdt,
37
+ )
33
38
  from eodag.utils import (
34
39
  DEFAULT_DOWNLOAD_TIMEOUT,
35
40
  DEFAULT_DOWNLOAD_WAIT,
@@ -58,7 +63,7 @@ logger = logging.getLogger("eodag.apis.ecmwf")
58
63
  ECMWF_MARS_KNOWN_FORMATS = {"grib": "grib", "netcdf": "nc"}
59
64
 
60
65
 
61
- class EcmwfApi(Api, BuildPostSearchResult):
66
+ class EcmwfApi(Api, ECMWFSearch):
62
67
  """A plugin that enables to build download-request and download data on ECMWF MARS.
63
68
 
64
69
  Builds a single ready-to-download :class:`~eodag.api.product._product.EOProduct`
@@ -69,14 +74,15 @@ class EcmwfApi(Api, BuildPostSearchResult):
69
74
  query).
70
75
 
71
76
  This class inherits from :class:`~eodag.plugins.apis.base.Api` for compatibility and
72
- :class:`~eodag.plugins.search.build_search_result.BuildPostSearchResult` for the creation
77
+ :class:`~eodag.plugins.search.build_search_result.ECMWFSearch` for the creation
73
78
  of the search result.
74
79
 
75
80
  :param provider: provider name
76
81
  :param config: Api plugin configuration:
77
82
 
78
83
  * :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): EcmwfApi
79
- * :attr:`~eodag.config.PluginConfig.api_endpoint` (``str``) (**mandatory**): url of the ecmwf api
84
+ * :attr:`~eodag.config.PluginConfig.auth_endpoint` (``str``) (**mandatory**): url of
85
+ the authentication endpoint of the ecmwf api
80
86
  * :attr:`~eodag.config.PluginConfig.metadata_mapping` (``Dict[str, Union[str, list]]``): how
81
87
  parameters should be mapped between the provider and eodag; If a string is given, this is
82
88
  the mapping parameter returned by provider -> eodag parameter. If a list with 2 elements
@@ -86,12 +92,17 @@ class EcmwfApi(Api, BuildPostSearchResult):
86
92
 
87
93
  def __init__(self, provider: str, config: PluginConfig) -> None:
88
94
  # init self.config.metadata_mapping using Search Base plugin
95
+ config.metadata_mapping = {
96
+ **keywords_to_mdt(ECMWF_KEYWORDS, "ecmwf"),
97
+ **config.metadata_mapping,
98
+ }
89
99
  Search.__init__(self, provider, config)
90
100
 
91
101
  # needed by QueryStringSearch.build_query_string / format_free_text_search
92
102
  self.config.__dict__.setdefault("free_text_search_operations", {})
93
103
  # needed for compatibility
94
104
  self.config.__dict__.setdefault("pagination", {"next_page_query_obj": "{{}}"})
105
+ self.config.__dict__.setdefault("api_endpoint", "")
95
106
 
96
107
  def do_search(self, *args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
97
108
  """Should perform the actual search request."""
@@ -108,9 +119,9 @@ class EcmwfApi(Api, BuildPostSearchResult):
108
119
  # productType
109
120
  if not kwargs.get("productType"):
110
121
  kwargs["productType"] = "%s_%s_%s" % (
111
- kwargs.get("dataset", "mars"),
112
- kwargs.get("type", ""),
113
- kwargs.get("levtype", ""),
122
+ kwargs.get("ecmwf:dataset", "mars"),
123
+ kwargs.get("ecmwf:type", ""),
124
+ kwargs.get("ecmwf:levtype", ""),
114
125
  )
115
126
  # start date
116
127
  if "startTimeFromAscendingNode" not in kwargs:
@@ -132,7 +143,7 @@ class EcmwfApi(Api, BuildPostSearchResult):
132
143
  if "geometry" in kwargs:
133
144
  kwargs["geometry"] = get_geometry_from_various(geometry=kwargs["geometry"])
134
145
 
135
- return BuildPostSearchResult.query(self, prep, **kwargs)
146
+ return ECMWFSearch.query(self, prep, **kwargs)
136
147
 
137
148
  def authenticate(self) -> Dict[str, Optional[str]]:
138
149
  """Check credentials and returns information needed for auth
@@ -143,7 +154,7 @@ class EcmwfApi(Api, BuildPostSearchResult):
143
154
  # Get credentials from eodag or using ecmwf conf
144
155
  email = getattr(self.config, "credentials", {}).get("username", None)
145
156
  key = getattr(self.config, "credentials", {}).get("password", None)
146
- url = getattr(self.config, "api_endpoint", None)
157
+ url = getattr(self.config, "auth_endpoint", None)
147
158
  if not all([email, key, url]):
148
159
  key, url, email = get_apikey_values()
149
160
 
@@ -167,8 +178,8 @@ class EcmwfApi(Api, BuildPostSearchResult):
167
178
  product: EOProduct,
168
179
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
169
180
  progress_callback: Optional[ProgressCallback] = None,
170
- wait: int = DEFAULT_DOWNLOAD_WAIT,
171
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
181
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
182
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
172
183
  **kwargs: Unpack[DownloadConf],
173
184
  ) -> Optional[str]:
174
185
  """Download data from ECMWF MARS"""
@@ -257,8 +268,8 @@ class EcmwfApi(Api, BuildPostSearchResult):
257
268
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
258
269
  downloaded_callback: Optional[DownloadedCallback] = None,
259
270
  progress_callback: Optional[ProgressCallback] = None,
260
- wait: int = DEFAULT_DOWNLOAD_WAIT,
261
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
271
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
272
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
262
273
  **kwargs: Unpack[DownloadConf],
263
274
  ) -> List[str]:
264
275
  """
@@ -277,3 +288,15 @@ class EcmwfApi(Api, BuildPostSearchResult):
277
288
  def clear(self) -> None:
278
289
  """Clear search context"""
279
290
  pass
291
+
292
+ def discover_queryables(
293
+ self, **kwargs: Any
294
+ ) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
295
+ """Fetch queryables list from provider using metadata mapping
296
+
297
+ :param kwargs: additional filters for queryables (`productType` and other search
298
+ arguments)
299
+ :returns: fetched queryable parameters dict
300
+ """
301
+ product_type = kwargs.get("productType", None)
302
+ return self.queryables_from_metadata_mapping(product_type)
@@ -297,8 +297,8 @@ class UsgsApi(Api):
297
297
  product: EOProduct,
298
298
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
299
299
  progress_callback: Optional[ProgressCallback] = None,
300
- wait: int = DEFAULT_DOWNLOAD_WAIT,
301
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
300
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
301
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
302
302
  **kwargs: Unpack[DownloadConf],
303
303
  ) -> Optional[str]:
304
304
  """Download data from USGS catalogues"""
@@ -375,7 +375,7 @@ class UsgsApi(Api):
375
375
  logger.debug(f"Downloading {req_url}")
376
376
  ssl_verify = getattr(self.config, "ssl_verify", True)
377
377
 
378
- @self._download_retry(product, wait, timeout)
378
+ @self._order_download_retry(product, wait, timeout)
379
379
  def download_request(
380
380
  product: EOProduct,
381
381
  fs_path: str,
@@ -467,8 +467,8 @@ class UsgsApi(Api):
467
467
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
468
468
  downloaded_callback: Optional[DownloadedCallback] = None,
469
469
  progress_callback: Optional[ProgressCallback] = None,
470
- wait: int = DEFAULT_DOWNLOAD_WAIT,
471
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
470
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
471
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
472
472
  **kwargs: Unpack[DownloadConf],
473
473
  ) -> List[str]:
474
474
  """
@@ -68,10 +68,10 @@ class OIDCRefreshTokenBase(Authentication):
68
68
  self.session = requests.Session()
69
69
 
70
70
  self.access_token = ""
71
- self.access_token_expiration = datetime.min
71
+ self.access_token_expiration = datetime.min.replace(tzinfo=timezone.utc)
72
72
 
73
73
  self.refresh_token = ""
74
- self.refresh_token_expiration = datetime.min
74
+ self.refresh_token_expiration = datetime.min.replace(tzinfo=timezone.utc)
75
75
 
76
76
  try:
77
77
  response = requests.get(self.config.oidc_config_url)
@@ -47,7 +47,12 @@ logger = logging.getLogger("eodag.authentication.token")
47
47
 
48
48
 
49
49
  class TokenAuth(Authentication):
50
- """TokenAuth authentication plugin - fetches a token which is added to search/download requests
50
+ """TokenAuth authentication plugin - fetches a token which is added to search/download requests.
51
+
52
+ When using headers, if only :attr:`~eodag.config.PluginConfig.headers` is given, it will be used for both token
53
+ retrieve and authentication. If :attr:`~eodag.config.PluginConfig.retrieve_headers` is given, it will be used for
54
+ token retrieve only. If both are given, :attr:`~eodag.config.PluginConfig.retrieve_headers` will be used for token
55
+ retrieve and :attr:`~eodag.config.PluginConfig.headers` for authentication.
51
56
 
52
57
  :param provider: provider name
53
58
  :param config: Authentication plugin configuration:
@@ -55,6 +60,10 @@ class TokenAuth(Authentication):
55
60
  * :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): TokenAuth
56
61
  * :attr:`~eodag.config.PluginConfig.auth_uri` (``str``) (**mandatory**): url used to fetch
57
62
  the access token with user/password
63
+ * :attr:`~eodag.config.PluginConfig.headers` (``Dict[str, str]``): Dictionary containing all
64
+ keys/value pairs that should be added to the headers
65
+ * :attr:`~eodag.config.PluginConfig.retrieve_headers` (``Dict[str, str]``): Dictionary containing all
66
+ keys/value pairs that should be added to the headers for token retrieve only
58
67
  * :attr:`~eodag.config.PluginConfig.refresh_uri` (``str``) : url used to fetch the
59
68
  access token with a refresh token
60
69
  * :attr:`~eodag.config.PluginConfig.token_type` (``str``): type of the token (``json``
@@ -91,11 +100,29 @@ class TokenAuth(Authentication):
91
100
  self.config.auth_uri = self.config.auth_uri.format(
92
101
  **self.config.credentials
93
102
  )
94
- # format headers if needed (and accepts {token} to be formatted later)
103
+
104
+ # Format headers if needed with values from the credentials. Note:
105
+ # if only 'headers' is given, it will be used for both token retrieve and authentication.
106
+ # if 'retrieve_headers' is given, it will be used for token retrieve only.
107
+ # if both are given, 'retrieve_headers' will be used for token retrieve and 'headers' for authentication.
108
+
109
+ # If the authentication headers are undefined or None: use an empty dict.
110
+ # And don't format '{token}' now, it will be done later.
111
+ raw_headers = getattr(self.config, "headers", None) or {}
95
112
  self.config.headers = {
96
113
  header: value.format(**{"token": "{token}", **self.config.credentials})
97
- for header, value in getattr(self.config, "headers", {}).items()
114
+ for header, value in raw_headers.items()
98
115
  }
116
+
117
+ # If the retrieve headers are undefined, their attribute must not be set in self.config.
118
+ # If they are defined but empty, use an empty dict instead of None.
119
+ if hasattr(self.config, "retrieve_headers"):
120
+ raw_retrieve_headers = self.config.retrieve_headers or {}
121
+ self.config.retrieve_headers = {
122
+ header: value.format(**self.config.credentials)
123
+ for header, value in raw_retrieve_headers.items()
124
+ }
125
+
99
126
  except KeyError as e:
100
127
  raise MisconfiguredError(
101
128
  f"Missing credentials inputs for provider {self.provider}: {e}"
@@ -166,10 +193,14 @@ class TokenAuth(Authentication):
166
193
  status_forcelist=retry_status_forcelist,
167
194
  )
168
195
 
196
+ # Use the headers for retrieval if defined, else the headers for authentication
197
+ try:
198
+ headers = self.config.retrieve_headers
199
+ except AttributeError:
200
+ headers = self.config.headers
201
+
169
202
  # append headers to req if some are specified in config
170
- req_kwargs: Dict[str, Any] = {
171
- "headers": dict(self.config.headers, **USER_AGENT)
172
- }
203
+ req_kwargs: Dict[str, Any] = {"headers": dict(headers, **USER_AGENT)}
173
204
  ssl_verify = getattr(self.config, "ssl_verify", True)
174
205
 
175
206
  if self.refresh_token:
@@ -78,10 +78,9 @@ class FilterProperty(Crunch):
78
78
  for product in products:
79
79
  if property_key not in product.properties.keys():
80
80
  logger.warning(
81
- "%s not found in product.properties, filtering disabled.",
82
- property_key,
81
+ f"{property_key} not found in {product}.properties, product skipped",
83
82
  )
84
- return products
83
+ continue
85
84
  if operator_method(product.properties[property_key], property_value):
86
85
  add_to_filtered(product)
87
86
 
@@ -251,8 +251,8 @@ class AwsDownload(Download):
251
251
  product: EOProduct,
252
252
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
253
253
  progress_callback: Optional[ProgressCallback] = None,
254
- wait: int = DEFAULT_DOWNLOAD_WAIT,
255
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
254
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
255
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
256
256
  **kwargs: Unpack[DownloadConf],
257
257
  ) -> Optional[str]:
258
258
  """Download method for AWS S3 API.
@@ -482,7 +482,8 @@ class AwsDownload(Download):
482
482
  ignore_assets: Optional[bool] = False,
483
483
  ) -> List[Tuple[str, Optional[str]]]:
484
484
  """
485
- retrieves the bucket names and path prefixes for the assets
485
+ Retrieves the bucket names and path prefixes for the assets
486
+
486
487
  :param product: product for which the assets shall be downloaded
487
488
  :param asset_filter: text for which the assets should be filtered
488
489
  :param ignore_assets: if product instead of individual assets should be used
@@ -649,8 +650,8 @@ class AwsDownload(Download):
649
650
  product: EOProduct,
650
651
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
651
652
  progress_callback: Optional[ProgressCallback] = None,
652
- wait: int = DEFAULT_DOWNLOAD_WAIT,
653
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
653
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
654
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
654
655
  **kwargs: Unpack[DownloadConf],
655
656
  ) -> StreamResponse:
656
657
  r"""
@@ -731,12 +732,12 @@ class AwsDownload(Download):
731
732
  else sanitize(product.properties.get("id", "download"))
732
733
  )
733
734
 
734
- if len(assets_values) == 1:
735
+ if len(assets_values) <= 1:
735
736
  first_chunks_tuple = next(chunks_tuples)
736
737
  # update headers
737
738
  filename = os.path.basename(list(unique_product_chunks)[0].key)
738
739
  headers = {"content-disposition": f"attachment; filename={filename}"}
739
- if assets_values[0].get("type", None):
740
+ if assets_values and assets_values[0].get("type", None):
740
741
  headers["content-type"] = assets_values[0]["type"]
741
742
 
742
743
  return StreamResponse(
@@ -799,7 +800,6 @@ class AwsDownload(Download):
799
800
  common_path = self._get_commonpath(
800
801
  product, unique_product_chunks, build_safe
801
802
  )
802
-
803
803
  for product_chunk in unique_product_chunks:
804
804
  try:
805
805
  chunk_rel_path = self.get_chunk_dest_path(
@@ -817,8 +817,7 @@ class AwsDownload(Download):
817
817
  # out of SAFE format chunk
818
818
  logger.warning(e)
819
819
  continue
820
-
821
- if len(assets_values) == 1:
820
+ if len(assets_values) <= 1:
822
821
  yield from get_chunk_parts(product_chunk, progress_callback)
823
822
  else:
824
823
  yield (
@@ -1330,8 +1329,8 @@ class AwsDownload(Download):
1330
1329
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
1331
1330
  downloaded_callback: Optional[DownloadedCallback] = None,
1332
1331
  progress_callback: Optional[ProgressCallback] = None,
1333
- wait: int = DEFAULT_DOWNLOAD_WAIT,
1334
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
1332
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
1333
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
1335
1334
  **kwargs: Unpack[DownloadConf],
1336
1335
  ) -> List[str]:
1337
1336
  """
@@ -112,8 +112,8 @@ class Download(PluginTopic):
112
112
  product: EOProduct,
113
113
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
114
114
  progress_callback: Optional[ProgressCallback] = None,
115
- wait: int = DEFAULT_DOWNLOAD_WAIT,
116
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
115
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
116
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
117
117
  **kwargs: Unpack[DownloadConf],
118
118
  ) -> Optional[str]:
119
119
  r"""
@@ -142,8 +142,8 @@ class Download(PluginTopic):
142
142
  product: EOProduct,
143
143
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
144
144
  progress_callback: Optional[ProgressCallback] = None,
145
- wait: int = DEFAULT_DOWNLOAD_WAIT,
146
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
145
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
146
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
147
147
  **kwargs: Unpack[DownloadConf],
148
148
  ) -> StreamResponse:
149
149
  r"""
@@ -202,8 +202,8 @@ class Download(PluginTopic):
202
202
  or getattr(self.config, "output_dir", tempfile.gettempdir())
203
203
  or tempfile.gettempdir()
204
204
  )
205
- output_extension = kwargs.get("output_extension", None) or getattr(
206
- self.config, "output_extension", ".zip"
205
+ output_extension = kwargs.get("output_extension") or getattr(
206
+ self.config, "output_extension", ""
207
207
  )
208
208
 
209
209
  # Strong asumption made here: all products downloaded will be zip files
@@ -233,9 +233,13 @@ class Download(PluginTopic):
233
233
  logger.warning(
234
234
  f"Unable to create records directory. Got:\n{tb.format_exc()}",
235
235
  )
236
+ url_hash = hashlib.md5(url.encode("utf-8")).hexdigest()
237
+ old_record_filename = os.path.join(download_records_dir, url_hash)
236
238
  record_filename = os.path.join(
237
239
  download_records_dir, self.generate_record_hash(product)
238
240
  )
241
+ if os.path.isfile(old_record_filename):
242
+ os.rename(old_record_filename, record_filename)
239
243
  if os.path.isfile(record_filename) and os.path.isfile(fs_path):
240
244
  logger.info(
241
245
  f"Product already downloaded: {fs_path}",
@@ -339,13 +343,7 @@ class Download(PluginTopic):
339
343
  if delete_archive is not None
340
344
  else getattr(self.config, "delete_archive", True)
341
345
  )
342
- output_extension = kwargs.pop("output_extension", ".zip")
343
-
344
- product_path = (
345
- fs_path[: fs_path.index(output_extension)]
346
- if output_extension in fs_path
347
- else fs_path
348
- )
346
+ product_path, _ = os.path.splitext(fs_path)
349
347
  product_path_exists = os.path.exists(product_path)
350
348
  if product_path_exists and os.path.isfile(product_path):
351
349
  logger.info(
@@ -422,10 +420,10 @@ class Download(PluginTopic):
422
420
 
423
421
  tmp_dir.cleanup()
424
422
 
425
- if delete_archive:
423
+ if delete_archive and os.path.isfile(fs_path):
426
424
  logger.info(f"Deleting archive {os.path.basename(fs_path)}")
427
425
  os.unlink(fs_path)
428
- else:
426
+ elif os.path.isfile(fs_path):
429
427
  logger.info(
430
428
  f"Archive deletion is deactivated, keeping {os.path.basename(fs_path)}"
431
429
  )
@@ -444,8 +442,8 @@ class Download(PluginTopic):
444
442
  auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
445
443
  downloaded_callback: Optional[DownloadedCallback] = None,
446
444
  progress_callback: Optional[ProgressCallback] = None,
447
- wait: int = DEFAULT_DOWNLOAD_WAIT,
448
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
445
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
446
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
449
447
  **kwargs: Unpack[DownloadConf],
450
448
  ) -> List[str]:
451
449
  """
@@ -541,7 +539,7 @@ class Download(PluginTopic):
541
539
  )
542
540
  raise
543
541
 
544
- except RuntimeError:
542
+ except (RuntimeError, Exception):
545
543
  import traceback as tb
546
544
 
547
545
  logger.error(
@@ -549,16 +547,9 @@ class Download(PluginTopic):
549
547
  "Skipping it"
550
548
  )
551
549
  logger.debug(f"\n{tb.format_exc()}")
552
- stop_time = datetime.now()
553
-
554
- except Exception:
555
- import traceback as tb
556
550
 
557
- logger.warning(
558
- f"A problem occurred during download of product: {product}. "
559
- "Skipping it",
560
- )
561
- logger.debug(f"\n{tb.format_exc()}")
551
+ # product skipped, to not retry it
552
+ products.remove(product)
562
553
 
563
554
  if (
564
555
  len(products) > 0
@@ -585,14 +576,14 @@ class Download(PluginTopic):
585
576
 
586
577
  return paths
587
578
 
588
- def _download_retry(
589
- self, product: EOProduct, wait: int, timeout: int
579
+ def _order_download_retry(
580
+ self, product: EOProduct, wait: float, timeout: float
590
581
  ) -> Callable[[Callable[..., T]], Callable[..., T]]:
591
582
  """
592
- Download retry decorator.
583
+ Order download retry decorator.
593
584
 
594
- Retries the wrapped download method after `wait` minutes if a NotAvailableError
595
- exception is thrown until `timeout` minutes.
585
+ Retries the wrapped order_download method after ``wait`` minutes if a
586
+ ``NotAvailableError`` exception is thrown until ``timeout`` minutes.
596
587
 
597
588
  :param product: The EO product to download
598
589
  :param wait: If download fails, wait time in minutes between two download tries
@@ -601,7 +592,7 @@ class Download(PluginTopic):
601
592
  :returns: decorator
602
593
  """
603
594
 
604
- def decorator(download: Callable[..., T]) -> Callable[..., T]:
595
+ def decorator(order_download: Callable[..., T]) -> Callable[..., T]:
605
596
  def download_and_retry(*args: Any, **kwargs: Unpack[DownloadConf]) -> T:
606
597
  # initiate retry loop
607
598
  start_time = datetime.now()
@@ -618,7 +609,7 @@ class Download(PluginTopic):
618
609
  if datetime_now >= product.next_try:
619
610
  product.next_try += timedelta(minutes=wait)
620
611
  try:
621
- return download(*args, **kwargs)
612
+ return order_download(*args, **kwargs)
622
613
 
623
614
  except NotAvailableError as e:
624
615
  if not getattr(self.config, "order_enabled", False):
@@ -634,7 +625,7 @@ class Download(PluginTopic):
634
625
  ).seconds
635
626
  retry_count += 1
636
627
  retry_info = (
637
- f"[Retry #{retry_count}] Waited {wait_seconds}s, trying again to download ordered product"
628
+ f"[Retry #{retry_count}] Waited {wait_seconds}s, checking order status again"
638
629
  f" (retry every {wait}' for {timeout}')"
639
630
  )
640
631
  logger.info(not_available_info)
@@ -656,8 +647,8 @@ class Download(PluginTopic):
656
647
  ).microseconds / 1e6
657
648
  retry_count += 1
658
649
  retry_info = (
659
- f"[Retry #{retry_count}] Waiting {wait_seconds}s until next download try"
660
- f" for ordered product (retry every {wait}' for {timeout}')"
650
+ f"[Retry #{retry_count}] Waiting {wait_seconds}s until next order status check"
651
+ f" (retry every {wait}' for {timeout}')"
661
652
  )
662
653
  logger.info(not_available_info)
663
654
  # Retry-After info from Response header
@@ -678,12 +669,12 @@ class Download(PluginTopic):
678
669
  logger.info(not_available_info)
679
670
  raise NotAvailableError(
680
671
  f"{product.properties['title']} is not available ({product.properties['storageStatus']})"
681
- f" and could not be downloaded, timeout reached"
672
+ f" and order was not successfull, timeout reached"
682
673
  )
683
674
  elif datetime_now >= stop_time:
684
675
  raise NotAvailableError(not_available_info)
685
676
 
686
- return download(*args, **kwargs)
677
+ return order_download(*args, **kwargs)
687
678
 
688
679
  return download_and_retry
689
680
 
@@ -15,10 +15,12 @@
15
15
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
+ from typing import List, Optional, Tuple
18
19
 
19
20
  import boto3
20
21
  from botocore.exceptions import ClientError
21
22
 
23
+ from eodag import EOProduct
22
24
  from eodag.plugins.download.aws import AwsDownload
23
25
  from eodag.utils.exceptions import MisconfiguredError
24
26
 
@@ -65,3 +67,30 @@ class CreodiasS3Download(AwsDownload):
65
67
  list(objects.filter(Prefix=prefix).limit(1))
66
68
  self.s3_session = s3_session
67
69
  return objects
70
+
71
+ def _get_bucket_names_and_prefixes(
72
+ self,
73
+ product: EOProduct,
74
+ asset_filter: Optional[str] = None,
75
+ ignore_assets: Optional[bool] = False,
76
+ ) -> List[Tuple[str, Optional[str]]]:
77
+ """
78
+ Retrieves the bucket names and path prefixes for the assets
79
+
80
+ :param product: product for which the assets shall be downloaded
81
+ :param asset_filter: text for which the assets should be filtered
82
+ :param ignore_assets: if product instead of individual assets should be used
83
+ :return: tuples of bucket names and prefixes
84
+ """
85
+ # if assets are defined, use them instead of scanning product.location
86
+ if len(product.assets) > 0 and not ignore_assets:
87
+ bucket_names_and_prefixes = super()._get_bucket_names_and_prefixes(
88
+ product, asset_filter, ignore_assets
89
+ )
90
+ else:
91
+ # if no assets are given, use productIdentifier to get S3 path for download
92
+ s3_url = "s3:/" + product.properties["productIdentifier"]
93
+ bucket_names_and_prefixes = [
94
+ self.get_product_bucket_name_and_prefix(product, s3_url)
95
+ ]
96
+ return bucket_names_and_prefixes