eodag 2.12.0__py3-none-any.whl → 3.0.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 (93) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +654 -538
  3. eodag/api/product/__init__.py +12 -2
  4. eodag/api/product/_assets.py +59 -16
  5. eodag/api/product/_product.py +100 -93
  6. eodag/api/product/drivers/__init__.py +7 -2
  7. eodag/api/product/drivers/base.py +0 -3
  8. eodag/api/product/metadata_mapping.py +192 -96
  9. eodag/api/search_result.py +69 -10
  10. eodag/cli.py +55 -25
  11. eodag/config.py +391 -116
  12. eodag/plugins/apis/base.py +11 -165
  13. eodag/plugins/apis/ecmwf.py +36 -25
  14. eodag/plugins/apis/usgs.py +80 -35
  15. eodag/plugins/authentication/aws_auth.py +13 -4
  16. eodag/plugins/authentication/base.py +10 -1
  17. eodag/plugins/authentication/generic.py +2 -2
  18. eodag/plugins/authentication/header.py +31 -6
  19. eodag/plugins/authentication/keycloak.py +17 -84
  20. eodag/plugins/authentication/oauth.py +3 -3
  21. eodag/plugins/authentication/openid_connect.py +268 -49
  22. eodag/plugins/authentication/qsauth.py +4 -1
  23. eodag/plugins/authentication/sas_auth.py +9 -2
  24. eodag/plugins/authentication/token.py +98 -47
  25. eodag/plugins/authentication/token_exchange.py +122 -0
  26. eodag/plugins/crunch/base.py +3 -1
  27. eodag/plugins/crunch/filter_date.py +3 -9
  28. eodag/plugins/crunch/filter_latest_intersect.py +0 -3
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
  30. eodag/plugins/crunch/filter_overlap.py +4 -8
  31. eodag/plugins/crunch/filter_property.py +5 -11
  32. eodag/plugins/download/aws.py +149 -185
  33. eodag/plugins/download/base.py +88 -97
  34. eodag/plugins/download/creodias_s3.py +1 -1
  35. eodag/plugins/download/http.py +638 -310
  36. eodag/plugins/download/s3rest.py +47 -45
  37. eodag/plugins/manager.py +228 -88
  38. eodag/plugins/search/__init__.py +36 -0
  39. eodag/plugins/search/base.py +239 -30
  40. eodag/plugins/search/build_search_result.py +382 -37
  41. eodag/plugins/search/cop_marine.py +441 -0
  42. eodag/plugins/search/creodias_s3.py +25 -20
  43. eodag/plugins/search/csw.py +5 -7
  44. eodag/plugins/search/data_request_search.py +61 -30
  45. eodag/plugins/search/qssearch.py +713 -255
  46. eodag/plugins/search/static_stac_search.py +106 -40
  47. eodag/resources/ext_product_types.json +1 -1
  48. eodag/resources/product_types.yml +1921 -34
  49. eodag/resources/providers.yml +4091 -3655
  50. eodag/resources/stac.yml +50 -216
  51. eodag/resources/stac_api.yml +71 -25
  52. eodag/resources/stac_provider.yml +5 -0
  53. eodag/resources/user_conf_template.yml +89 -32
  54. eodag/rest/__init__.py +6 -0
  55. eodag/rest/cache.py +70 -0
  56. eodag/rest/config.py +68 -0
  57. eodag/rest/constants.py +26 -0
  58. eodag/rest/core.py +735 -0
  59. eodag/rest/errors.py +178 -0
  60. eodag/rest/server.py +264 -431
  61. eodag/rest/stac.py +442 -836
  62. eodag/rest/types/collections_search.py +44 -0
  63. eodag/rest/types/eodag_search.py +238 -47
  64. eodag/rest/types/queryables.py +164 -0
  65. eodag/rest/types/stac_search.py +273 -0
  66. eodag/rest/utils/__init__.py +216 -0
  67. eodag/rest/utils/cql_evaluate.py +119 -0
  68. eodag/rest/utils/rfc3339.py +64 -0
  69. eodag/types/__init__.py +106 -10
  70. eodag/types/bbox.py +15 -14
  71. eodag/types/download_args.py +40 -0
  72. eodag/types/search_args.py +57 -7
  73. eodag/types/whoosh.py +79 -0
  74. eodag/utils/__init__.py +110 -91
  75. eodag/utils/constraints.py +37 -45
  76. eodag/utils/exceptions.py +39 -22
  77. eodag/utils/import_system.py +0 -4
  78. eodag/utils/logging.py +37 -80
  79. eodag/utils/notebook.py +4 -4
  80. eodag/utils/repr.py +113 -0
  81. eodag/utils/requests.py +128 -0
  82. eodag/utils/rest.py +100 -0
  83. eodag/utils/stac_reader.py +93 -21
  84. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
  85. eodag-3.0.0.dist-info/RECORD +109 -0
  86. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
  87. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
  88. eodag/plugins/apis/cds.py +0 -540
  89. eodag/rest/types/stac_queryables.py +0 -134
  90. eodag/rest/utils.py +0 -1133
  91. eodag-2.12.0.dist-info/RECORD +0 -94
  92. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
  93. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/top_level.txt +0 -0
@@ -34,6 +34,7 @@ from typing import (
34
34
  Optional,
35
35
  Set,
36
36
  Tuple,
37
+ TypedDict,
37
38
  Union,
38
39
  cast,
39
40
  )
@@ -43,6 +44,7 @@ import requests
43
44
  from botocore.exceptions import ClientError, ProfileNotFound
44
45
  from botocore.handlers import disable_signing
45
46
  from lxml import etree
47
+ from requests.auth import AuthBase
46
48
  from stream_zip import ZIP_AUTO, stream_zip
