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
@@ -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
@@ -70,9 +56,11 @@ from eodag.utils.exceptions import (
70
56
  AuthenticationError,
71
57
  DownloadError,
72
58
  MisconfiguredError,
59
+ NoMatchingProductType,
73
60
  NotAvailableError,
74
61
  TimeOutError,
75
62
  )
63
+ from eodag.utils.s3 import open_s3_zipped_object
76
64
 
77
65
  if TYPE_CHECKING:
78
66
  from boto3.resources.collection import ResourceCollection
@@ -80,6 +68,7 @@ if TYPE_CHECKING:
80
68
  from eodag.api.product import EOProduct
81
69
  from eodag.api.search_result import SearchResult
82
70
  from eodag.config import PluginConfig
71
+ from eodag.types import S3SessionKwargs
83
72
  from eodag.types.download_args import DownloadConf
84
73
  from eodag.utils import DownloadedCallback, Unpack
85
74
 
@@ -207,6 +196,7 @@ AWS_AUTH_ERROR_MESSAGES = [
207
196
  "AccessDenied",
208
197
  "InvalidAccessKeyId",
209
198
  "SignatureDoesNotMatch",
199
+ "InvalidRequest",
210
200
  ]
211
201
 
212
202
 
@@ -216,26 +206,43 @@ class AwsDownload(Download):
216
206
  :param provider: provider name
217
207
  :param config: Download plugin configuration:
218
208
 
219
- * ``config.base_uri`` (str) - s3 endpoint url
220
- * ``config.requester_pays`` (bool) - (optional) whether download is done from a
221
- requester-pays bucket or not
222
- * ``config.flatten_top_dirs`` (bool) - (optional) flatten directory structure
223
- * ``config.products`` (dict) - (optional) product_type specific configuration
224
- * ``config.ignore_assets`` (bool) - (optional) ignore assets and download using downloadLink
209
+ * :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): AwsDownload
210
+ * :attr:`~eodag.config.PluginConfig.base_uri` (``str``) (**mandatory**): s3 endpoint url
211
+ * :attr:`~eodag.config.PluginConfig.requester_pays` (``bool``): whether download is done
212
+ from a requester-pays bucket or not; default: ``False``
213
+ * :attr:`~eodag.config.PluginConfig.flatten_top_dirs` (``bool``): if the directory structure
214
+ should be flattened; default: ``True``
215
+ * :attr:`~eodag.config.PluginConfig.ignore_assets` (``bool``): ignore assets and download
216
+ using ``downloadLink``; default: ``False``
217
+ * :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates should
218
+ be verified in requests; default: ``True``
219
+ * :attr:`~eodag.config.PluginConfig.bucket_path_level` (``int``): at which level of the
220
+ path part of the url the bucket can be found; If no bucket_path_level is given, the bucket
221
+ is taken from the first element of the netloc part.
222
+ * :attr:`~eodag.config.PluginConfig.products` (``dict[str, dict[str, Any]``): product type
223
+ specific config; the keys are the product types, the values are dictionaries which can contain the keys:
224
+
225
+ * **default_bucket** (``str``): bucket where the product type can be found
226
+ * **complementary_url_key** (``str``): keys to add additional urls
227
+ * **build_safe** (``bool``): if a SAFE (Standard Archive Format for Europe) product should
228
+ be created; used for Sentinel products; default: False
229
+ * **fetch_metadata** (``dict[str, Any]``): config for metadata to be fetched for the SAFE product
230
+
225
231
  """
226
232
 
227
233
  def __init__(self, provider: str, config: PluginConfig) -> None:
228
234
  super(AwsDownload, self).__init__(provider, config)
229
235
  self.requester_pays = getattr(self.config, "requester_pays", False)
230
236
  self.s3_session: Optional[boto3.session.Session] = None
237
+ self.s3_resource: Optional[boto3.resources.base.ServiceResource] = None
231
238
 
232
239
  def download(
233
240
  self,
234
241
  product: EOProduct,
235
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
242
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
236
243
  progress_callback: Optional[ProgressCallback] = None,
237
- wait: int = DEFAULT_DOWNLOAD_WAIT,
238
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
244
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
245
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
239
246
  **kwargs: Unpack[DownloadConf],
240
247
  ) -> Optional[str]:
241
248
  """Download method for AWS S3 API.
@@ -321,19 +328,32 @@ class AwsDownload(Download):
321
328
  bucket_names_and_prefixes, auth
322
329
  )
