eodag 3.0.1__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 (87) hide show
  1. eodag/api/core.py +174 -138
  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 +117 -90
  10. eodag/api/search_result.py +13 -23
  11. eodag/cli.py +26 -5
  12. eodag/config.py +86 -92
  13. eodag/plugins/apis/base.py +1 -1
  14. eodag/plugins/apis/ecmwf.py +42 -22
  15. eodag/plugins/apis/usgs.py +17 -16
  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 +22 -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 +146 -87
  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 +90 -46
  40. eodag/plugins/search/build_search_result.py +1048 -361
  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 +19 -18
  45. eodag/plugins/search/qssearch.py +99 -258
  46. eodag/plugins/search/stac_list_assets.py +85 -0
  47. eodag/plugins/search/static_stac_search.py +4 -4
  48. eodag/resources/ext_product_types.json +1 -1
  49. eodag/resources/product_types.yml +1134 -325
  50. eodag/resources/providers.yml +906 -2006
  51. eodag/resources/stac_api.yml +2 -2
  52. eodag/resources/user_conf_template.yml +10 -9
  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 +41 -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 +42 -31
  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 +141 -32
  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 +153 -51
  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 +231 -0
  79. eodag/utils/stac_reader.py +10 -10
  80. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/METADATA +77 -76
  81. eodag-3.1.0.dist-info/RECORD +113 -0
  82. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
  83. {eodag-3.0.1.dist-info → eodag-3.1.0.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.0.dist-info}/LICENSE +0 -0
  87. {eodag-3.0.1.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
@@ -74,6 +60,7 @@ from eodag.utils.exceptions import (
74
60
  NotAvailableError,
75
61
  TimeOutError,
76
62
  )
63
+ from eodag.utils.s3 import open_s3_zipped_object
77
64
 
78
65
  if TYPE_CHECKING:
79
66
  from boto3.resources.collection import ResourceCollection
@@ -81,6 +68,7 @@ if TYPE_CHECKING:
81
68
  from eodag.api.product import EOProduct
82
69
  from eodag.api.search_result import SearchResult
83
70
  from eodag.config import PluginConfig
71
+ from eodag.types import S3SessionKwargs
84
72
  from eodag.types.download_args import DownloadConf
85
73
  from eodag.utils import DownloadedCallback, Unpack
86
74
 
@@ -208,6 +196,7 @@ AWS_AUTH_ERROR_MESSAGES = [
208
196
  "AccessDenied",
209
197
  "InvalidAccessKeyId",
210
198
  "SignatureDoesNotMatch",
199
+ "InvalidRequest",
211
200
  ]
212
201
 
213
202
 
@@ -230,14 +219,14 @@ class AwsDownload(Download):
230
219
  * :attr:`~eodag.config.PluginConfig.bucket_path_level` (``int``): at which level of the
231
220
  path part of the url the bucket can be found; If no bucket_path_level is given, the bucket
232
221
  is taken from the first element of the netloc part.
233
- * :attr:`~eodag.config.PluginConfig.products` (``Dict[str, Dict[str, Any]``): product type
222
+ * :attr:`~eodag.config.PluginConfig.products` (``dict[str, dict[str, Any]``): product type
234
223
  specific config; the keys are the product types, the values are dictionaries which can contain the keys:
235
224
 
236
225
  * **default_bucket** (``str``): bucket where the product type can be found
237
226
  * **complementary_url_key** (``str``): keys to add additional urls
238
227
  * **build_safe** (``bool``): if a SAFE (Standard Archive Format for Europe) product should
239
228
  be created; used for Sentinel products; default: False
240
- * **fetch_metadata** (``Dict[str, Any]``): config for metadata to be fetched for the SAFE product
229
+ * **fetch_metadata** (``dict[str, Any]``): config for metadata to be fetched for the SAFE product
241
230
 
242
231
  """
243
232
 
@@ -245,14 +234,15 @@ class AwsDownload(Download):
245
234
  super(AwsDownload, self).__init__(provider, config)
246
235
  self.requester_pays = getattr(self.config, "requester_pays", False)
247
236
  self.s3_session: Optional[boto3.session.Session] = None
237
+ self.s3_resource: Optional[boto3.resources.base.ServiceResource] = None
248
238
 
249
239
  def download(
250
240
  self,
251
241
  product: EOProduct,
252
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
242
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
253
243
  progress_callback: Optional[ProgressCallback] = None,
254
- wait: int = DEFAULT_DOWNLOAD_WAIT,
255
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
244
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
245
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
256
246
  **kwargs: Unpack[DownloadConf],
257
247
  ) -> Optional[str]:
258
248
  """Download method for AWS S3 API.
@@ -338,19 +328,32 @@ class AwsDownload(Download):
338
328
  bucket_names_and_prefixes, auth
339
329
  )
340
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
+
341
342
  # downloadable files
342
343
  unique_product_chunks = self._get_unique_products(
343
- bucket_names_and_prefixes,
344
+ updated_bucket_names_and_prefixes,
344
345
  authenticated_objects,
345
346
  asset_filter,
346
347
  ignore_assets,
347
348
  product,
349
+ raise_error=raise_error,
348
350
  )
349
351
 
350
352
  total_size = sum([p.size for p in unique_product_chunks]) or None
351
353
 
352
354
  # download
353
- progress_callback.reset(total=total_size)
355
+ if len(unique_product_chunks) > 0:
356
+ progress_callback.reset(total=total_size)
354
357
  try:
355
358
  for product_chunk in unique_product_chunks:
356
359
  try:
@@ -402,17 +405,65 @@ class AwsDownload(Download):
402
405
 
403
406
  return product_local_path
404
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
+
405
454
  def _download_preparation(
406
455
  self,
407
456
  product: EOProduct,
408
457
  progress_callback: ProgressCallback,
409
458
  **kwargs: Unpack[DownloadConf],
410
- ) -> Tuple[Optional[str], Optional[str]]:
459
+ ) -> tuple[Optional[str], Optional[str]]:
411
460
  """
412
- preparation for the download:
461
+ Preparation for the download:
462
+
413
463
  - check if file was already downloaded
414
464
  - get file path
415
465
  - create directories
466
+
416
467
  :param product: product to be downloaded
417
468
  :param progress_callback: progress callback to be used
418
469
  :param kwargs: additional arguments
@@ -436,7 +487,8 @@ class AwsDownload(Download):
436
487
 
437
488
  def _configure_safe_build(self, build_safe: bool, product: EOProduct):
438
489
  """
439
- 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
+
440
492
  :param build_safe: if safe build is enabled
441
493
  :param product: product to be updated
442
494
  """
@@ -480,9 +532,10 @@ class AwsDownload(Download):
480
532
  product: EOProduct,
481
533
  asset_filter: Optional[str] = None,
482
534
  ignore_assets: Optional[bool] = False,
483
- ) -> List[Tuple[str, Optional[str]]]:
535
+ ) -> list[tuple[str, Optional[str]]]:
484
536
  """
485
- retrieves the bucket names and path prefixes for the assets
537
+ Retrieves the bucket names and path prefixes for the assets
538
+
486
539
  :param product: product for which the assets shall be downloaded
487
540
  :param asset_filter: text for which the assets should be filtered
488
541
  :param ignore_assets: if product instead of individual assets should be used
@@ -521,14 +574,15 @@ class AwsDownload(Download):
521
574
 
522
575
  def _do_authentication(
523
576
  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]:
577
+ bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
578
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
579
+ ) -> tuple[dict[str, Any], ResourceCollection]:
527
580
  """
528
- authenticates with s3 and retrieves the available objects
529
- raises an error when authentication is not possible
581
+ Authenticates with s3 and retrieves the available objects
582
+
530
583
  :param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
531
584
  :param auth: authentication information
585
+ :raises AuthenticationError: authentication is not possible
532
586
  :return: authenticated objects per bucket, list of available objects
533
587
  """
