eodag 3.0.1__py3-none-any.whl → 3.1.0b2__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 +164 -127
- eodag/api/product/_assets.py +11 -11
- eodag/api/product/_product.py +45 -30
- 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 +101 -85
- eodag/api/search_result.py +13 -23
- eodag/cli.py +26 -5
- eodag/config.py +78 -81
- eodag/plugins/apis/base.py +1 -1
- eodag/plugins/apis/ecmwf.py +46 -22
- eodag/plugins/apis/usgs.py +16 -15
- eodag/plugins/authentication/aws_auth.py +16 -13
- eodag/plugins/authentication/base.py +5 -3
- eodag/plugins/authentication/header.py +3 -3
- eodag/plugins/authentication/keycloak.py +4 -4
- eodag/plugins/authentication/oauth.py +7 -3
- eodag/plugins/authentication/openid_connect.py +16 -16
- eodag/plugins/authentication/sas_auth.py +4 -4
- eodag/plugins/authentication/token.py +41 -10
- eodag/plugins/authentication/token_exchange.py +1 -1
- eodag/plugins/base.py +4 -4
- eodag/plugins/crunch/base.py +4 -4
- eodag/plugins/crunch/filter_date.py +4 -4
- eodag/plugins/crunch/filter_latest_intersect.py +6 -6
- eodag/plugins/crunch/filter_latest_tpl_name.py +7 -7
- eodag/plugins/crunch/filter_overlap.py +4 -4
- eodag/plugins/crunch/filter_property.py +6 -7
- eodag/plugins/download/aws.py +58 -78
- eodag/plugins/download/base.py +38 -56
- eodag/plugins/download/creodias_s3.py +29 -0
- eodag/plugins/download/http.py +173 -183
- eodag/plugins/download/s3rest.py +10 -11
- eodag/plugins/manager.py +10 -20
- eodag/plugins/search/__init__.py +6 -5
- eodag/plugins/search/base.py +87 -44
- eodag/plugins/search/build_search_result.py +1067 -329
- eodag/plugins/search/cop_marine.py +22 -12
- eodag/plugins/search/creodias_s3.py +9 -73
- eodag/plugins/search/csw.py +11 -11
- eodag/plugins/search/data_request_search.py +16 -15
- eodag/plugins/search/qssearch.py +103 -187
- eodag/plugins/search/stac_list_assets.py +85 -0
- eodag/plugins/search/static_stac_search.py +3 -3
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +663 -304
- eodag/resources/providers.yml +823 -1749
- eodag/resources/stac_api.yml +2 -2
- eodag/resources/user_conf_template.yml +11 -0
- eodag/rest/cache.py +2 -2
- eodag/rest/config.py +3 -3
- eodag/rest/core.py +112 -82
- eodag/rest/errors.py +5 -5
- eodag/rest/server.py +33 -14
- eodag/rest/stac.py +40 -38
- eodag/rest/types/collections_search.py +3 -3
- eodag/rest/types/eodag_search.py +29 -23
- eodag/rest/types/queryables.py +15 -16
- eodag/rest/types/stac_search.py +15 -25
- 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 +75 -28
- eodag/types/bbox.py +2 -2
- eodag/types/download_args.py +3 -3
- eodag/types/queryables.py +183 -72
- eodag/types/search_args.py +4 -4
- eodag/types/whoosh.py +127 -3
- eodag/utils/__init__.py +152 -50
- eodag/utils/exceptions.py +28 -21
- eodag/utils/import_system.py +2 -2
- eodag/utils/repr.py +65 -6
- eodag/utils/requests.py +13 -13
- eodag/utils/rest.py +2 -2
- eodag/utils/s3.py +208 -0
- eodag/utils/stac_reader.py +10 -10
- {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/METADATA +77 -76
- eodag-3.1.0b2.dist-info/RECORD +113 -0
- {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/WHEEL +1 -1
- {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/entry_points.txt +4 -2
- eodag/utils/constraints.py +0 -244
- eodag-3.0.1.dist-info/RECORD +0 -109
- {eodag-3.0.1.dist-info → eodag-3.1.0b2.dist-info}/LICENSE +0 -0
- {eodag-3.0.1.dist-info → eodag-3.1.0b2.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
|
|
@@ -81,6 +67,7 @@ if TYPE_CHECKING:
|
|
|
81
67
|
from eodag.api.product import EOProduct
|
|
82
68
|
from eodag.api.search_result import SearchResult
|
|
83
69
|
from eodag.config import PluginConfig
|
|
70
|
+
from eodag.types import S3SessionKwargs
|
|
84
71
|
from eodag.types.download_args import DownloadConf
|
|
85
72
|
from eodag.utils import DownloadedCallback, Unpack
|
|
86
73
|
|
|
@@ -230,14 +217,14 @@ class AwsDownload(Download):
|
|
|
230
217
|
* :attr:`~eodag.config.PluginConfig.bucket_path_level` (``int``): at which level of the
|
|
231
218
|
path part of the url the bucket can be found; If no bucket_path_level is given, the bucket
|
|
232
219
|
is taken from the first element of the netloc part.
|
|
233
|
-
* :attr:`~eodag.config.PluginConfig.products` (``
|
|
220
|
+
* :attr:`~eodag.config.PluginConfig.products` (``dict[str, dict[str, Any]``): product type
|
|
234
221
|
specific config; the keys are the product types, the values are dictionaries which can contain the keys:
|
|
235
222
|
|
|
236
223
|
* **default_bucket** (``str``): bucket where the product type can be found
|
|
237
224
|
* **complementary_url_key** (``str``): keys to add additional urls
|
|
238
225
|
* **build_safe** (``bool``): if a SAFE (Standard Archive Format for Europe) product should
|
|
239
226
|
be created; used for Sentinel products; default: False
|
|
240
|
-
* **fetch_metadata** (``
|
|
227
|
+
* **fetch_metadata** (``dict[str, Any]``): config for metadata to be fetched for the SAFE product
|
|
241
228
|
|
|
242
229
|
"""
|
|
243
230
|
|
|
@@ -249,10 +236,10 @@ class AwsDownload(Download):
|
|
|
249
236
|
def download(
|
|
250
237
|
self,
|
|
251
238
|
product: EOProduct,
|
|
252
|
-
auth: Optional[Union[AuthBase,
|
|
239
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
253
240
|
progress_callback: Optional[ProgressCallback] = None,
|
|
254
|
-
wait:
|
|
255
|
-
timeout:
|
|
241
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
242
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
256
243
|
**kwargs: Unpack[DownloadConf],
|
|
257
244
|
) -> Optional[str]:
|
|
258
245
|
"""Download method for AWS S3 API.
|
|
@@ -407,7 +394,7 @@ class AwsDownload(Download):
|
|
|
407
394
|
product: EOProduct,
|
|
408
395
|
progress_callback: ProgressCallback,
|
|
409
396
|
**kwargs: Unpack[DownloadConf],
|
|
410
|
-
) ->
|
|
397
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
411
398
|
"""
|
|
412
399
|
preparation for the download:
|
|
413
400
|
- check if file was already downloaded
|
|
@@ -480,9 +467,10 @@ class AwsDownload(Download):
|
|
|
480
467
|
product: EOProduct,
|
|
481
468
|
asset_filter: Optional[str] = None,
|
|
482
469
|
ignore_assets: Optional[bool] = False,
|
|
483
|
-
) ->
|
|
470
|
+
) -> list[tuple[str, Optional[str]]]:
|
|
484
471
|
"""
|
|
485
|
-
|
|
472
|
+
Retrieves the bucket names and path prefixes for the assets
|
|
473
|
+
|
|
486
474
|
:param product: product for which the assets shall be downloaded
|
|
487
475
|
:param asset_filter: text for which the assets should be filtered
|
|
488
476
|
:param ignore_assets: if product instead of individual assets should be used
|
|
@@ -521,9 +509,9 @@ class AwsDownload(Download):
|
|
|
521
509
|
|
|
522
510
|
def _do_authentication(
|
|
523
511
|
self,
|
|
524
|
-
bucket_names_and_prefixes:
|
|
525
|
-
auth: Optional[Union[AuthBase,
|
|
526
|
-
) ->
|
|
512
|
+
bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
|
|
513
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
514
|
+
) -> tuple[dict[str, Any], ResourceCollection]:
|
|
527
515
|
"""
|
|
528
516
|
authenticates with s3 and retrieves the available objects
|
|
529
517
|
raises an error when authentication is not possible
|
|
@@ -537,8 +525,8 @@ class AwsDownload(Download):
|
|
|
537
525
|
)
|
|
538
526
|
if auth is None:
|
|
539
527
|
auth = {}
|
|
540
|
-
authenticated_objects:
|
|
541
|
-
auth_error_messages:
|
|
528
|
+
authenticated_objects: dict[str, Any] = {}
|
|
529
|
+
auth_error_messages: set[str] = set()
|
|
542
530
|
for _, pack in enumerate(bucket_names_and_prefixes):
|
|
543
531
|
try:
|
|
544
532
|
bucket_name, prefix = pack
|
|
@@ -590,12 +578,12 @@ class AwsDownload(Download):
|
|
|
590
578
|
|
|
591
579
|
def _get_unique_products(
|
|
592
580
|
self,
|
|
593
|
-
bucket_names_and_prefixes:
|
|
594
|
-
authenticated_objects:
|
|
581
|
+
bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
|
|
582
|
+
authenticated_objects: dict[str, Any],
|
|
595
583
|
asset_filter: Optional[str],
|
|
596
584
|
ignore_assets: bool,
|
|
597
585
|
product: EOProduct,
|
|
598
|
-
) ->
|
|
586
|
+
) -> set[Any]:
|
|
599
587
|
"""
|
|
600
588
|
retrieve unique product chunks based on authenticated objects and asset filters
|
|
601
589
|
:param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
|
|
@@ -605,7 +593,7 @@ class AwsDownload(Download):
|
|
|
605
593
|
:param product: product that shall be downloaded
|
|
606
594
|
:return: set of product chunks that can be downloaded
|
|
607
595
|
"""
|
|
608
|
-
product_chunks:
|
|
596
|
+
product_chunks: list[Any] = []
|
|
609
597
|
for bucket_name, prefix in bucket_names_and_prefixes:
|
|
610
598
|
# unauthenticated items filtered out
|
|
611
599
|
if bucket_name in authenticated_objects.keys():
|
|
@@ -636,7 +624,7 @@ class AwsDownload(Download):
|
|
|
636
624
|
|
|
637
625
|
def _raise_if_auth_error(self, exception: ClientError) -> None:
|
|
638
626
|
"""Raises an error if given exception is an authentication error"""
|
|
639
|
-
err = cast(
|
|
627
|
+
err = cast(dict[str, str], exception.response["Error"])
|
|
640
628
|
if err["Code"] in AWS_AUTH_ERROR_MESSAGES and "key" in err["Message"].lower():
|
|
641
629
|
raise AuthenticationError(
|
|
642
630
|
f"Please check your credentials for {self.provider}.",
|
|
@@ -647,10 +635,10 @@ class AwsDownload(Download):
|
|
|
647
635
|
def _stream_download_dict(
|
|
648
636
|
self,
|
|
649
637
|
product: EOProduct,
|
|
650
|
-
auth: Optional[Union[AuthBase,
|
|
638
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
651
639
|
progress_callback: Optional[ProgressCallback] = None,
|
|
652
|
-
wait:
|
|
653
|
-
timeout:
|
|
640
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
641
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
654
642
|
**kwargs: Unpack[DownloadConf],
|
|
655
643
|
) -> StreamResponse:
|
|
656
644
|
r"""
|
|
@@ -731,12 +719,12 @@ class AwsDownload(Download):
|
|
|
731
719
|
else sanitize(product.properties.get("id", "download"))
|
|
732
720
|
)
|
|
733
721
|
|
|
734
|
-
if len(assets_values)
|
|
722
|
+
if len(assets_values) <= 1:
|
|
735
723
|
first_chunks_tuple = next(chunks_tuples)
|
|
736
724
|
# update headers
|
|
737
725
|
filename = os.path.basename(list(unique_product_chunks)[0].key)
|
|
738
726
|
headers = {"content-disposition": f"attachment; filename={filename}"}
|
|
739
|
-
if assets_values[0].get("type", None):
|
|
727
|
+
if assets_values and assets_values[0].get("type", None):
|
|
740
728
|
headers["content-type"] = assets_values[0]["type"]
|
|
741
729
|
|
|
742
730
|
return StreamResponse(
|
|
@@ -753,11 +741,11 @@ class AwsDownload(Download):
|
|
|
753
741
|
|
|
754
742
|
def _stream_download(
|
|
755
743
|
self,
|
|
756
|
-
unique_product_chunks:
|
|
744
|
+
unique_product_chunks: set[Any],
|
|
757
745
|
product: EOProduct,
|
|
758
746
|
build_safe: bool,
|
|
759
747
|
progress_callback: ProgressCallback,
|
|
760
|
-
assets_values:
|
|
748
|
+
assets_values: list[dict[str, Any]],
|
|
761
749
|
) -> Iterator[Any]:
|
|
762
750
|
"""Yield product data chunks"""
|
|
763
751
|
|
|
@@ -799,7 +787,6 @@ class AwsDownload(Download):
|
|
|
799
787
|
common_path = self._get_commonpath(
|
|
800
788
|
product, unique_product_chunks, build_safe
|
|
801
789
|
)
|
|
802
|
-
|
|
803
790
|
for product_chunk in unique_product_chunks:
|
|
804
791
|
try:
|
|
805
792
|
chunk_rel_path = self.get_chunk_dest_path(
|
|
@@ -817,8 +804,7 @@ class AwsDownload(Download):
|
|
|
817
804
|
# out of SAFE format chunk
|
|
818
805
|
logger.warning(e)
|
|
819
806
|
continue
|
|
820
|
-
|
|
821
|
-
if len(assets_values) == 1:
|
|
807
|
+
if len(assets_values) <= 1:
|
|
822
808
|
yield from get_chunk_parts(product_chunk, progress_callback)
|
|
823
809
|
else:
|
|
824
810
|
yield (
|
|
@@ -830,7 +816,7 @@ class AwsDownload(Download):
|
|
|
830
816
|
)
|
|
831
817
|
|
|
832
818
|
def _get_commonpath(
|
|
833
|
-
self, product: EOProduct, product_chunks:
|
|
819
|
+
self, product: EOProduct, product_chunks: set[Any], build_safe: bool
|
|
834
820
|
) -> str:
|
|
835
821
|
chunk_paths = []
|
|
836
822
|
for product_chunk in product_chunks:
|
|
@@ -840,8 +826,8 @@ class AwsDownload(Download):
|
|
|
840
826
|
return os.path.commonpath(chunk_paths)
|
|
841
827
|
|
|
842
828
|
def get_rio_env(
|
|
843
|
-
self, bucket_name: str, prefix: str, auth_dict:
|
|
844
|
-
) ->
|
|
829
|
+
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
830
|
+
) -> dict[str, Any]:
|
|
845
831
|
"""Get rasterio environment variables needed for data access authentication.
|
|
846
832
|
|
|
847
833
|
:param bucket_name: Bucket containg objects
|
|
@@ -849,23 +835,26 @@ class AwsDownload(Download):
|
|
|
849
835
|
:param auth_dict: Dictionary containing authentication keys
|
|
850
836
|
:returns: The rasterio environement variables
|
|
851
837
|
"""
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
838
|
+
rio_env_kwargs = {}
|
|
839
|
+
if endpoint_url := getattr(self.config, "s3_endpoint", None):
|
|
840
|
+
rio_env_kwargs["endpoint_url"] = endpoint_url.split("://")[-1]
|
|
841
|
+
rio_env_kwargs |= auth_dict
|
|
842
|
+
|
|
843
|
+
if self.s3_session is None:
|
|
844
|
+
_ = self.get_authenticated_objects(bucket_name, prefix, auth_dict)
|
|
857
845
|
|
|
858
|
-
_ = self.get_authenticated_objects(bucket_name, prefix, auth_dict)
|
|
859
846
|
if self.s3_session is not None:
|
|
860
847
|
if self.requester_pays:
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
848
|
+
rio_env_kwargs["requester_pays"] = True
|
|
849
|
+
return {
|
|
850
|
+
"session": self.s3_session,
|
|
851
|
+
**rio_env_kwargs,
|
|
852
|
+
}
|
|
864
853
|
else:
|
|
865
|
-
return {"aws_unsigned": True}
|
|
854
|
+
return {"aws_unsigned": True, **rio_env_kwargs}
|
|
866
855
|
|
|
867
856
|
def get_authenticated_objects(
|
|
868
|
-
self, bucket_name: str, prefix: str, auth_dict:
|
|
857
|
+
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
869
858
|
) -> ResourceCollection:
|
|
870
859
|
"""Get boto3 authenticated objects for the given bucket using
|
|
871
860
|
the most adapted auth strategy.
|
|
@@ -877,8 +866,8 @@ class AwsDownload(Download):
|
|
|
877
866
|
:param auth_dict: Dictionary containing authentication keys
|
|
878
867
|
:returns: The boto3 authenticated objects
|
|
879
868
|
"""
|
|
880
|
-
auth_methods:
|
|
881
|
-
Callable[[str, str,
|
|
869
|
+
auth_methods: list[
|
|
870
|
+
Callable[[str, str, S3SessionKwargs], Optional[ResourceCollection]]
|
|
882
871
|
] = [
|
|
883
872
|
self._get_authenticated_objects_unsigned,
|
|
884
873
|
self._get_authenticated_objects_from_auth_profile,
|
|
@@ -913,7 +902,7 @@ class AwsDownload(Download):
|
|
|
913
902
|
)
|
|
914
903
|
|
|
915
904
|
def _get_authenticated_objects_unsigned(
|
|
916
|
-
self, bucket_name: str, prefix: str, auth_dict:
|
|
905
|
+
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
917
906
|
) -> Optional[ResourceCollection]:
|
|
918
907
|
"""Auth strategy using no-sign-request"""
|
|
919
908
|
|
|
@@ -928,7 +917,7 @@ class AwsDownload(Download):
|
|
|
928
917
|
return objects
|
|
929
918
|
|
|
930
919
|
def _get_authenticated_objects_from_auth_profile(
|
|
931
|
-
self, bucket_name: str, prefix: str, auth_dict:
|
|
920
|
+
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
932
921
|
) -> Optional[ResourceCollection]:
|
|
933
922
|
"""Auth strategy using RequestPayer=requester and ``aws_profile`` from provided credentials"""
|
|
934
923
|
|
|
@@ -951,21 +940,12 @@ class AwsDownload(Download):
|
|
|
951
940
|
return None
|
|
952
941
|
|
|
953
942
|
def _get_authenticated_objects_from_auth_keys(
|
|
954
|
-
self, bucket_name: str, prefix: str, auth_dict:
|
|
943
|
+
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
955
944
|
) -> Optional[ResourceCollection]:
|
|
956
945
|
"""Auth strategy using RequestPayer=requester and ``aws_access_key_id``/``aws_secret_access_key``
|
|
957
946
|
from provided credentials"""
|
|
958
947
|
|
|
959
948
|
if all(k in auth_dict for k in ("aws_access_key_id", "aws_secret_access_key")):
|
|
960
|
-
S3SessionKwargs = TypedDict(
|
|
961
|
-
"S3SessionKwargs",
|
|
962
|
-
{
|
|
963
|
-
"aws_access_key_id": str,
|
|
964
|
-
"aws_secret_access_key": str,
|
|
965
|
-
"aws_session_token": str,
|
|
966
|
-
},
|
|
967
|
-
total=False,
|
|
968
|
-
)
|
|
969
949
|
s3_session_kwargs: S3SessionKwargs = {
|
|
970
950
|
"aws_access_key_id": auth_dict["aws_access_key_id"],
|
|
971
951
|
"aws_secret_access_key": auth_dict["aws_secret_access_key"],
|
|
@@ -990,7 +970,7 @@ class AwsDownload(Download):
|
|
|
990
970
|
return None
|
|
991
971
|
|
|
992
972
|
def _get_authenticated_objects_from_env(
|
|
993
|
-
self, bucket_name: str, prefix: str, auth_dict:
|
|
973
|
+
self, bucket_name: str, prefix: str, auth_dict: S3SessionKwargs
|
|
994
974
|
) -> Optional[ResourceCollection]:
|
|
995
975
|
"""Auth strategy using RequestPayer=requester and current environment"""
|
|
996
976
|
|
|
@@ -1010,7 +990,7 @@ class AwsDownload(Download):
|
|
|
1010
990
|
|
|
1011
991
|
def get_product_bucket_name_and_prefix(
|
|
1012
992
|
self, product: EOProduct, url: Optional[str] = None
|
|
1013
|
-
) ->
|
|
993
|
+
) -> tuple[str, Optional[str]]:
|
|
1014
994
|
"""Extract bucket name and prefix from product URL
|
|
1015
995
|
|
|
1016
996
|
:param product: The EO product to download
|
|
@@ -1141,7 +1121,7 @@ class AwsDownload(Download):
|
|
|
1141
1121
|
s1_title_suffix: Optional[str] = None
|
|
1142
1122
|
# S2 common
|
|
1143
1123
|
if product.product_type and "S2_MSI" in product.product_type:
|
|
1144
|
-
title_search: Optional[Match[str]] = re.search(
|
|
1124
|
+
title_search: Optional[re.Match[str]] = re.search(
|
|
1145
1125
|
r"^\w+_\w+_(\w+)_(\w+)_(\w+)_(\w+)_(\w+)$",
|
|
1146
1126
|
product.properties["title"],
|
|
1147
1127
|
)
|
|
@@ -1327,13 +1307,13 @@ class AwsDownload(Download):
|
|
|
1327
1307
|
def download_all(
|
|
1328
1308
|
self,
|
|
1329
1309
|
products: SearchResult,
|
|
1330
|
-
auth: Optional[Union[AuthBase,
|
|
1310
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
1331
1311
|
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
1332
1312
|
progress_callback: Optional[ProgressCallback] = None,
|
|
1333
|
-
wait:
|
|
1334
|
-
timeout:
|
|
1313
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
1314
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
1335
1315
|
**kwargs: Unpack[DownloadConf],
|
|
1336
|
-
) ->
|
|
1316
|
+
) -> list[str]:
|
|
1337
1317
|
"""
|
|
1338
1318
|
download_all using parent (base plugin) method
|
|
1339
1319
|
"""
|
eodag/plugins/download/base.py
CHANGED
|
@@ -26,17 +26,7 @@ import tempfile
|
|
|
26
26
|
import zipfile
|
|
27
27
|
from datetime import datetime, timedelta
|
|
28
28
|
from time import sleep
|
|
29
|
-
from typing import
|
|
30
|
-
TYPE_CHECKING,
|
|
31
|
-
Any,
|
|
32
|
-
Callable,
|
|
33
|
-
Dict,
|
|
34
|
-
List,
|
|
35
|
-
Optional,
|
|
36
|
-
Tuple,
|
|
37
|
-
TypeVar,
|
|
38
|
-
Union,
|
|
39
|
-
)
|
|
29
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
|
|
40
30
|
|
|
41
31
|
from eodag.plugins.base import PluginTopic
|
|
42
32
|
from eodag.utils import (
|
|
@@ -60,6 +50,7 @@ if TYPE_CHECKING:
|
|
|
60
50
|
from eodag.api.product import EOProduct
|
|
61
51
|
from eodag.api.search_result import SearchResult
|
|
62
52
|
from eodag.config import PluginConfig
|
|
53
|
+
from eodag.types import S3SessionKwargs
|
|
63
54
|
from eodag.types.download_args import DownloadConf
|
|
64
55
|
from eodag.utils import DownloadedCallback, Unpack
|
|
65
56
|
|
|
@@ -110,10 +101,10 @@ class Download(PluginTopic):
|
|
|
110
101
|
def download(
|
|
111
102
|
self,
|
|
112
103
|
product: EOProduct,
|
|
113
|
-
auth: Optional[Union[AuthBase,
|
|
104
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
114
105
|
progress_callback: Optional[ProgressCallback] = None,
|
|
115
|
-
wait:
|
|
116
|
-
timeout:
|
|
106
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
107
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
117
108
|
**kwargs: Unpack[DownloadConf],
|
|
118
109
|
) -> Optional[str]:
|
|
119
110
|
r"""
|
|
@@ -140,10 +131,10 @@ class Download(PluginTopic):
|
|
|
140
131
|
def _stream_download_dict(
|
|
141
132
|
self,
|
|
142
133
|
product: EOProduct,
|
|
143
|
-
auth: Optional[Union[AuthBase,
|
|
134
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
144
135
|
progress_callback: Optional[ProgressCallback] = None,
|
|
145
|
-
wait:
|
|
146
|
-
timeout:
|
|
136
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
137
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
147
138
|
**kwargs: Unpack[DownloadConf],
|
|
148
139
|
) -> StreamResponse:
|
|
149
140
|
r"""
|
|
@@ -170,7 +161,7 @@ class Download(PluginTopic):
|
|
|
170
161
|
product: EOProduct,
|
|
171
162
|
progress_callback: Optional[ProgressCallback] = None,
|
|
172
163
|
**kwargs: Unpack[DownloadConf],
|
|
173
|
-
) ->
|
|
164
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
174
165
|
"""Check if file has already been downloaded, and prepare product download
|
|
175
166
|
|
|
176
167
|
:param product: The EO product to download
|
|
@@ -202,8 +193,8 @@ class Download(PluginTopic):
|
|
|
202
193
|
or getattr(self.config, "output_dir", tempfile.gettempdir())
|
|
203
194
|
or tempfile.gettempdir()
|
|
204
195
|
)
|
|
205
|
-
output_extension = kwargs.get("output_extension"
|
|
206
|
-
self.config, "output_extension", "
|
|
196
|
+
output_extension = kwargs.get("output_extension") or getattr(
|
|
197
|
+
self.config, "output_extension", ""
|
|
207
198
|
)
|
|
208
199
|
|
|
209
200
|
# Strong asumption made here: all products downloaded will be zip files
|
|
@@ -233,9 +224,13 @@ class Download(PluginTopic):
|
|
|
233
224
|
logger.warning(
|
|
234
225
|
f"Unable to create records directory. Got:\n{tb.format_exc()}",
|
|
235
226
|
)
|
|
227
|
+
url_hash = hashlib.md5(url.encode("utf-8")).hexdigest()
|
|
228
|
+
old_record_filename = os.path.join(download_records_dir, url_hash)
|
|
236
229
|
record_filename = os.path.join(
|
|
237
230
|
download_records_dir, self.generate_record_hash(product)
|
|
238
231
|
)
|
|
232
|
+
if os.path.isfile(old_record_filename):
|
|
233
|
+
os.rename(old_record_filename, record_filename)
|
|
239
234
|
if os.path.isfile(record_filename) and os.path.isfile(fs_path):
|
|
240
235
|
logger.info(
|
|
241
236
|
f"Product already downloaded: {fs_path}",
|
|
@@ -339,13 +334,7 @@ class Download(PluginTopic):
|
|
|
339
334
|
if delete_archive is not None
|
|
340
335
|
else getattr(self.config, "delete_archive", True)
|
|
341
336
|
)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
product_path = (
|
|
345
|
-
fs_path[: fs_path.index(output_extension)]
|
|
346
|
-
if output_extension in fs_path
|
|
347
|
-
else fs_path
|
|
348
|
-
)
|
|
337
|
+
product_path, _ = os.path.splitext(fs_path)
|
|
349
338
|
product_path_exists = os.path.exists(product_path)
|
|
350
339
|
if product_path_exists and os.path.isfile(product_path):
|
|
351
340
|
logger.info(
|
|
@@ -422,10 +411,10 @@ class Download(PluginTopic):
|
|
|
422
411
|
|
|
423
412
|
tmp_dir.cleanup()
|
|
424
413
|
|
|
425
|
-
if delete_archive:
|
|
414
|
+
if delete_archive and os.path.isfile(fs_path):
|
|
426
415
|
logger.info(f"Deleting archive {os.path.basename(fs_path)}")
|
|
427
416
|
os.unlink(fs_path)
|
|
428
|
-
|
|
417
|
+
elif os.path.isfile(fs_path):
|
|
429
418
|
logger.info(
|
|
430
419
|
f"Archive deletion is deactivated, keeping {os.path.basename(fs_path)}"
|
|
431
420
|
)
|
|
@@ -441,13 +430,13 @@ class Download(PluginTopic):
|
|
|
441
430
|
def download_all(
|
|
442
431
|
self,
|
|
443
432
|
products: SearchResult,
|
|
444
|
-
auth: Optional[Union[AuthBase,
|
|
433
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
445
434
|
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
446
435
|
progress_callback: Optional[ProgressCallback] = None,
|
|
447
|
-
wait:
|
|
448
|
-
timeout:
|
|
436
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
437
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
449
438
|
**kwargs: Unpack[DownloadConf],
|
|
450
|
-
) ->
|
|
439
|
+
) -> list[str]:
|
|
451
440
|
"""
|
|
452
441
|
Base download_all method.
|
|
453
442
|
|
|
@@ -476,7 +465,7 @@ class Download(PluginTopic):
|
|
|
476
465
|
# Products are going to be removed one by one from this sequence once
|
|
477
466
|
# downloaded.
|
|
478
467
|
products = products[:]
|
|
479
|
-
paths:
|
|
468
|
+
paths: list[str] = []
|
|
480
469
|
# initiate retry loop
|
|
481
470
|
start_time = datetime.now()
|
|
482
471
|
stop_time = start_time + timedelta(minutes=timeout)
|
|
@@ -541,7 +530,7 @@ class Download(PluginTopic):
|
|
|
541
530
|
)
|
|
542
531
|
raise
|
|
543
532
|
|
|
544
|
-
except RuntimeError:
|
|
533
|
+
except (RuntimeError, Exception):
|
|
545
534
|
import traceback as tb
|
|
546
535
|
|
|
547
536
|
logger.error(
|
|
@@ -549,16 +538,9 @@ class Download(PluginTopic):
|
|
|
549
538
|
"Skipping it"
|
|
550
539
|
)
|
|
551
540
|
logger.debug(f"\n{tb.format_exc()}")
|
|
552
|
-
stop_time = datetime.now()
|
|
553
|
-
|
|
554
|
-
except Exception:
|
|
555
|
-
import traceback as tb
|
|
556
541
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
"Skipping it",
|
|
560
|
-
)
|
|
561
|
-
logger.debug(f"\n{tb.format_exc()}")
|
|
542
|
+
# product skipped, to not retry it
|
|
543
|
+
products.remove(product)
|
|
562
544
|
|
|
563
545
|
if (
|
|
564
546
|
len(products) > 0
|
|
@@ -585,14 +567,14 @@ class Download(PluginTopic):
|
|
|
585
567
|
|
|
586
568
|
return paths
|
|
587
569
|
|
|
588
|
-
def
|
|
589
|
-
self, product: EOProduct, wait:
|
|
570
|
+
def _order_download_retry(
|
|
571
|
+
self, product: EOProduct, wait: float, timeout: float
|
|
590
572
|
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
591
573
|
"""
|
|
592
|
-
|
|
574
|
+
Order download retry decorator.
|
|
593
575
|
|
|
594
|
-
Retries the wrapped
|
|
595
|
-
exception is thrown until
|
|
576
|
+
Retries the wrapped order_download method after ``wait`` minutes if a
|
|
577
|
+
``NotAvailableError`` exception is thrown until ``timeout`` minutes.
|
|
596
578
|
|
|
597
579
|
:param product: The EO product to download
|
|
598
580
|
:param wait: If download fails, wait time in minutes between two download tries
|
|
@@ -601,7 +583,7 @@ class Download(PluginTopic):
|
|
|
601
583
|
:returns: decorator
|
|
602
584
|
"""
|
|
603
585
|
|
|
604
|
-
def decorator(
|
|
586
|
+
def decorator(order_download: Callable[..., T]) -> Callable[..., T]:
|
|
605
587
|
def download_and_retry(*args: Any, **kwargs: Unpack[DownloadConf]) -> T:
|
|
606
588
|
# initiate retry loop
|
|
607
589
|
start_time = datetime.now()
|
|
@@ -618,7 +600,7 @@ class Download(PluginTopic):
|
|
|
618
600
|
if datetime_now >= product.next_try:
|
|
619
601
|
product.next_try += timedelta(minutes=wait)
|
|
620
602
|
try:
|
|
621
|
-
return
|
|
603
|
+
return order_download(*args, **kwargs)
|
|
622
604
|
|
|
623
605
|
except NotAvailableError as e:
|
|
624
606
|
if not getattr(self.config, "order_enabled", False):
|
|
@@ -634,7 +616,7 @@ class Download(PluginTopic):
|
|
|
634
616
|
).seconds
|
|
635
617
|
retry_count += 1
|
|
636
618
|
retry_info = (
|
|
637
|
-
f"[Retry #{retry_count}] Waited {wait_seconds}s,
|
|
619
|
+
f"[Retry #{retry_count}] Waited {wait_seconds}s, checking order status again"
|
|
638
620
|
f" (retry every {wait}' for {timeout}')"
|
|
639
621
|
)
|
|
640
622
|
logger.info(not_available_info)
|
|
@@ -656,8 +638,8 @@ class Download(PluginTopic):
|
|
|
656
638
|
).microseconds / 1e6
|
|
657
639
|
retry_count += 1
|
|
658
640
|
retry_info = (
|
|
659
|
-
f"[Retry #{retry_count}] Waiting {wait_seconds}s until next
|
|
660
|
-
f"
|
|
641
|
+
f"[Retry #{retry_count}] Waiting {wait_seconds}s until next order status check"
|
|
642
|
+
f" (retry every {wait}' for {timeout}')"
|
|
661
643
|
)
|
|
662
644
|
logger.info(not_available_info)
|
|
663
645
|
# Retry-After info from Response header
|
|
@@ -678,12 +660,12 @@ class Download(PluginTopic):
|
|
|
678
660
|
logger.info(not_available_info)
|
|
679
661
|
raise NotAvailableError(
|
|
680
662
|
f"{product.properties['title']} is not available ({product.properties['storageStatus']})"
|
|
681
|
-
f" and
|
|
663
|
+
f" and order was not successfull, timeout reached"
|
|
682
664
|
)
|
|
683
665
|
elif datetime_now >= stop_time:
|
|
684
666
|
raise NotAvailableError(not_available_info)
|
|
685
667
|
|
|
686
|
-
return
|
|
668
|
+
return order_download(*args, **kwargs)
|
|
687
669
|
|
|
688
670
|
return download_and_retry
|
|
689
671
|
|
|
@@ -15,10 +15,12 @@
|
|
|
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 Optional
|
|
18
19
|
|
|
19
20
|
import boto3
|
|
20
21
|
from botocore.exceptions import ClientError
|
|
21
22
|
|
|
23
|
+
from eodag import EOProduct
|
|
22
24
|
from eodag.plugins.download.aws import AwsDownload
|
|
23
25
|
from eodag.utils.exceptions import MisconfiguredError
|
|
24
26
|
|
|
@@ -65,3 +67,30 @@ class CreodiasS3Download(AwsDownload):
|
|
|
65
67
|
list(objects.filter(Prefix=prefix).limit(1))
|
|
66
68
|
self.s3_session = s3_session
|
|
67
69
|
return objects
|
|
70
|
+
|
|
71
|
+
def _get_bucket_names_and_prefixes(
|
|
72
|
+
self,
|
|
73
|
+
product: EOProduct,
|
|
74
|
+
asset_filter: Optional[str] = None,
|
|
75
|
+
ignore_assets: Optional[bool] = False,
|
|
76
|
+
) -> list[tuple[str, Optional[str]]]:
|
|
77
|
+
"""
|
|
78
|
+
Retrieves the bucket names and path prefixes for the assets
|
|
79
|
+
|
|
80
|
+
:param product: product for which the assets shall be downloaded
|
|
81
|
+
:param asset_filter: text for which the assets should be filtered
|
|
82
|
+
:param ignore_assets: if product instead of individual assets should be used
|
|
83
|
+
:return: tuples of bucket names and prefixes
|
|
84
|
+
"""
|
|
85
|
+
# if assets are defined, use them instead of scanning product.location
|
|
86
|
+
if len(product.assets) > 0 and not ignore_assets:
|
|
87
|
+
bucket_names_and_prefixes = super()._get_bucket_names_and_prefixes(
|
|
88
|
+
product, asset_filter, ignore_assets
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
# if no assets are given, use productIdentifier to get S3 path for download
|
|
92
|
+
s3_url = "s3:/" + product.properties["productIdentifier"]
|
|
93
|
+
bucket_names_and_prefixes = [
|
|
94
|
+
self.get_product_bucket_name_and_prefix(product, s3_url)
|
|
95
|
+
]
|
|
96
|
+
return bucket_names_and_prefixes
|