47
49
 
48
50
  from eodag.api.product.metadata_mapping import (
@@ -57,6 +59,7 @@ from eodag.utils import (
57
59
  HTTP_REQ_TIMEOUT,
58
60
  USER_AGENT,
59
61
  ProgressCallback,
62
+ StreamResponse,
60
63
  flatten_top_directories,
61
64
  get_bucket_name_and_prefix,
62
65
  path_to_uri,
@@ -66,6 +69,8 @@ from eodag.utils import (
66
69
  from eodag.utils.exceptions import (
67
70
  AuthenticationError,
68
71
  DownloadError,
72
+ MisconfiguredError,
73
+ NoMatchingProductType,
69
74
  NotAvailableError,
70
75
  TimeOutError,
71
76
  )
@@ -76,7 +81,8 @@ if TYPE_CHECKING:
76
81
  from eodag.api.product import EOProduct
77
82
  from eodag.api.search_result import SearchResult
78
83
  from eodag.config import PluginConfig
79
- from eodag.utils import DownloadedCallback
84
+ from eodag.types.download_args import DownloadConf
85
+ from eodag.utils import DownloadedCallback, Unpack
80
86
 
81
87
 
82
88
  logger = logging.getLogger("eodag.download.aws")
@@ -209,59 +215,56 @@ class AwsDownload(Download):
209
215
  """Download on AWS using S3 protocol.
210
216
 
211
217
  :param provider: provider name
212
- :type provider: str
213
218
  :param config: Download plugin configuration:
214
219
 
215
- * ``config.base_uri`` (str) - s3 endpoint url
220
+ * ``config.s3_endpoint`` (str) - s3 endpoint url
216
221
  * ``config.requester_pays`` (bool) - (optional) whether download is done from a
217
222
  requester-pays bucket or not
218
223
  * ``config.flatten_top_dirs`` (bool) - (optional) flatten directory structure
219
224
  * ``config.products`` (dict) - (optional) product_type specific configuration
220
225
  * ``config.ignore_assets`` (bool) - (optional) ignore assets and download using downloadLink
221
-
222
- :type config: :class:`~eodag.config.PluginConfig`
223
226
  """
224
227
 
225
228
  def __init__(self, provider: str, config: PluginConfig) -> None:
226
229
  super(AwsDownload, self).__init__(provider, config)
227
230
  self.requester_pays = getattr(self.config, "requester_pays", False)
228
- self.s3_session = None
231
+ self.s3_session: Optional[boto3.session.Session] = None
229
232
 
230
233
  def download(
231
234
  self,
232
235
  product: EOProduct,
233
- auth: Optional[PluginConfig] = None,
236
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
234
237
  progress_callback: Optional[ProgressCallback] = None,
235
238
  wait: int = DEFAULT_DOWNLOAD_WAIT,
236
239
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
237
- **kwargs: Union[str, bool, Dict[str, Any]],
238
- ) -> str:
240
+ **kwargs: Unpack[DownloadConf],
241
+ ) -> Optional[str]:
239
242
  """Download method for AWS S3 API.
240
243
 
241
244
  The product can be downloaded as it is, or as SAFE-formatted product.
242
245
  SAFE-build is configured for a given provider and product type.
243
246
  If the product title is configured to be updated during download and
244
247
  SAFE-formatted, its destination path will be:
245
- `{outputs_prefix}/{title}/{updated_title}.SAFE`
248
+ `{output_dir}/{title}`
246
249
 
247
250
  :param product: The EO product to download
248
- :type product: :class:`~eodag.api.product._product.EOProduct`
249
- :param auth: (optional) The configuration of a plugin of type Authentication
250
- :type auth: Union[AuthBase, Dict[str, str]]
251
+ :param auth: (optional) authenticated object
251
252
  :param progress_callback: (optional) A method or a callable object
252
253
  which takes a current size and a maximum
253
254
  size as inputs and handle progress bar
254
255
  creation and update to give the user a
255
256
  feedback on the download progress
256
- :type progress_callback: :class:`~eodag.utils.ProgressCallback` or None
257
- :param kwargs: `outputs_prefix` (str), `extract` (bool), `delete_archive` (bool)
257
+ :param kwargs: `output_dir` (str), `extract` (bool), `delete_archive` (bool)
258
258
  and `dl_url_params` (dict) can be provided as additional kwargs
259
259
  and will override any other values defined in a configuration
260
260
  file or with environment variables.
261
- :type kwargs: Union[str, bool, dict]
262
261
  :returns: The absolute path to the downloaded product in the local filesystem
263
- :rtype: str
264
262
  """
263
+ if auth is None:
264
+ auth = {}
265
+ if isinstance(auth, AuthBase):
266
+ raise MisconfiguredError("Please use AwsAuth plugin with AwsDownload")
267
+
265
268
  if progress_callback is None:
266
269
  logger.info(
267
270
  "Progress bar unavailable, please call product.download() instead of plugin.download()"
@@ -272,7 +275,7 @@ class AwsDownload(Download):
272
275
  product_local_path, record_filename = self._download_preparation(
273
276
  product, progress_callback=progress_callback, **kwargs
274
277
  )
275
- if not record_filename:
278
+ if not record_filename or not product_local_path:
276
279
  return product_local_path
277
280
 
278
281
  product_conf = getattr(self.config, "products", {}).get(
@@ -290,7 +293,7 @@ class AwsDownload(Download):
290
293
 
291
294
  # product conf overrides provider conf for "flatten_top_dirs"
292
295
  flatten_top_dirs = product_conf.get(
293
- "flatten_top_dirs", getattr(self.config, "flatten_top_dirs", False)
296
+ "flatten_top_dirs", getattr(self.config, "flatten_top_dirs", True)
294
297
  )
295
298
 
296
299
  # xtra metadata needed for SAFE product
@@ -328,7 +331,7 @@ class AwsDownload(Download):
328
331
  product,
329
332
  )
330
333
 
331
- total_size = sum([p.size for p in unique_product_chunks])
334
+ total_size = sum([p.size for p in unique_product_chunks]) or None
332
335
 
333
336
  # download
334
337
  progress_callback.reset(total=total_size)
@@ -384,21 +387,20 @@ class AwsDownload(Download):
384
387
  return product_local_path
385
388
 
386
389
  def _download_preparation(
387
- self, product: EOProduct, progress_callback: ProgressCallback, **kwargs: Any
388
- ) -> Tuple[str, Optional[str]]:
390
+ self,
391
+ product: EOProduct,
392
+ progress_callback: ProgressCallback,
393
+ **kwargs: Unpack[DownloadConf],
394
+ ) -> Tuple[Optional[str], Optional[str]]:
389
395
  """
390
396
  preparation for the download:
391
397
  - check if file was already downloaded
392
398
  - get file path
393
399
  - create directories
394
400
  :param product: product to be downloaded
395
- :type product: EOProduct
396
401
  :param progress_callback: progress callback to be used
397
- :type progress_callback: ProgressCallback
398
402
  :param kwargs: additional arguments
399
- :type kwargs: Any
400
403
  :return: local path and file name
401
- :rtype: Tuple[str, Optional[str]]
402
404
  """
403
405
  product_local_path, record_filename = self._prepare_download(
404
406
  product, progress_callback=progress_callback, **kwargs
@@ -420,13 +422,14 @@ class AwsDownload(Download):
420
422
  """
421
423
  updates the product properties with fetch metadata if safe build is enabled
422
424
  :param build_safe: if safe build is enabled
423
- :type build_safe: bool
424
425
  :param product: product to be updated
425
- :type product: EOProduct
426
426
  """
427
427
  product_conf = getattr(self.config, "products", {}).get(
428
428
  product.product_type, {}
429
429
  )
430
+ ssl_verify = getattr(self.config, "ssl_verify", True)
431
+ timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
432
+
430
433
  if build_safe and "fetch_metadata" in product_conf.keys():
431
434
  fetch_format = product_conf["fetch_metadata"]["fetch_format"]
432
435
  update_metadata = product_conf["fetch_metadata"]["update_metadata"]
@@ -436,10 +439,13 @@ class AwsDownload(Download):
436
439
  logger.info("Fetching extra metadata from %s" % fetch_url)
437
440
  try:
438
441
  resp = requests.get(
439
- fetch_url, headers=USER_AGENT, timeout=HTTP_REQ_TIMEOUT
442
+ fetch_url,
443
+ headers=USER_AGENT,
444
+ timeout=timeout,
445
+ verify=ssl_verify,
440
446
  )
441
447
  except requests.exceptions.Timeout as exc:
442
- raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
448
+ raise TimeOutError(exc, timeout=timeout) from exc
443
449
  update_metadata = mtd_cfg_as_conversion_and_querypath(update_metadata)
444
450
  if fetch_format == "json":
445
451
  json_resp = resp.json()
@@ -454,18 +460,17 @@ class AwsDownload(Download):
454
460
  )
455
461
 
456
462
  def _get_bucket_names_and_prefixes(
457
- self, product: EOProduct, asset_filter: str, ignore_assets: bool
463
+ self,
464
+ product: EOProduct,
465
+ asset_filter: Optional[str] = None,
466
+ ignore_assets: Optional[bool] = False,
458
467
  ) -> List[Tuple[str, Optional[str]]]:
459
468
  """
460
469
  retrieves the bucket names and path prefixes for the assets
461
470
  :param product: product for which the assets shall be downloaded
462
- :type product: EOProduct
463
471
  :param asset_filter: text for which the assets should be filtered
464
- :type asset_filter: str
465
472
  :param ignore_assets: if product instead of individual assets should be used
466
- :type ignore_assets: bool
467
473
  :return: tuples of bucket names and prefixes
468
- :rtype: List[Tuple[str, Optional[str]]]
469
474
  """
470
475
  # if assets are defined, use them instead of scanning product.location
471
476
  if len(product.assets) > 0 and not ignore_assets:
@@ -483,7 +488,7 @@ class AwsDownload(Download):
483
488
  rf"No asset key matching re.fullmatch(r'{asset_filter}') was found in {product}"
484
489
  )
485
490
  else:
486
- assets_values = getattr(product, "assets", {}).values()
491
+ assets_values = product.assets.values()
487
492
 
488
493
  bucket_names_and_prefixes = []
489
494
  for complementary_url in assets_values:
@@ -501,23 +506,28 @@ class AwsDownload(Download):
501
506
  def _do_authentication(
502
507
  self,
503
508
  bucket_names_and_prefixes: List[Tuple[str, Optional[str]]],
504
- auth: Dict[str, str],
505
- ) -> Tuple[Dict[str, Any], ResourceCollection[Any]]:
509
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
510
+ ) -> Tuple[Dict[str, Any], ResourceCollection]:
506
511
  """
507
512
  authenticates with s3 and retrieves the available objects
508
513
  raises an error when authentication is not possible
509
514
  :param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
510
- :type bucket_names_and_prefixes: List[Tuple[str, Optional[str]]]
511
515
  :param auth: authentication information
512
- :type auth: Dict[str, str]
513
516
  :return: authenticated objects per bucket, list of available objects
514
- :rtype: Tuple[Dict[str, Any], ResourceCollection[Any]]
515
517
  """
