eodag 3.0.1__py3-none-any.whl → 3.1.0b1__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 +116 -86
- eodag/api/product/_assets.py +6 -6
- eodag/api/product/_product.py +18 -18
- eodag/api/product/metadata_mapping.py +39 -11
- eodag/cli.py +22 -1
- eodag/config.py +14 -14
- eodag/plugins/apis/ecmwf.py +37 -14
- eodag/plugins/apis/usgs.py +5 -5
- eodag/plugins/authentication/openid_connect.py +2 -2
- eodag/plugins/authentication/token.py +37 -6
- eodag/plugins/crunch/filter_property.py +2 -3
- eodag/plugins/download/aws.py +11 -12
- eodag/plugins/download/base.py +30 -39
- eodag/plugins/download/creodias_s3.py +29 -0
- eodag/plugins/download/http.py +144 -152
- eodag/plugins/download/s3rest.py +5 -7
- eodag/plugins/search/base.py +73 -25
- eodag/plugins/search/build_search_result.py +1047 -310
- eodag/plugins/search/creodias_s3.py +25 -19
- eodag/plugins/search/data_request_search.py +1 -1
- eodag/plugins/search/qssearch.py +51 -139
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +391 -32
- eodag/resources/providers.yml +678 -1744
- eodag/rest/core.py +92 -62
- eodag/rest/server.py +31 -4
- eodag/rest/types/eodag_search.py +6 -0
- eodag/rest/types/queryables.py +5 -6
- eodag/rest/utils/__init__.py +3 -0
- eodag/types/__init__.py +56 -15
- eodag/types/download_args.py +2 -2
- eodag/types/queryables.py +180 -72
- eodag/types/whoosh.py +126 -0
- eodag/utils/__init__.py +71 -10
- eodag/utils/exceptions.py +27 -20
- eodag/utils/repr.py +65 -6
- eodag/utils/requests.py +11 -11
- {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/METADATA +76 -76
- {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/RECORD +43 -44
- {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/WHEEL +1 -1
- {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/entry_points.txt +3 -2
- eodag/utils/constraints.py +0 -244
- {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/LICENSE +0 -0
- {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/top_level.txt +0 -0
eodag/plugins/apis/ecmwf.py
CHANGED
|
@@ -20,16 +20,21 @@ from __future__ import annotations
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
22
|
from datetime import datetime, timezone
|
|
23
|
-
from typing import TYPE_CHECKING
|
|
23
|
+
from typing import TYPE_CHECKING, Annotated
|
|
24
24
|
|
|
25
25
|
import geojson
|
|
26
26
|
from ecmwfapi import ECMWFDataServer, ECMWFService
|
|
27
27
|
from ecmwfapi.api import APIException, Connection, get_apikey_values
|
|
28
|
+
from pydantic.fields import FieldInfo
|
|
28
29
|
|
|
29
30
|
from eodag.plugins.apis.base import Api
|
|
30
31
|
from eodag.plugins.search import PreparedSearch
|
|
31
32
|
from eodag.plugins.search.base import Search
|
|
32
|
-
from eodag.plugins.search.build_search_result import
|
|
33
|
+
from eodag.plugins.search.build_search_result import (
|
|
34
|
+
ECMWF_KEYWORDS,
|
|
35
|
+
ECMWFSearch,
|
|
36
|
+
keywords_to_mdt,
|
|
37
|
+
)
|
|
33
38
|
from eodag.utils import (
|
|
34
39
|
DEFAULT_DOWNLOAD_TIMEOUT,
|
|
35
40
|
DEFAULT_DOWNLOAD_WAIT,
|
|
@@ -58,7 +63,7 @@ logger = logging.getLogger("eodag.apis.ecmwf")
|
|
|
58
63
|
ECMWF_MARS_KNOWN_FORMATS = {"grib": "grib", "netcdf": "nc"}
|
|
59
64
|
|
|
60
65
|
|
|
61
|
-
class EcmwfApi(Api,
|
|
66
|
+
class EcmwfApi(Api, ECMWFSearch):
|
|
62
67
|
"""A plugin that enables to build download-request and download data on ECMWF MARS.
|
|
63
68
|
|
|
64
69
|
Builds a single ready-to-download :class:`~eodag.api.product._product.EOProduct`
|
|
@@ -69,14 +74,15 @@ class EcmwfApi(Api, BuildPostSearchResult):
|
|
|
69
74
|
query).
|
|
70
75
|
|
|
71
76
|
This class inherits from :class:`~eodag.plugins.apis.base.Api` for compatibility and
|
|
72
|
-
:class:`~eodag.plugins.search.build_search_result.
|
|
77
|
+
:class:`~eodag.plugins.search.build_search_result.ECMWFSearch` for the creation
|
|
73
78
|
of the search result.
|
|
74
79
|
|
|
75
80
|
:param provider: provider name
|
|
76
81
|
:param config: Api plugin configuration:
|
|
77
82
|
|
|
78
83
|
* :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): EcmwfApi
|
|
79
|
-
* :attr:`~eodag.config.PluginConfig.
|
|
84
|
+
* :attr:`~eodag.config.PluginConfig.auth_endpoint` (``str``) (**mandatory**): url of
|
|
85
|
+
the authentication endpoint of the ecmwf api
|
|
80
86
|
* :attr:`~eodag.config.PluginConfig.metadata_mapping` (``Dict[str, Union[str, list]]``): how
|
|
81
87
|
parameters should be mapped between the provider and eodag; If a string is given, this is
|
|
82
88
|
the mapping parameter returned by provider -> eodag parameter. If a list with 2 elements
|
|
@@ -86,12 +92,17 @@ class EcmwfApi(Api, BuildPostSearchResult):
|
|
|
86
92
|
|
|
87
93
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
88
94
|
# init self.config.metadata_mapping using Search Base plugin
|
|
95
|
+
config.metadata_mapping = {
|
|
96
|
+
**keywords_to_mdt(ECMWF_KEYWORDS, "ecmwf"),
|
|
97
|
+
**config.metadata_mapping,
|
|
98
|
+
}
|
|
89
99
|
Search.__init__(self, provider, config)
|
|
90
100
|
|
|
91
101
|
# needed by QueryStringSearch.build_query_string / format_free_text_search
|
|
92
102
|
self.config.__dict__.setdefault("free_text_search_operations", {})
|
|
93
103
|
# needed for compatibility
|
|
94
104
|
self.config.__dict__.setdefault("pagination", {"next_page_query_obj": "{{}}"})
|
|
105
|
+
self.config.__dict__.setdefault("api_endpoint", "")
|
|
95
106
|
|
|
96
107
|
def do_search(self, *args: Any, **kwargs: Any) -> List[Dict[str, Any]]:
|
|
97
108
|
"""Should perform the actual search request."""
|
|
@@ -108,9 +119,9 @@ class EcmwfApi(Api, BuildPostSearchResult):
|
|
|
108
119
|
# productType
|
|
109
120
|
if not kwargs.get("productType"):
|
|
110
121
|
kwargs["productType"] = "%s_%s_%s" % (
|
|
111
|
-
kwargs.get("dataset", "mars"),
|
|
112
|
-
kwargs.get("type", ""),
|
|
113
|
-
kwargs.get("levtype", ""),
|
|
122
|
+
kwargs.get("ecmwf:dataset", "mars"),
|
|
123
|
+
kwargs.get("ecmwf:type", ""),
|
|
124
|
+
kwargs.get("ecmwf:levtype", ""),
|
|
114
125
|
)
|
|
115
126
|
# start date
|
|
116
127
|
if "startTimeFromAscendingNode" not in kwargs:
|
|
@@ -132,7 +143,7 @@ class EcmwfApi(Api, BuildPostSearchResult):
|
|
|
132
143
|
if "geometry" in kwargs:
|
|
133
144
|
kwargs["geometry"] = get_geometry_from_various(geometry=kwargs["geometry"])
|
|
134
145
|
|
|
135
|
-
return
|
|
146
|
+
return ECMWFSearch.query(self, prep, **kwargs)
|
|
136
147
|
|
|
137
148
|
def authenticate(self) -> Dict[str, Optional[str]]:
|
|
138
149
|
"""Check credentials and returns information needed for auth
|
|
@@ -143,7 +154,7 @@ class EcmwfApi(Api, BuildPostSearchResult):
|
|
|
143
154
|
# Get credentials from eodag or using ecmwf conf
|
|
144
155
|
email = getattr(self.config, "credentials", {}).get("username", None)
|
|
145
156
|
key = getattr(self.config, "credentials", {}).get("password", None)
|
|
146
|
-
url = getattr(self.config, "
|
|
157
|
+
url = getattr(self.config, "auth_endpoint", None)
|
|
147
158
|
if not all([email, key, url]):
|
|
148
159
|
key, url, email = get_apikey_values()
|
|
149
160
|
|
|
@@ -167,8 +178,8 @@ class EcmwfApi(Api, BuildPostSearchResult):
|
|
|
167
178
|
product: EOProduct,
|
|
168
179
|
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
169
180
|
progress_callback: Optional[ProgressCallback] = None,
|
|
170
|
-
wait:
|
|
171
|
-
timeout:
|
|
181
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
182
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
172
183
|
**kwargs: Unpack[DownloadConf],
|
|
173
184
|
) -> Optional[str]:
|
|
174
185
|
"""Download data from ECMWF MARS"""
|
|
@@ -257,8 +268,8 @@ class EcmwfApi(Api, BuildPostSearchResult):
|
|
|
257
268
|
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
258
269
|
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
259
270
|
progress_callback: Optional[ProgressCallback] = None,
|
|
260
|
-
wait:
|
|
261
|
-
timeout:
|
|
271
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
272
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
262
273
|
**kwargs: Unpack[DownloadConf],
|
|
263
274
|
) -> List[str]:
|
|
264
275
|
"""
|
|
@@ -277,3 +288,15 @@ class EcmwfApi(Api, BuildPostSearchResult):
|
|
|
277
288
|
def clear(self) -> None:
|
|
278
289
|
"""Clear search context"""
|
|
279
290
|
pass
|
|
291
|
+
|
|
292
|
+
def discover_queryables(
|
|
293
|
+
self, **kwargs: Any
|
|
294
|
+
) -> Optional[Dict[str, Annotated[Any, FieldInfo]]]:
|
|
295
|
+
"""Fetch queryables list from provider using metadata mapping
|
|
296
|
+
|
|
297
|
+
:param kwargs: additional filters for queryables (`productType` and other search
|
|
298
|
+
arguments)
|
|
299
|
+
:returns: fetched queryable parameters dict
|
|
300
|
+
"""
|
|
301
|
+
product_type = kwargs.get("productType", None)
|
|
302
|
+
return self.queryables_from_metadata_mapping(product_type)
|
eodag/plugins/apis/usgs.py
CHANGED
|
@@ -297,8 +297,8 @@ class UsgsApi(Api):
|
|
|
297
297
|
product: EOProduct,
|
|
298
298
|
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
299
299
|
progress_callback: Optional[ProgressCallback] = None,
|
|
300
|
-
wait:
|
|
301
|
-
timeout:
|
|
300
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
301
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
302
302
|
**kwargs: Unpack[DownloadConf],
|
|
303
303
|
) -> Optional[str]:
|
|
304
304
|
"""Download data from USGS catalogues"""
|
|
@@ -375,7 +375,7 @@ class UsgsApi(Api):
|
|
|
375
375
|
logger.debug(f"Downloading {req_url}")
|
|
376
376
|
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
377
377
|
|
|
378
|
-
@self.
|
|
378
|
+
@self._order_download_retry(product, wait, timeout)
|
|
379
379
|
def download_request(
|
|
380
380
|
product: EOProduct,
|
|
381
381
|
fs_path: str,
|
|
@@ -467,8 +467,8 @@ class UsgsApi(Api):
|
|
|
467
467
|
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
468
468
|
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
469
469
|
progress_callback: Optional[ProgressCallback] = None,
|
|
470
|
-
wait:
|
|
471
|
-
timeout:
|
|
470
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
471
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
472
472
|
**kwargs: Unpack[DownloadConf],
|
|
473
473
|
) -> List[str]:
|
|
474
474
|
"""
|
|
@@ -68,10 +68,10 @@ class OIDCRefreshTokenBase(Authentication):
|
|
|
68
68
|
self.session = requests.Session()
|
|
69
69
|
|
|
70
70
|
self.access_token = ""
|
|
71
|
-
self.access_token_expiration = datetime.min
|
|
71
|
+
self.access_token_expiration = datetime.min.replace(tzinfo=timezone.utc)
|
|
72
72
|
|
|
73
73
|
self.refresh_token = ""
|
|
74
|
-
self.refresh_token_expiration = datetime.min
|
|
74
|
+
self.refresh_token_expiration = datetime.min.replace(tzinfo=timezone.utc)
|
|
75
75
|
|
|
76
76
|
try:
|
|
77
77
|
response = requests.get(self.config.oidc_config_url)
|
|
@@ -47,7 +47,12 @@ logger = logging.getLogger("eodag.authentication.token")
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class TokenAuth(Authentication):
|
|
50
|
-
"""TokenAuth authentication plugin - fetches a token which is added to search/download requests
|
|
50
|
+
"""TokenAuth authentication plugin - fetches a token which is added to search/download requests.
|
|
51
|
+
|
|
52
|
+
When using headers, if only :attr:`~eodag.config.PluginConfig.headers` is given, it will be used for both token
|
|
53
|
+
retrieve and authentication. If :attr:`~eodag.config.PluginConfig.retrieve_headers` is given, it will be used for
|
|
54
|
+
token retrieve only. If both are given, :attr:`~eodag.config.PluginConfig.retrieve_headers` will be used for token
|
|
55
|
+
retrieve and :attr:`~eodag.config.PluginConfig.headers` for authentication.
|
|
51
56
|
|
|
52
57
|
:param provider: provider name
|
|
53
58
|
:param config: Authentication plugin configuration:
|
|
@@ -55,6 +60,10 @@ class TokenAuth(Authentication):
|
|
|
55
60
|
* :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): TokenAuth
|
|
56
61
|
* :attr:`~eodag.config.PluginConfig.auth_uri` (``str``) (**mandatory**): url used to fetch
|
|
57
62
|
the access token with user/password
|
|
63
|
+
* :attr:`~eodag.config.PluginConfig.headers` (``Dict[str, str]``): Dictionary containing all
|
|
64
|
+
keys/value pairs that should be added to the headers
|
|
65
|
+
* :attr:`~eodag.config.PluginConfig.retrieve_headers` (``Dict[str, str]``): Dictionary containing all
|
|
66
|
+
keys/value pairs that should be added to the headers for token retrieve only
|
|
58
67
|
* :attr:`~eodag.config.PluginConfig.refresh_uri` (``str``) : url used to fetch the
|
|
59
68
|
access token with a refresh token
|
|
60
69
|
* :attr:`~eodag.config.PluginConfig.token_type` (``str``): type of the token (``json``
|
|
@@ -91,11 +100,29 @@ class TokenAuth(Authentication):
|
|
|
91
100
|
self.config.auth_uri = self.config.auth_uri.format(
|
|
92
101
|
**self.config.credentials
|
|
93
102
|
)
|
|
94
|
-
|
|
103
|
+
|
|
104
|
+
# Format headers if needed with values from the credentials. Note:
|
|
105
|
+
# if only 'headers' is given, it will be used for both token retrieve and authentication.
|
|
106
|
+
# if 'retrieve_headers' is given, it will be used for token retrieve only.
|
|
107
|
+
# if both are given, 'retrieve_headers' will be used for token retrieve and 'headers' for authentication.
|
|
108
|
+
|
|
109
|
+
# If the authentication headers are undefined or None: use an empty dict.
|
|
110
|
+
# And don't format '{token}' now, it will be done later.
|
|
111
|
+
raw_headers = getattr(self.config, "headers", None) or {}
|
|
95
112
|
self.config.headers = {
|
|
96
113
|
header: value.format(**{"token": "{token}", **self.config.credentials})
|
|
97
|
-
for header, value in
|
|
114
|
+
for header, value in raw_headers.items()
|
|
98
115
|
}
|
|
116
|
+
|
|
117
|
+
# If the retrieve headers are undefined, their attribute must not be set in self.config.
|
|
118
|
+
# If they are defined but empty, use an empty dict instead of None.
|
|
119
|
+
if hasattr(self.config, "retrieve_headers"):
|
|
120
|
+
raw_retrieve_headers = self.config.retrieve_headers or {}
|
|
121
|
+
self.config.retrieve_headers = {
|
|
122
|
+
header: value.format(**self.config.credentials)
|
|
123
|
+
for header, value in raw_retrieve_headers.items()
|
|
124
|
+
}
|
|
125
|
+
|
|
99
126
|
except KeyError as e:
|
|
100
127
|
raise MisconfiguredError(
|
|
101
128
|
f"Missing credentials inputs for provider {self.provider}: {e}"
|
|
@@ -166,10 +193,14 @@ class TokenAuth(Authentication):
|
|
|
166
193
|
status_forcelist=retry_status_forcelist,
|
|
167
194
|
)
|
|
168
195
|
|
|
196
|
+
# Use the headers for retrieval if defined, else the headers for authentication
|
|
197
|
+
try:
|
|
198
|
+
headers = self.config.retrieve_headers
|
|
199
|
+
except AttributeError:
|
|
200
|
+
headers = self.config.headers
|
|
201
|
+
|
|
169
202
|
# append headers to req if some are specified in config
|
|
170
|
-
req_kwargs: Dict[str, Any] = {
|
|
171
|
-
"headers": dict(self.config.headers, **USER_AGENT)
|
|
172
|
-
}
|
|
203
|
+
req_kwargs: Dict[str, Any] = {"headers": dict(headers, **USER_AGENT)}
|
|
173
204
|
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
174
205
|
|
|
175
206
|
if self.refresh_token:
|
|
@@ -78,10 +78,9 @@ class FilterProperty(Crunch):
|
|
|
78
78
|
for product in products:
|
|
79
79
|
if property_key not in product.properties.keys():
|
|
80
80
|
logger.warning(
|
|
81
|
-
"
|
|
82
|
-
property_key,
|
|
81
|
+
f"{property_key} not found in {product}.properties, product skipped",
|
|
83
82
|
)
|
|
84
|
-
|
|
83
|
+
continue
|
|
85
84
|
if operator_method(product.properties[property_key], property_value):
|
|
86
85
|
add_to_filtered(product)
|
|
87
86
|
|
eodag/plugins/download/aws.py
CHANGED
|
@@ -251,8 +251,8 @@ class AwsDownload(Download):
|
|
|
251
251
|
product: EOProduct,
|
|
252
252
|
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
253
253
|
progress_callback: Optional[ProgressCallback] = None,
|
|
254
|
-
wait:
|
|
255
|
-
timeout:
|
|
254
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
255
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
256
256
|
**kwargs: Unpack[DownloadConf],
|
|
257
257
|
) -> Optional[str]:
|
|
258
258
|
"""Download method for AWS S3 API.
|
|
@@ -482,7 +482,8 @@ class AwsDownload(Download):
|
|
|
482
482
|
ignore_assets: Optional[bool] = False,
|
|
483
483
|
) -> List[Tuple[str, Optional[str]]]:
|
|
484
484
|
"""
|
|
485
|
-
|
|
485
|
+
Retrieves the bucket names and path prefixes for the assets
|
|
486
|
+
|
|
486
487
|
:param product: product for which the assets shall be downloaded
|
|
487
488
|
:param asset_filter: text for which the assets should be filtered
|
|
488
489
|
:param ignore_assets: if product instead of individual assets should be used
|
|
@@ -649,8 +650,8 @@ class AwsDownload(Download):
|
|
|
649
650
|
product: EOProduct,
|
|
650
651
|
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
651
652
|
progress_callback: Optional[ProgressCallback] = None,
|
|
652
|
-
wait:
|
|
653
|
-
timeout:
|
|
653
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
654
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
654
655
|
**kwargs: Unpack[DownloadConf],
|
|
655
656
|
) -> StreamResponse:
|
|
656
657
|
r"""
|
|
@@ -731,12 +732,12 @@ class AwsDownload(Download):
|
|
|
731
732
|
else sanitize(product.properties.get("id", "download"))
|
|
732
733
|
)
|
|
733
734
|
|
|
734
|
-
if len(assets_values)
|
|
735
|
+
if len(assets_values) <= 1:
|
|
735
736
|
first_chunks_tuple = next(chunks_tuples)
|
|
736
737
|
# update headers
|
|
737
738
|
filename = os.path.basename(list(unique_product_chunks)[0].key)
|
|
738
739
|
headers = {"content-disposition": f"attachment; filename={filename}"}
|
|
739
|
-
if assets_values[0].get("type", None):
|
|
740
|
+
if assets_values and assets_values[0].get("type", None):
|
|
740
741
|
headers["content-type"] = assets_values[0]["type"]
|
|
741
742
|
|
|
742
743
|
return StreamResponse(
|
|
@@ -799,7 +800,6 @@ class AwsDownload(Download):
|
|
|
799
800
|
common_path = self._get_commonpath(
|
|
800
801
|
product, unique_product_chunks, build_safe
|
|
801
802
|
)
|
|
802
|
-
|
|
803
803
|
for product_chunk in unique_product_chunks:
|
|
804
804
|
try:
|
|
805
805
|
chunk_rel_path = self.get_chunk_dest_path(
|
|
@@ -817,8 +817,7 @@ class AwsDownload(Download):
|
|
|
817
817
|
# out of SAFE format chunk
|
|
818
818
|
logger.warning(e)
|
|
819
819
|
continue
|
|
820
|
-
|
|
821
|
-
if len(assets_values) == 1:
|
|
820
|
+
if len(assets_values) <= 1:
|
|
822
821
|
yield from get_chunk_parts(product_chunk, progress_callback)
|
|
823
822
|
else:
|
|
824
823
|
yield (
|
|
@@ -1330,8 +1329,8 @@ class AwsDownload(Download):
|
|
|
1330
1329
|
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
1331
1330
|
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
1332
1331
|
progress_callback: Optional[ProgressCallback] = None,
|
|
1333
|
-
wait:
|
|
1334
|
-
timeout:
|
|
1332
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
1333
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
1335
1334
|
**kwargs: Unpack[DownloadConf],
|
|
1336
1335
|
) -> List[str]:
|
|
1337
1336
|
"""
|
eodag/plugins/download/base.py
CHANGED
|
@@ -112,8 +112,8 @@ class Download(PluginTopic):
|
|
|
112
112
|
product: EOProduct,
|
|
113
113
|
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
114
114
|
progress_callback: Optional[ProgressCallback] = None,
|
|
115
|
-
wait:
|
|
116
|
-
timeout:
|
|
115
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
116
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
117
117
|
**kwargs: Unpack[DownloadConf],
|
|
118
118
|
) -> Optional[str]:
|
|
119
119
|
r"""
|
|
@@ -142,8 +142,8 @@ class Download(PluginTopic):
|
|
|
142
142
|
product: EOProduct,
|
|
143
143
|
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
144
144
|
progress_callback: Optional[ProgressCallback] = None,
|
|
145
|
-
wait:
|
|
146
|
-
timeout:
|
|
145
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
146
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
147
147
|
**kwargs: Unpack[DownloadConf],
|
|
148
148
|
) -> StreamResponse:
|
|
149
149
|
r"""
|
|
@@ -202,8 +202,8 @@ class Download(PluginTopic):
|
|
|
202
202
|
or getattr(self.config, "output_dir", tempfile.gettempdir())
|
|
203
203
|
or tempfile.gettempdir()
|
|
204
204
|
)
|
|
205
|
-
output_extension = kwargs.get("output_extension"
|
|
206
|
-
self.config, "output_extension", "
|
|
205
|
+
output_extension = kwargs.get("output_extension") or getattr(
|
|
206
|
+
self.config, "output_extension", ""
|
|
207
207
|
)
|
|
208
208
|
|
|
209
209
|
# Strong asumption made here: all products downloaded will be zip files
|
|
@@ -233,9 +233,13 @@ class Download(PluginTopic):
|
|
|
233
233
|
logger.warning(
|
|
234
234
|
f"Unable to create records directory. Got:\n{tb.format_exc()}",
|
|
235
235
|
)
|
|
236
|
+
url_hash = hashlib.md5(url.encode("utf-8")).hexdigest()
|
|
237
|
+
old_record_filename = os.path.join(download_records_dir, url_hash)
|
|
236
238
|
record_filename = os.path.join(
|
|
237
239
|
download_records_dir, self.generate_record_hash(product)
|
|
238
240
|
)
|
|
241
|
+
if os.path.isfile(old_record_filename):
|
|
242
|
+
os.rename(old_record_filename, record_filename)
|
|
239
243
|
if os.path.isfile(record_filename) and os.path.isfile(fs_path):
|
|
240
244
|
logger.info(
|
|
241
245
|
f"Product already downloaded: {fs_path}",
|
|
@@ -339,13 +343,7 @@ class Download(PluginTopic):
|
|
|
339
343
|
if delete_archive is not None
|
|
340
344
|
else getattr(self.config, "delete_archive", True)
|
|
341
345
|
)
|
|
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
|
-
)
|
|
346
|
+
product_path, _ = os.path.splitext(fs_path)
|
|
349
347
|
product_path_exists = os.path.exists(product_path)
|
|
350
348
|
if product_path_exists and os.path.isfile(product_path):
|
|
351
349
|
logger.info(
|
|
@@ -422,10 +420,10 @@ class Download(PluginTopic):
|
|
|
422
420
|
|
|
423
421
|
tmp_dir.cleanup()
|
|
424
422
|
|
|
425
|
-
if delete_archive:
|
|
423
|
+
if delete_archive and os.path.isfile(fs_path):
|
|
426
424
|
logger.info(f"Deleting archive {os.path.basename(fs_path)}")
|
|
427
425
|
os.unlink(fs_path)
|
|
428
|
-
|
|
426
|
+
elif os.path.isfile(fs_path):
|
|
429
427
|
logger.info(
|
|
430
428
|
f"Archive deletion is deactivated, keeping {os.path.basename(fs_path)}"
|
|
431
429
|
)
|
|
@@ -444,8 +442,8 @@ class Download(PluginTopic):
|
|
|
444
442
|
auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
|
|
445
443
|
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
446
444
|
progress_callback: Optional[ProgressCallback] = None,
|
|
447
|
-
wait:
|
|
448
|
-
timeout:
|
|
445
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
446
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
449
447
|
**kwargs: Unpack[DownloadConf],
|
|
450
448
|
) -> List[str]:
|
|
451
449
|
"""
|
|
@@ -541,7 +539,7 @@ class Download(PluginTopic):
|
|
|
541
539
|
)
|
|
542
540
|
raise
|
|
543
541
|
|
|
544
|
-
except RuntimeError:
|
|
542
|
+
except (RuntimeError, Exception):
|
|
545
543
|
import traceback as tb
|
|
546
544
|
|
|
547
545
|
logger.error(
|
|
@@ -549,16 +547,9 @@ class Download(PluginTopic):
|
|
|
549
547
|
"Skipping it"
|
|
550
548
|
)
|
|
551
549
|
logger.debug(f"\n{tb.format_exc()}")
|
|
552
|
-
stop_time = datetime.now()
|
|
553
|
-
|
|
554
|
-
except Exception:
|
|
555
|
-
import traceback as tb
|
|
556
550
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
"Skipping it",
|
|
560
|
-
)
|
|
561
|
-
logger.debug(f"\n{tb.format_exc()}")
|
|
551
|
+
# product skipped, to not retry it
|
|
552
|
+
products.remove(product)
|
|
562
553
|
|
|
563
554
|
if (
|
|
564
555
|
len(products) > 0
|
|
@@ -585,14 +576,14 @@ class Download(PluginTopic):
|
|
|
585
576
|
|
|
586
577
|
return paths
|
|
587
578
|
|
|
588
|
-
def
|
|
589
|
-
self, product: EOProduct, wait:
|
|
579
|
+
def _order_download_retry(
|
|
580
|
+
self, product: EOProduct, wait: float, timeout: float
|
|
590
581
|
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
591
582
|
"""
|
|
592
|
-
|
|
583
|
+
Order download retry decorator.
|
|
593
584
|
|
|
594
|
-
Retries the wrapped
|
|
595
|
-
exception is thrown until
|
|
585
|
+
Retries the wrapped order_download method after ``wait`` minutes if a
|
|
586
|
+
``NotAvailableError`` exception is thrown until ``timeout`` minutes.
|
|
596
587
|
|
|
597
588
|
:param product: The EO product to download
|
|
598
589
|
:param wait: If download fails, wait time in minutes between two download tries
|
|
@@ -601,7 +592,7 @@ class Download(PluginTopic):
|
|
|
601
592
|
:returns: decorator
|
|
602
593
|
"""
|
|
603
594
|
|
|
604
|
-
def decorator(
|
|
595
|
+
def decorator(order_download: Callable[..., T]) -> Callable[..., T]:
|
|
605
596
|
def download_and_retry(*args: Any, **kwargs: Unpack[DownloadConf]) -> T:
|
|
606
597
|
# initiate retry loop
|
|
607
598
|
start_time = datetime.now()
|
|
@@ -618,7 +609,7 @@ class Download(PluginTopic):
|
|
|
618
609
|
if datetime_now >= product.next_try:
|
|
619
610
|
product.next_try += timedelta(minutes=wait)
|
|
620
611
|
try:
|
|
621
|
-
return
|
|
612
|
+
return order_download(*args, **kwargs)
|
|
622
613
|
|
|
623
614
|
except NotAvailableError as e:
|
|
624
615
|
if not getattr(self.config, "order_enabled", False):
|
|
@@ -634,7 +625,7 @@ class Download(PluginTopic):
|
|
|
634
625
|
).seconds
|
|
635
626
|
retry_count += 1
|
|
636
627
|
retry_info = (
|
|
637
|
-
f"[Retry #{retry_count}] Waited {wait_seconds}s,
|
|
628
|
+
f"[Retry #{retry_count}] Waited {wait_seconds}s, checking order status again"
|
|
638
629
|
f" (retry every {wait}' for {timeout}')"
|
|
639
630
|
)
|
|
640
631
|
logger.info(not_available_info)
|
|
@@ -656,8 +647,8 @@ class Download(PluginTopic):
|
|
|
656
647
|
).microseconds / 1e6
|
|
657
648
|
retry_count += 1
|
|
658
649
|
retry_info = (
|
|
659
|
-
f"[Retry #{retry_count}] Waiting {wait_seconds}s until next
|
|
660
|
-
f"
|
|
650
|
+
f"[Retry #{retry_count}] Waiting {wait_seconds}s until next order status check"
|
|
651
|
+
f" (retry every {wait}' for {timeout}')"
|
|
661
652
|
)
|
|
662
653
|
logger.info(not_available_info)
|
|
663
654
|
# Retry-After info from Response header
|
|
@@ -678,12 +669,12 @@ class Download(PluginTopic):
|
|
|
678
669
|
logger.info(not_available_info)
|
|
679
670
|
raise NotAvailableError(
|
|
680
671
|
f"{product.properties['title']} is not available ({product.properties['storageStatus']})"
|
|
681
|
-
f" and
|
|
672
|
+
f" and order was not successfull, timeout reached"
|
|
682
673
|
)
|
|
683
674
|
elif datetime_now >= stop_time:
|
|
684
675
|
raise NotAvailableError(not_available_info)
|
|
685
676
|
|
|
686
|
-
return
|
|
677
|
+
return order_download(*args, **kwargs)
|
|
687
678
|
|
|
688
679
|
return download_and_retry
|
|
689
680
|
|
|
@@ -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 List, Optional, Tuple
|
|
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
|