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/http.py
CHANGED
|
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
|
+
import re
|
|
22
23
|
import shutil
|
|
23
24
|
import tarfile
|
|
24
25
|
import zipfile
|
|
@@ -26,17 +27,8 @@ from datetime import datetime
|
|
|
26
27
|
from email.message import Message
|
|
27
28
|
from itertools import chain
|
|
28
29
|
from json import JSONDecodeError
|
|
29
|
-
from
|
|
30
|
-
|
|
31
|
-
Any,
|
|
32
|
-
Dict,
|
|
33
|
-
Iterator,
|
|
34
|
-
List,
|
|
35
|
-
Optional,
|
|
36
|
-
TypedDict,
|
|
37
|
-
Union,
|
|
38
|
-
cast,
|
|
39
|
-
)
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import TYPE_CHECKING, Any, Iterator, Optional, TypedDict, Union, cast
|
|
40
32
|
from urllib.parse import parse_qs, urlparse
|
|
41
33
|
|
|
42
34
|
import geojson
|
|
@@ -70,6 +62,7 @@ from eodag.utils import (
|
|
|
70
62
|
guess_file_type,
|
|
71
63
|
parse_header,
|
|
72
64
|
path_to_uri,
|
|
65
|
+
rename_with_version,
|
|
73
66
|
sanitize,
|
|
74
67
|
string_to_jsonpath,
|
|
75
68
|
uri_to_path,
|
|
@@ -80,6 +73,7 @@ from eodag.utils.exceptions import (
|
|
|
80
73
|
MisconfiguredError,
|
|
81
74
|
NotAvailableError,
|
|
82
75
|
TimeOutError,
|
|
76
|
+
ValidationError,
|
|
83
77
|
)
|
|
84
78
|
|
|
85
79
|
if TYPE_CHECKING:
|
|
@@ -88,6 +82,7 @@ if TYPE_CHECKING:
|
|
|
88
82
|
from eodag.api.product import Asset, EOProduct # type: ignore
|
|
89
83
|
from eodag.api.search_result import SearchResult
|
|
90
84
|
from eodag.config import PluginConfig
|
|
85
|
+
from eodag.types import S3SessionKwargs
|
|
91
86
|
from eodag.types.download_args import DownloadConf
|
|
92
87
|
from eodag.utils import DownloadedCallback, Unpack
|
|
93
88
|
|
|
@@ -100,42 +95,69 @@ class HTTPDownload(Download):
|
|
|
100
95
|
:param provider: provider name
|
|
101
96
|
:param config: Download plugin configuration:
|
|
102
97
|
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
*
|
|
110
|
-
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
*
|
|
114
|
-
|
|
98
|
+
* :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): ``HTTPDownload``
|
|
99
|
+
* :attr:`~eodag.config.PluginConfig.base_uri` (``str``): default endpoint url
|
|
100
|
+
* :attr:`~eodag.config.PluginConfig.method` (``str``): HTTP request method for the download request (``GET`` or
|
|
101
|
+
``POST``); default: ``GET``
|
|
102
|
+
* :attr:`~eodag.config.PluginConfig.extract` (``bool``): if the content of the downloaded file should be
|
|
103
|
+
extracted; default: ``True``
|
|
104
|
+
* :attr:`~eodag.config.PluginConfig.auth_error_code` (``int``): which error code is returned in case of an
|
|
105
|
+
authentication error
|
|
106
|
+
* :attr:`~eodag.config.PluginConfig.dl_url_params` (``dict[str, Any]``): parameters to be
|
|
107
|
+
added to the query params of the request
|
|
108
|
+
* :attr:`~eodag.config.PluginConfig.archive_depth` (``int``): level in extracted path tree where to find data;
|
|
109
|
+
default: ``1``
|
|
110
|
+
* :attr:`~eodag.config.PluginConfig.flatten_top_dirs` (``bool``): if the directory structure should be
|
|
111
|
+
flattened; default: ``True``
|
|
112
|
+
* :attr:`~eodag.config.PluginConfig.ignore_assets` (``bool``): ignore assets and download using downloadLink;
|
|
113
|
+
default: ``False``
|
|
114
|
+
* :attr:`~eodag.config.PluginConfig.timeout` (``int``): time to wait until request timeout in seconds;
|
|
115
|
+
default: ``5``
|
|
116
|
+
* :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates should be verified in
|
|
117
|
+
requests; default: ``True``
|
|
118
|
+
* :attr:`~eodag.config.PluginConfig.no_auth_download` (``bool``): if the download should be done without
|
|
119
|
+
authentication; default: ``True``
|
|
120
|
+
* :attr:`~eodag.config.PluginConfig.order_enabled` (``bool``): if the product has to be ordered to download it;
|
|
121
|
+
if this parameter is set to true, a mapping for the orderLink has to be added to the metadata mapping of
|
|
122
|
+
the search plugin used for the provider; default: ``False``
|
|
123
|
+
* :attr:`~eodag.config.PluginConfig.order_method` (``str``): HTTP request method for the order request (``GET``
|
|
124
|
+
or ``POST``); default: ``GET``
|
|
125
|
+
* :attr:`~eodag.config.PluginConfig.order_headers` (``[dict[str, str]]``): headers to be added to the order
|
|
126
|
+
request
|
|
127
|
+
* :attr:`~eodag.config.PluginConfig.order_on_response` (:class:`~eodag.config.PluginConfig.OrderOnResponse`):
|
|
128
|
+
a typed dictionary containing the key ``metadata_mapping`` which can be used to add new product properties
|
|
129
|
+
based on the data in response to the order request
|
|
130
|
+
* :attr:`~eodag.config.PluginConfig.order_status` (:class:`~eodag.config.PluginConfig.OrderStatus`):
|
|
131
|
+
configuration to handle the order status; contains information which method to use, how the response data is
|
|
132
|
+
interpreted, which status corresponds to success, ordered and error and what should be done on success.
|
|
133
|
+
* :attr:`~eodag.config.PluginConfig.products` (``dict[str, dict[str, Any]``): product type specific config; the
|
|
134
|
+
keys are the product types, the values are dictionaries which can contain the key
|
|
135
|
+
:attr:`~eodag.config.PluginConfig.extract` to overwrite the provider config for a specific product type
|
|
115
136
|
|
|
116
137
|
"""
|
|
117
138
|
|
|
118
139
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
119
140
|
super(HTTPDownload, self).__init__(provider, config)
|
|
120
141
|
|
|
121
|
-
def
|
|
142
|
+
def _order(
|
|
122
143
|
self,
|
|
123
144
|
product: EOProduct,
|
|
124
145
|
auth: Optional[AuthBase] = None,
|
|
125
146
|
**kwargs: Unpack[DownloadConf],
|
|
126
|
-
) -> Optional[
|
|
147
|
+
) -> Optional[dict[str, Any]]:
|
|
127
148
|
"""Send product order request.
|
|
128
149
|
|
|
129
150
|
It will be executed once before the download retry loop, if the product is OFFLINE
|
|
130
151
|
and has `orderLink` in its properties.
|
|
131
152
|
Product ordering can be configured using the following download plugin parameters:
|
|
132
153
|
|
|
133
|
-
-
|
|
154
|
+
- :attr:`~eodag.config.PluginConfig.order_enabled`: Wether order is enabled or not (may not use this method
|
|
134
155
|
if no `orderLink` exists)
|
|
135
156
|
|
|
136
|
-
-
|
|
157
|
+
- :attr:`~eodag.config.PluginConfig.order_method`: (optional) HTTP request method, GET (default) or POST
|
|
137
158
|
|
|
138
|
-
-
|
|
159
|
+
- :attr:`~eodag.config.PluginConfig.order_on_response`: (optional) things to do with obtained order
|
|
160
|
+
response:
|
|
139
161
|
|
|
140
162
|
- *metadata_mapping*: edit or add new product propoerties properties
|
|
141
163
|
|
|
@@ -154,7 +176,7 @@ class HTTPDownload(Download):
|
|
|
154
176
|
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
155
177
|
timeout = getattr(self.config, "timeout", HTTP_REQ_TIMEOUT)
|
|
156
178
|
OrderKwargs = TypedDict(
|
|
157
|
-
"OrderKwargs", {"json":
|
|
179
|
+
"OrderKwargs", {"json": dict[str, Union[Any, list[str]]]}, total=False
|
|
158
180
|
)
|
|
159
181
|
order_kwargs: OrderKwargs = {}
|
|
160
182
|
if order_method == "POST":
|
|
@@ -193,25 +215,20 @@ class HTTPDownload(Download):
|
|
|
193
215
|
logger.debug(ordered_message)
|
|
194
216
|
product.properties["storageStatus"] = STAGING_STATUS
|
|
195
217
|
except RequestException as e:
|
|
196
|
-
if hasattr(e, "response") and (
|
|
197
|
-
content := getattr(e.response, "content", None)
|
|
198
|
-
):
|
|
199
|
-
error_message = f"{content.decode('utf-8')} - {e}"
|
|
200
|
-
else:
|
|
201
|
-
error_message = str(e)
|
|
202
|
-
logger.warning(
|
|
203
|
-
"%s could not be ordered, request returned %s",
|
|
204
|
-
product.properties["title"],
|
|
205
|
-
error_message,
|
|
206
|
-
)
|
|
207
218
|
self._check_auth_exception(e)
|
|
219
|
+
msg = f'{product.properties["title"]} could not be ordered'
|
|
220
|
+
if e.response is not None and e.response.status_code == 400:
|
|
221
|
+
raise ValidationError.from_error(e, msg) from e
|
|
222
|
+
else:
|
|
223
|
+
raise DownloadError.from_error(e, msg) from e
|
|
224
|
+
|
|
208
225
|
return self.order_response_process(response, product)
|
|
209
226
|
except requests.exceptions.Timeout as exc:
|
|
210
227
|
raise TimeOutError(exc, timeout=timeout) from exc
|
|
211
228
|
|
|
212
229
|
def order_response_process(
|
|
213
230
|
self, response: Response, product: EOProduct
|
|
214
|
-
) -> Optional[
|
|
231
|
+
) -> Optional[dict[str, Any]]:
|
|
215
232
|
"""Process order response
|
|
216
233
|
|
|
217
234
|
:param response: The order response
|
|
@@ -246,7 +263,7 @@ class HTTPDownload(Download):
|
|
|
246
263
|
|
|
247
264
|
return json_response
|
|
248
265
|
|
|
249
|
-
def
|
|
266
|
+
def _order_status(
|
|
250
267
|
self,
|
|
251
268
|
product: EOProduct,
|
|
252
269
|
auth: Optional[AuthBase] = None,
|
|
@@ -256,7 +273,7 @@ class HTTPDownload(Download):
|
|
|
256
273
|
It will be executed before each download retry.
|
|
257
274
|
Product order status request can be configured using the following download plugin parameters:
|
|
258
275
|
|
|
259
|
-
-
|
|
276
|
+
- :attr:`~eodag.config.PluginConfig.order_status`: :class:`~eodag.config.PluginConfig.OrderStatus`
|
|
260
277
|
|
|
261
278
|
Product properties used for order status:
|
|
262
279
|
|
|
@@ -275,7 +292,7 @@ class HTTPDownload(Download):
|
|
|
275
292
|
def _request(
|
|
276
293
|
url: str,
|
|
277
294
|
method: str = "GET",
|
|
278
|
-
headers: Optional[
|
|
295
|
+
headers: Optional[dict[str, Any]] = None,
|
|
279
296
|
json: Optional[Any] = None,
|
|
280
297
|
timeout: int = HTTP_REQ_TIMEOUT,
|
|
281
298
|
) -> Response:
|
|
@@ -311,7 +328,7 @@ class HTTPDownload(Download):
|
|
|
311
328
|
except requests.exceptions.Timeout as exc:
|
|
312
329
|
raise TimeOutError(exc, timeout=timeout) from exc
|
|
313
330
|
|
|
314
|
-
status_request:
|
|
331
|
+
status_request: dict[str, Any] = status_config.get("request", {})
|
|
315
332
|
status_request_method = str(status_request.get("method", "GET")).upper()
|
|
316
333
|
|
|
317
334
|
if status_request_method == "POST":
|
|
@@ -328,8 +345,8 @@ class HTTPDownload(Download):
|
|
|
328
345
|
|
|
329
346
|
# check header for success before full status request
|
|
330
347
|
skip_parsing_status_response = False
|
|
331
|
-
status_dict:
|
|
332
|
-
config_on_success:
|
|
348
|
+
status_dict: dict[str, Any] = {}
|
|
349
|
+
config_on_success: dict[str, Any] = status_config.get("on_success", {})
|
|
333
350
|
on_success_mm = config_on_success.get("metadata_mapping", {})
|
|
334
351
|
|
|
335
352
|
status_response_content_needed = (
|
|
@@ -373,13 +390,11 @@ class HTTPDownload(Download):
|
|
|
373
390
|
# success and no need to get status response content
|
|
374
391
|
skip_parsing_status_response = True
|
|
375
392
|
except RequestException as e:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
)
|
|
382
|
-
) from e
|
|
393
|
+
msg = f'{product.properties["title"]} order status could not be checked'
|
|
394
|
+
if e.response is not None and e.response.status_code == 400:
|
|
395
|
+
raise ValidationError.from_error(e, msg) from e
|
|
396
|
+
else:
|
|
397
|
+
raise DownloadError.from_error(e, msg) from e
|
|
383
398
|
|
|
384
399
|
if not skip_parsing_status_response:
|
|
385
400
|
# status request
|
|
@@ -415,13 +430,13 @@ class HTTPDownload(Download):
|
|
|
415
430
|
product.properties["orderStatus"] = status_dict.get("status")
|
|
416
431
|
|
|
417
432
|
# handle status error
|
|
418
|
-
errors:
|
|
433
|
+
errors: dict[str, Any] = status_config.get("error", {})
|
|
419
434
|
if errors and errors.items() <= status_dict.items():
|
|
420
435
|
raise DownloadError(
|
|
421
436
|
f"Provider {product.provider} returned: {status_dict.get('error_message', status_message)}"
|
|
422
437
|
)
|
|
423
438
|
|
|
424
|
-
success_status:
|
|
439
|
+
success_status: dict[str, Any] = status_config.get("success", {}).get("status")
|
|
425
440
|
# if not success
|
|
426
441
|
if (success_status and success_status != status_dict.get("status")) or (
|
|
427
442
|
success_code and success_code != response.status_code
|
|
@@ -539,10 +554,10 @@ class HTTPDownload(Download):
|
|
|
539
554
|
def download(
|
|
540
555
|
self,
|
|
541
556
|
product: EOProduct,
|
|
542
|
-
auth: Optional[Union[AuthBase,
|
|
557
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
543
558
|
progress_callback: Optional[ProgressCallback] = None,
|
|
544
|
-
wait:
|
|
545
|
-
timeout:
|
|
559
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
560
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
546
561
|
**kwargs: Unpack[DownloadConf],
|
|
547
562
|
) -> Optional[str]:
|
|
548
563
|
"""Download a product using HTTP protocol.
|
|
@@ -560,13 +575,6 @@ class HTTPDownload(Download):
|
|
|
560
575
|
)
|
|
561
576
|
progress_callback = ProgressCallback(disable=True)
|
|
562
577
|
|
|
563
|
-
output_extension = getattr(self.config, "products", {}).get(
|
|
564
|
-
product.product_type, {}
|
|
565
|
-
).get("output_extension", None) or getattr(
|
|
566
|
-
self.config, "output_extension", ".zip"
|
|
567
|
-
)
|
|
568
|
-
kwargs["output_extension"] = kwargs.get("output_extension", output_extension)
|
|
569
|
-
|
|
570
578
|
fs_path, record_filename = self._prepare_download(
|
|
571
579
|
product,
|
|
572
580
|
progress_callback=progress_callback,
|
|
@@ -585,7 +593,7 @@ class HTTPDownload(Download):
|
|
|
585
593
|
try:
|
|
586
594
|
fs_path = self._download_assets(
|
|
587
595
|
product,
|
|
588
|
-
fs_path
|
|
596
|
+
fs_path,
|
|
589
597
|
record_filename,
|
|
590
598
|
auth,
|
|
591
599
|
progress_callback,
|
|
@@ -602,82 +610,64 @@ class HTTPDownload(Download):
|
|
|
602
610
|
|
|
603
611
|
url = product.remote_location
|
|
604
612
|
|
|
605
|
-
@self.
|
|
613
|
+
@self._order_download_retry(product, wait, timeout)
|
|
606
614
|
def download_request(
|
|
607
615
|
product: EOProduct,
|
|
608
616
|
auth: AuthBase,
|
|
609
617
|
progress_callback: ProgressCallback,
|
|
610
|
-
wait:
|
|
611
|
-
timeout:
|
|
618
|
+
wait: float,
|
|
619
|
+
timeout: float,
|
|
612
620
|
**kwargs: Unpack[DownloadConf],
|
|
613
|
-
) ->
|
|
614
|
-
chunks = self._stream_download(product, auth, progress_callback, **kwargs)
|
|
621
|
+
) -> os.PathLike:
|
|
615
622
|
is_empty = True
|
|
623
|
+
chunk_iterator = self._stream_download(
|
|
624
|
+
product, auth, progress_callback, **kwargs
|
|
625
|
+
)
|
|
626
|
+
if fs_path is not None:
|
|
627
|
+
ext = Path(product.filename).suffix
|
|
628
|
+
path = Path(fs_path).with_suffix(ext)
|
|
629
|
+
|
|
630
|
+
with open(path, "wb") as fhandle:
|
|
631
|
+
for chunk in chunk_iterator:
|
|
632
|
+
is_empty = False
|
|
633
|
+
progress_callback(len(chunk))
|
|
634
|
+
fhandle.write(chunk)
|
|
635
|
+
self.stream.close() # Closing response stream
|
|
616
636
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
is_empty = False
|
|
620
|
-
fhandle.write(chunk)
|
|
637
|
+
if is_empty:
|
|
638
|
+
raise DownloadError(f"product {product.properties['id']} is empty")
|
|
621
639
|
|
|
622
|
-
|
|
623
|
-
|
|
640
|
+
return path
|
|
641
|
+
else:
|
|
642
|
+
raise DownloadError(
|
|
643
|
+
f"download of product {product.properties['id']} failed"
|
|
644
|
+
)
|
|
624
645
|
|
|
625
|
-
download_request(
|
|
646
|
+
path = download_request(
|
|
647
|
+
product, auth, progress_callback, wait, timeout, **kwargs
|
|
648
|
+
)
|
|
626
649
|
|
|
627
650
|
with open(record_filename, "w") as fh:
|
|
628
651
|
fh.write(url)
|
|
629
652
|
logger.debug("Download recorded in %s", record_filename)
|
|
630
653
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
logger.warning(
|
|
634
|
-
"Downloaded product is not a Zip File. Please check its file type before using it"
|
|
635
|
-
)
|
|
636
|
-
new_fs_path = os.path.join(
|
|
637
|
-
os.path.dirname(fs_path),
|
|
638
|
-
sanitize(product.properties["title"]),
|
|
639
|
-
)
|
|
640
|
-
if os.path.isfile(fs_path) and not tarfile.is_tarfile(fs_path):
|
|
641
|
-
if not os.path.isdir(new_fs_path):
|
|
642
|
-
os.makedirs(new_fs_path)
|
|
643
|
-
shutil.move(fs_path, new_fs_path)
|
|
644
|
-
file_path = os.path.join(new_fs_path, os.path.basename(fs_path))
|
|
645
|
-
new_file_path = file_path[: file_path.index(".zip")]
|
|
646
|
-
shutil.move(file_path, new_file_path)
|
|
647
|
-
# in the case where the outputs extension has not been set
|
|
648
|
-
# to ".tar" in the product type nor provider configuration
|
|
649
|
-
elif tarfile.is_tarfile(fs_path):
|
|
650
|
-
if not new_fs_path.endswith(".tar"):
|
|
651
|
-
new_fs_path += ".tar"
|
|
652
|
-
shutil.move(fs_path, new_fs_path)
|
|
653
|
-
kwargs["output_extension"] = ".tar"
|
|
654
|
-
product_path = self._finalize(
|
|
655
|
-
new_fs_path,
|
|
656
|
-
progress_callback=progress_callback,
|
|
657
|
-
**kwargs,
|
|
658
|
-
)
|
|
659
|
-
product.location = path_to_uri(product_path)
|
|
660
|
-
return product_path
|
|
661
|
-
else:
|
|
662
|
-
# not a file (dir with zip extension)
|
|
663
|
-
shutil.move(fs_path, new_fs_path)
|
|
664
|
-
product.location = path_to_uri(new_fs_path)
|
|
665
|
-
return new_fs_path
|
|
666
|
-
|
|
667
|
-
if os.path.isfile(fs_path) and not (
|
|
668
|
-
zipfile.is_zipfile(fs_path) or tarfile.is_tarfile(fs_path)
|
|
654
|
+
if os.path.isfile(path) and not (
|
|
655
|
+
zipfile.is_zipfile(path) or tarfile.is_tarfile(path)
|
|
669
656
|
):
|
|
670
657
|
new_fs_path = os.path.join(
|
|
671
|
-
os.path.dirname(
|
|
658
|
+
os.path.dirname(path),
|
|
672
659
|
sanitize(product.properties["title"]),
|
|
673
660
|
)
|
|
661
|
+
if os.path.isfile(new_fs_path):
|
|
662
|
+
rename_with_version(new_fs_path)
|
|
674
663
|
if not os.path.isdir(new_fs_path):
|
|
675
664
|
os.makedirs(new_fs_path)
|
|
676
|
-
shutil.move(
|
|
665
|
+
shutil.move(path, new_fs_path)
|
|
677
666
|
product.location = path_to_uri(new_fs_path)
|
|
678
667
|
return new_fs_path
|
|
668
|
+
|
|
679
669
|
product_path = self._finalize(
|
|
680
|
-
|
|
670
|
+
str(path),
|
|
681
671
|
progress_callback=progress_callback,
|
|
682
672
|
**kwargs,
|
|
683
673
|
)
|
|
@@ -718,28 +708,19 @@ class HTTPDownload(Download):
|
|
|
718
708
|
ext = guess_extension(content_type)
|
|
719
709
|
if ext:
|
|
720
710
|
filename += ext
|
|
721
|
-
else:
|
|
722
|
-
output_extension: Optional[str] = (
|
|
723
|
-
getattr(self.config, "products", {})
|
|
724
|
-
.get(product.product_type, {})
|
|
725
|
-
.get("output_extension")
|
|
726
|
-
)
|
|
727
|
-
if output_extension:
|
|
728
|
-
filename += output_extension
|
|
729
|
-
|
|
730
711
|
return filename
|
|
731
712
|
|
|
732
713
|
def _stream_download_dict(
|
|
733
714
|
self,
|
|
734
715
|
product: EOProduct,
|
|
735
|
-
auth: Optional[Union[AuthBase,
|
|
716
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
736
717
|
progress_callback: Optional[ProgressCallback] = None,
|
|
737
|
-
wait:
|
|
738
|
-
timeout:
|
|
718
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
719
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
739
720
|
**kwargs: Unpack[DownloadConf],
|
|
740
721
|
) -> StreamResponse:
|
|
741
722
|
r"""
|
|
742
|
-
Returns
|
|
723
|
+
Returns dictionary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments.
|
|
743
724
|
It contains a generator to streamed download chunks and the response headers.
|
|
744
725
|
|
|
745
726
|
:param product: The EO product to download
|
|
@@ -752,7 +733,7 @@ class HTTPDownload(Download):
|
|
|
752
733
|
and `dl_url_params` (dict) can be provided as additional kwargs
|
|
753
734
|
and will override any other values defined in a configuration
|
|
754
735
|
file or with environment variables.
|
|
755
|
-
:returns:
|
|
736
|
+
:returns: Dictionary of :class:`~fastapi.responses.StreamingResponse` keyword-arguments
|
|
756
737
|
"""
|
|
757
738
|
if auth is not None and not isinstance(auth, AuthBase):
|
|
758
739
|
raise MisconfiguredError(f"Incompatible auth plugin: {type(auth)}")
|
|
@@ -791,13 +772,17 @@ class HTTPDownload(Download):
|
|
|
791
772
|
)
|
|
792
773
|
|
|
793
774
|
else:
|
|
775
|
+
# get first chunk to check if it does not contain an error (if it does, that error will be raised)
|
|
776
|
+
first_chunks_tuple = next(chunks_tuples)
|
|
794
777
|
outputs_filename = (
|
|
795
778
|
sanitize(product.properties["title"])
|
|
796
779
|
if "title" in product.properties
|
|
797
780
|
else sanitize(product.properties.get("id", "download"))
|
|
798
781
|
)
|
|
799
782
|
return StreamResponse(
|
|
800
|
-
content=stream_zip(
|
|
783
|
+
content=stream_zip(
|
|
784
|
+
chain(iter([first_chunks_tuple]), chunks_tuples)
|
|
785
|
+
),
|
|
801
786
|
media_type="application/zip",
|
|
802
787
|
headers={
|
|
803
788
|
"content-disposition": f"attachment; filename={outputs_filename}.zip",
|
|
@@ -809,17 +794,20 @@ class HTTPDownload(Download):
|
|
|
809
794
|
else:
|
|
810
795
|
pass
|
|
811
796
|
|
|
812
|
-
|
|
797
|
+
chunk_iterator = self._stream_download(
|
|
798
|
+
product, auth, progress_callback, **kwargs
|
|
799
|
+
)
|
|
800
|
+
|
|
813
801
|
# start reading chunks to set product.headers
|
|
814
802
|
try:
|
|
815
|
-
first_chunk = next(
|
|
803
|
+
first_chunk = next(chunk_iterator)
|
|
816
804
|
except StopIteration:
|
|
817
805
|
# product is empty file
|
|
818
806
|
logger.error("product %s is empty", product.properties["id"])
|
|
819
807
|
raise NotAvailableError(f"product {product.properties['id']} is empty")
|
|
820
808
|
|
|
821
809
|
return StreamResponse(
|
|
822
|
-
content=chain(iter([first_chunk]),
|
|
810
|
+
content=chain(iter([first_chunk]), chunk_iterator),
|
|
823
811
|
headers=product.headers,
|
|
824
812
|
)
|
|
825
813
|
|
|
@@ -837,12 +825,9 @@ class HTTPDownload(Download):
|
|
|
837
825
|
and e.response.status_code in auth_errors
|
|
838
826
|
):
|
|
839
827
|
raise AuthenticationError(
|
|
840
|
-
"
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
response_text,
|
|
844
|
-
self.provider,
|
|
845
|
-
)
|
|
828
|
+
f"Please check your credentials for {self.provider}.",
|
|
829
|
+
f"HTTP Error {e.response.status_code} returned.",
|
|
830
|
+
response_text,
|
|
846
831
|
)
|
|
847
832
|
|
|
848
833
|
def _process_exception(
|
|
@@ -880,6 +865,44 @@ class HTTPDownload(Download):
|
|
|
880
865
|
else:
|
|
881
866
|
logger.error("Error while getting resource :\n%s", tb.format_exc())
|
|
882
867
|
|
|
868
|
+
def _order_request(
|
|
869
|
+
self,
|
|
870
|
+
product: EOProduct,
|
|
871
|
+
auth: Optional[AuthBase],
|
|
872
|
+
) -> None:
|
|
873
|
+
if (
|
|
874
|
+
"orderLink" in product.properties
|
|
875
|
+
and product.properties.get("storageStatus") == OFFLINE_STATUS
|
|
876
|
+
and not product.properties.get("orderStatus")
|
|
877
|
+
):
|
|
878
|
+
self._order(product=product, auth=auth)
|
|
879
|
+
|
|
880
|
+
if (
|
|
881
|
+
product.properties.get("orderStatusLink", None)
|
|
882
|
+
and product.properties.get("storageStatus") != ONLINE_STATUS
|
|
883
|
+
):
|
|
884
|
+
self._order_status(product=product, auth=auth)
|
|
885
|
+
|
|
886
|
+
def order(
|
|
887
|
+
self,
|
|
888
|
+
product: EOProduct,
|
|
889
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
890
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
891
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
892
|
+
) -> None:
|
|
893
|
+
"""
|
|
894
|
+
Order product and poll to check its status
|
|
895
|
+
|
|
896
|
+
:param product: The EO product to download
|
|
897
|
+
:param auth: (optional) authenticated object
|
|
898
|
+
:param wait: (optional) Wait time in minutes between two order status check
|
|
899
|
+
:param timeout: (optional) Maximum time in minutes before stop checking
|
|
900
|
+
order status
|
|
901
|
+
"""
|
|
902
|
+
self._order_download_retry(product, wait, timeout)(self._order_request)(
|
|
903
|
+
product, auth
|
|
904
|
+
)
|
|
905
|
+
|
|
883
906
|
def _stream_download(
|
|
884
907
|
self,
|
|
885
908
|
product: EOProduct,
|
|
@@ -888,8 +911,9 @@ class HTTPDownload(Download):
|
|
|
888
911
|
**kwargs: Unpack[DownloadConf],
|
|
889
912
|
) -> Iterator[Any]:
|
|
890
913
|
"""
|
|
891
|
-
|
|
914
|
+
Fetches a zip file containing the assets of a given product as a stream
|
|
892
915
|
and returns a generator yielding the chunks of the file
|
|
916
|
+
|
|
893
917
|
:param product: product for which the assets should be downloaded
|
|
894
918
|
:param auth: The configuration of a plugin of type Authentication
|
|
895
919
|
:param progress_callback: A method or a callable object
|
|
@@ -903,19 +927,11 @@ class HTTPDownload(Download):
|
|
|
903
927
|
logger.info("Progress bar unavailable, please call product.download()")
|
|
904
928
|
progress_callback = ProgressCallback(disable=True)
|
|
905
929
|
|
|
906
|
-
|
|
907
|
-
if (
|
|
908
|
-
"orderLink" in product.properties
|
|
909
|
-
and product.properties.get("storageStatus") == OFFLINE_STATUS
|
|
910
|
-
and not product.properties.get("orderStatus")
|
|
911
|
-
):
|
|
912
|
-
self.order_download(product=product, auth=auth)
|
|
930
|
+
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
913
931
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
):
|
|
918
|
-
self.order_download_status(product=product, auth=auth)
|
|
932
|
+
ordered_message = ""
|
|
933
|
+
# retry handled at download level
|
|
934
|
+
self._order_request(product, auth)
|
|
919
935
|
|
|
920
936
|
params = kwargs.pop("dl_url_params", None) or getattr(
|
|
921
937
|
self.config, "dl_url_params", {}
|
|
@@ -933,7 +949,7 @@ class HTTPDownload(Download):
|
|
|
933
949
|
if not query_dict and parts.query:
|
|
934
950
|
query_dict = geojson.loads(parts.query)
|
|
935
951
|
req_url = parts._replace(query="").geturl()
|
|
936
|
-
req_kwargs:
|
|
952
|
+
req_kwargs: dict[str, Any] = {"json": query_dict} if query_dict else {}
|
|
937
953
|
else:
|
|
938
954
|
req_url = url
|
|
939
955
|
req_kwargs = {}
|
|
@@ -945,7 +961,7 @@ class HTTPDownload(Download):
|
|
|
945
961
|
auth = None
|
|
946
962
|
|
|
947
963
|
s = requests.Session()
|
|
948
|
-
|
|
964
|
+
self.stream = s.request(
|
|
949
965
|
req_method,
|
|
950
966
|
req_url,
|
|
951
967
|
stream=True,
|
|
@@ -953,56 +969,55 @@ class HTTPDownload(Download):
|
|
|
953
969
|
params=params,
|
|
954
970
|
headers=USER_AGENT,
|
|
955
971
|
timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
|
|
972
|
+
verify=ssl_verify,
|
|
956
973
|
**req_kwargs,
|
|
957
|
-
)
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
product.headers["Content-Type"] = guessed_content_type
|
|
974
|
+
)
|
|
975
|
+
try:
|
|
976
|
+
self.stream.raise_for_status()
|
|
977
|
+
except requests.exceptions.Timeout as exc:
|
|
978
|
+
raise TimeOutError(exc, timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT) from exc
|
|
979
|
+
except RequestException as e:
|
|
980
|
+
self._process_exception(e, product, ordered_message)
|
|
981
|
+
raise DownloadError(
|
|
982
|
+
f"download of {product.properties['id']} is empty"
|
|
983
|
+
) from e
|
|
984
|
+
else:
|
|
985
|
+
# check if product was ordered
|
|
986
|
+
|
|
987
|
+
if getattr(
|
|
988
|
+
self.stream, "status_code", None
|
|
989
|
+
) is not None and self.stream.status_code == getattr(
|
|
990
|
+
self.config, "order_status", {}
|
|
991
|
+
).get(
|
|
992
|
+
"ordered", {}
|
|
993
|
+
).get(
|
|
994
|
+
"http_code"
|
|
995
|
+
):
|
|
996
|
+
product.properties["storageStatus"] = "ORDERED"
|
|
997
|
+
self._process_exception(None, product, ordered_message)
|
|
998
|
+
stream_size = self._check_stream_size(product) or None
|
|
999
|
+
|
|
1000
|
+
product.headers = self.stream.headers
|
|
1001
|
+
filename = self._check_product_filename(product)
|
|
1002
|
+
product.headers["content-disposition"] = f"attachment; filename={filename}"
|
|
1003
|
+
content_type = product.headers.get("Content-Type")
|
|
1004
|
+
guessed_content_type = (
|
|
1005
|
+
guess_file_type(filename) if filename and not content_type else None
|
|
1006
|
+
)
|
|
1007
|
+
if guessed_content_type is not None:
|
|
1008
|
+
product.headers["Content-Type"] = guessed_content_type
|
|
993
1009
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
yield chunk
|
|
1010
|
+
progress_callback.reset(total=stream_size)
|
|
1011
|
+
|
|
1012
|
+
product.filename = filename
|
|
1013
|
+
return self.stream.iter_content(chunk_size=64 * 1024)
|
|
999
1014
|
|
|
1000
1015
|
def _stream_download_assets(
|
|
1001
1016
|
self,
|
|
1002
1017
|
product: EOProduct,
|
|
1003
1018
|
auth: Optional[AuthBase] = None,
|
|
1004
1019
|
progress_callback: Optional[ProgressCallback] = None,
|
|
1005
|
-
assets_values:
|
|
1020
|
+
assets_values: list[Asset] = [],
|
|
1006
1021
|
**kwargs: Unpack[DownloadConf],
|
|
1007
1022
|
) -> Iterator[Any]:
|
|
1008
1023
|
if progress_callback is None:
|
|
@@ -1055,6 +1070,16 @@ class HTTPDownload(Download):
|
|
|
1055
1070
|
"flatten_top_dirs", getattr(self.config, "flatten_top_dirs", True)
|
|
1056
1071
|
)
|
|
1057
1072
|
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
1073
|
+
matching_url = (
|
|
1074
|
+
getattr(product.downloader_auth.config, "matching_url", "")
|
|
1075
|
+
if product.downloader_auth
|
|
1076
|
+
else ""
|
|
1077
|
+
)
|
|
1078
|
+
matching_conf = (
|
|
1079
|
+
getattr(product.downloader_auth.config, "matching_conf", None)
|
|
1080
|
+
if product.downloader_auth
|
|
1081
|
+
else None
|
|
1082
|
+
)
|
|
1058
1083
|
|
|
1059
1084
|
# loop for assets download
|
|
1060
1085
|
for asset in assets_values:
|
|
@@ -1063,11 +1088,16 @@ class HTTPDownload(Download):
|
|
|
1063
1088
|
f"Local asset detected. Download skipped for {asset['href']}"
|
|
1064
1089
|
)
|
|
1065
1090
|
continue
|
|
1066
|
-
|
|
1091
|
+
if matching_conf or (
|
|
1092
|
+
matching_url and re.match(matching_url, asset["href"])
|
|
1093
|
+
):
|
|
1094
|
+
auth_object = auth
|
|
1095
|
+
else:
|
|
1096
|
+
auth_object = None
|
|
1067
1097
|
with requests.get(
|
|
1068
1098
|
asset["href"],
|
|
1069
1099
|
stream=True,
|
|
1070
|
-
auth=
|
|
1100
|
+
auth=auth_object,
|
|
1071
1101
|
params=params,
|
|
1072
1102
|
headers=USER_AGENT,
|
|
1073
1103
|
timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
|
|
@@ -1080,8 +1110,7 @@ class HTTPDownload(Download):
|
|
|
1080
1110
|
exc, timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT
|
|
1081
1111
|
) from exc
|
|
1082
1112
|
except RequestException as e:
|
|
1083
|
-
|
|
1084
|
-
self._handle_asset_exception(e, asset, raise_errors=raise_errors)
|
|
1113
|
+
self._handle_asset_exception(e, asset)
|
|
1085
1114
|
else:
|
|
1086
1115
|
asset_rel_path = (
|
|
1087
1116
|
asset.rel_path.replace(assets_common_subdir, "").strip(os.sep)
|
|
@@ -1239,33 +1268,28 @@ class HTTPDownload(Download):
|
|
|
1239
1268
|
|
|
1240
1269
|
return fs_dir_path
|
|
1241
1270
|
|
|
1242
|
-
def _handle_asset_exception(
|
|
1243
|
-
self, e: RequestException, asset: Asset, raise_errors: bool = False
|
|
1244
|
-
) -> None:
|
|
1271
|
+
def _handle_asset_exception(self, e: RequestException, asset: Asset) -> None:
|
|
1245
1272
|
# check if error is identified as auth_error in provider conf
|
|
1246
1273
|
auth_errors = getattr(self.config, "auth_error_code", [None])
|
|
1247
1274
|
if not isinstance(auth_errors, list):
|
|
1248
1275
|
auth_errors = [auth_errors]
|
|
1249
1276
|
if e.response is not None and e.response.status_code in auth_errors:
|
|
1250
1277
|
raise AuthenticationError(
|
|
1251
|
-
"
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
e.response.text.strip(),
|
|
1255
|
-
self.provider,
|
|
1256
|
-
)
|
|
1278
|
+
f"Please check your credentials for {self.provider}.",
|
|
1279
|
+
f"HTTP Error {e.response.status_code} returned.",
|
|
1280
|
+
e.response.text.strip(),
|
|
1257
1281
|
)
|
|
1258
|
-
elif raise_errors:
|
|
1259
|
-
raise DownloadError(e)
|
|
1260
1282
|
else:
|
|
1261
|
-
logger.
|
|
1262
|
-
|
|
1283
|
+
logger.error(
|
|
1284
|
+
"Unexpected error at download of asset %s: %s", asset["href"], e
|
|
1285
|
+
)
|
|
1286
|
+
raise DownloadError(e)
|
|
1263
1287
|
|
|
1264
1288
|
def _get_asset_sizes(
|
|
1265
1289
|
self,
|
|
1266
|
-
assets_values:
|
|
1290
|
+
assets_values: list[Asset],
|
|
1267
1291
|
auth: Optional[AuthBase],
|
|
1268
|
-
params: Optional[
|
|
1292
|
+
params: Optional[dict[str, str]],
|
|
1269
1293
|
zipped: bool = False,
|
|
1270
1294
|
) -> int:
|
|
1271
1295
|
total_size = 0
|
|
@@ -1338,11 +1362,11 @@ class HTTPDownload(Download):
|
|
|
1338
1362
|
def download_all(
|
|
1339
1363
|
self,
|
|
1340
1364
|
products: SearchResult,
|
|
1341
|
-
auth: Optional[Union[AuthBase,
|
|
1365
|
+
auth: Optional[Union[AuthBase, S3SessionKwargs]] = None,
|
|
1342
1366
|
downloaded_callback: Optional[DownloadedCallback] = None,
|
|
1343
1367
|
progress_callback: Optional[ProgressCallback] = None,
|
|
1344
|
-
wait:
|
|
1345
|
-
timeout:
|
|
1368
|
+
wait: float = DEFAULT_DOWNLOAD_WAIT,
|
|
1369
|
+
timeout: float = DEFAULT_DOWNLOAD_TIMEOUT,
|
|
1346
1370
|
**kwargs: Unpack[DownloadConf],
|
|
1347
1371
|
):
|
|
1348
1372
|
"""
|