534
588
  if not isinstance(auth, (dict, type(None))):
@@ -537,8 +591,8 @@ class AwsDownload(Download):
537
591
  )
538
592
  if auth is None:
539
593
  auth = {}
540
- authenticated_objects: Dict[str, Any] = {}
541
- auth_error_messages: Set[str] = set()
594
+ authenticated_objects: dict[str, Any] = {}
595
+ auth_error_messages: set[str] = set()
542
596
  for _, pack in enumerate(bucket_names_and_prefixes):
543
597
  try:
544
598
  bucket_name, prefix = pack
@@ -590,22 +644,25 @@ class AwsDownload(Download):
590
644
 
591
645
  def _get_unique_products(
592
646
  self,
593
- bucket_names_and_prefixes: List[Tuple[str, Optional[str]]],
594
- authenticated_objects: Dict[str, Any],
647
+ bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
648
+ authenticated_objects: dict[str, Any],
595
649
  asset_filter: Optional[str],
596
650
  ignore_assets: bool,
597
651
  product: EOProduct,
598
- ) -> Set[Any]:
652
+ raise_error: bool = True,
653
+ ) -> set[Any]:
599
654
  """
600
- retrieve unique product chunks based on authenticated objects and asset filters
655
+ Retrieve unique product chunks based on authenticated objects and asset filters
656
+
601
657
  :param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
602
658
  :param authenticated_objects: available objects per bucket
603
659
  :param asset_filter: text for which assets should be filtered
604
660
  :param ignore_assets: if product instead of individual assets should be used
605
661
  :param product: product that shall be downloaded
662
+ :param raise_error: raise error if there is nothing to download
606
663
  :return: set of product chunks that can be downloaded
607
664
  """
