eodag 3.8.1__py3-none-any.whl → 3.9.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.
- eodag/api/core.py +1 -1
- eodag/api/product/drivers/generic.py +5 -1
- eodag/api/product/metadata_mapping.py +109 -8
- eodag/cli.py +36 -4
- eodag/config.py +5 -2
- eodag/plugins/apis/ecmwf.py +3 -1
- eodag/plugins/apis/usgs.py +2 -1
- eodag/plugins/authentication/aws_auth.py +228 -37
- eodag/plugins/authentication/base.py +12 -2
- eodag/plugins/authentication/oauth.py +5 -0
- eodag/plugins/base.py +3 -2
- eodag/plugins/download/aws.py +44 -285
- eodag/plugins/download/base.py +3 -2
- eodag/plugins/download/creodias_s3.py +1 -38
- eodag/plugins/download/http.py +111 -103
- eodag/plugins/download/s3rest.py +3 -1
- eodag/plugins/manager.py +2 -1
- eodag/plugins/search/__init__.py +2 -1
- eodag/plugins/search/base.py +2 -1
- eodag/plugins/search/build_search_result.py +2 -2
- eodag/plugins/search/creodias_s3.py +9 -1
- eodag/plugins/search/qssearch.py +3 -1
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +220 -30
- eodag/resources/providers.yml +633 -88
- eodag/resources/stac_provider.yml +5 -2
- eodag/resources/user_conf_template.yml +0 -5
- eodag/rest/core.py +8 -0
- eodag/rest/errors.py +9 -0
- eodag/rest/server.py +8 -0
- eodag/rest/stac.py +8 -0
- eodag/rest/utils/__init__.py +2 -4
- eodag/rest/utils/rfc3339.py +1 -1
- eodag/utils/__init__.py +69 -54
- eodag/utils/dates.py +204 -0
- eodag/utils/s3.py +187 -168
- {eodag-3.8.1.dist-info → eodag-3.9.0.dist-info}/METADATA +4 -3
- {eodag-3.8.1.dist-info → eodag-3.9.0.dist-info}/RECORD +42 -42
- {eodag-3.8.1.dist-info → eodag-3.9.0.dist-info}/entry_points.txt +1 -1
- eodag/utils/rest.py +0 -100
- {eodag-3.8.1.dist-info → eodag-3.9.0.dist-info}/WHEEL +0 -0
- {eodag-3.8.1.dist-info → eodag-3.9.0.dist-info}/licenses/LICENSE +0 -0
- {eodag-3.8.1.dist-info → eodag-3.9.0.dist-info}/top_level.txt +0 -0
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
from typing import TYPE_CHECKING, Union
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
21
21
|
|
|
22
22
|
from eodag.plugins.base import PluginTopic
|
|
23
23
|
from eodag.utils.exceptions import MisconfiguredError
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
+
from mypy_boto3_s3 import S3ServiceResource
|
|
26
27
|
from requests.auth import AuthBase
|
|
27
28
|
|
|
28
29
|
from eodag.types import S3SessionKwargs
|
|
@@ -40,7 +41,7 @@ class Authentication(PluginTopic):
|
|
|
40
41
|
configuration that needs authentication and helps identifying it
|
|
41
42
|
"""
|
|
42
43
|
|
|
43
|
-
def authenticate(self) -> Union[AuthBase, S3SessionKwargs]:
|
|
44
|
+
def authenticate(self) -> Union[AuthBase, S3SessionKwargs, S3ServiceResource]:
|
|
44
45
|
"""Authenticate"""
|
|
45
46
|
raise NotImplementedError
|
|
46
47
|
|
|
@@ -70,3 +71,12 @@ class Authentication(PluginTopic):
|
|
|
70
71
|
self.provider, ", ".join(missing_credentials)
|
|
71
72
|
)
|
|
72
73
|
)
|
|
74
|
+
|
|
75
|
+
def authenticate_objects(
|
|
76
|
+
self,
|
|
77
|
+
bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
"""
|
|
80
|
+
Authenticates with s3 and retrieves the available objects
|
|
81
|
+
"""
|
|
82
|
+
raise NotImplementedError
|
|
@@ -20,12 +20,17 @@ from __future__ import annotations
|
|
|
20
20
|
from typing import TYPE_CHECKING, Optional
|
|
21
21
|
|
|
22
22
|
from eodag.plugins.authentication.base import Authentication
|
|
23
|
+
from eodag.utils import _deprecated
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
26
|
from eodag.config import PluginConfig
|
|
26
27
|
from eodag.types import S3SessionKwargs
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
@_deprecated(
|
|
31
|
+
reason="Plugin was used to authenticate using S3 credentials, use AwsAuth instead",
|
|
32
|
+
version="3.8.1",
|
|
33
|
+
)
|
|
29
34
|
class OAuth(Authentication):
|
|
30
35
|
"""OAuth authentication plugin
|
|
31
36
|
|
eodag/plugins/base.py
CHANGED
|
@@ -65,9 +65,10 @@ class PluginTopic(metaclass=EODAGPluginMount):
|
|
|
65
65
|
self.provider = provider
|
|
66
66
|
|
|
67
67
|
def __repr__(self) -> str:
|
|
68
|
+
config = getattr(self, "config", None)
|
|
68
69
|
return "{}(provider={}, priority={}, topic={})".format(
|
|
69
70
|
self.__class__.__name__,
|
|
70
|
-
self
|
|
71
|
-
|
|
71
|
+
getattr(self, "provider", ""),
|
|
72
|
+
config.priority if config else "",
|
|
72
73
|
self.__class__.mro()[-3].__name__,
|
|
73
74
|
)
|
eodag/plugins/download/aws.py
CHANGED
|
@@ -21,12 +21,11 @@ import logging
|
|
|
21
21
|
import os
|
|
22
22
|
import re
|
|
23
23
|
from pathlib import Path
|
|
24
|
-
from typing import TYPE_CHECKING, Any,
|
|
24
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast
|
|
25
25
|
|
|
26
26
|
import boto3
|
|
27
27
|
import requests
|
|
28
|
-
from botocore.exceptions import ClientError
|
|
29
|
-
from botocore.handlers import disable_signing
|
|
28
|
+
from botocore.exceptions import ClientError
|
|
30
29
|
from lxml import etree
|
|
31
30
|
from requests.auth import AuthBase
|
|
32
31
|
|
|
@@ -35,6 +34,7 @@ from eodag.api.product.metadata_mapping import (
|
|
|
35
34
|
properties_from_json,
|
|
36
35
|
properties_from_xml,
|
|
37
36
|
)
|
|
37
|
+
from eodag.plugins.authentication.aws_auth import raise_if_auth_error
|
|
38
38
|
from eodag.plugins.download.base import Download
|
|
39
39
|
from eodag.utils import (
|
|
40
40
|
DEFAULT_DOWNLOAD_TIMEOUT,
|
|
@@ -52,7 +52,6 @@ from eodag.utils import (
|
|
|
52
52
|
from eodag.utils.exceptions import (
|
|
53
53
|
AuthenticationError,
|
|
54
54
|
DownloadError,
|
|
55
|
-
EodagError,
|
|
56
55
|
MisconfiguredError,
|
|
57
56
|
NoMatchingProductType,
|
|
58
57
|
NotAvailableError,
|
|
@@ -61,8 +60,8 @@ from eodag.utils.exceptions import (
|
|
|
61
60
|
from eodag.utils.s3 import S3FileInfo, open_s3_zipped_object, stream_download_from_s3
|
|
62
61
|
|
|
63
62
|
if TYPE_CHECKING:
|
|
63
|
+
from mypy_boto3_s3 import S3ServiceResource
|
|
64
64
|
from mypy_boto3_s3.client import S3Client
|
|
65
|
-
from mypy_boto3_s3.service_resource import BucketObjectsCollection
|
|
66
65
|
|
|
67
66
|
from eodag.api.product import EOProduct
|
|
68
67
|
from eodag.api.search_result import SearchResult
|
|
@@ -191,13 +190,6 @@ S1_IMG_NB_PER_POLAR = {
|
|
|
191
190
|
"VH": {"VH": 1},
|
|
192
191
|
}
|
|
193
192
|
|
|
194
|
-
AWS_AUTH_ERROR_MESSAGES = [
|
|
195
|
-
"AccessDenied",
|
|
196
|
-
"InvalidAccessKeyId",
|
|
197
|
-
"SignatureDoesNotMatch",
|
|
198
|
-
"InvalidRequest",
|
|
199
|
-
]
|
|
200
|
-
|
|
201
193
|
|
|
202
194
|
class AwsDownload(Download):
|
|
203
195
|
"""Download on AWS using S3 protocol.
|
|
@@ -207,8 +199,6 @@ class AwsDownload(Download):
|
|
|
207
199
|
|
|
208
200
|
* :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): AwsDownload
|
|
209
201
|
* :attr:`~eodag.config.PluginConfig.s3_endpoint` (``str``): s3 endpoint url
|
|
210
|
-
* :attr:`~eodag.config.PluginConfig.requester_pays` (``bool``): whether download is done
|
|
211
|
-
from a requester-pays bucket or not; default: ``False``
|
|
212
202
|
* :attr:`~eodag.config.PluginConfig.flatten_top_dirs` (``bool``): if the directory structure
|
|
213
203
|
should be flattened; default: ``True``
|
|
214
204
|
* :attr:`~eodag.config.PluginConfig.ignore_assets` (``bool``): ignore assets and download
|
|
@@ -231,14 +221,11 @@ class AwsDownload(Download):
|
|
|
231
221
|
|
|
232
222
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
233
223
|
super(AwsDownload, self).__init__(provider, config)
|
|
234
|
-
self.requester_pays = getattr(self.config, "requester_pays", False)
|
|
235
|
-
self.s3_session: Optional[boto3.session.Session] = None
|
|
236
|
-
self.s3_resource: Optional[boto3.resources.base.ServiceResource] = None
|
|
237
224
|
|
|
238
225
|
def download(
|
|
239
226
|
self,
|
|
240
227
|
product: EOProduct,
|
|
241
|
-
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
228
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs, S3ServiceResource]] = None,
|
|
242
229
|
progress_callback: Optional[ProgressCallback] = None,
|
|
243
230
|
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
244
231
|
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
@@ -265,10 +252,6 @@ class AwsDownload(Download):
|
|
|
265
252
|
file or with environment variables.
|
|
266
253
|
:returns: The absolute path to the downloaded product in the local filesystem
|
|
267
254
|
"""
|
|
268
|
-
if auth is None:
|
|
269
|
-
auth = {}
|
|
270
|
-
if isinstance(auth, AuthBase):
|
|
271
|
-
raise MisconfiguredError("Please use AwsAuth plugin with AwsDownload")
|
|
272
255
|
|
|
273
256
|
if progress_callback is None:
|
|
274
257
|
logger.info(
|
|
@@ -312,9 +295,14 @@ class AwsDownload(Download):
|
|
|
312
295
|
)
|
|
313
296
|
|
|
314
297
|
# authenticate
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
298
|
+
if product.downloader_auth:
|
|
299
|
+
authenticated_objects = product.downloader_auth.authenticate_objects(
|
|
300
|
+
bucket_names_and_prefixes
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
raise MisconfiguredError(
|
|
304
|
+
"Authentication plugin (AwsAuth) has to be configured if AwsDownload is used"
|
|
305
|
+
)
|
|
318
306
|
|
|
319
307
|
# files in zip
|
|
320
308
|
updated_bucket_names_and_prefixes = self._download_file_in_zip(
|
|
@@ -359,22 +347,28 @@ class AwsDownload(Download):
|
|
|
359
347
|
if not os.path.isdir(chunk_abs_path_dir):
|
|
360
348
|
os.makedirs(chunk_abs_path_dir)
|
|
361
349
|
|
|
350
|
+
bucket_objects = authenticated_objects.get(product_chunk.bucket_name)
|
|
351
|
+
extra_args = (
|
|
352
|
+
getattr(bucket_objects, "_params", {}).copy()
|
|
353
|
+
if bucket_objects
|
|
354
|
+
else {}
|
|
355
|
+
)
|
|
362
356
|
if not os.path.isfile(chunk_abs_path):
|
|
363
357
|
product_chunk.Bucket().download_file(
|
|
364
358
|
product_chunk.key,
|
|
365
359
|
chunk_abs_path,
|
|
366
|
-
ExtraArgs=
|
|
360
|
+
ExtraArgs=extra_args,
|
|
367
361
|
Callback=progress_callback,
|
|
368
362
|
)
|
|
369
363
|
|
|
370
364
|
except AuthenticationError as e:
|
|
371
365
|
logger.warning("Unexpected error: %s" % e)
|
|
372
366
|
except ClientError as e:
|
|
373
|
-
self.
|
|
367
|
+
raise_if_auth_error(e, self.provider)
|
|
374
368
|
logger.warning("Unexpected error: %s" % e)
|
|
375
369
|
|
|
376
370
|
# finalize safe product
|
|
377
|
-
if build_safe and "S2_MSI" in product.product_type:
|
|
371
|
+
if build_safe and product.product_type and "S2_MSI" in product.product_type:
|
|
378
372
|
self.finalize_s2_safe_product(product_local_path)
|
|
379
373
|
# flatten directory structure
|
|
380
374
|
elif flatten_top_dirs:
|
|
@@ -399,11 +393,14 @@ class AwsDownload(Download):
|
|
|
399
393
|
"""
|
|
400
394
|
Download file in zip from a prefix like `foo/bar.zip!file.txt`
|
|
401
395
|
"""
|
|
402
|
-
if
|
|
396
|
+
if (
|
|
397
|
+
not getattr(product, "downloader_auth", None)
|
|
398
|
+
or product.downloader_auth.s3_resource is None
|
|
399
|
+
):
|
|
403
400
|
logger.debug("Cannot check files in s3 zip without s3 resource")
|
|
404
401
|
return bucket_names_and_prefixes
|
|
405
402
|
|
|
406
|
-
s3_client =
|
|
403
|
+
s3_client = product.downloader_auth.get_s3_client()
|
|
407
404
|
|
|
408
405
|
downloaded = []
|
|
409
406
|
for i, pack in enumerate(bucket_names_and_prefixes):
|
|
@@ -577,76 +574,6 @@ class AwsDownload(Download):
|
|
|
577
574
|
)
|
|
578
575
|
return bucket_names_and_prefixes
|
|
579
576
|
|
|
580
|
-
def _do_authentication(
|
|
581
|
-
self,
|
|
582
|
-
bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
|
|
583
|
-
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
584
|
-
) -> tuple[dict[str, Any], BucketObjectsCollection]:
|
|
585
|
-
"""
|
|
586
|
-
Authenticates with s3 and retrieves the available objects
|
|
587
|
-
|
|
588
|
-
:param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
|
|
589
|
-
:param auth: authentication information
|
|
590
|
-
:raises AuthenticationError: authentication is not possible
|
|
591
|
-
:return: authenticated objects per bucket, list of available objects
|
|
592
|
-
"""
|
|
593
|
-
if not isinstance(auth, (dict, type(None))):
|
|
594
|
-
raise AuthenticationError(
|
|
595
|
-
f"Incompatible authentication information, expected dict or None, got {type(auth)}"
|
|
596
|
-
)
|
|
597
|
-
if auth is None:
|
|
598
|
-
auth = {}
|
|
599
|
-
authenticated_objects: dict[str, Any] = {}
|
|
600
|
-
auth_error_messages: set[str] = set()
|
|
601
|
-
for _, pack in enumerate(bucket_names_and_prefixes):
|
|
602
|
-
try:
|
|
603
|
-
bucket_name, prefix = pack
|
|
604
|
-
if not prefix:
|
|
605
|
-
continue
|
|
606
|
-
if bucket_name not in authenticated_objects:
|
|
607
|
-
# get Prefixes longest common base path
|
|
608
|
-
common_prefix = ""
|
|
609
|
-
prefix_split = prefix.split("/")
|
|
610
|
-
prefixes_in_bucket = len(
|
|
611
|
-
[p for b, p in bucket_names_and_prefixes if b == bucket_name]
|
|
612
|
-
)
|
|
613
|
-
for i in range(1, len(prefix_split)):
|
|
614
|
-
common_prefix = "/".join(prefix_split[0:i])
|
|
615
|
-
if (
|
|
616
|
-
len(
|
|
617
|
-
[
|
|
618
|
-
p
|
|
619
|
-
for b, p in bucket_names_and_prefixes
|
|
620
|
-
if p and b == bucket_name and common_prefix in p
|
|
621
|
-
]
|
|
622
|
-
)
|
|
623
|
-
< prefixes_in_bucket
|
|
624
|
-
):
|
|
625
|
-
common_prefix = "/".join(prefix_split[0 : i - 1])
|
|
626
|
-
break
|
|
627
|
-
# connect to aws s3 and get bucket auhenticated objects
|
|
628
|
-
s3_objects = self.get_authenticated_objects(
|
|
629
|
-
bucket_name, common_prefix, auth
|
|
630
|
-
)
|
|
631
|
-
authenticated_objects[bucket_name] = s3_objects
|
|
632
|
-
else:
|
|
633
|
-
s3_objects = authenticated_objects[bucket_name]
|
|
634
|
-
|
|
635
|
-
except AuthenticationError as e:
|
|
636
|
-
logger.warning("Unexpected error: %s" % e)
|
|
637
|
-
logger.warning("Skipping %s/%s" % (bucket_name, prefix))
|
|
638
|
-
auth_error_messages.add(str(e))
|
|
639
|
-
except ClientError as e:
|
|
640
|
-
self._raise_if_auth_error(e)
|
|
641
|
-
logger.warning("Unexpected error: %s" % e)
|
|
642
|
-
logger.warning("Skipping %s/%s" % (bucket_name, prefix))
|
|
643
|
-
auth_error_messages.add(str(e))
|
|
644
|
-
|
|
645
|
-
# could not auth on any bucket
|
|
646
|
-
if not authenticated_objects:
|
|
647
|
-
raise AuthenticationError(", ".join(auth_error_messages))
|
|
648
|
-
return authenticated_objects, s3_objects
|
|
649
|
-
|
|
650
577
|
def _get_unique_products(
|
|
651
578
|
self,
|
|
652
579
|
bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
|
|
@@ -696,20 +623,10 @@ class AwsDownload(Download):
|
|
|
696
623
|
|
|
697
624
|
return unique_product_chunks
|
|
698
625
|
|
|
699
|
-
def _raise_if_auth_error(self, exception: ClientError) -> None:
|
|
700
|
-
"""Raises an error if given exception is an authentication error"""
|
|
701
|
-
err = cast(dict[str, str], exception.response["Error"])
|
|
702
|
-
if err["Code"] in AWS_AUTH_ERROR_MESSAGES and "key" in err["Message"].lower():
|
|
703
|
-
raise AuthenticationError(
|
|
704
|
-
f"Please check your credentials for {self.provider}.",
|
|
705
|
-
f"HTTP Error {exception.response['ResponseMetadata']['HTTPStatusCode']} returned.",
|
|
706
|
-
err["Code"] + ": " + err["Message"],
|
|
707
|
-
)
|
|
708
|
-
|
|
709
626
|
def _stream_download_dict(
|
|
710
627
|
self,
|
|
711
628
|
product: EOProduct,
|
|
712
|
-
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
629
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs, S3ServiceResource]] = None,
|
|
713
630
|
byte_range: tuple[Optional[int], Optional[int]] = (None, None),
|
|
714
631
|
compress: Literal["zip", "raw", "auto"] = "auto",
|
|
715
632
|
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
@@ -782,9 +699,14 @@ class AwsDownload(Download):
|
|
|
782
699
|
)
|
|
783
700
|
|
|
784
701
|
# authenticate
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
702
|
+
if product.downloader_auth:
|
|
703
|
+
authenticated_objects = product.downloader_auth.authenticate_objects(
|
|
704
|
+
bucket_names_and_prefixes
|
|
705
|
+
)
|
|
706
|
+
else:
|
|
707
|
+
raise MisconfiguredError(
|
|
708
|
+
"Authentication plugin (AwsAuth) has to be configured if AwsDownload is used"
|
|
709
|
+
)
|
|
788
710
|
|
|
789
711
|
# downloadable files
|
|
790
712
|
product_objects = self._get_unique_products(
|
|
@@ -794,9 +716,13 @@ class AwsDownload(Download):
|
|
|
794
716
|
ignore_assets,
|
|
795
717
|
product,
|
|
796
718
|
)
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
719
|
+
if auth and isinstance(auth, boto3.resources.base.ServiceResource):
|
|
720
|
+
s3_resource = auth
|
|
721
|
+
else:
|
|
722
|
+
s3_resource = boto3.resource(
|
|
723
|
+
service_name="s3",
|
|
724
|
+
endpoint_url=getattr(self.config, "s3_endpoint", None),
|
|
725
|
+
)
|
|
800
726
|
|
|
801
727
|
product_conf = getattr(self.config, "products", {}).get(
|
|
802
728
|
product.product_type, {}
|
|
@@ -850,7 +776,7 @@ class AwsDownload(Download):
|
|
|
850
776
|
zip_filename = sanitize(title)
|
|
851
777
|
|
|
852
778
|
return stream_download_from_s3(
|
|
853
|
-
cast("S3Client",
|
|
779
|
+
cast("S3Client", s3_resource.meta.client),
|
|
854
780
|
files_info,
|
|
855
781
|
byte_range,
|
|
856
782
|
compress,
|
|
@@ -867,173 +793,6 @@ class AwsDownload(Download):
|
|
|
867
793
|
)
|
|
868
794
|
return os.path.commonpath(chunk_paths)
|
|
869
795
|
|
|
870
|
-
def get_rio_env(
|
|
871
|
-
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
872
|
-
) -> dict[str, Any]:
|
|
873
|
-
"""Get rasterio environment variables needed for data access authentication.
|
|
874
|
-
|
|
875
|
-
:param bucket_name: Bucket containg objects
|
|
876
|
-
:param prefix: Prefix used to try auth
|
|
877
|
-
:param auth_dict: Dictionary containing authentication keys
|
|
878
|
-
:returns: The rasterio environement variables
|
|
879
|
-
"""
|
|
880
|
-
rio_env_kwargs = {}
|
|
881
|
-
if endpoint_url := getattr(self.config, "s3_endpoint", None):
|
|
882
|
-
rio_env_kwargs["endpoint_url"] = endpoint_url.split("://")[-1]
|
|
883
|
-
rio_env_kwargs |= auth_dict
|
|
884
|
-
|
|
885
|
-
if self.s3_session is None:
|
|
886
|
-
_ = self.get_authenticated_objects(bucket_name, prefix, auth_dict)
|
|
887
|
-
|
|
888
|
-
if self.s3_session is not None:
|
|
889
|
-
if self.requester_pays:
|
|
890
|
-
rio_env_kwargs["requester_pays"] = True
|
|
891
|
-
return {
|
|
892
|
-
"session": self.s3_session,
|
|
893
|
-
**rio_env_kwargs,
|
|
894
|
-
}
|
|
895
|
-
else:
|
|
896
|
-
return {"aws_unsigned": True, **rio_env_kwargs}
|
|
897
|
-
|
|
898
|
-
def get_authenticated_objects(
|
|
899
|
-
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
900
|
-
) -> BucketObjectsCollection:
|
|
901
|
-
"""Get boto3 authenticated objects for the given bucket using
|
|
902
|
-
the most adapted auth strategy.
|
|
903
|
-
Also expose ``s3_session`` as class variable if available.
|
|
904
|
-
|
|
905
|
-
:param bucket_name: Bucket containg objects
|
|
906
|
-
:param prefix: Prefix used to filter objects on auth try
|
|
907
|
-
(not used to filter returned objects)
|
|
908
|
-
:param auth_dict: Dictionary containing authentication keys
|
|
909
|
-
:returns: The boto3 authenticated objects
|
|
910
|
-
"""
|
|
911
|
-
auth_methods: list[
|
|
912
|
-
Callable[[str, str, S3SessionKwargs], Optional[BucketObjectsCollection]]
|
|
913
|
-
] = [
|
|
914
|
-
self._get_authenticated_objects_unsigned,
|
|
915
|
-
self._get_authenticated_objects_from_auth_profile,
|
|
916
|
-
self._get_authenticated_objects_from_auth_keys,
|
|
917
|
-
self._get_authenticated_objects_from_env,
|
|
918
|
-
]
|
|
919
|
-
# skip _get_authenticated_objects_from_env if credentials were filled in eodag conf
|
|
920
|
-
if auth_dict:
|
|
921
|
-
del auth_methods[-1]
|
|
922
|
-
|
|
923
|
-
for try_auth_method in auth_methods:
|
|
924
|
-
try:
|
|
925
|
-
s3_objects = try_auth_method(bucket_name, prefix, auth_dict)
|
|
926
|
-
if s3_objects:
|
|
927
|
-
logger.debug("Auth using %s succeeded", try_auth_method.__name__)
|
|
928
|
-
return s3_objects
|
|
929
|
-
except ClientError as e:
|
|
930
|
-
if (
|
|
931
|
-
e.response.get("Error", {}).get("Code", {})
|
|
932
|
-
in AWS_AUTH_ERROR_MESSAGES
|
|
933
|
-
):
|
|
934
|
-
pass
|
|
935
|
-
else:
|
|
936
|
-
raise e
|
|
937
|
-
except ProfileNotFound:
|
|
938
|
-
pass
|
|
939
|
-
logger.debug("Auth using %s failed", try_auth_method.__name__)
|
|
940
|
-
|
|
941
|
-
raise AuthenticationError(
|
|
942
|
-
"Unable do authenticate on s3://%s using any available credendials configuration"
|
|
943
|
-
% bucket_name
|
|
944
|
-
)
|
|
945
|
-
|
|
946
|
-
def _get_authenticated_objects_unsigned(
|
|
947
|
-
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
948
|
-
) -> Optional[BucketObjectsCollection]:
|
|
949
|
-
"""Auth strategy using no-sign-request"""
|
|
950
|
-
|
|
951
|
-
s3_resource = boto3.resource(
|
|
952
|
-
service_name="s3", endpoint_url=getattr(self.config, "s3_endpoint", None)
|
|
953
|
-
)
|
|
954
|
-
s3_resource.meta.client.meta.events.register(
|
|
955
|
-
"choose-signer.s3.*", disable_signing
|
|
956
|
-
)
|
|
957
|
-
objects = s3_resource.Bucket(bucket_name).objects
|
|
958
|
-
list(objects.filter(Prefix=prefix).limit(1))
|
|
959
|
-
self.s3_resource = s3_resource
|
|
960
|
-
return objects
|
|
961
|
-
|
|
962
|
-
def _get_authenticated_objects_from_auth_profile(
|
|
963
|
-
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
964
|
-
) -> Optional[BucketObjectsCollection]:
|
|
965
|
-
"""Auth strategy using RequestPayer=requester and ``aws_profile`` from provided credentials"""
|
|
966
|
-
|
|
967
|
-
if "profile_name" in auth_dict.keys():
|
|
968
|
-
s3_session = boto3.session.Session(profile_name=auth_dict["profile_name"])
|
|
969
|
-
s3_resource = s3_session.resource(
|
|
970
|
-
service_name="s3",
|
|
971
|
-
endpoint_url=getattr(self.config, "s3_endpoint", None),
|
|
972
|
-
)
|
|
973
|
-
if self.requester_pays:
|
|
974
|
-
objects = s3_resource.Bucket(bucket_name).objects.filter(
|
|
975
|
-
RequestPayer="requester"
|
|
976
|
-
)
|
|
977
|
-
else:
|
|
978
|
-
objects = s3_resource.Bucket(bucket_name).objects
|
|
979
|
-
list(objects.filter(Prefix=prefix).limit(1))
|
|
980
|
-
self.s3_session = s3_session
|
|
981
|
-
self.s3_resource = s3_resource
|
|
982
|
-
return objects
|
|
983
|
-
else:
|
|
984
|
-
return None
|
|
985
|
-
|
|
986
|
-
def _get_authenticated_objects_from_auth_keys(
|
|
987
|
-
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
988
|
-
) -> Optional[BucketObjectsCollection]:
|
|
989
|
-
"""Auth strategy using RequestPayer=requester and ``aws_access_key_id``/``aws_secret_access_key``
|
|
990
|
-
from provided credentials"""
|
|
991
|
-
|
|
992
|
-
if all(k in auth_dict for k in ("aws_access_key_id", "aws_secret_access_key")):
|
|
993
|
-
s3_session_kwargs: S3SessionKwargs = {
|
|
994
|
-
"aws_access_key_id": auth_dict["aws_access_key_id"],
|
|
995
|
-
"aws_secret_access_key": auth_dict["aws_secret_access_key"],
|
|
996
|
-
}
|
|
997
|
-
if auth_dict.get("aws_session_token"):
|
|
998
|
-
s3_session_kwargs["aws_session_token"] = auth_dict["aws_session_token"]
|
|
999
|
-
s3_session = boto3.session.Session(**s3_session_kwargs)
|
|
1000
|
-
s3_resource = s3_session.resource(
|
|
1001
|
-
service_name="s3",
|
|
1002
|
-
endpoint_url=getattr(self.config, "s3_endpoint", None),
|
|
1003
|
-
)
|
|
1004
|
-
if self.requester_pays:
|
|
1005
|
-
objects = s3_resource.Bucket(bucket_name).objects.filter(
|
|
1006
|
-
RequestPayer="requester"
|
|
1007
|
-
)
|
|
1008
|
-
else:
|
|
1009
|
-
objects = s3_resource.Bucket(bucket_name).objects
|
|
1010
|
-
list(objects.filter(Prefix=prefix).limit(1))
|
|
1011
|
-
self.s3_session = s3_session
|
|
1012
|
-
self.s3_resource = s3_resource
|
|
1013
|
-
return objects
|
|
1014
|
-
else:
|
|
1015
|
-
return None
|
|
1016
|
-
|
|
1017
|
-
def _get_authenticated_objects_from_env(
|
|
1018
|
-
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
1019
|
-
) -> Optional[BucketObjectsCollection]:
|
|
1020
|
-
"""Auth strategy using RequestPayer=requester and current environment"""
|
|
1021
|
-
|
|
1022
|
-
s3_session = boto3.session.Session()
|
|
1023
|
-
s3_resource = s3_session.resource(
|
|
1024
|
-
service_name="s3", endpoint_url=getattr(self.config, "s3_endpoint", None)
|
|
1025
|
-
)
|
|
1026
|
-
if self.requester_pays:
|
|
1027
|
-
objects = s3_resource.Bucket(bucket_name).objects.filter(
|
|
1028
|
-
RequestPayer="requester"
|
|
1029
|
-
)
|
|
1030
|
-
else:
|
|
1031
|
-
objects = s3_resource.Bucket(bucket_name).objects
|
|
1032
|
-
list(objects.filter(Prefix=prefix).limit(1))
|
|
1033
|
-
self.s3_session = s3_session
|
|
1034
|
-
self.s3_resource = s3_resource
|
|
1035
|
-
return objects
|
|
1036
|
-
|
|
1037
796
|
def get_product_bucket_name_and_prefix(
|
|
1038
797
|
self, product: EOProduct, url: Optional[str] = None
|
|
1039
798
|
) -> tuple[str, Optional[str]]:
|
eodag/plugins/download/base.py
CHANGED
|
@@ -47,6 +47,7 @@ from eodag.utils.exceptions import (
|
|
|
47
47
|
from eodag.utils.notebook import NotebookWidgets
|
|
48
48
|
|
|
49
49
|
if TYPE_CHECKING:
|
|
50
|
+
from mypy_boto3_s3 import S3ServiceResource
|
|
50
51
|
from requests.auth import AuthBase
|
|
51
52
|
|
|
52
53
|
from eodag.api.product import EOProduct
|
|
@@ -103,7 +104,7 @@ class Download(PluginTopic):
|
|
|
103
104
|
def download(
|
|
104
105
|
self,
|
|
105
106
|
product: EOProduct,
|
|
106
|
-
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
107
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs, S3ServiceResource]] = None,
|
|
107
108
|
progress_callback: Optional[ProgressCallback] = None,
|
|
108
109
|
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
109
110
|
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
@@ -133,7 +134,7 @@ class Download(PluginTopic):
|
|
|
133
134
|
def _stream_download_dict(
|
|
134
135
|
self,
|
|
135
136
|
product: EOProduct,
|
|
136
|
-
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
137
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs, S3ServiceResource]] = None,
|
|
137
138
|
byte_range: tuple[Optional[int], Optional[int]] = (None, None),
|
|
138
139
|
compress: Literal["zip", "raw", "auto"] = "auto",
|
|
139
140
|
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
@@ -15,17 +15,10 @@
|
|
|
15
15
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
16
|
# See the License for the specific language governing permissions and
|
|
17
17
|
# limitations under the License.
|
|
18
|
-
from typing import
|
|
19
|
-
|
|
20
|
-
import boto3
|
|
21
|
-
from botocore.exceptions import ClientError
|
|
18
|
+
from typing import Optional
|
|
22
19
|
|
|
23
20
|
from eodag import EOProduct
|
|
24
21
|
from eodag.plugins.download.aws import AwsDownload
|
|
25
|
-
from eodag.utils.exceptions import MisconfiguredError
|
|
26
|
-
|
|
27
|
-
if TYPE_CHECKING:
|
|
28
|
-
from mypy_boto3_s3.service_resource import S3ServiceResource
|
|
29
22
|
|
|
30
23
|
|
|
31
24
|
class CreodiasS3Download(AwsDownload):
|
|
@@ -42,36 +35,6 @@ class CreodiasS3Download(AwsDownload):
|
|
|
42
35
|
verified in requests; default: ``True``
|
|
43
36
|
"""
|
|
44
37
|
|
|
45
|
-
def _get_authenticated_objects_unsigned(self, bucket_name, prefix, auth_dict):
|
|
46
|
-
"""Auth strategy using no-sign-request"""
|
|
47
|
-
|
|
48
|
-
raise ClientError(
|
|
49
|
-
{"Error": {"Code": "AccessDenied", "Message": "skip unsigned"}},
|
|
50
|
-
"_get_authenticated_objects_unsigned",
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
def _get_authenticated_objects_from_auth_keys(self, bucket_name, prefix, auth_dict):
|
|
54
|
-
"""Auth strategy using RequestPayer=requester and ``aws_access_key_id``/``aws_secret_access_key``
|
|
55
|
-
from provided credentials"""
|
|
56
|
-
|
|
57
|
-
# check if credentials are missing
|
|
58
|
-
required_creds = ["aws_access_key_id", "aws_secret_access_key"]
|
|
59
|
-
if not all(auth_dict.get(x, None) for x in required_creds):
|
|
60
|
-
raise MisconfiguredError(
|
|
61
|
-
f"Incomplete credentials for {self.provider}, missing "
|
|
62
|
-
f"{[x for x in required_creds if not auth_dict.get(x, None)]}"
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
s3_session = boto3.session.Session(**auth_dict)
|
|
66
|
-
s3_resource: S3ServiceResource = s3_session.resource(
|
|
67
|
-
"s3", endpoint_url=getattr(self.config, "s3_endpoint", None)
|
|
68
|
-
)
|
|
69
|
-
objects = s3_resource.Bucket(bucket_name).objects.filter()
|
|
70
|
-
list(objects.filter(Prefix=prefix).limit(1))
|
|
71
|
-
self.s3_session = s3_session
|
|
72
|
-
self.s3_resource = s3_resource
|
|
73
|
-
return objects
|
|
74
|
-
|
|
75
38
|
def _get_bucket_names_and_prefixes(
|
|
76
39
|
self,
|
|
77
40
|
product: EOProduct,
|