323
330
 
331
+ # files in zip
332
+ updated_bucket_names_and_prefixes = self._download_file_in_zip(
333
+ product, bucket_names_and_prefixes, product_local_path, progress_callback
334
+ )
335
+ # prevent nothing-to-download errors if download was performed in zip
336
+ raise_error = (
337
+ False
338
+ if len(updated_bucket_names_and_prefixes) != len(bucket_names_and_prefixes)
339
+ else True
340
+ )
341
+
324
342
  # downloadable files
325
343
  unique_product_chunks = self._get_unique_products(
326
- bucket_names_and_prefixes,
344
+ updated_bucket_names_and_prefixes,
327
345
  authenticated_objects,
328
346
  asset_filter,
329
347
  ignore_assets,
330
348
  product,
349
+ raise_error=raise_error,
331
350
  )
332
351
 
333
352
  total_size = sum([p.size for p in unique_product_chunks]) or None
334
353
 
335
354
  # download
336
- progress_callback.reset(total=total_size)
355
+ if len(unique_product_chunks) > 0:
356
+ progress_callback.reset(total=total_size)
337
357
  try:
338
358
  for product_chunk in unique_product_chunks:
339
359
  try:
@@ -385,17 +405,65 @@ class AwsDownload(Download):
385
405
 
386
406
  return product_local_path
387
407
 
408
+ def _download_file_in_zip(
409
+ self, product, bucket_names_and_prefixes, product_local_path, progress_callback
410
+ ):
411
+ """
412
+ Download file in zip from a prefix like `foo/bar.zip!file.txt`
413
+ """
414
+ if self.s3_resource is None:
415
+ logger.debug("Cannot check files in s3 zip without s3 resource")
416
+ return bucket_names_and_prefixes
417
+
418
+ s3_client = self.s3_resource.meta.client
419
+
420
+ downloaded = []
421
+ for i, pack in enumerate(bucket_names_and_prefixes):
422
+ bucket_name, prefix = pack
423
+ if ".zip!" in prefix:
424
+ splitted_path = prefix.split(".zip!")
425
+ zip_prefix = f"{splitted_path[0]}.zip"
426
+ rel_path = splitted_path[-1]
427
+ dest_file = os.path.join(product_local_path, rel_path)
428
+ dest_abs_path_dir = os.path.dirname(dest_file)
429
+ if not os.path.isdir(dest_abs_path_dir):
430
+ os.makedirs(dest_abs_path_dir)
431
+
432
+ with open_s3_zipped_object(
433
+ bucket_name, zip_prefix, s3_client, partial=False
434
+ ) as zip_file:
435
+ # file size
436
+ file_info = zip_file.getinfo(rel_path)
437
+ progress_callback.reset(total=file_info.file_size)
438
+ with zip_file.open(rel_path) as extracted, open(
439
+ dest_file, "wb"
440
+ ) as output_file:
441
+ # Read in 1MB chunks
442
+ for zchunk in iter(lambda: extracted.read(1024 * 1024), b""):
443
+ output_file.write(zchunk)
444
+ progress_callback(len(zchunk))
445
+
446
+ downloaded.append(i)
447
+
448
+ return [
449
+ pack
450
+ for i, pack in enumerate(bucket_names_and_prefixes)
451
+ if i not in downloaded
452
+ ]
453
+
388
454
  def _download_preparation(
389
455
  self,
390
456
  product: EOProduct,
391
457
  progress_callback: ProgressCallback,
392
458
  **kwargs: Unpack[DownloadConf],
393
- ) -> Tuple[Optional[str], Optional[str]]:
459
+ ) -> tuple[Optional[str], Optional[str]]:
394
460
  """
395
- preparation for the download:
461
+ Preparation for the download:
462
+
396
463
  - check if file was already downloaded
397
464
  - get file path
398
465
  - create directories
466
+
399
467
  :param product: product to be downloaded
400
468
  :param progress_callback: progress callback to be used
401
469
  :param kwargs: additional arguments
@@ -419,7 +487,8 @@ class AwsDownload(Download):
419
487
 
420
488
  def _configure_safe_build(self, build_safe: bool, product: EOProduct):
421
489
  """