518
+ if not isinstance(auth, (dict, type(None))):
519
+ raise AuthenticationError(
520
+ f"Incompatible authentication information, expected dict or None, got {type(auth)}"
521
+ )
522
+ if auth is None:
523
+ auth = {}
516
524
  authenticated_objects: Dict[str, Any] = {}
517
525
  auth_error_messages: Set[str] = set()
518
526
  for _, pack in enumerate(bucket_names_and_prefixes):
519
527
  try:
520
528
  bucket_name, prefix = pack
529
+ if not prefix:
530
+ continue
521
531
  if bucket_name not in authenticated_objects:
522
532
  # get Prefixes longest common base path
523
533
  common_prefix = ""
@@ -532,7 +542,7 @@ class AwsDownload(Download):
532
542
  [
533
543
  p
534
544
  for b, p in bucket_names_and_prefixes
535
- if b == bucket_name and common_prefix in p
545
+ if p and b == bucket_name and common_prefix in p
536
546
  ]
537
547
  )
538
548
  < prefixes_in_bucket
@@ -566,24 +576,18 @@ class AwsDownload(Download):
566
576
  self,
567
577
  bucket_names_and_prefixes: List[Tuple[str, Optional[str]]],
568
578
  authenticated_objects: Dict[str, Any],
569
- asset_filter: str,
579
+ asset_filter: Optional[str],
570
580
  ignore_assets: bool,
