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