422
- updates the product properties with fetch metadata if safe build is enabled
490
+ Updates the product properties with fetch metadata if safe build is enabled
491
+
423
492
  :param build_safe: if safe build is enabled
424
493
  :param product: product to be updated
425
494
  """
@@ -463,9 +532,10 @@ class AwsDownload(Download):
463
532
  product: EOProduct,
464
533
  asset_filter: Optional[str] = None,
465
534
  ignore_assets: Optional[bool] = False,
466
- ) -> List[Tuple[str, Optional[str]]]:
535
+ ) -> list[tuple[str, Optional[str]]]:
467
536
  """
468
- retrieves the bucket names and path prefixes for the assets
537
+ Retrieves the bucket names and path prefixes for the assets
538
+
469
539
  :param product: product for which the assets shall be downloaded
470
540
  :param asset_filter: text for which the assets should be filtered
471
541
  :param ignore_assets: if product instead of individual assets should be used
@@ -504,14 +574,15 @@ class AwsDownload(Download):
504
574
 
505
575
  def _do_authentication(
506
576
  self,
507
- bucket_names_and_prefixes: List[Tuple[str, Optional[str]]],
508
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
509
- ) -> Tuple[Dict[str, Any], ResourceCollection]:
577
+ bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
578
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
579
+ ) -> tuple[dict[str, Any], ResourceCollection]:
510
580
  """
511
- authenticates with s3 and retrieves the available objects
512
- raises an error when authentication is not possible
581
+ Authenticates with s3 and retrieves the available objects
582
+
513
583
  :param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
514
584
  :param auth: authentication information
585
+ :raises AuthenticationError: authentication is not possible
515
586
  :return: authenticated objects per bucket, list of available objects
516
587
  """
517
588
  if not isinstance(auth, (dict, type(None))):
@@ -520,8 +591,8 @@ class AwsDownload(Download):
520
591
  )
521
592
  if auth is None:
522
593
  auth = {}
523
- authenticated_objects: Dict[str, Any] = {}
524
- auth_error_messages: Set[str] = set()
594
+ authenticated_objects: dict[str, Any] = {}
595
+ auth_error_messages: set[str] = set()
525
596
  for _, pack in enumerate(bucket_names_and_prefixes):
526
597
  try:
527
598
  bucket_name, prefix = pack
@@ -573,22 +644,25 @@ class AwsDownload(Download):
573
644
 
574
645
  def _get_unique_products(
575
646
  self,
576
- bucket_names_and_prefixes: List[Tuple[str, Optional[str]]],
577
- authenticated_objects: Dict[str, Any],
647
+ bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
648
+ authenticated_objects: dict[str, Any],
578
649
  asset_filter: Optional[str],
579
650
  ignore_assets: bool,
580
651
  product: EOProduct,
581
- ) -> Set[Any]:
652
+ raise_error: bool = True,
653
+ ) -> set[Any]:
582
654
  """
583
- retrieve unique product chunks based on authenticated objects and asset filters
655
+ Retrieve unique product chunks based on authenticated objects and asset filters
656
+
584
657
  :param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
585
658
  :param authenticated_objects: available objects per bucket
586
659
  :param asset_filter: text for which assets should be filtered
587
660
  :param ignore_assets: if product instead of individual assets should be used
588
661
  :param product: product that shall be downloaded
662
+ :param raise_error: raise error if there is nothing to download
589
663
  :return: set of product chunks that can be downloaded
590
664
  """