571
581
  product: EOProduct,
572
582
  ) -> Set[Any]:
573
583
  """
574
584
  retrieve unique product chunks based on authenticated objects and asset filters
575
585
  :param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
576
- :type bucket_names_and_prefixes: List[Tuple[str, Optional[str]]]
577
586
  :param authenticated_objects: available objects per bucket
578
- :type authenticated_objects: Dict[str, Any]
579
587
  :param asset_filter: text for which assets should be filtered
580
- :type asset_filter: str
581
588
  :param ignore_assets: if product instead of individual assets should be used
582
- :type ignore_assets: bool
583
589
  :param product: product that shall be downloaded
584
- :type product: EOProduct
585
590
  :return: set of product chunks that can be downloaded
586
- :rtype: Set[Any]
587
591
  """
588
592
  product_chunks: List[Any] = []
589
593
  for bucket_name, prefix in bucket_names_and_prefixes:
@@ -608,52 +612,46 @@ class AwsDownload(Download):
608
612
  raise NotAvailableError(
609
613
  rf"No file basename matching re.fullmatch(r'{asset_filter}') was found in {product.remote_location}"
610
614
  )
615
+
616
+ if not unique_product_chunks:
617
+ raise NoMatchingProductType("No product found to download.")
618
+
611
619
  return unique_product_chunks
612
620
 
613
621
  def _raise_if_auth_error(self, exception: ClientError) -> None:
614
622
  """Raises an error if given exception is an authentication error"""
615
- err = exception.response["Error"]
623
+ err = cast(Dict[str, str], exception.response["Error"])
616
624
  if err["Code"] in AWS_AUTH_ERROR_MESSAGES and "key" in err["Message"].lower():
617
625
  raise AuthenticationError(
618
- "HTTP error {} returned\n{}: {}\nPlease check your credentials for {}".format(
619
- exception.response["ResponseMetadata"]["HTTPStatusCode"],
620
- err["Code"],
621
- err["Message"],
622
- self.provider,
623
- )
626
+ f"Please check your credentials for {self.provider}.",
627
+ f"HTTP Error {exception.response['ResponseMetadata']['HTTPStatusCode']} returned.",
628
+ err["Code"] + ": " + err["Message"],
624
629
  )
625
630
 
626
631
  def _stream_download_dict(
627
632
  self,
628
633
  product: EOProduct,
629
- auth: Optional[PluginConfig] = None,
634
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
630
635
  progress_callback: Optional[ProgressCallback] = None,
631
636
  wait: int = DEFAULT_DOWNLOAD_WAIT,
632
637
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
633
- **kwargs: Union[str, bool, Dict[str, Any]],
634
- ) -> Dict[str, Any]:
638
+ **kwargs: Unpack[DownloadConf],
639
+ ) -> StreamResponse:
635
640
  r"""
636
- Returns dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
641
+ Returns dictionary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
637
642
  It contains a generator to streamed download chunks and the response headers.
638
643
 
639
644
  :param product: The EO product to download
640
- :type product: :class:`~eodag.api.product._product.EOProduct`
641
645
  :param auth: (optional) The configuration of a plugin of type Authentication
642
- :type auth: :class:`~eodag.config.PluginConfig`
643
646
  :param progress_callback: (optional) A progress callback
644
- :type progress_callback: :class:`~eodag.utils.ProgressCallback`
645
647
  :param wait: (optional) If download fails, wait time in minutes between two download tries
646
- :type wait: int
647
648
  :param timeout: (optional) If download fails, maximum time in minutes before stop retrying
648
649
  to download
649
- :type timeout: int
650
- :param kwargs: `outputs_prefix` (str), `extract` (bool), `delete_archive` (bool)
650
+ :param kwargs: `output_dir` (str), `extract` (bool), `delete_archive` (bool)
651
651
  and `dl_url_params` (dict) can be provided as additional kwargs
652
652
  and will override any other values defined in a configuration
653
653
  file or with environment variables.
654
- :type kwargs: Union[str, bool, dict]
655
- :returns: Dictionnary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments
656
- :rtype: dict
654
+ :returns: Dictionary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments
657
655
  """
658
656
  if progress_callback is None:
659
657
  logger.info(
@@ -725,11 +723,11 @@ class AwsDownload(Download):
725
723
  if assets_values[0].get("type", None):
726
724
  headers["content-type"] = assets_values[0]["type"]
727
725
 
728
- return dict(
726
+ return StreamResponse(
729
727
  content=chain(iter([first_chunks_tuple]), chunks_tuples),
730
728
  headers=headers,
731
729
  )
732
- return dict(
730
+ return StreamResponse(
733
731
  content=stream_zip(chunks_tuples),
734
732
  media_type="application/zip",
735
733
  headers={
@@ -744,7 +742,7 @@ class AwsDownload(Download):
744
742
  build_safe: bool,
745
743
  progress_callback: ProgressCallback,
746
744
  assets_values: List[Dict[str, Any]],
747
- ) -> Iterator[Tuple[str, datetime, int, Any, Iterator[Any]]]:
745
+ ) -> Iterator[Any]:
748
746
  """Yield product data chunks"""
749
747
 
750
748
  chunk_size = 4096 * 1024
@@ -755,7 +753,6 @@ class AwsDownload(Download):
755
753
  product_chunk: Any, progress_callback: ProgressCallback
756
754
  ) -> Any:
757
755
  try:
758
-
759
756
  chunk_start = 0
760
757
  chunk_end = chunk_start + chunk_size - 1
761
758
 
@@ -779,7 +776,7 @@ class AwsDownload(Download):
779
776
  product.product_type, {}
780
777
  )
781
778
  flatten_top_dirs = product_conf.get(
782
- "flatten_top_dirs", getattr(self.config, "flatten_top_dirs", False)
779
+ "flatten_top_dirs", getattr(self.config, "flatten_top_dirs", True)
783
780
  )
784
781
  common_path = ""
785
782
  if flatten_top_dirs:
@@ -832,13 +829,9 @@ class AwsDownload(Download):
832
829
  """Get rasterio environment variables needed for data access authentication.
833
830
 
834
831
  :param bucket_name: Bucket containg objects
835
- :type bucket_name: str
836
832
  :param prefix: Prefix used to try auth
837
- :type prefix: str
838
- :param auth_dict: Dictionnary containing authentication keys
839
- :type auth_dict: dict
833
+ :param auth_dict: Dictionary containing authentication keys
840
834
  :returns: The rasterio environement variables
841
- :rtype: dict
842
835
  """
843
836
  if self.s3_session is not None:
844
837
  if self.requester_pays:
@@ -863,14 +856,10 @@ class AwsDownload(Download):
863
856
  Also expose ``s3_session`` as class variable if available.
864
857
 
865
858
  :param bucket_name: Bucket containg objects
866
- :type bucket_name: str
867
859
  :param prefix: Prefix used to filter objects on auth try
868
860
  (not used to filter returned objects)
869
- :type prefix: str
870
- :param auth_dict: Dictionnary containing authentication keys
871
- :type auth_dict: dict
861
+ :param auth_dict: Dictionary containing authentication keys
872
862
  :returns: The boto3 authenticated objects
873
- :rtype: :class:`~boto3.resources.collection.s3.Bucket.objectsCollection`
874
863
  """
875
864
  auth_methods: List[
876
865
  Callable[[str, str, Dict[str, str]], Optional[ResourceCollection]]
@@ -912,15 +901,15 @@ class AwsDownload(Download):
912
901
  ) -> Optional[ResourceCollection]:
913
902
  """Auth strategy using no-sign-request"""
914
903
 
915
- s3_resource = boto3.resource( # type: ignore
916
- service_name="s3", endpoint_url=getattr(self.config, "base_uri", None)
904
+ s3_resource = boto3.resource(
905
+ service_name="s3", endpoint_url=getattr(self.config, "s3_endpoint", None)
917
906
  )
918
- s3_resource.meta.client.meta.events.register( # type: ignore
907
+ s3_resource.meta.client.meta.events.register(
919
908
  "choose-signer.s3.*", disable_signing
920
909
  )
921
- objects = s3_resource.Bucket(bucket_name).objects # type: ignore
922
- list(objects.filter(Prefix=prefix).limit(1)) # type: ignore
923
- return objects # type: ignore
910
+ objects = s3_resource.Bucket(bucket_name).objects
911
+ list(objects.filter(Prefix=prefix).limit(1))
912
+ return objects
924
913
 
925
914
  def _get_authenticated_objects_from_auth_profile(
926
915
  self, bucket_name: str, prefix: str, auth_dict: Dict[str, str]
@@ -928,20 +917,20 @@ class AwsDownload(Download):
928
917
  """Auth strategy using RequestPayer=requester and ``aws_profile`` from provided credentials"""
929
918
 
930
919
  if "profile_name" in auth_dict.keys():
931
- s3_session = boto3.session.Session(profile_name=auth_dict["profile_name"]) # type: ignore
932
- s3_resource = s3_session.resource( # type: ignore
920
+ s3_session = boto3.session.Session(profile_name=auth_dict["profile_name"])
921
+ s3_resource = s3_session.resource(
933
922
  service_name="s3",
934
- endpoint_url=getattr(self.config, "base_uri", None),
923
+ endpoint_url=getattr(self.config, "s3_endpoint", None),
935
924
  )
936
925
  if self.requester_pays:
937
- objects = s3_resource.Bucket(bucket_name).objects.filter( # type: ignore
926
+ objects = s3_resource.Bucket(bucket_name).objects.filter(
938
927
  RequestPayer="requester"
939
928
  )
940
929
  else:
941
- objects = s3_resource.Bucket(bucket_name).objects # type: ignore
942
- list(objects.filter(Prefix=prefix).limit(1)) # type: ignore
943
- self.s3_session = s3_session # type: ignore
944
- return objects # type: ignore
930
+ objects = s3_resource.Bucket(bucket_name).objects
931
+ list(objects.filter(Prefix=prefix).limit(1))
932
+ self.s3_session = s3_session
933
+ return objects
945
934
  else:
946
935
  return None
947
936
 
@@ -952,23 +941,35 @@ class AwsDownload(Download):
952
941
  from provided credentials"""
953
942
 
954
943
  if all(k in auth_dict for k in ("aws_access_key_id", "aws_secret_access_key")):
955
- s3_session = boto3.session.Session( # type: ignore
956
- aws_access_key_id=auth_dict["aws_access_key_id"],
957
- aws_secret_access_key=auth_dict["aws_secret_access_key"],
944
+ S3SessionKwargs = TypedDict(
945
+ "S3SessionKwargs",
946
+ {
947
+ "aws_access_key_id": str,
948
+ "aws_secret_access_key": str,
949
+ "aws_session_token": str,
950
+ },
951
+ total=False,
958
952
  )
959
- s3_resource = s3_session.resource( # type: ignore
953
+ s3_session_kwargs: S3SessionKwargs = {
954
+ "aws_access_key_id": auth_dict["aws_access_key_id"],
955
+ "aws_secret_access_key": auth_dict["aws_secret_access_key"],
956
+ }
957
+ if auth_dict.get("aws_session_token"):
958
+ s3_session_kwargs["aws_session_token"] = auth_dict["aws_session_token"]
959
+ s3_session = boto3.session.Session(**s3_session_kwargs)
960
+ s3_resource = s3_session.resource(
960
961
  service_name="s3",
961
- endpoint_url=getattr(self.config, "base_uri", None),
962
+ endpoint_url=getattr(self.config, "s3_endpoint", None),
962
963
  )
963
964
  if self.requester_pays:
964
- objects = s3_resource.Bucket(bucket_name).objects.filter( # type: ignore
965
+ objects = s3_resource.Bucket(bucket_name).objects.filter(
965
966
  RequestPayer="requester"
966
967
  )
967
968
  else:
968
- objects = s3_resource.Bucket(bucket_name).objects # type: ignore
969
- list(objects.filter(Prefix=prefix).limit(1)) # type: ignore
970
- self.s3_session = s3_session # type: ignore
971
- return objects # type: ignore
969
+ objects = s3_resource.Bucket(bucket_name).objects
970
+ list(objects.filter(Prefix=prefix).limit(1))
971
+ self.s3_session = s3_session
972
+ return objects
972
973
  else:
973
974
  return None
974
975
 
@@ -977,19 +978,19 @@ class AwsDownload(Download):
977
978
  ) -> Optional[ResourceCollection]:
978
979
  """Auth strategy using RequestPayer=requester and current environment"""
979
980
 
980
- s3_session = boto3.session.Session() # type: ignore
981
- s3_resource = s3_session.resource( # type: ignore
982
- service_name="s3", endpoint_url=getattr(self.config, "base_uri", None)
981
+ s3_session = boto3.session.Session()
982
+ s3_resource = s3_session.resource(
983
+ service_name="s3", endpoint_url=getattr(self.config, "s3_endpoint", None)
983
984
  )
984
985
  if self.requester_pays:
985
- objects = s3_resource.Bucket(bucket_name).objects.filter( # type: ignore
986
+ objects = s3_resource.Bucket(bucket_name).objects.filter(
986
987
  RequestPayer="requester"
987
988
  )
988
989
  else:
989
- objects = s3_resource.Bucket(bucket_name).objects # type: ignore
990
- list(objects.filter(Prefix=prefix).limit(1)) # type: ignore
991
- self.s3_session = s3_session # type: ignore
992
- return objects # type: ignore
990
+ objects = s3_resource.Bucket(bucket_name).objects
991
+ list(objects.filter(Prefix=prefix).limit(1))
992
+ self.s3_session = s3_session
993
+ return objects
993
994
 
994
995
  def get_product_bucket_name_and_prefix(
995
996
  self, product: EOProduct, url: Optional[str] = None
@@ -997,11 +998,8 @@ class AwsDownload(Download):
997
998
  """Extract bucket name and prefix from product URL
998
999
 
999
1000
  :param product: The EO product to download
1000
- :type product: :class:`~eodag.api.product._product.EOProduct`
1001
1001
  :param url: (optional) URL to use as product.location
1002
- :type url: str
1003
1002
  :returns: bucket_name and prefix as str
1004
- :rtype: tuple
1005
1003
  """
1006
1004
  if url is None:
1007
1005
  url = product.location
@@ -1154,8 +1152,7 @@ class AwsDownload(Download):
1154
1152
  # S2 L2A Tile files -----------------------------------------------
1155
1153
  if matched := S2L2A_TILE_IMG_REGEX.match(chunk.key):
1156
1154
  found_dict = matched.groupdict()
1157
- product_path = "%s.SAFE/GRANULE/%s/IMG_DATA/R%s/T%s%s%s_%s_%s_%s.jp2" % (
1158
- product.properties["title"],
1155
+ product_path = "GRANULE/%s/IMG_DATA/R%s/T%s%s%s_%s_%s_%s.jp2" % (
1159
1156
  found_dict["num"],
1160
1157
  found_dict["res"],
1161
1158
  found_dict["tile1"],
@@ -1167,16 +1164,14 @@ class AwsDownload(Download):
1167
1164
  )
1168
1165
  elif matched := S2L2A_TILE_AUX_DIR_REGEX.match(chunk.key):
1169
1166
  found_dict = matched.groupdict()
1170
- product_path = "%s.SAFE/GRANULE/%s/AUX_DATA/%s" % (
1171
- product.properties["title"],
1167
+ product_path = "GRANULE/%s/AUX_DATA/%s" % (
1172
1168
  found_dict["num"],
1173
1169
  found_dict["file"],
1174
1170
  )
1175
1171
  # S2 L2A QI Masks
1176
1172
  elif matched := S2_TILE_QI_MSK_REGEX.match(chunk.key):
1177
1173
  found_dict = matched.groupdict()
1178
- product_path = "%s.SAFE/GRANULE/%s/QI_DATA/MSK_%sPRB_%s" % (
1179
- product.properties["title"],
1174
+ product_path = "GRANULE/%s/QI_DATA/MSK_%sPRB_%s" % (
1180
1175
  found_dict["num"],
1181
1176
  found_dict["file_base"],
1182
1177
  found_dict["file_suffix"],
@@ -1184,8 +1179,7 @@ class AwsDownload(Download):
1184
1179
  # S2 L2A QI PVI
1185
1180
  elif matched := S2_TILE_QI_PVI_REGEX.match(chunk.key):
1186
1181
  found_dict = matched.groupdict()
1187
- product_path = "%s.SAFE/GRANULE/%s/QI_DATA/%s_%s_PVI.jp2" % (
1188
- product.properties["title"],
1182
+ product_path = "GRANULE/%s/QI_DATA/%s_%s_PVI.jp2" % (
1189
1183
  found_dict["num"],
1190
1184
  title_part3,
1191
1185
  title_date1,
@@ -1193,15 +1187,13 @@ class AwsDownload(Download):
1193
1187
  # S2 Tile files ---------------------------------------------------
1194
1188
  elif matched := S2_TILE_PREVIEW_DIR_REGEX.match(chunk.key):
1195
1189
  found_dict = matched.groupdict()
1196
- product_path = "%s.SAFE/GRANULE/%s/preview/%s" % (
1197
- product.properties["title"],
1190
+ product_path = "GRANULE/%s/preview/%s" % (
1198
1191
  found_dict["num"],
1199
1192
  found_dict["file"],
1200
1193
  )
1201
1194
  elif matched := S2_TILE_IMG_REGEX.match(chunk.key):
1202
1195
  found_dict = matched.groupdict()
1203
- product_path = "%s.SAFE/GRANULE/%s/IMG_DATA/T%s%s%s_%s_%s" % (
1204
- product.properties["title"],
1196
+ product_path = "GRANULE/%s/IMG_DATA/T%s%s%s_%s_%s" % (
1205
1197
  found_dict["num"],
1206
1198
  found_dict["tile1"],
1207
1199
  found_dict["tile2"],
@@ -1211,97 +1203,74 @@ class AwsDownload(Download):
1211
1203
  )
1212
1204
  elif matched := S2_TILE_THUMBNAIL_REGEX.match(chunk.key):
1213
1205
  found_dict = matched.groupdict()
1214
- product_path = "%s.SAFE/GRANULE/%s/%s" % (
1215
- product.properties["title"],
1206
+ product_path = "GRANULE/%s/%s" % (
1216
1207
  found_dict["num"],
1217
1208
  found_dict["file"],
1218
1209
  )
1219
1210
  elif matched := S2_TILE_MTD_REGEX.match(chunk.key):
1220
1211
  found_dict = matched.groupdict()
1221
- product_path = "%s.SAFE/GRANULE/%s/MTD_TL.xml" % (
1222
- product.properties["title"],
1223
- found_dict["num"],
1224
- )
1212
+ product_path = "GRANULE/%s/MTD_TL.xml" % found_dict["num"]
1225
1213
  elif matched := S2_TILE_AUX_DIR_REGEX.match(chunk.key):
1226
1214
  found_dict = matched.groupdict()
1227
- product_path = "%s.SAFE/GRANULE/%s/AUX_DATA/AUX_%s" % (
1228
- product.properties["title"],
1215
+ product_path = "GRANULE/%s/AUX_DATA/AUX_%s" % (
1229
1216
  found_dict["num"],
1230
1217
  found_dict["file"],
1231
1218
  )
1232
1219
  elif matched := S2_TILE_QI_DIR_REGEX.match(chunk.key):
1233
1220
  found_dict = matched.groupdict()
1234
- product_path = "%s.SAFE/GRANULE/%s/QI_DATA/%s" % (
1235
- product.properties["title"],
1221
+ product_path = "GRANULE/%s/QI_DATA/%s" % (
1236
1222
  found_dict["num"],
1237
1223
  found_dict["file"],
1238
1224
  )
1239
1225
  # S2 Tiles generic
1240
1226
  elif matched := S2_TILE_REGEX.match(chunk.key):
1241
1227
  found_dict = matched.groupdict()
1242
- product_path = "%s.SAFE/GRANULE/%s/%s" % (
1243
- product.properties["title"],
1228
+ product_path = "GRANULE/%s/%s" % (
1244
1229
  found_dict["num"],
1245
1230
  found_dict["file"],
1246
1231
  )
1247
1232
  # S2 Product files
1248
1233
  elif matched := S2_PROD_DS_MTD_REGEX.match(chunk.key):
1249
1234
  found_dict = matched.groupdict()
1250
- product_path = "%s.SAFE/DATASTRIP/%s/MTD_DS.xml" % (
1251
- product.properties["title"],
1252
- ds_dir,
1253
- )
1235
+ product_path = "DATASTRIP/%s/MTD_DS.xml" % ds_dir
1254
1236
  elif matched := S2_PROD_DS_QI_REPORT_REGEX.match(chunk.key):
1255
1237
  found_dict = matched.groupdict()
1256
- product_path = "%s.SAFE/DATASTRIP/%s/QI_DATA/%s.xml" % (
1257
- product.properties["title"],
1238
+ product_path = "DATASTRIP/%s/QI_DATA/%s.xml" % (
1258
1239
  ds_dir,
1259
1240
  found_dict["filename"],
1260
1241
  )
1261
1242
  elif matched := S2_PROD_DS_QI_REGEX.match(chunk.key):
1262
1243
  found_dict = matched.groupdict()
1263
- product_path = "%s.SAFE/DATASTRIP/%s/QI_DATA/%s" % (
1264
- product.properties["title"],
1244
+ product_path = "DATASTRIP/%s/QI_DATA/%s" % (
1265
1245
  ds_dir,
1266
1246
  found_dict["file"],
1267
1247
  )
1268
1248
  elif matched := S2_PROD_INSPIRE_REGEX.match(chunk.key):
1269
1249
  found_dict = matched.groupdict()
1270
- product_path = "%s.SAFE/INSPIRE.xml" % (product.properties["title"],)
1250
+ product_path = "INSPIRE.xml"
1271
1251
  elif matched := S2_PROD_MTD_REGEX.match(chunk.key):
1272
1252
  found_dict = matched.groupdict()
1273
- product_path = "%s.SAFE/MTD_MSI%s.xml" % (
1274
- product.properties["title"],
1275
- s2_processing_level,
1276
- )
1253
+ product_path = "MTD_MSI%s.xml" % s2_processing_level
1277
1254
  # S2 Product generic
1278
1255
  elif matched := S2_PROD_REGEX.match(chunk.key):
1279
1256
  found_dict = matched.groupdict()
1280
- product_path = "%s.SAFE/%s" % (
1281
- product.properties["title"],
1282
- found_dict["file"],
1283
- )
1257
+ product_path = "%s" % found_dict["file"]
1284
1258
  # S1 --------------------------------------------------------------
1285
1259
  elif matched := S1_CALIB_REGEX.match(chunk.key):
1286
1260
  found_dict = matched.groupdict()
1287
- product_path = (
1288
- "%s.SAFE/annotation/calibration/%s-%s-%s-grd-%s-%s-%03d.xml"
1289
- % (
1290
- product.properties["title"],
1291
- found_dict["file_prefix"],
1292
- product.properties["platformSerialIdentifier"].lower(),
1293
- found_dict["file_beam"],
1294
- found_dict["file_pol"],
1295
- s1_title_suffix,
1296
- S1_IMG_NB_PER_POLAR.get(
1297
- product.properties["polarizationMode"], {}
1298
- ).get(found_dict["file_pol"].upper(), 1),
1299
- )
1261
+ product_path = "annotation/calibration/%s-%s-%s-grd-%s-%s-%03d.xml" % (
1262
+ found_dict["file_prefix"],
1263
+ product.properties["platformSerialIdentifier"].lower(),
1264
+ found_dict["file_beam"],
1265
+ found_dict["file_pol"],
1266
+ s1_title_suffix,
1267
+ S1_IMG_NB_PER_POLAR.get(product.properties["polarizationMode"], {}).get(
1268
+ found_dict["file_pol"].upper(), 1
1269
+ ),
1300
1270
  )
1301
1271
  elif matched := S1_ANNOT_REGEX.match(chunk.key):
1302
1272
  found_dict = matched.groupdict()
1303
- product_path = "%s.SAFE/annotation/%s-%s-grd-%s-%s-%03d.xml" % (
1304
- product.properties["title"],
1273
+ product_path = "annotation/%s-%s-grd-%s-%s-%03d.xml" % (
1305
1274
  product.properties["platformSerialIdentifier"].lower(),
1306
1275
  found_dict["file_beam"],
1307
1276
  found_dict["file_pol"],
@@ -1312,8 +1281,7 @@ class AwsDownload(Download):
1312
1281
  )
1313
1282
  elif matched := S1_MEAS_REGEX.match(chunk.key):
1314
1283
  found_dict = matched.groupdict()
1315
- product_path = "%s.SAFE/measurement/%s-%s-grd-%s-%s-%03d.%s" % (
1316
- product.properties["title"],
1284
+ product_path = "measurement/%s-%s-grd-%s-%s-%03d.%s" % (
1317
1285
  product.properties["platformSerialIdentifier"].lower(),
1318
1286
  found_dict["file_beam"],
1319
1287
  found_dict["file_pol"],
@@ -1325,18 +1293,14 @@ class AwsDownload(Download):
1325
1293
  )
1326
1294
  elif matched := S1_REPORT_REGEX.match(chunk.key):
1327
1295
  found_dict = matched.groupdict()
1328
- product_path = "%s.SAFE/%s.SAFE-%s" % (
1329
- product.properties["title"],
1296
+ product_path = "%s.SAFE-%s" % (
1330
1297
  product.properties["title"],
1331
1298
  found_dict["file"],
1332
1299
  )
1333
1300
  # S1 generic
1334
1301
  elif matched := S1_REGEX.match(chunk.key):
1335
1302
  found_dict = matched.groupdict()
1336
- product_path = "%s.SAFE/%s" % (
1337
- product.properties["title"],
1338
- found_dict["file"],
1339
- )
1303
+ product_path = "%s" % found_dict["file"]
1340
1304
  # out of SAFE format
1341
1305
  else:
1342
1306
  raise NotAvailableError(f"Ignored {chunk.key} out of SAFE matching pattern")
@@ -1347,12 +1311,12 @@ class AwsDownload(Download):
1347
1311
  def download_all(
1348
1312
  self,
1349
1313
  products: SearchResult,
1350
- auth: Optional[PluginConfig] = None,
1314
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
1351
1315
  downloaded_callback: Optional[DownloadedCallback] = None,
1352
1316
  progress_callback: Optional[ProgressCallback] = None,
1353
1317
  wait: int = DEFAULT_DOWNLOAD_WAIT,
1354
1318
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
1355
- **kwargs: Union[str, bool, Dict[str, Any]],
1319
+ **kwargs: Unpack[DownloadConf],
1356
1320
  ) -> List[str]:
1357
1321
  """
1358
1322
  download_all using parent (base plugin) method