eodag 3.0.1__py3-none-any.whl → 3.1.0b2__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 (87) hide show
  1. eodag/api/core.py +164 -127
  2. eodag/api/product/_assets.py +11 -11
  3. eodag/api/product/_product.py +45 -30
  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 +101 -85
  10. eodag/api/search_result.py +13 -23
  11. eodag/cli.py +26 -5
  12. eodag/config.py +78 -81
  13. eodag/plugins/apis/base.py +1 -1
  14. eodag/plugins/apis/ecmwf.py +46 -22
  15. eodag/plugins/apis/usgs.py +16 -15
  16. eodag/plugins/authentication/aws_auth.py +16 -13
  17. eodag/plugins/authentication/base.py +5 -3
  18. eodag/plugins/authentication/header.py +3 -3
  19. eodag/plugins/authentication/keycloak.py +4 -4
  20. eodag/plugins/authentication/oauth.py +7 -3
  21. eodag/plugins/authentication/openid_connect.py +16 -16
  22. eodag/plugins/authentication/sas_auth.py +4 -4
  23. eodag/plugins/authentication/token.py +41 -10
  24. eodag/plugins/authentication/token_exchange.py +1 -1
  25. eodag/plugins/base.py +4 -4
  26. eodag/plugins/crunch/base.py +4 -4
  27. eodag/plugins/crunch/filter_date.py +4 -4
  28. eodag/plugins/crunch/filter_latest_intersect.py +6 -6
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +7 -7
  30. eodag/plugins/crunch/filter_overlap.py +4 -4
  31. eodag/plugins/crunch/filter_property.py +6 -7
  32. eodag/plugins/download/aws.py +58 -78
  33. eodag/plugins/download/base.py +38 -56
  34. eodag/plugins/download/creodias_s3.py +29 -0
  35. eodag/plugins/download/http.py +173 -183
  36. eodag/plugins/download/s3rest.py +10 -11
  37. eodag/plugins/manager.py +10 -20
  38. eodag/plugins/search/__init__.py +6 -5
  39. eodag/plugins/search/base.py +87 -44
  40. eodag/plugins/search/build_search_result.py +1067 -329
  41. eodag/plugins/search/cop_marine.py +22 -12
  42. eodag/plugins/search/creodias_s3.py +9 -73
  43. eodag/plugins/search/csw.py +11 -11
  44. eodag/plugins/search/data_request_search.py +16 -15
  45. eodag/plugins/search/qssearch.py +103 -187
  46. eodag/plugins/search/stac_list_assets.py +85 -0
  47. eodag/plugins/search/static_stac_search.py +3 -3
  48. eodag/resources/ext_product_types.json +1 -1
  49. eodag/resources/product_types.yml +663 -304
  50. eodag/resources/providers.yml +823 -1749
  51. eodag/resources/stac_api.yml +2 -2
  52. eodag/resources/user_conf_template.yml +11 -0
  53. eodag/rest/cache.py +2 -2
  54. eodag/rest/config.py +3 -3
  55. eodag/rest/core.py +112 -82
  56. eodag/rest/errors.py +5 -5
  57. eodag/rest/server.py +33 -14
  58. eodag/rest/stac.py +40 -38
  59. eodag/rest/types/collections_search.py +3 -3
  60. eodag/rest/types/eodag_search.py +29 -23
  61. eodag/rest/types/queryables.py +15 -16
  62. eodag/rest/types/stac_search.py +15 -25
  63. eodag/rest/utils/__init__.py +14 -21
  64. eodag/rest/utils/cql_evaluate.py +6 -6
  65. eodag/rest/utils/rfc3339.py +2 -2
  66. eodag/types/__init__.py +75 -28
  67. eodag/types/bbox.py +2 -2
  68. eodag/types/download_args.py +3 -3
  69. eodag/types/queryables.py +183 -72
  70. eodag/types/search_args.py +4 -4
  71. eodag/types/whoosh.py +127 -3
  72. eodag/utils/__init__.py +152 -50
  73. eodag/utils/exceptions.py +28 -21
  74. eodag/utils/import_system.py +2 -2
  75. eodag/utils/repr.py +65 -6
  76. eodag/utils/requests.py +13 -13
  77. eodag/utils/rest.py +2 -2
  78. eodag/utils/s3.py +208 -0
  79. eodag/utils/stac_reader.py +10 -10
  80. {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/METADATA +77 -76
  81. eodag-3.1.0b2.dist-info/RECORD +113 -0
  82. {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/WHEEL +1 -1
  83. {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/entry_points.txt +4 -2
  84. eodag/utils/constraints.py +0 -244
  85. eodag-3.0.1.dist-info/RECORD +0 -109
  86. {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/LICENSE +0 -0
  87. {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/top_level.txt +0 -0
@@ -23,21 +23,7 @@ import re
23
23
  from datetime import datetime
24
24
  from itertools import chain
25
25
  from pathlib import Path
26
- from typing import (
27
- TYPE_CHECKING,
28
- Any,
29
- Callable,
30
- Dict,
31
- Iterator,
32
- List,
33
- Match,
34
- Optional,
35
- Set,
36
- Tuple,
37
- TypedDict,
38
- Union,
39
- cast,
40
- )
26
+ from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, Union, cast
41
27
 
42
28
  import boto3
43
29
  import requests
@@ -81,6 +67,7 @@ if TYPE_CHECKING:
81
67
  from eodag.api.product import EOProduct
82
68
  from eodag.api.search_result import SearchResult
83
69
  from eodag.config import PluginConfig
70
+ from eodag.types import S3SessionKwargs
84
71
  from eodag.types.download_args import DownloadConf
85
72
  from eodag.utils import DownloadedCallback, Unpack
86
73
 
@@ -230,14 +217,14 @@ class AwsDownload(Download):
230
217
  * :attr:`~eodag.config.PluginConfig.bucket_path_level` (``int``): at which level of the
231
218
  path part of the url the bucket can be found; If no bucket_path_level is given, the bucket
232
219
  is taken from the first element of the netloc part.
233
- * :attr:`~eodag.config.PluginConfig.products` (``Dict[str, Dict[str, Any]``): product type
220
+ * :attr:`~eodag.config.PluginConfig.products` (``dict[str, dict[str, Any]``): product type
234
221
  specific config; the keys are the product types, the values are dictionaries which can contain the keys:
235
222
 
236
223
  * **default_bucket** (``str``): bucket where the product type can be found
237
224
  * **complementary_url_key** (``str``): keys to add additional urls
238
225
  * **build_safe** (``bool``): if a SAFE (Standard Archive Format for Europe) product should
239
226
  be created; used for Sentinel products; default: False
240
- * **fetch_metadata** (``Dict[str, Any]``): config for metadata to be fetched for the SAFE product
227
+ * **fetch_metadata** (``dict[str, Any]``): config for metadata to be fetched for the SAFE product
241
228
 
242
229
  """
243
230
 
@@ -249,10 +236,10 @@ class AwsDownload(Download):
249
236
  def download(
250
237
  self,
251
238
  product: EOProduct,
252
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
239
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
253
240
  progress_callback: Optional[ProgressCallback] = None,
254
- wait: int = DEFAULT_DOWNLOAD_WAIT,
255
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
241
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
242
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
256
243
  **kwargs: Unpack[DownloadConf],
257
244
  ) -> Optional[str]:
258
245
  """Download method for AWS S3 API.
@@ -407,7 +394,7 @@ class AwsDownload(Download):
407
394
  product: EOProduct,
408
395
  progress_callback: ProgressCallback,
409
396
  **kwargs: Unpack[DownloadConf],
410
- ) -> Tuple[Optional[str], Optional[str]]:
397
+ ) -> tuple[Optional[str], Optional[str]]:
411
398
  """
412
399
  preparation for the download:
413
400
  - check if file was already downloaded
@@ -480,9 +467,10 @@ class AwsDownload(Download):
480
467
  product: EOProduct,
481
468
  asset_filter: Optional[str] = None,
482
469
  ignore_assets: Optional[bool] = False,
483
- ) -> List[Tuple[str, Optional[str]]]:
470
+ ) -> list[tuple[str, Optional[str]]]:
484
471
  """
485
- retrieves the bucket names and path prefixes for the assets
472
+ Retrieves the bucket names and path prefixes for the assets
473
+
486
474
  :param product: product for which the assets shall be downloaded
487
475
  :param asset_filter: text for which the assets should be filtered
488
476
  :param ignore_assets: if product instead of individual assets should be used
@@ -521,9 +509,9 @@ class AwsDownload(Download):
521
509
 
522
510
  def _do_authentication(
523
511
  self,
524
- bucket_names_and_prefixes: List[Tuple[str, Optional[str]]],
525
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
526
- ) -> Tuple[Dict[str, Any], ResourceCollection]:
512
+ bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
513
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
514
+ ) -> tuple[dict[str, Any], ResourceCollection]:
527
515
  """
528
516
  authenticates with s3 and retrieves the available objects
529
517
  raises an error when authentication is not possible
@@ -537,8 +525,8 @@ class AwsDownload(Download):
537
525
  )
538
526
  if auth is None:
539
527
  auth = {}
540
- authenticated_objects: Dict[str, Any] = {}
541
- auth_error_messages: Set[str] = set()
528
+ authenticated_objects: dict[str, Any] = {}
529
+ auth_error_messages: set[str] = set()
542
530
  for _, pack in enumerate(bucket_names_and_prefixes):
543
531
  try:
544
532
  bucket_name, prefix = pack
@@ -590,12 +578,12 @@ class AwsDownload(Download):
590
578
 
591
579
  def _get_unique_products(
592
580
  self,
593
- bucket_names_and_prefixes: List[Tuple[str, Optional[str]]],
594
- authenticated_objects: Dict[str, Any],
581
+ bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
582
+ authenticated_objects: dict[str, Any],
595
583
  asset_filter: Optional[str],
596
584
  ignore_assets: bool,
597
585
  product: EOProduct,
598
- ) -> Set[Any]:
586
+ ) -> set[Any]:
599
587
  """
600
588
  retrieve unique product chunks based on authenticated objects and asset filters
601
589
  :param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
@@ -605,7 +593,7 @@ class AwsDownload(Download):
605
593
  :param product: product that shall be downloaded
606
594
  :return: set of product chunks that can be downloaded
607
595
  """
608
- product_chunks: List[Any] = []
596
+ product_chunks: list[Any] = []
609
597
  for bucket_name, prefix in bucket_names_and_prefixes:
610
598
  # unauthenticated items filtered out
611
599
  if bucket_name in authenticated_objects.keys():
@@ -636,7 +624,7 @@ class AwsDownload(Download):
636
624
 
637
625
  def _raise_if_auth_error(self, exception: ClientError) -> None:
638
626
  """Raises an error if given exception is an authentication error"""
639
- err = cast(Dict[str, str], exception.response["Error"])
627
+ err = cast(dict[str, str], exception.response["Error"])
640
628
  if err["Code"] in AWS_AUTH_ERROR_MESSAGES and "key" in err["Message"].lower():
641
629
  raise AuthenticationError(
642
630
  f"Please check your credentials for {self.provider}.",
@@ -647,10 +635,10 @@ class AwsDownload(Download):
647
635
  def _stream_download_dict(
648
636
  self,
649
637
  product: EOProduct,
650
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
638
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
651
639
  progress_callback: Optional[ProgressCallback] = None,
652
- wait: int = DEFAULT_DOWNLOAD_WAIT,
653
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
640
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
641
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
654
642
  **kwargs: Unpack[DownloadConf],
655
643
  ) -> StreamResponse:
656
644
  r"""
@@ -731,12 +719,12 @@ class AwsDownload(Download):
731
719
  else sanitize(product.properties.get("id", "download"))
732
720
  )
733
721
 
734
- if len(assets_values) == 1:
722
+ if len(assets_values) <= 1:
735
723
  first_chunks_tuple = next(chunks_tuples)
736
724
  # update headers
737
725
  filename = os.path.basename(list(unique_product_chunks)[0].key)
738
726
  headers = {"content-disposition": f"attachment; filename={filename}"}
739
- if assets_values[0].get("type", None):
727
+ if assets_values and assets_values[0].get("type", None):
740
728
  headers["content-type"] = assets_values[0]["type"]
741
729
 
742
730
  return StreamResponse(
@@ -753,11 +741,11 @@ class AwsDownload(Download):
753
741
 
754
742
  def _stream_download(
755
743
  self,
756
- unique_product_chunks: Set[Any],
744
+ unique_product_chunks: set[Any],
757
745
  product: EOProduct,
758
746
  build_safe: bool,
759
747
  progress_callback: ProgressCallback,
760
- assets_values: List[Dict[str, Any]],
748
+ assets_values: list[dict[str, Any]],
761
749
  ) -> Iterator[Any]:
762
750
  """Yield product data chunks"""
763
751
 
@@ -799,7 +787,6 @@ class AwsDownload(Download):
799
787
  common_path = self._get_commonpath(
800
788
  product, unique_product_chunks, build_safe
801
789
  )
802
-
803
790
  for product_chunk in unique_product_chunks:
804
791
  try:
805
792
  chunk_rel_path = self.get_chunk_dest_path(
@@ -817,8 +804,7 @@ class AwsDownload(Download):
817
804
  # out of SAFE format chunk
818
805
  logger.warning(e)
819
806
  continue
820
-
821
- if len(assets_values) == 1:
807
+ if len(assets_values) <= 1:
822
808
  yield from get_chunk_parts(product_chunk, progress_callback)
823
809
  else:
824
810
  yield (
@@ -830,7 +816,7 @@ class AwsDownload(Download):
830
816
  )
831
817
 
832
818
  def _get_commonpath(
833
- self, product: EOProduct, product_chunks: Set[Any], build_safe: bool
819
+ self, product: EOProduct, product_chunks: set[Any], build_safe: bool
834
820
  ) -> str:
835
821
  chunk_paths = []
836
822
  for product_chunk in product_chunks:
@@ -840,8 +826,8 @@ class AwsDownload(Download):
840
826
  return os.path.commonpath(chunk_paths)
841
827
 
842
828
  def get_rio_env(
843
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
844
- ) -> Dict[str, Any]:
829
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
830
+ ) -> dict[str, Any]:
845
831
  """Get rasterio environment variables needed for data access authentication.
846
832
 
847
833
  :param bucket_name: Bucket containg objects
@@ -849,23 +835,26 @@ class AwsDownload(Download):
849
835
  :param auth_dict: Dictionary containing authentication keys
850
836
  :returns: The rasterio environement variables
851
837
  """
852
- if self.s3_session is not None:
853
- if self.requester_pays:
854
- return {"session": self.s3_session, "requester_pays": True}
855
- else:
856
- return {"session": self.s3_session}
838
+ rio_env_kwargs = {}
839
+ if endpoint_url := getattr(self.config, "s3_endpoint", None):
840
+ rio_env_kwargs["endpoint_url"] = endpoint_url.split("://")[-1]
841
+ rio_env_kwargs |= auth_dict
842
+
843
+ if self.s3_session is None:
844
+ _ = self.get_authenticated_objects(bucket_name, prefix, auth_dict)
857
845
 
858
- _ = self.get_authenticated_objects(bucket_name, prefix, auth_dict)
859
846
  if self.s3_session is not None:
860
847
  if self.requester_pays:
861
- return {"session": self.s3_session, "requester_pays": True}
862
- else:
863
- return {"session": self.s3_session}
848
+ rio_env_kwargs["requester_pays"] = True
849
+ return {
850
+ "session": self.s3_session,
851
+ **rio_env_kwargs,
852
+ }
864
853
  else:
865
- return {"aws_unsigned": True}
854
+ return {"aws_unsigned": True, **rio_env_kwargs}
866
855
 
867
856
  def get_authenticated_objects(
868
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
857
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
869
858
  ) -> ResourceCollection:
870
859
  """Get boto3 authenticated objects for the given bucket using
871
860
  the most adapted auth strategy.
@@ -877,8 +866,8 @@ class AwsDownload(Download):
877
866
  :param auth_dict: Dictionary containing authentication keys
878
867
  :returns: The boto3 authenticated objects
879
868
  """
880
- auth_methods: List[
881
- Callable[[str, str, Dict[str, str]], Optional[ResourceCollection]]
869
+ auth_methods: list[
870
+ Callable[[str, str, S3SessionKwargs], Optional[ResourceCollection]]
882
871
  ] = [
883
872
  self._get_authenticated_objects_unsigned,
884
873
  self._get_authenticated_objects_from_auth_profile,
@@ -913,7 +902,7 @@ class AwsDownload(Download):
913
902
  )
914
903
 
915
904
  def _get_authenticated_objects_unsigned(
916
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
905
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
917
906
  ) -> Optional[ResourceCollection]:
918
907
  """Auth strategy using no-sign-request"""
919
908
 
@@ -928,7 +917,7 @@ class AwsDownload(Download):
928
917
  return objects
929
918
 
930
919
  def _get_authenticated_objects_from_auth_profile(
931
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
920
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
932
921
  ) -> Optional[ResourceCollection]:
933
922
  """Auth strategy using RequestPayer=requester and ``aws_profile`` from provided credentials"""
934
923
 
@@ -951,21 +940,12 @@ class AwsDownload(Download):
951
940
  return None
952
941
 
953
942
  def _get_authenticated_objects_from_auth_keys(
954
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
943
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
955
944
  ) -> Optional[ResourceCollection]:
956
945
  """Auth strategy using RequestPayer=requester and ``aws_access_key_id``/``aws_secret_access_key``
957
946
  from provided credentials"""
958
947
 
959
948
  if all(k in auth_dict for k in ("aws_access_key_id", "aws_secret_access_key")):
960
- S3SessionKwargs = TypedDict(
961
- "S3SessionKwargs",
962
- {
963
- "aws_access_key_id": str,
964
- "aws_secret_access_key": str,
965
- "aws_session_token": str,
966
- },
967
- total=False,
968
- )
969
949
  s3_session_kwargs: S3SessionKwargs = {
970
950
  "aws_access_key_id": auth_dict["aws_access_key_id"],
971
951
  "aws_secret_access_key": auth_dict["aws_secret_access_key"],
@@ -990,7 +970,7 @@ class AwsDownload(Download):
990
970
  return None
991
971
 
992
972
  def _get_authenticated_objects_from_env(
993
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
973
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
994
974
  ) -> Optional[ResourceCollection]:
995
975
  """Auth strategy using RequestPayer=requester and current environment"""
996
976
 
@@ -1010,7 +990,7 @@ class AwsDownload(Download):
1010
990
 
1011
991
  def get_product_bucket_name_and_prefix(
1012
992
  self, product: EOProduct, url: Optional[str] = None
1013
- ) -> Tuple[str, Optional[str]]:
993
+ ) -> tuple[str, Optional[str]]:
1014
994
  """Extract bucket name and prefix from product URL
1015
995
 
1016
996
  :param product: The EO product to download
@@ -1141,7 +1121,7 @@ class AwsDownload(Download):
1141
1121
  s1_title_suffix: Optional[str] = None
1142
1122
  # S2 common
1143
1123
  if product.product_type and "S2_MSI" in product.product_type:
1144
- title_search: Optional[Match[str]] = re.search(
1124
+ title_search: Optional[re.Match[str]] = re.search(
1145
1125
  r"^\w+_\w+_(\w+)_(\w+)_(\w+)_(\w+)_(\w+)$",
1146
1126
  product.properties["title"],
1147
1127
  )
@@ -1327,13 +1307,13 @@ class AwsDownload(Download):
1327
1307
  def download_all(
1328
1308
  self,
1329
1309
  products: SearchResult,
1330
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
1310
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
1331
1311
  downloaded_callback: Optional[DownloadedCallback] = None,
1332
1312
  progress_callback: Optional[ProgressCallback] = None,
1333
- wait: int = DEFAULT_DOWNLOAD_WAIT,
1334
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
1313
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
1314
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
1335
1315
  **kwargs: Unpack[DownloadConf],
1336
- ) -> List[str]:
1316
+ ) -> list[str]:
1337
1317
  """
1338
1318
  download_all using parent (base plugin) method
1339
1319
  """
@@ -26,17 +26,7 @@ import tempfile
26
26
  import zipfile
27
27
  from datetime import datetime, timedelta
28
28
  from time import sleep
29
- from typing import (
30
- TYPE_CHECKING,
31
- Any,
32
- Callable,
33
- Dict,
34
- List,
35
- Optional,
36
- Tuple,
37
- TypeVar,
38
- Union,
39
- )
29
+ from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
40
30
 
41
31
  from eodag.plugins.base import PluginTopic
42
32
  from eodag.utils import (
@@ -60,6 +50,7 @@ if TYPE_CHECKING:
60
50
  from eodag.api.product import EOProduct
61
51
  from eodag.api.search_result import SearchResult
62
52
  from eodag.config import PluginConfig
53
+ from eodag.types import S3SessionKwargs
63
54
  from eodag.types.download_args import DownloadConf
64
55
  from eodag.utils import DownloadedCallback, Unpack
65
56
 
@@ -110,10 +101,10 @@ class Download(PluginTopic):
110
101
  def download(
111
102
  self,
112
103
  product: EOProduct,
113
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
104
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
114
105
  progress_callback: Optional[ProgressCallback] = None,
115
- wait: int = DEFAULT_DOWNLOAD_WAIT,
116
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
106
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
107
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
117
108
  **kwargs: Unpack[DownloadConf],
118
109
  ) -> Optional[str]:
119
110
  r"""
@@ -140,10 +131,10 @@ class Download(PluginTopic):
140
131
  def _stream_download_dict(
141
132
  self,
142
133
  product: EOProduct,
143
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
134
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
144
135
  progress_callback: Optional[ProgressCallback] = None,
145
- wait: int = DEFAULT_DOWNLOAD_WAIT,
146
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
136
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
137
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
147
138
  **kwargs: Unpack[DownloadConf],
148
139
  ) -> StreamResponse:
149
140
  r"""
@@ -170,7 +161,7 @@ class Download(PluginTopic):
170
161
  product: EOProduct,
171
162
  progress_callback: Optional[ProgressCallback] = None,
172
163
  **kwargs: Unpack[DownloadConf],
173
- ) -> Tuple[Optional[str], Optional[str]]:
164
+ ) -> tuple[Optional[str], Optional[str]]:
174
165
  """Check if file has already been downloaded, and prepare product download
175
166
 
176
167
  :param product: The EO product to download
@@ -202,8 +193,8 @@ class Download(PluginTopic):
202
193
  or getattr(self.config, "output_dir", tempfile.gettempdir())
203
194
  or tempfile.gettempdir()
204
195
  )
205
- output_extension = kwargs.get("output_extension", None) or getattr(
206
- self.config, "output_extension", ".zip"
196
+ output_extension = kwargs.get("output_extension") or getattr(
197
+ self.config, "output_extension", ""
207
198
  )
208
199
 
209
200
  # Strong asumption made here: all products downloaded will be zip files
@@ -233,9 +224,13 @@ class Download(PluginTopic):
233
224
  logger.warning(
234
225
  f"Unable to create records directory. Got:\n{tb.format_exc()}",
235
226
  )
227
+ url_hash = hashlib.md5(url.encode("utf-8")).hexdigest()
228
+ old_record_filename = os.path.join(download_records_dir, url_hash)
236
229
  record_filename = os.path.join(
237
230
  download_records_dir, self.generate_record_hash(product)
238
231
  )
232
+ if os.path.isfile(old_record_filename):
233
+ os.rename(old_record_filename, record_filename)
239
234
  if os.path.isfile(record_filename) and os.path.isfile(fs_path):
240
235
  logger.info(
241
236
  f"Product already downloaded: {fs_path}",
@@ -339,13 +334,7 @@ class Download(PluginTopic):
339
334
  if delete_archive is not None
340
335
  else getattr(self.config, "delete_archive", True)
341
336
  )
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
- )
337
+ product_path, _ = os.path.splitext(fs_path)
349
338
  product_path_exists = os.path.exists(product_path)
350
339
  if product_path_exists and os.path.isfile(product_path):
351
340
  logger.info(
@@ -422,10 +411,10 @@ class Download(PluginTopic):
422
411
 
423
412
  tmp_dir.cleanup()
424
413
 
425
- if delete_archive:
414
+ if delete_archive and os.path.isfile(fs_path):
426
415
  logger.info(f"Deleting archive {os.path.basename(fs_path)}")
427
416
  os.unlink(fs_path)
428
- else:
417
+ elif os.path.isfile(fs_path):
429
418
  logger.info(
430
419
  f"Archive deletion is deactivated, keeping {os.path.basename(fs_path)}"
431
420
  )
@@ -441,13 +430,13 @@ class Download(PluginTopic):
441
430
  def download_all(
442
431
  self,
443
432
  products: SearchResult,
444
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
433
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
445
434
  downloaded_callback: Optional[DownloadedCallback] = None,
446
435
  progress_callback: Optional[ProgressCallback] = None,
447
- wait: int = DEFAULT_DOWNLOAD_WAIT,
448
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
436
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
437
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
449
438
  **kwargs: Unpack[DownloadConf],
450
- ) -> List[str]:
439
+ ) -> list[str]:
451
440
  """
452
441
  Base download_all method.
453
442
 
@@ -476,7 +465,7 @@ class Download(PluginTopic):
476
465
  # Products are going to be removed one by one from this sequence once
477
466
  # downloaded.
478
467
  products = products[:]
479
- paths: List[str] = []
468
+ paths: list[str] = []
480
469
  # initiate retry loop
481
470
  start_time = datetime.now()
482
471
  stop_time = start_time + timedelta(minutes=timeout)
@@ -541,7 +530,7 @@ class Download(PluginTopic):
541
530
  )
542
531
  raise
543
532
 
544
- except RuntimeError:
533
+ except (RuntimeError, Exception):
545
534
  import traceback as tb
546
535
 
547
536
  logger.error(
@@ -549,16 +538,9 @@ class Download(PluginTopic):
549
538
  "Skipping it"
550
539
  )
551
540
  logger.debug(f"\n{tb.format_exc()}")
552
- stop_time = datetime.now()
553
-
554
- except Exception:
555
- import traceback as tb
556
541
 
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()}")
542
+ # product skipped, to not retry it
543
+ products.remove(product)
562
544
 
563
545
  if (
564
546
  len(products) > 0
@@ -585,14 +567,14 @@ class Download(PluginTopic):
585
567
 
586
568
  return paths
587
569
 
588
- def _download_retry(
589
- self, product: EOProduct, wait: int, timeout: int
570
+ def _order_download_retry(
571
+ self, product: EOProduct, wait: float, timeout: float
590
572
  ) -> Callable[[Callable[..., T]], Callable[..., T]]:
591
573
  """
592
- Download retry decorator.
574
+ Order download retry decorator.
593
575
 
594
- Retries the wrapped download method after `wait` minutes if a NotAvailableError
595
- exception is thrown until `timeout` minutes.
576
+ Retries the wrapped order_download method after ``wait`` minutes if a
577
+ ``NotAvailableError`` exception is thrown until ``timeout`` minutes.
596
578
 
597
579
  :param product: The EO product to download
598
580
  :param wait: If download fails, wait time in minutes between two download tries
@@ -601,7 +583,7 @@ class Download(PluginTopic):
601
583
  :returns: decorator
602
584
  """
603
585
 
604
- def decorator(download: Callable[..., T]) -> Callable[..., T]:
586
+ def decorator(order_download: Callable[..., T]) -> Callable[..., T]:
605
587
  def download_and_retry(*args: Any, **kwargs: Unpack[DownloadConf]) -> T:
606
588
  # initiate retry loop
607
589
  start_time = datetime.now()
@@ -618,7 +600,7 @@ class Download(PluginTopic):
618
600
  if datetime_now >= product.next_try:
619
601
  product.next_try += timedelta(minutes=wait)
620
602
  try:
621
- return download(*args, **kwargs)
603
+ return order_download(*args, **kwargs)
622
604
 
623
605
  except NotAvailableError as e:
624
606
  if not getattr(self.config, "order_enabled", False):
@@ -634,7 +616,7 @@ class Download(PluginTopic):
634
616
  ).seconds
635
617
  retry_count += 1
636
618
  retry_info = (
637
- f"[Retry #{retry_count}] Waited {wait_seconds}s, trying again to download ordered product"
619
+ f"[Retry #{retry_count}] Waited {wait_seconds}s, checking order status again"
638
620
  f" (retry every {wait}' for {timeout}')"
639
621
  )
640
622
  logger.info(not_available_info)
@@ -656,8 +638,8 @@ class Download(PluginTopic):
656
638
  ).microseconds / 1e6
657
639
  retry_count += 1
658
640
  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}')"
641
+ f"[Retry #{retry_count}] Waiting {wait_seconds}s until next order status check"
642
+ f" (retry every {wait}' for {timeout}')"
661
643
  )
662
644
  logger.info(not_available_info)
663
645
  # Retry-After info from Response header
@@ -678,12 +660,12 @@ class Download(PluginTopic):
678
660
  logger.info(not_available_info)
679
661
  raise NotAvailableError(
680
662
  f"{product.properties['title']} is not available ({product.properties['storageStatus']})"
681
- f" and could not be downloaded, timeout reached"
663
+ f" and order was not successfull, timeout reached"
682
664
  )
683
665
  elif datetime_now >= stop_time:
684
666
  raise NotAvailableError(not_available_info)
685
667
 
686
- return download(*args, **kwargs)
668
+ return order_download(*args, **kwargs)
687
669
 
688
670
  return download_and_retry
689
671
 
@@ -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 Optional
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