608
- product_chunks: List[Any] = []
665
+ product_chunks: list[Any] = []
609
666
  for bucket_name, prefix in bucket_names_and_prefixes:
610
667
  # unauthenticated items filtered out
611
668
  if bucket_name in authenticated_objects.keys():
@@ -624,19 +681,19 @@ class AwsDownload(Download):
624
681
  unique_product_chunks,
625
682
  )
626
683
  )
627
- if not unique_product_chunks:
684
+ if not unique_product_chunks and raise_error:
628
685
  raise NotAvailableError(
629
686
  rf"No file basename matching re.fullmatch(r'{asset_filter}') was found in {product.remote_location}"
630
687
  )
631
688
 
632
- if not unique_product_chunks:
689
+ if not unique_product_chunks and raise_error:
633
690
  raise NoMatchingProductType("No product found to download.")
634
691
 
635
692
  return unique_product_chunks
636
693
 
637
694
  def _raise_if_auth_error(self, exception: ClientError) -> None:
638
695
  """Raises an error if given exception is an authentication error"""
639
- err = cast(Dict[str, str], exception.response["Error"])
696
+ err = cast(dict[str, str], exception.response["Error"])
640
697
  if err["Code"] in AWS_AUTH_ERROR_MESSAGES and "key" in err["Message"].lower():