591
- product_chunks: List[Any] = []
665
+ product_chunks: list[Any] = []
592
666
  for bucket_name, prefix in bucket_names_and_prefixes:
593
667
  # unauthenticated items filtered out
594
668
  if bucket_name in authenticated_objects.keys():
@@ -607,36 +681,37 @@ class AwsDownload(Download):
607
681
  unique_product_chunks,
608
682
  )
609
683
  )
610
- if not unique_product_chunks:
684
+ if not unique_product_chunks and raise_error:
611
685
  raise NotAvailableError(
612
686
  rf"No file basename matching re.fullmatch(r'{asset_filter}') was found in {product.remote_location}"
613
687
  )
688
+
689
+ if not unique_product_chunks and raise_error:
690
+ raise NoMatchingProductType("No product found to download.")
691
+
614
692
  return unique_product_chunks
615
693
 
616
694
  def _raise_if_auth_error(self, exception: ClientError) -> None:
617
695
  """Raises an error if given exception is an authentication error"""
618
- err = exception.response["Error"]
696
+ err = cast(dict[str, str], exception.response["Error"])
619
697
  if err["Code"] in AWS_AUTH_ERROR_MESSAGES and "key" in err["Message"].lower():
620
698
  raise AuthenticationError(
621
- "HTTP error {} returned\n{}: {}\nPlease check your credentials for {}".format(
622
- exception.response["ResponseMetadata"]["HTTPStatusCode"],
623
- err["Code"],
624
- err["Message"],
625
- self.provider,
626
- )
699
+ f"Please check your credentials for {self.provider}.",
700
+ f"HTTP Error {exception.response['ResponseMetadata']['HTTPStatusCode']} returned.",
701
+ err["Code"] + ": " + err["Message"],
627
702
  )
628
703
 
629
704
  def _stream_download_dict(
630
705
  self,
631
706
  product: EOProduct,
632
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
707
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
633
708
  progress_callback: Optional[ProgressCallback] = None,
634
- wait: int = DEFAULT_DOWNLOAD_WAIT,
635
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
709
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
710
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
636
711
  **kwargs: Unpack[DownloadConf],
637
712
  ) -> StreamResponse:
638
713
  r"""
639
- Returns dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
714
+ Returns dictionary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
640
715
  It contains a generator to streamed download chunks and the response headers.
641
716
 
642
717
  :param product: The EO product to download
@@ -649,7 +724,7 @@ class AwsDownload(Download):
649
724
  and `dl_url_params` (dict) can be provided as additional kwargs
650
725
  and will override any other values defined in a configuration
651
726
  file or with environment variables.
652
- :returns: Dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments
727
+ :returns: Dictionary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments
653
728
  """
654
729
  if progress_callback is None:
655
730
  logger.info(
@@ -695,6 +770,13 @@ class AwsDownload(Download):
695
770
  bucket_names_and_prefixes, auth
696
771
  )
697
772
 
773
+ # stream not implemented for prefixes like `foo/bar.zip!file.txt`
774
+ for _, prefix in bucket_names_and_prefixes:
775
+ if prefix and ".zip!" in prefix:
776
+ raise NotImplementedError(
777
+ "Download streaming is not implemented for files in zip on S3"
778
+ )
779
+
698
780
  # downloadable files
699
781
  unique_product_chunks = self._get_unique_products(
700
782
  bucket_names_and_prefixes,
@@ -713,12 +795,12 @@ class AwsDownload(Download):
713
795
  else sanitize(product.properties.get("id", "download"))
714
796
  )
715
797
 
716
- if len(assets_values) == 1:
798
+ if len(assets_values) <= 1:
717
799
  first_chunks_tuple = next(chunks_tuples)
718
800
  # update headers
