eodag 3.0.0b3__py3-none-any.whl → 3.1.0__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 (94) hide show
  1. eodag/api/core.py +347 -247
  2. eodag/api/product/_assets.py +44 -15
  3. eodag/api/product/_product.py +58 -47
  4. eodag/api/product/drivers/__init__.py +81 -4
  5. eodag/api/product/drivers/base.py +65 -4
  6. eodag/api/product/drivers/generic.py +65 -0
  7. eodag/api/product/drivers/sentinel1.py +97 -0
  8. eodag/api/product/drivers/sentinel2.py +95 -0
  9. eodag/api/product/metadata_mapping.py +129 -93
  10. eodag/api/search_result.py +28 -12
  11. eodag/cli.py +61 -24
  12. eodag/config.py +457 -167
  13. eodag/plugins/apis/base.py +10 -4
  14. eodag/plugins/apis/ecmwf.py +53 -23
  15. eodag/plugins/apis/usgs.py +41 -17
  16. eodag/plugins/authentication/aws_auth.py +30 -18
  17. eodag/plugins/authentication/base.py +14 -3
  18. eodag/plugins/authentication/generic.py +14 -3
  19. eodag/plugins/authentication/header.py +14 -6
  20. eodag/plugins/authentication/keycloak.py +44 -25
  21. eodag/plugins/authentication/oauth.py +18 -4
  22. eodag/plugins/authentication/openid_connect.py +192 -171
  23. eodag/plugins/authentication/qsauth.py +12 -4
  24. eodag/plugins/authentication/sas_auth.py +22 -5
  25. eodag/plugins/authentication/token.py +95 -17
  26. eodag/plugins/authentication/token_exchange.py +19 -19
  27. eodag/plugins/base.py +4 -4
  28. eodag/plugins/crunch/base.py +8 -5
  29. eodag/plugins/crunch/filter_date.py +9 -6
  30. eodag/plugins/crunch/filter_latest_intersect.py +9 -8
  31. eodag/plugins/crunch/filter_latest_tpl_name.py +8 -8
  32. eodag/plugins/crunch/filter_overlap.py +9 -11
  33. eodag/plugins/crunch/filter_property.py +10 -10
  34. eodag/plugins/download/aws.py +181 -105
  35. eodag/plugins/download/base.py +49 -67
  36. eodag/plugins/download/creodias_s3.py +40 -2
  37. eodag/plugins/download/http.py +247 -223
  38. eodag/plugins/download/s3rest.py +29 -28
  39. eodag/plugins/manager.py +176 -41
  40. eodag/plugins/search/__init__.py +6 -5
  41. eodag/plugins/search/base.py +123 -60
  42. eodag/plugins/search/build_search_result.py +1046 -355
  43. eodag/plugins/search/cop_marine.py +132 -39
  44. eodag/plugins/search/creodias_s3.py +19 -68
  45. eodag/plugins/search/csw.py +48 -8
  46. eodag/plugins/search/data_request_search.py +124 -23
  47. eodag/plugins/search/qssearch.py +531 -310
  48. eodag/plugins/search/stac_list_assets.py +85 -0
  49. eodag/plugins/search/static_stac_search.py +23 -24
  50. eodag/resources/ext_product_types.json +1 -1
  51. eodag/resources/product_types.yml +1295 -355
  52. eodag/resources/providers.yml +1819 -3010
  53. eodag/resources/stac.yml +3 -163
  54. eodag/resources/stac_api.yml +2 -2
  55. eodag/resources/user_conf_template.yml +115 -99
  56. eodag/rest/cache.py +2 -2
  57. eodag/rest/config.py +3 -4
  58. eodag/rest/constants.py +0 -1
  59. eodag/rest/core.py +157 -117
  60. eodag/rest/errors.py +181 -0
  61. eodag/rest/server.py +57 -339
  62. eodag/rest/stac.py +133 -581
  63. eodag/rest/types/collections_search.py +3 -3
  64. eodag/rest/types/eodag_search.py +41 -30
  65. eodag/rest/types/queryables.py +42 -32
  66. eodag/rest/types/stac_search.py +15 -16
  67. eodag/rest/utils/__init__.py +14 -21
  68. eodag/rest/utils/cql_evaluate.py +6 -6
  69. eodag/rest/utils/rfc3339.py +2 -2
  70. eodag/types/__init__.py +153 -32
  71. eodag/types/bbox.py +2 -2
  72. eodag/types/download_args.py +4 -4
  73. eodag/types/queryables.py +183 -73
  74. eodag/types/search_args.py +6 -6
  75. eodag/types/whoosh.py +127 -3
  76. eodag/utils/__init__.py +228 -106
  77. eodag/utils/exceptions.py +47 -26
  78. eodag/utils/import_system.py +2 -2
  79. eodag/utils/logging.py +37 -77
  80. eodag/utils/repr.py +65 -6
  81. eodag/utils/requests.py +13 -15
  82. eodag/utils/rest.py +2 -2
  83. eodag/utils/s3.py +231 -0
  84. eodag/utils/stac_reader.py +11 -11
  85. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/METADATA +81 -81
  86. eodag-3.1.0.dist-info/RECORD +113 -0
  87. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
  88. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +5 -2
  89. eodag/resources/constraints/climate-dt.json +0 -13
  90. eodag/resources/constraints/extremes-dt.json +0 -8
  91. eodag/utils/constraints.py +0 -244
  92. eodag-3.0.0b3.dist-info/RECORD +0 -110
  93. {eodag-3.0.0b3.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
  94. {eodag-3.0.0b3.dist-info → eodag-3.1.0.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
@@ -26,17 +27,8 @@ from datetime import datetime
26
27
  from email.message import Message
27
28
  from itertools import chain
28
29
  from json import JSONDecodeError
29
- from typing import (
30
- TYPE_CHECKING,
31
- Any,
32
- Dict,
33
- Iterator,
34
- List,
35
- Optional,
36
- TypedDict,
37
- Union,
38
- cast,
39
- )
30
+ from pathlib import Path
31
+ from typing import TYPE_CHECKING, Any, Iterator, Optional, TypedDict, Union, cast
40
32
  from urllib.parse import parse_qs, urlparse
41
33
 
42
34
  import geojson
@@ -70,6 +62,7 @@ from eodag.utils import (
70
62
  guess_file_type,
71
63
  parse_header,
72
64
  path_to_uri,
65
+ rename_with_version,
73
66
  sanitize,
74
67
  string_to_jsonpath,
75
68
  uri_to_path,
@@ -80,6 +73,7 @@ from eodag.utils.exceptions import (
80
73
  MisconfiguredError,
81
74
  NotAvailableError,
82
75
  TimeOutError,
76
+ ValidationError,
83
77
  )
84
78
 
85
79
  if TYPE_CHECKING:
@@ -88,6 +82,7 @@ if TYPE_CHECKING:
88
82
  from eodag.api.product import Asset, EOProduct # type: ignore
89
83
  from eodag.api.search_result import SearchResult
90
84
  from eodag.config import PluginConfig
85
+ from eodag.types import S3SessionKwargs
91
86
  from eodag.types.download_args import DownloadConf
92
87
  from eodag.utils import DownloadedCallback, Unpack
93
88
 
@@ -100,42 +95,69 @@ class HTTPDownload(Download):
100
95
  :param provider: provider name
101
96
  :param config: Download plugin configuration:
102
97
 
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
98
+ * :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): ``HTTPDownload``
99
+ * :attr:`~eodag.config.PluginConfig.base_uri` (``str``): default endpoint url
100
+ * :attr:`~eodag.config.PluginConfig.method` (``str``): HTTP request method for the download request (``GET`` or
101
+ ``POST``); default: ``GET``
102
+ * :attr:`~eodag.config.PluginConfig.extract` (``bool``): if the content of the downloaded file should be
103
+ extracted; default: ``True``
104
+ * :attr:`~eodag.config.PluginConfig.auth_error_code` (``int``): which error code is returned in case of an
105
+ authentication error
106
+ * :attr:`~eodag.config.PluginConfig.dl_url_params` (``dict[str, Any]``): parameters to be
107
+ added to the query params of the request
108
+ * :attr:`~eodag.config.PluginConfig.archive_depth` (``int``): level in extracted path tree where to find data;
109
+ default: ``1``
110
+ * :attr:`~eodag.config.PluginConfig.flatten_top_dirs` (``bool``): if the directory structure should be
111
+ flattened; default: ``True``
112
+ * :attr:`~eodag.config.PluginConfig.ignore_assets` (``bool``): ignore assets and download using downloadLink;
113
+ default: ``False``
114
+ * :attr:`~eodag.config.PluginConfig.timeout` (``int``): time to wait until request timeout in seconds;
115
+ default: ``5``
116
+ * :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates should be verified in
117
+ requests; default: ``True``
118
+ * :attr:`~eodag.config.PluginConfig.no_auth_download` (``bool``): if the download should be done without
119
+ authentication; default: ``True``
120
+ * :attr:`~eodag.config.PluginConfig.order_enabled` (``bool``): if the product has to be ordered to download it;
121
+ if this parameter is set to true, a mapping for the orderLink has to be added to the metadata mapping of
122
+ the search plugin used for the provider; default: ``False``
123
+ * :attr:`~eodag.config.PluginConfig.order_method` (``str``): HTTP request method for the order request (``GET``
124
+ or ``POST``); default: ``GET``
125
+ * :attr:`~eodag.config.PluginConfig.order_headers` (``[dict[str, str]]``): headers to be added to the order
126
+ request
127
+ * :attr:`~eodag.config.PluginConfig.order_on_response` (:class:`~eodag.config.PluginConfig.OrderOnResponse`):
128
+ a typed dictionary containing the key ``metadata_mapping`` which can be used to add new product properties
129
+ based on the data in response to the order request
130
+ * :attr:`~eodag.config.PluginConfig.order_status` (:class:`~eodag.config.PluginConfig.OrderStatus`):
131
+ configuration to handle the order status; contains information which method to use, how the response data is
132
+ interpreted, which status corresponds to success, ordered and error and what should be done on success.
133
+ * :attr:`~eodag.config.PluginConfig.products` (``dict[str, dict[str, Any]``): product type specific config; the
134
+ keys are the product types, the values are dictionaries which can contain the key
135
+ :attr:`~eodag.config.PluginConfig.extract` to overwrite the provider config for a specific product type
115
136
 
116
137
  """
117
138
 
118
139
  def __init__(self, provider: str, config: PluginConfig) -> None:
119
140
  super(HTTPDownload, self).__init__(provider, config)
120
141
 
121
- def order_download(
142
+ def _order(
122
143
  self,
123
144
  product: EOProduct,
124
145
  auth: Optional[AuthBase] = None,
125
146
  **kwargs: Unpack[DownloadConf],
126
- ) -> Optional[Dict[str, Any]]:
147
+ ) -> Optional[dict[str, Any]]:
127
148
  """Send product order request.
128
149
 
129
150
  It will be executed once before the download retry loop, if the product is OFFLINE
130
151
  and has `orderLink` in its properties.
131
152
  Product ordering can be configured using the following download plugin parameters:
132
153
 
133
- - **order_enabled**: Wether order is enabled or not (may not use this method
154
+ - :attr:`~eodag.config.PluginConfig.order_enabled`: Wether order is enabled or not (may not use this method
134
155
  if no `orderLink` exists)
135
156
 
136
- - **order_method**: (optional) HTTP request method, GET (default) or POST
157
+ - :attr:`~eodag.config.PluginConfig.order_method`: (optional) HTTP request method, GET (default) or POST
137
158
 
138
- - **order_on_response**: (optional) things to do with obtained order response:
159
+ - :attr:`~eodag.config.PluginConfig.order_on_response`: (optional) things to do with obtained order
160
+ response:
139
161
 
140
162
  - *metadata_mapping*: edit or add new product propoerties properties
141
163
 
@@ -154,7 +176,7 @@ class HTTPDownload(Download):
154
176
  ssl_verify = getattr(self.config, "ssl_verify", True)
155
177
  timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
156
178
  OrderKwargs = TypedDict(
157
- "OrderKwargs", {"json": Dict[str, Union[Any, List[str]]]}, total=False
179
+ "OrderKwargs", {"json": dict[str, Union[Any, list[str]]]}, total=False
158
180
  )
159
181
  order_kwargs: OrderKwargs = {}
160
182
  if order_method == "POST":
@@ -193,25 +215,20 @@ class HTTPDownload(Download):
193
215
  logger.debug(ordered_message)
194
216
  product.properties["storageStatus"] = STAGING_STATUS
195
217
  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
218
  self._check_auth_exception(e)
219
+ msg = f'{product.properties["title"]} could not be ordered'
220
+ if e.response is not None and e.response.status_code == 400:
221
+ raise ValidationError.from_error(e, msg) from e
222
+ else:
223
+ raise DownloadError.from_error(e, msg) from e
224
+
208
225
  return self.order_response_process(response, product)
209
226
  except requests.exceptions.Timeout as exc:
210
227
  raise TimeOutError(exc, timeout=timeout) from exc
211
228
 
212
229
  def order_response_process(
213
230
  self, response: Response, product: EOProduct
214
- ) -> Optional[Dict[str, Any]]:
231
+ ) -> Optional[dict[str, Any]]:
215
232
  """Process order response
216
233
 
217
234
  :param response: The order response
@@ -246,7 +263,7 @@ class HTTPDownload(Download):
246
263
 
247
264
  return json_response
248
265
 
249
- def order_download_status(
266
+ def _order_status(
250
267
  self,
251
268
  product: EOProduct,
252
269
  auth: Optional[AuthBase] = None,
@@ -256,7 +273,7 @@ class HTTPDownload(Download):
256
273
  It will be executed before each download retry.
257
274
  Product order status request can be configured using the following download plugin parameters:
258
275
 
259
- - **order_status**: :class:`~eodag.config.PluginConfig.OrderStatus`
276
+ - :attr:`~eodag.config.PluginConfig.order_status`: :class:`~eodag.config.PluginConfig.OrderStatus`
260
277
 
261
278
  Product properties used for order status:
262
279
 
@@ -275,7 +292,7 @@ class HTTPDownload(Download):
275
292
  def _request(
276
293
  url: str,
277
294
  method: str = "GET",
278
- headers: Optional[Dict[str, Any]] = None,
295
+ headers: Optional[dict[str, Any]] = None,
279
296
  json: Optional[Any] = None,
280
297
  timeout: int = HTTP_REQ_TIMEOUT,
281
298
  ) -> Response:
@@ -311,7 +328,7 @@ class HTTPDownload(Download):
311
328
  except requests.exceptions.Timeout as exc:
312
329
  raise TimeOutError(exc, timeout=timeout) from exc
313
330
 
314
- status_request: Dict[str, Any] = status_config.get("request", {})
331
+ status_request: dict[str, Any] = status_config.get("request", {})
315
332
  status_request_method = str(status_request.get("method", "GET")).upper()
316
333
 
317
334
  if status_request_method == "POST":
@@ -328,8 +345,8 @@ class HTTPDownload(Download):
328
345
 
329
346
  # check header for success before full status request
330
347
  skip_parsing_status_response = False
331
- status_dict: Dict[str, Any] = {}
332
- config_on_success: Dict[str, Any] = status_config.get("on_success", {})
348
+ status_dict: dict[str, Any] = {}
349
+ config_on_success: dict[str, Any] = status_config.get("on_success", {})
333
350
  on_success_mm = config_on_success.get("metadata_mapping", {})
334
351
 
335
352
  status_response_content_needed = (
@@ -373,13 +390,11 @@ class HTTPDownload(Download):
373
390
  # success and no need to get status response content
374
391
  skip_parsing_status_response = True
375
392
  except RequestException as e:
376
- raise DownloadError(
377
- "%s order status could not be checked, request returned %s"
378
- % (
379
- product.properties["title"],
380
- e,
381
- )
382
- ) from e
393
+ msg = f'{product.properties["title"]} order status could not be checked'
394
+ if e.response is not None and e.response.status_code == 400:
395
+ raise ValidationError.from_error(e, msg) from e
396
+ else:
397
+ raise DownloadError.from_error(e, msg) from e
383
398
 
384
399
  if not skip_parsing_status_response:
385
400
  # status request
@@ -415,13 +430,13 @@ class HTTPDownload(Download):
415
430
  product.properties["orderStatus"] = status_dict.get("status")
416
431
 
417
432
  # handle status error
418
- errors: Dict[str, Any] = status_config.get("error", {})
433
+ errors: dict[str, Any] = status_config.get("error", {})
419
434
  if errors and errors.items() <= status_dict.items():
420
435
  raise DownloadError(
421
436
  f"Provider {product.provider} returned: {status_dict.get('error_message', status_message)}"
422
437
  )
423
438
 
424
- success_status: Dict[str, Any] = status_config.get("success", {}).get("status")
439
+ success_status: dict[str, Any] = status_config.get("success", {}).get("status")
425
440
  # if not success
426
441
  if (success_status and success_status != status_dict.get("status")) or (
427
442
  success_code and success_code != response.status_code
@@ -539,10 +554,10 @@ class HTTPDownload(Download):
539
554
  def download(
540
555
  self,
541
556
  product: EOProduct,
542
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
557
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
543
558
  progress_callback: Optional[ProgressCallback] = None,
544
- wait: int = DEFAULT_DOWNLOAD_WAIT,
545
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
559
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
560
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
546
561
  **kwargs: Unpack[DownloadConf],
547
562
  ) -> Optional[str]:
548
563
  """Download a product using HTTP protocol.
@@ -560,13 +575,6 @@ class HTTPDownload(Download):
560
575
  )
561
576
  progress_callback = ProgressCallback(disable=True)
562
577
 
563
- output_extension = getattr(self.config, "products", {}).get(
564
- product.product_type, {}
565
- ).get("output_extension", None) or getattr(
566
- self.config, "output_extension", ".zip"
567
- )
568
- kwargs["output_extension"] = kwargs.get("output_extension", output_extension)
569
-
570
578
  fs_path, record_filename = self._prepare_download(
571
579
  product,
572
580
  progress_callback=progress_callback,
@@ -585,7 +593,7 @@ class HTTPDownload(Download):
585
593
  try:
586
594
  fs_path = self._download_assets(
587
595
  product,
588
- fs_path.replace(".zip", ""),
596
+ fs_path,
589
597
  record_filename,
590
598
  auth,
591
599
  progress_callback,
@@ -602,82 +610,64 @@ class HTTPDownload(Download):
602
610
 
603
611
  url = product.remote_location
604
612
 
605
- @self._download_retry(product, wait, timeout)
613
+ @self._order_download_retry(product, wait, timeout)
606
614
  def download_request(
607
615
  product: EOProduct,
608
616
  auth: AuthBase,
609
617
  progress_callback: ProgressCallback,
610
- wait: int,
611
- timeout: int,
618
+ wait: float,
619
+ timeout: float,
612
620
  **kwargs: Unpack[DownloadConf],
613
- ) -> None:
614
- chunks = self._stream_download(product, auth, progress_callback, **kwargs)
621
+ ) -> os.PathLike:
615
622
  is_empty = True
623
+ chunk_iterator = self._stream_download(
624
+ product, auth, progress_callback, **kwargs
625
+ )
626
+ if fs_path is not None:
627
+ ext = Path(product.filename).suffix
628
+ path = Path(fs_path).with_suffix(ext)
629
+
630
+ with open(path, "wb") as fhandle:
631
+ for chunk in chunk_iterator:
632
+ is_empty = False
633
+ progress_callback(len(chunk))
634
+ fhandle.write(chunk)
635
+ self.stream.close() # Closing response stream
616
636
 
617
- with open(fs_path, "wb") as fhandle:
618
- for chunk in chunks:
619
- is_empty = False
620
- fhandle.write(chunk)
637
+ if is_empty:
638
+ raise DownloadError(f"product {product.properties['id']} is empty")
621
639
 
622
- if is_empty:
623
- raise DownloadError(f"product {product.properties['id']} is empty")
640
+ return path
641
+ else:
642
+ raise DownloadError(
643
+ f"download of product {product.properties['id']} failed"
644
+ )
624
645
 
625
- download_request(product, auth, progress_callback, wait, timeout, **kwargs)
646
+ path = download_request(
647
+ product, auth, progress_callback, wait, timeout, **kwargs
648
+ )
626
649
 
627
650
  with open(record_filename, "w") as fh:
628
651
  fh.write(url)
629
652
  logger.debug("Download recorded in %s", record_filename)
630
653
 
631
- # Check that the downloaded file is really a zip file
632
- if not zipfile.is_zipfile(fs_path) and output_extension == ".zip":
633
- logger.warning(
634
- "Downloaded product is not a Zip File. Please check its file type before using it"
635
- )
636
- new_fs_path = os.path.join(
637
- os.path.dirname(fs_path),
638
- sanitize(product.properties["title"]),
639
- )
640
- if os.path.isfile(fs_path) and not tarfile.is_tarfile(fs_path):
641
- if not os.path.isdir(new_fs_path):
642
- os.makedirs(new_fs_path)
643
- shutil.move(fs_path, new_fs_path)
644
- file_path = os.path.join(new_fs_path, os.path.basename(fs_path))
645
- new_file_path = file_path[: file_path.index(".zip")]
646
- shutil.move(file_path, new_file_path)
647
- # in the case where the outputs extension has not been set
648
- # to ".tar" in the product type nor provider configuration
649
- elif tarfile.is_tarfile(fs_path):
650
- if not new_fs_path.endswith(".tar"):
651
- new_fs_path += ".tar"
652
- shutil.move(fs_path, new_fs_path)
653
- kwargs["output_extension"] = ".tar"
654
- product_path = self._finalize(
655
- new_fs_path,
656
- progress_callback=progress_callback,
657
- **kwargs,
658
- )
659
- product.location = path_to_uri(product_path)
660
- return product_path
661
- else:
662
- # not a file (dir with zip extension)
663
- shutil.move(fs_path, new_fs_path)
664
- product.location = path_to_uri(new_fs_path)
665
- return new_fs_path
666
-
667
- if os.path.isfile(fs_path) and not (
668
- zipfile.is_zipfile(fs_path) or tarfile.is_tarfile(fs_path)
654
+ if os.path.isfile(path) and not (
655
+ zipfile.is_zipfile(path) or tarfile.is_tarfile(path)
669
656
  ):
670
657
  new_fs_path = os.path.join(
671
- os.path.dirname(fs_path),
658
+ os.path.dirname(path),
672
659
  sanitize(product.properties["title"]),
673
660
  )
661
+ if os.path.isfile(new_fs_path):
662
+ rename_with_version(new_fs_path)
674
663
  if not os.path.isdir(new_fs_path):
675
664
  os.makedirs(new_fs_path)
676
- shutil.move(fs_path, new_fs_path)
665
+ shutil.move(path, new_fs_path)
677
666
  product.location = path_to_uri(new_fs_path)
678
667
  return new_fs_path
668
+
679
669
  product_path = self._finalize(
680
- fs_path,
670
+ str(path),
681
671
  progress_callback=progress_callback,
682
672
  **kwargs,
683
673
  )
@@ -718,28 +708,19 @@ class HTTPDownload(Download):
718
708
  ext = guess_extension(content_type)
719
709
  if ext:
720
710
  filename += ext
721
- else:
722
- output_extension: Optional[str] = (
723
- getattr(self.config, "products", {})
724
- .get(product.product_type, {})
725
- .get("output_extension")
726
- )
727
- if output_extension:
728
- filename += output_extension
729
-
730
711
  return filename
731
712
 
732
713
  def _stream_download_dict(
733
714
  self,
734
715
  product: EOProduct,
735
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
716
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
736
717
  progress_callback: Optional[ProgressCallback] = None,
737
- wait: int = DEFAULT_DOWNLOAD_WAIT,
738
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
718
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
719
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
739
720
  **kwargs: Unpack[DownloadConf],
740
721
  ) -> StreamResponse:
741
722
  r"""
742
- Returns dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
723
+ Returns dictionary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
743
724
  It contains a generator to streamed download chunks and the response headers.
744
725
 
745
726
  :param product: The EO product to download
@@ -752,7 +733,7 @@ class HTTPDownload(Download):
752
733
  and `dl_url_params` (dict) can be provided as additional kwargs
753
734
  and will override any other values defined in a configuration
754
735
  file or with environment variables.
755
- :returns: Dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments
736
+ :returns: Dictionary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments
756
737
  """
757
738
  if auth is not None and not isinstance(auth, AuthBase):
758
739
  raise MisconfiguredError(f"Incompatible auth plugin: {type(auth)}")
@@ -791,13 +772,17 @@ class HTTPDownload(Download):
791
772
  )
792
773
 
793
774
  else:
775
+ # get first chunk to check if it does not contain an error (if it does, that error will be raised)
776
+ first_chunks_tuple = next(chunks_tuples)
794
777
  outputs_filename = (
795
778
  sanitize(product.properties["title"])
796
779
  if "title" in product.properties
797
780
  else sanitize(product.properties.get("id", "download"))
798
781
  )
799
782
  return StreamResponse(
800
- content=stream_zip(chunks_tuples),
783
+ content=stream_zip(
784
+ chain(iter([first_chunks_tuple]), chunks_tuples)
785
+ ),
801
786
  media_type="application/zip",
802
787
  headers={
803
788
  "content-disposition": f"attachment; filename={outputs_filename}.zip",
@@ -809,17 +794,20 @@ class HTTPDownload(Download):
809
794
  else:
810
795
  pass
811
796
 
812
- chunks = self._stream_download(product, auth, progress_callback, **kwargs)
797
+ chunk_iterator = self._stream_download(
798
+ product, auth, progress_callback, **kwargs
799
+ )
800
+
813
801
  # start reading chunks to set product.headers
814
802
  try:
815
- first_chunk = next(chunks)
803
+ first_chunk = next(chunk_iterator)
816
804
  except StopIteration:
817
805
  # product is empty file
818
806
  logger.error("product %s is empty", product.properties["id"])
819
807
  raise NotAvailableError(f"product {product.properties['id']} is empty")
820
808
 
821
809
  return StreamResponse(
822
- content=chain(iter([first_chunk]), chunks),
810
+ content=chain(iter([first_chunk]), chunk_iterator),
823
811
  headers=product.headers,
824
812
  )
825
813
 
@@ -837,12 +825,9 @@ class HTTPDownload(Download):
837
825
  and e.response.status_code in auth_errors
838
826
  ):
839
827
  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
- )
828
+ f"Please check your credentials for {self.provider}.",
829
+ f"HTTP Error {e.response.status_code} returned.",
830
+ response_text,
846
831
  )
847
832
 
848
833
  def _process_exception(
@@ -880,6 +865,44 @@ class HTTPDownload(Download):
880
865
  else:
881
866
  logger.error("Error while getting resource :\n%s", tb.format_exc())
882
867
 
868
+ def _order_request(
869
+ self,
870
+ product: EOProduct,
871
+ auth: Optional[AuthBase],
872
+ ) -> None:
873
+ if (
874
+ "orderLink" in product.properties
875
+ and product.properties.get("storageStatus") == OFFLINE_STATUS
876
+ and not product.properties.get("orderStatus")
877
+ ):
878
+ self._order(product=product, auth=auth)
879
+
880
+ if (
881
+ product.properties.get("orderStatusLink", None)
882
+ and product.properties.get("storageStatus") != ONLINE_STATUS
883
+ ):
884
+ self._order_status(product=product, auth=auth)
885
+
886
+ def order(
887
+ self,
888
+ product: EOProduct,
889
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
890
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
891
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
892
+ ) -> None:
893
+ """
894
+ Order product and poll to check its status
895
+
896
+ :param product: The EO product to download
897
+ :param auth: (optional) authenticated object
898
+ :param wait: (optional) Wait time in minutes between two order status check
899
+ :param timeout: (optional) Maximum time in minutes before stop checking
900
+ order status
901
+ """
902
+ self._order_download_retry(product, wait, timeout)(self._order_request)(
903
+ product, auth
904
+ )
905
+
883
906
  def _stream_download(
884
907
  self,
885
908
  product: EOProduct,
@@ -888,8 +911,9 @@ class HTTPDownload(Download):
888
911
  **kwargs: Unpack[DownloadConf],
889
912
  ) -> Iterator[Any]:
890
913
  """
891
- fetches a zip file containing the assets of a given product as a stream
914
+ Fetches a zip file containing the assets of a given product as a stream
892
915
  and returns a generator yielding the chunks of the file
916
+
893
917
  :param product: product for which the assets should be downloaded
894
918
  :param auth: The configuration of a plugin of type Authentication
895
919
  :param progress_callback: A method or a callable object
@@ -903,19 +927,11 @@ class HTTPDownload(Download):
903
927
  logger.info("Progress bar unavailable, please call product.download()")
904
928
  progress_callback = ProgressCallback(disable=True)
905
929
 
906
- ordered_message = ""
907
- if (
908
- "orderLink" in product.properties
909
- and product.properties.get("storageStatus") == OFFLINE_STATUS
910
- and not product.properties.get("orderStatus")
911
- ):
912
- self.order_download(product=product, auth=auth)
930
+ ssl_verify = getattr(self.config, "ssl_verify", True)
913
931
 
914
- if (
915
- product.properties.get("orderStatusLink", None)
916
- and product.properties.get("storageStatus") != ONLINE_STATUS
917
- ):
918
- self.order_download_status(product=product, auth=auth)
932
+ ordered_message = ""
933
+ # retry handled at download level
934
+ self._order_request(product, auth)
919
935
 
920
936
  params = kwargs.pop("dl_url_params", None) or getattr(
921
937
  self.config, "dl_url_params", {}
@@ -933,7 +949,7 @@ class HTTPDownload(Download):
933
949
  if not query_dict and parts.query:
934
950
  query_dict = geojson.loads(parts.query)
935
951
  req_url = parts._replace(query="").geturl()
936
- req_kwargs: Dict[str, Any] = {"json": query_dict} if query_dict else {}
952
+ req_kwargs: dict[str, Any] = {"json": query_dict} if query_dict else {}
937
953
  else:
938
954
  req_url = url
939
955
  req_kwargs = {}
@@ -945,7 +961,7 @@ class HTTPDownload(Download):
945
961
  auth = None
946
962
 
947
963
  s = requests.Session()
948
- with s.request(
964
+ self.stream = s.request(
949
965
  req_method,
950
966
  req_url,
951
967
  stream=True,
@@ -953,56 +969,55 @@ class HTTPDownload(Download):
953
969
  params=params,
954
970
  headers=USER_AGENT,
955
971
  timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
972
+ verify=ssl_verify,
956
973
  **req_kwargs,
957
- ) as self.stream:
958
- try:
959
- self.stream.raise_for_status()
960
- except requests.exceptions.Timeout as exc:
961
- raise TimeOutError(
962
- exc, timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT
963
- ) from exc
964
- except RequestException as e:
965
- self._process_exception(e, product, ordered_message)
966
- else:
967
- # check if product was ordered
968
-
969
- if getattr(
970
- self.stream, "status_code", None
971
- ) is not None and self.stream.status_code == getattr(
972
- self.config, "order_status", {}
973
- ).get(
974
- "ordered", {}
975
- ).get(
976
- "http_code"
977
- ):
978
- product.properties["storageStatus"] = "ORDERED"
979
- self._process_exception(None, product, ordered_message)
980
- stream_size = self._check_stream_size(product) or None
981
-
982
- product.headers = self.stream.headers
983
- filename = self._check_product_filename(product) or None
984
- product.headers[
985
- "content-disposition"
986
- ] = f"attachment; filename={filename}"
987
- content_type = product.headers.get("Content-Type")
988
- guessed_content_type = (
989
- guess_file_type(filename) if filename and not content_type else None
990
- )
991
- if guessed_content_type is not None:
992
- product.headers["Content-Type"] = guessed_content_type
974
+ )
975
+ try:
976
+ self.stream.raise_for_status()
977
+ except requests.exceptions.Timeout as exc:
978
+ raise TimeOutError(exc, timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT) from exc
979
+ except RequestException as e:
980
+ self._process_exception(e, product, ordered_message)
981
+ raise DownloadError(
982
+ f"download of {product.properties['id']} is empty"
983
+ ) from e
984
+ else:
985
+ # check if product was ordered
986
+
987
+ if getattr(
988
+ self.stream, "status_code", None
989
+ ) is not None and self.stream.status_code == getattr(
990
+ self.config, "order_status", {}
991
+ ).get(
992
+ "ordered", {}
993
+ ).get(
994
+ "http_code"
995
+ ):
996
+ product.properties["storageStatus"] = "ORDERED"
997
+ self._process_exception(None, product, ordered_message)
998
+ stream_size = self._check_stream_size(product) or None
999
+
1000
+ product.headers = self.stream.headers
1001
+ filename = self._check_product_filename(product)
1002
+ product.headers["content-disposition"] = f"attachment; filename={filename}"
1003
+ content_type = product.headers.get("Content-Type")
1004
+ guessed_content_type = (
1005
+ guess_file_type(filename) if filename and not content_type else None
1006
+ )
1007
+ if guessed_content_type is not None:
1008
+ product.headers["Content-Type"] = guessed_content_type
993
1009
 
994
- progress_callback.reset(total=stream_size)
995
- for chunk in self.stream.iter_content(chunk_size=64 * 1024):
996
- if chunk:
997
- progress_callback(len(chunk))
998
- yield chunk
1010
+ progress_callback.reset(total=stream_size)
1011
+
1012
+ product.filename = filename
1013
+ return self.stream.iter_content(chunk_size=64 * 1024)
999
1014
 
1000
1015
  def _stream_download_assets(
1001
1016
  self,
1002
1017
  product: EOProduct,
1003
1018
  auth: Optional[AuthBase] = None,
1004
1019
  progress_callback: Optional[ProgressCallback] = None,
1005
- assets_values: List[Asset] = [],
1020
+ assets_values: list[Asset] = [],
1006
1021
  **kwargs: Unpack[DownloadConf],
1007
1022
  ) -> Iterator[Any]:
1008
1023
  if progress_callback is None:
@@ -1055,6 +1070,16 @@ class HTTPDownload(Download):
1055
1070
  "flatten_top_dirs", getattr(self.config, "flatten_top_dirs", True)
1056
1071
  )
1057
1072
  ssl_verify = getattr(self.config, "ssl_verify", True)
1073
+ matching_url = (
1074
+ getattr(product.downloader_auth.config, "matching_url", "")
1075
+ if product.downloader_auth
1076
+ else ""
1077
+ )
1078
+ matching_conf = (
1079
+ getattr(product.downloader_auth.config, "matching_conf", None)
1080
+ if product.downloader_auth
1081
+ else None
1082
+ )
1058
1083
 
1059
1084
  # loop for assets download
1060
1085
  for asset in assets_values:
@@ -1063,11 +1088,16 @@ class HTTPDownload(Download):
1063
1088
  f"Local asset detected. Download skipped for {asset['href']}"
1064
1089
  )
1065
1090
  continue
1066
-
1091
+ if matching_conf or (
1092
+ matching_url and re.match(matching_url, asset["href"])
1093
+ ):
1094
+ auth_object = auth
1095
+ else:
1096
+ auth_object = None
1067
1097
  with requests.get(
1068
1098
  asset["href"],
1069
1099
  stream=True,
1070
- auth=auth,
1100
+ auth=auth_object,
1071
1101
  params=params,
1072
1102
  headers=USER_AGENT,
1073
1103
  timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
@@ -1080,8 +1110,7 @@ class HTTPDownload(Download):
1080
1110
  exc, timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT
1081
1111
  ) from exc
1082
1112
  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)
1113
+ self._handle_asset_exception(e, asset)
1085
1114
  else:
1086
1115
  asset_rel_path = (
1087
1116
  asset.rel_path.replace(assets_common_subdir, "").strip(os.sep)
@@ -1239,33 +1268,28 @@ class HTTPDownload(Download):
1239
1268
 
1240
1269
  return fs_dir_path
1241
1270
 
1242
- def _handle_asset_exception(
1243
- self, e: RequestException, asset: Asset, raise_errors: bool = False
1244
- ) -> None:
1271
+ def _handle_asset_exception(self, e: RequestException, asset: Asset) -> None:
1245
1272
  # check if error is identified as auth_error in provider conf
1246
1273
  auth_errors = getattr(self.config, "auth_error_code", [None])
1247
1274
  if not isinstance(auth_errors, list):
1248
1275
  auth_errors = [auth_errors]
1249
1276
  if e.response is not None and e.response.status_code in auth_errors:
1250
1277
  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
- )
1278
+ f"Please check your credentials for {self.provider}.",
1279
+ f"HTTP Error {e.response.status_code} returned.",
1280
+ e.response.text.strip(),
1257
1281
  )
1258
- elif raise_errors:
1259
- raise DownloadError(e)
1260
1282
  else:
1261
- logger.warning("Unexpected error: %s" % e)
1262
- logger.warning("Skipping %s" % asset["href"])
1283
+ logger.error(
1284
+ "Unexpected error at download of asset %s: %s", asset["href"], e
1285
+ )
1286
+ raise DownloadError(e)
1263
1287
 
1264
1288
  def _get_asset_sizes(
1265
1289
  self,
1266
- assets_values: List[Asset],
1290
+ assets_values: list[Asset],
1267
1291
  auth: Optional[AuthBase],
1268
- params: Optional[Dict[str, str]],
1292
+ params: Optional[dict[str, str]],
1269
1293
  zipped: bool = False,
1270
1294
  ) -> int:
1271
1295
  total_size = 0
@@ -1338,11 +1362,11 @@ class HTTPDownload(Download):
1338
1362
  def download_all(
1339
1363
  self,
1340
1364
  products: SearchResult,
1341
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
1365
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
1342
1366
  downloaded_callback: Optional[DownloadedCallback] = None,
1343
1367
  progress_callback: Optional[ProgressCallback] = None,
1344
- wait: int = DEFAULT_DOWNLOAD_WAIT,
1345
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
1368
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
1369
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
1346
1370
  **kwargs: Unpack[DownloadConf],
1347
1371
  ):
1348
1372
  """