641
698
  raise AuthenticationError(
642
699
  f"Please check your credentials for {self.provider}.",
@@ -647,10 +704,10 @@ class AwsDownload(Download):
647
704
  def _stream_download_dict(
648
705
  self,
649
706
  product: EOProduct,
650
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
707
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
651
708
  progress_callback: Optional[ProgressCallback] = None,
652
- wait: int = DEFAULT_DOWNLOAD_WAIT,
653
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
709
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
710
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
654
711
  **kwargs: Unpack[DownloadConf],
655
712
  ) -> StreamResponse:
656
713
  r"""
@@ -713,6 +770,13 @@ class AwsDownload(Download):
713
770
  bucket_names_and_prefixes, auth
714
771
  )
715
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
+
716
780
  # downloadable files
717
781
  unique_product_chunks = self._get_unique_products(
718
782
  bucket_names_and_prefixes,
@@ -731,12 +795,12 @@ class AwsDownload(Download):
731
795
  else sanitize(product.properties.get("id", "download"))
732
796
  )
733
797
 
734
- if len(assets_values) == 1:
798
+ if len(assets_values) <= 1:
735
799
  first_chunks_tuple = next(chunks_tuples)
736
800
  # update headers
737
801
  filename = os.path.basename(list(unique_product_chunks)[0].key)
738
802
  headers = {"content-disposition": f"attachment; filename={filename}"}
739
- if assets_values[0].get("type", None):
803
+ if assets_values and assets_values[0].get("type", None):
740
804
  headers["content-type"] = assets_values[0]["type"]
741
805
 
742
806
  return StreamResponse(
@@ -753,11 +817,11 @@ class AwsDownload(Download):
753
817
 
754
818
  def _stream_download(
755
819
  self,
756
- unique_product_chunks: Set[Any],
820
+ unique_product_chunks: set[Any],
757
821
  product: EOProduct,
758
822
  build_safe: bool,
759
823
  progress_callback: ProgressCallback,
760
- assets_values: List[Dict[str, Any]],
824
+ assets_values: list[dict[str, Any]],
761
825
  ) -> Iterator[Any]:
762
826
  """Yield product data chunks"""
763
827
 
@@ -799,7 +863,6 @@ class AwsDownload(Download):
799
863
  common_path = self._get_commonpath(
800
864
  product, unique_product_chunks, build_safe
801
865
  )
802
-
803
866
  for product_chunk in unique_product_chunks:
804
867
  try:
805
868
  chunk_rel_path = self.get_chunk_dest_path(
@@ -817,8 +880,7 @@ class AwsDownload(Download):
817
880
  # out of SAFE format chunk
818
881
  logger.warning(e)
819
882
  continue
820
-
821
- if len(assets_values) == 1:
883
+ if len(assets_values) <= 1:
822
884
  yield from get_chunk_parts(product_chunk, progress_callback)
823
885
  else:
824
886
  yield (
@@ -830,7 +892,7 @@ class AwsDownload(Download):
830
892
  )
831
893
 
832
894
  def _get_commonpath(
833
- self, product: EOProduct, product_chunks: Set[Any], build_safe: bool
895
+ self, product: EOProduct, product_chunks: set[Any], build_safe: bool
834
896
  ) -> str:
835
897
  chunk_paths = []
836
898
  for product_chunk in product_chunks:
@@ -840,8 +902,8 @@ class AwsDownload(Download):
840
902
  return os.path.commonpath(chunk_paths)
841
903
 
842
904
  def get_rio_env(
843
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
844
- ) -> Dict[str, Any]:
905
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
906
+ ) -> dict[str, Any]:
845
907
  """Get rasterio environment variables needed for data access authentication.
846
908
 
847
909
  :param bucket_name: Bucket containg objects
@@ -849,23 +911,26 @@ class AwsDownload(Download):
849
911
  :param auth_dict: Dictionary containing authentication keys
850
912
  :returns: The rasterio environement variables
851
913
  """
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}
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)
857
921
 
858
- _ = self.get_authenticated_objects(bucket_name, prefix, auth_dict)
859
922
  if self.s3_session is not None:
860
923
  if self.requester_pays:
861
- return {"session": self.s3_session, "requester_pays": True}
862
- else:
863
- return {"session": self.s3_session}
924
+ rio_env_kwargs["requester_pays"] = True
925
+ return {
926
+ "session": self.s3_session,
927
+ **rio_env_kwargs,
928
+ }
864
929
  else:
865
- return {"aws_unsigned": True}
930
+ return {"aws_unsigned": True, **rio_env_kwargs}
866
931
 
867
932
  def get_authenticated_objects(
868
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
933
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
869
934
  ) -> ResourceCollection:
870
935
  """Get boto3 authenticated objects for the given bucket using
871
936
  the most adapted auth strategy.
@@ -877,8 +942,8 @@ class AwsDownload(Download):
877
942
  :param auth_dict: Dictionary containing authentication keys
878
943
  :returns: The boto3 authenticated objects
879
944
  """
880
- auth_methods: List[
881
- Callable[[str, str, Dict[str, str]], Optional[ResourceCollection]]
945
+ auth_methods: list[
946
+ Callable[[str, str, S3SessionKwargs], Optional[ResourceCollection]]
882
947
  ] = [
883
948
  self._get_authenticated_objects_unsigned,
884
949
  self._get_authenticated_objects_from_auth_profile,
@@ -913,7 +978,7 @@ class AwsDownload(Download):
913
978
  )
914
979
 
915
980
  def _get_authenticated_objects_unsigned(
916
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
981
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
917
982
  ) -> Optional[ResourceCollection]:
918
983
  """Auth strategy using no-sign-request"""
919
984
 
@@ -928,7 +993,7 @@ class AwsDownload(Download):
928
993
  return objects
929
994
 
930
995
  def _get_authenticated_objects_from_auth_profile(
931
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
996
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
932
997
  ) -> Optional[ResourceCollection]:
933
998
  """Auth strategy using RequestPayer=requester and ``aws_profile`` from provided credentials"""
934
999
 
@@ -946,26 +1011,18 @@ class AwsDownload(Download):
946
1011
  objects = s3_resource.Bucket(bucket_name).objects
947
1012
  list(objects.filter(Prefix=prefix).limit(1))
948
1013
  self.s3_session = s3_session
1014
+ self.s3_resource = s3_resource
949
1015
  return objects
950
1016
  else:
951
1017
  return None
952
1018
 
953
1019
  def _get_authenticated_objects_from_auth_keys(
954
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
1020
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
955
1021
  ) -> Optional[ResourceCollection]:
956
1022
  """Auth strategy using RequestPayer=requester and ``aws_access_key_id``/``aws_secret_access_key``
957
1023
  from provided credentials"""
958
1024
 
959
1025
  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
1026
  s3_session_kwargs: S3SessionKwargs = {
970
1027
  "aws_access_key_id": auth_dict["aws_access_key_id"],
971
1028
  "aws_secret_access_key": auth_dict["aws_secret_access_key"],
@@ -985,12 +1042,13 @@ class AwsDownload(Download):
985
1042
  objects = s3_resource.Bucket(bucket_name).objects
986
1043
  list(objects.filter(Prefix=prefix).limit(1))
987
1044
  self.s3_session = s3_session
1045
+ self.s3_resource = s3_resource
988
1046
  return objects
989
1047
  else:
990
1048
  return None
991
1049
 
992
1050
  def _get_authenticated_objects_from_env(
993
- self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
1051
+ self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
994
1052
  ) -> Optional[ResourceCollection]:
995
1053
  """Auth strategy using RequestPayer=requester and current environment"""
996
1054
 
@@ -1006,11 +1064,12 @@ class AwsDownload(Download):
1006
1064
  objects = s3_resource.Bucket(bucket_name).objects
1007
1065
  list(objects.filter(Prefix=prefix).limit(1))
1008
1066
  self.s3_session = s3_session
1067
+ self.s3_resource = s3_resource
1009
1068
  return objects
1010
1069
 
1011
1070
  def get_product_bucket_name_and_prefix(
1012
1071
  self, product: EOProduct, url: Optional[str] = None
1013
- ) -> Tuple[str, Optional[str]]:
1072
+ ) -> tuple[str, Optional[str]]:
1014
1073
  """Extract bucket name and prefix from product URL
1015
1074
 
1016
1075
  :param product: The EO product to download
@@ -1141,7 +1200,7 @@ class AwsDownload(Download):
1141
1200
  s1_title_suffix: Optional[str] = None
1142
1201
  # S2 common
1143
1202
  if product.product_type and "S2_MSI" in product.product_type:
1144
- title_search: Optional[Match[str]] = re.search(
1203
+ title_search: Optional[re.Match[str]] = re.search(
1145
1204
  r"^\w+_\w+_(\w+)_(\w+)_(\w+)_(\w+)_(\w+)$",
1146
1205
  product.properties["title"],
1147
1206
  )
@@ -1327,13 +1386,13 @@ class AwsDownload(Download):
1327
1386
  def download_all(
1328
1387
  self,
1329
1388
  products: SearchResult,
1330
- auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
1389
+ auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
1331
1390
  downloaded_callback: Optional[DownloadedCallback] = None,
1332
1391
  progress_callback: Optional[ProgressCallback] = None,
1333
- wait: int = DEFAULT_DOWNLOAD_WAIT,
1334
- timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
1392
+ wait: float = DEFAULT_DOWNLOAD_WAIT,
1393
+ timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
1335
1394
  **kwargs: Unpack[DownloadConf],
1336
- ) -> List[str]:
1395
+ ) -> list[str]:
1337
1396
  """
1338
1397
  download_all using parent (base plugin) method
1339
1398
  """