719
801
  filename = os.path.basename(list(unique_product_chunks)[0].key)
720
802
  headers = {"content-disposition": f"attachment; filename={filename}"}
721
- if assets_values[0].get("type", None):
803
+ if assets_values and assets_values[0].get("type", None):
722
804
  headers["content-type"] = assets_values[0]["type"]
723
805
 
724
806
  return StreamResponse(
@@ -735,11 +817,11 @@ class AwsDownload(Download):
735
817
 
736
818
  def _stream_download(
737
819
  self,
738
- unique_product_chunks: Set[Any],
820
+ unique_product_chunks: set[Any],
739
821
  product: EOProduct,
740
822
  build_safe: bool,
741
823
  progress_callback: ProgressCallback,
742
- assets_values: List[Dict[str, Any]],
824
+ assets_values: list[dict[str, Any]],
743
825
  ) -> Iterator[Any]:
744
826
  """Yield product data chunks"""
745
827
 
@@ -751,7 +833,6 @@ class AwsDownload(Download):
751
833
  product_chunk: Any, progress_callback: ProgressCallback
752
834
  ) -> Any:
753
835
  try:
754
-
755
836
  chunk_start = 0
756
837
  chunk_end = chunk_start + chunk_size - 1
757
838
 
@@ -782,7 +863,6 @@ class AwsDownload(Download):
782
863
  common_path = self._get_commonpath(
783
864
  product, unique_product_chunks, build_safe
784
865
  )
785
-
786
866
  for product_chunk in unique_product_chunks:
787
867
  try:
788
868
  chunk_rel_path = self.get_chunk_dest_path(
@@ -800,8 +880,7 @@ class AwsDownload(Download):
800
880
  # out of SAFE format chunk
801
881
  logger.warning(e)
802
882
  continue
803
-
804
- if len(assets_values) == 1:
883
+ if len(assets_values) <= 1:
805
884
  yield from get_chunk_parts(product_chunk, progress_callback)
806
885
  else:
807
886
  yield (
@@ -813,7 +892,7 @@ class AwsDownload(Download):
813
892
  )
814
893
 
815
894
  def _get_commonpath(
816
- self, product: EOProduct, product_chunks: Set[Any], build_safe: bool
895
+ self, product: EOProduct, product_chunks: set[Any], build_safe: bool
817
896
  ) -> str:
818
897
  chunk_paths = []
819
898
  for product_chunk in product_chunks:
@@ -823,32 +902,35 @@ class AwsDownload(Download):
823
902
  return os.path.commonpath(chunk_paths)
824
903
 
825
904
  def get_rio_env(
826
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
827
- ) -> Dict[str, Any]:
905
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
906
+ ) -> dict[str, Any]:
828
907
  """Get rasterio environment variables needed for data access authentication.
829
908
 
830
909
  :param bucket_name: Bucket containg objects
831
910
  :param prefix: Prefix used to try auth
832
- :param auth_dict: Dictionnary containing authentication keys
911
+ :param auth_dict: Dictionary containing authentication keys
833
912
  :returns: The rasterio environement variables
834
913
  """
835
- if self.s3_session is not None:
836
- if self.requester_pays:
837
- return {"session": self.s3_session, "requester_pays": True}
838
- else:
839
- return {"session": self.s3_session}
914
+ rio_env_kwargs = {}
915
+ if endpoint_url := getattr(self.config, "s3_endpoint", None):
916
+ rio_env_kwargs["endpoint_url"] = endpoint_url.split("://")[-1]
917
+ rio_env_kwargs |= auth_dict
918
+
919
+ if self.s3_session is None:
920
+ _ = self.get_authenticated_objects(bucket_name, prefix, auth_dict)
840
921
 
841
- _ = self.get_authenticated_objects(bucket_name, prefix, auth_dict)
842
922
  if self.s3_session is not None:
843
923
  if self.requester_pays:
844
- return {"session": self.s3_session, "requester_pays": True}
845
- else:
846
- return {"session": self.s3_session}
924
+ rio_env_kwargs["requester_pays"] = True
925
+ return {
926
+ "session": self.s3_session,
927
+ **rio_env_kwargs,
928
+ }
847
929
  else:
848
- return {"aws_unsigned": True}
930
+ return {"aws_unsigned": True, **rio_env_kwargs}
849
931
 
850
932
  def get_authenticated_objects(
851
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
933
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
852
934
  ) -> ResourceCollection:
853
935
  """Get boto3 authenticated objects for the given bucket using
854
936
  the most adapted auth strategy.
@@ -857,11 +939,11 @@ class AwsDownload(Download):
857
939
  :param bucket_name: Bucket containg objects
858
940
  :param prefix: Prefix used to filter objects on auth try
859
941
  (not used to filter returned objects)
860
- :param auth_dict: Dictionnary containing authentication keys
942
+ :param auth_dict: Dictionary containing authentication keys
861
943
  :returns: The boto3 authenticated objects
862
944
  """
863
- auth_methods: List[
864
- Callable[[str, str, Dict[str, str]], Optional[ResourceCollection]]
945
+ auth_methods: list[
946
+ Callable[[str, str, S3SessionKwargs], Optional[ResourceCollection]]
865
947
  ] = [
866
948
  self._get_authenticated_objects_unsigned,
867
949
  self._get_authenticated_objects_from_auth_profile,
@@ -896,12 +978,12 @@ class AwsDownload(Download):
896
978
  )
897
979
 
898
980
  def _get_authenticated_objects_unsigned(
899
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
981
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
900
982
  ) -> Optional[ResourceCollection]:
901
983
  """Auth strategy using no-sign-request"""
902
984
 
903
985
  s3_resource = boto3.resource(
904
- service_name="s3", endpoint_url=getattr(self.config, "base_uri", None)
986
+ service_name="s3", endpoint_url=getattr(self.config, "s3_endpoint", None)
905
987
  )
906
988
  s3_resource.meta.client.meta.events.register(
907
989
  "choose-signer.s3.*", disable_signing
@@ -911,7 +993,7 @@ class AwsDownload(Download):
911
993
  return objects
912
994
 
913
995
  def _get_authenticated_objects_from_auth_profile(
914
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
996
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
915
997
  ) -> Optional[ResourceCollection]:
916
998
  """Auth strategy using RequestPayer=requester and ``aws_profile`` from provided credentials"""
917
999
 
@@ -919,7 +1001,7 @@ class AwsDownload(Download):
919
1001
  s3_session = boto3.session.Session(profile_name=auth_dict["profile_name"])
920
1002
  s3_resource = s3_session.resource(
921
1003
  service_name="s3",
922
- endpoint_url=getattr(self.config, "base_uri", None),
1004
+ endpoint_url=getattr(self.config, "s3_endpoint", None),
923
1005
  )
924
1006
  if self.requester_pays:
925
1007
  objects = s3_resource.Bucket(bucket_name).objects.filter(
@@ -929,26 +1011,18 @@ class AwsDownload(Download):
929
1011
  objects = s3_resource.Bucket(bucket_name).objects
930
1012
  list(objects.filter(Prefix=prefix).limit(1))
931
1013
  self.s3_session = s3_session
1014
+ self.s3_resource = s3_resource
932
1015
  return objects
933
1016
  else:
934
1017
  return None
935
1018
 
936
1019
  def _get_authenticated_objects_from_auth_keys(
937
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
1020
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
938
1021
  ) -> Optional[ResourceCollection]:
939
1022
  """Auth strategy using RequestPayer=requester and ``aws_access_key_id``/``aws_secret_access_key``
940
1023
  from provided credentials"""
941
1024
 
942
1025
  if all(k in auth_dict for k in ("aws_access_key_id", "aws_secret_access_key")):
943
- S3SessionKwargs = TypedDict(
944
- "S3SessionKwargs",
945
- {
946
- "aws_access_key_id": str,
947
- "aws_secret_access_key": str,
948
- "aws_session_token": str,
949
- },
950
- total=False,
951
- )
952
1026
  s3_session_kwargs: S3SessionKwargs = {
953
1027
  "aws_access_key_id": auth_dict["aws_access_key_id"],
954
1028
  "aws_secret_access_key": auth_dict["aws_secret_access_key"],
@@ -958,7 +1032,7 @@ class AwsDownload(Download):
958
1032
  s3_session = boto3.session.Session(**s3_session_kwargs)
959
1033
  s3_resource = s3_session.resource(
960
1034
  service_name="s3",
961
- endpoint_url=getattr(self.config, "base_uri", None),
1035
+ endpoint_url=getattr(self.config, "s3_endpoint", None),
962
1036
  )
963
1037
  if self.requester_pays:
964
1038
  objects = s3_resource.Bucket(bucket_name).objects.filter(
@@ -968,18 +1042,19 @@ class AwsDownload(Download):
968
1042
  objects = s3_resource.Bucket(bucket_name).objects
969
1043
  list(objects.filter(Prefix=prefix).limit(1))
970
1044
  self.s3_session = s3_session
1045
+ self.s3_resource = s3_resource
971
1046
  return objects
972
1047
  else:
973
1048
  return None
974
1049
 
975
1050
  def _get_authenticated_objects_from_env(
976
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
1051
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
977
1052
  ) -> Optional[ResourceCollection]:
978
1053
  """Auth strategy using RequestPayer=requester and current environment"""
979
1054
 
980
1055
  s3_session = boto3.session.Session()
981
1056
  s3_resource = s3_session.resource(
982
- service_name="s3", endpoint_url=getattr(self.config, "base_uri", None)
1057
+ service_name="s3", endpoint_url=getattr(self.config, "s3_endpoint", None)
983
1058
  )
984
1059
  if self.requester_pays:
985
1060
  objects = s3_resource.Bucket(bucket_name).objects.filter(
@@ -989,11 +1064,12 @@ class AwsDownload(Download):
989
1064
  objects = s3_resource.Bucket(bucket_name).objects
990
1065
  list(objects.filter(Prefix=prefix).limit(1))
991
1066
  self.s3_session = s3_session
1067
+ self.s3_resource = s3_resource
992
1068
  return objects
993
1069
 
994
1070
  def get_product_bucket_name_and_prefix(
995
1071
  self, product: EOProduct, url: Optional[str] = None
996
- ) -> Tuple[str, Optional[str]]:
1072
+ ) -> tuple[str, Optional[str]]:
997
1073
  """Extract bucket name and prefix from product URL
998
1074
 
999
1075
  :param product: The EO product to download
@@ -1124,7 +1200,7 @@ class AwsDownload(Download):
1124
1200
  s1_title_suffix: Optional[str] = None
1125
1201
  # S2 common
1126
1202
  if product.product_type and "S2_MSI" in product.product_type:
1127
- title_search: Optional[Match[str]] = re.search(
1203
+ title_search: Optional[re.Match[str]] = re.search(
1128
1204
  r"^\w+_\w+_(\w+)_(\w+)_(\w+)_(\w+)_(\w+)$",
1129
1205
  product.properties["title"],
1130
1206
  )
@@ -1310,13 +1386,13 @@ class AwsDownload(Download):
1310
1386
  def download_all(
1311
1387
  self,
1312
1388
  products: SearchResult,
1313
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
1389
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
1314
1390
  downloaded_callback: Optional[DownloadedCallback] = None,
1315
1391
  progress_callback: Optional[ProgressCallback] = None,
1316
- wait: int = DEFAULT_DOWNLOAD_WAIT,
1317
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
1392
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
1393
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
1318
1394
  **kwargs: Unpack[DownloadConf],
1319
- ) -> List[str]:
1395
+ ) -> list[str]:
1320
1396
  """
1321
1397
  download_all using parent (base plugin) method
1322
1398
  """