eodag 3.2.0__py3-none-any.whl → 3.3.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 +2 -1
- eodag/api/search_result.py +3 -4
- eodag/config.py +3 -0
- eodag/plugins/authentication/token.py +16 -1
- eodag/plugins/download/http.py +46 -17
- eodag/plugins/search/build_search_result.py +390 -86
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/providers.yml +66 -19
- eodag/utils/__init__.py +8 -2
- {eodag-3.2.0.dist-info → eodag-3.3.0.dist-info}/METADATA +2 -2
- {eodag-3.2.0.dist-info → eodag-3.3.0.dist-info}/RECORD +15 -15
- {eodag-3.2.0.dist-info → eodag-3.3.0.dist-info}/WHEEL +0 -0
- {eodag-3.2.0.dist-info → eodag-3.3.0.dist-info}/entry_points.txt +0 -0
- {eodag-3.2.0.dist-info → eodag-3.3.0.dist-info}/licenses/LICENSE +0 -0
- {eodag-3.2.0.dist-info → eodag-3.3.0.dist-info}/top_level.txt +0 -0
eodag/api/core.py
CHANGED
|
@@ -1599,6 +1599,7 @@ class EODataAccessGateway:
|
|
|
1599
1599
|
if kwargs.get("raise_errors"):
|
|
1600
1600
|
raise
|
|
1601
1601
|
logger.warning(e)
|
|
1602
|
+
results.errors.append((plugin.provider, e))
|
|
1602
1603
|
continue
|
|
1603
1604
|
|
|
1604
1605
|
# try using crunch to get unique result
|
|
@@ -1622,7 +1623,7 @@ class EODataAccessGateway:
|
|
|
1622
1623
|
"Several products found for this id (%s). You may try searching using more selective criteria.",
|
|
1623
1624
|
results,
|
|
1624
1625
|
)
|
|
1625
|
-
return SearchResult([], 0)
|
|
1626
|
+
return SearchResult([], 0, results.errors)
|
|
1626
1627
|
|
|
1627
1628
|
def _fetch_external_product_type(self, provider: str, product_type: str):
|
|
1628
1629
|
plugins = self._plugins_manager.get_search_plugins(provider=provider)
|
eodag/api/search_result.py
CHANGED
|
@@ -193,8 +193,8 @@ class SearchResult(UserList):
|
|
|
193
193
|
<details><summary style='color: grey; font-family: monospace;'>
|
|
194
194
|
{i} 
|
|
195
195
|
{type(p).__name__}(id=<span style='color: black;'>{
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
p.properties["id"]
|
|
197
|
+
}</span>, provider={p.provider})
|
|
198
198
|
</summary>
|
|
199
199
|
{p._repr_html_()}
|
|
200
200
|
</details>
|
|
@@ -214,13 +214,12 @@ class SearchResult(UserList):
|
|
|
214
214
|
return super().extend(other)
|
|
215
215
|
|
|
216
216
|
|
|
217
|
-
class RawSearchResult(UserList):
|
|
217
|
+
class RawSearchResult(UserList[dict[str, Any]]):
|
|
218
218
|
"""An object representing a collection of raw/unparsed search results obtained from a provider.
|
|
219
219
|
|
|
220
220
|
:param results: A list of raw/unparsed search results
|
|
221
221
|
"""
|
|
222
222
|
|
|
223
|
-
data: list[Any]
|
|
224
223
|
query_params: dict[str, Any]
|
|
225
224
|
product_type_def_params: dict[str, Any]
|
|
226
225
|
|
eodag/config.py
CHANGED
|
@@ -615,6 +615,9 @@ class PluginConfig(yaml.YAMLObject):
|
|
|
615
615
|
#: :class:`~eodag.plugins.authentication.token.TokenAuth`
|
|
616
616
|
#: type of the token
|
|
617
617
|
token_type: str
|
|
618
|
+
#: :class:`~eodag.plugins.authentication.token.TokenAuth`
|
|
619
|
+
#: key to get the expiration time of the token
|
|
620
|
+
token_expiration_key: str
|
|
618
621
|
#: :class:`~eodag.plugins.authentication.token_exchange.OIDCTokenExchangeAuth`
|
|
619
622
|
#: The full :class:`~eodag.plugins.authentication.openid_connect.OIDCAuthorizationCodeFlowAuth` plugin configuration
|
|
620
623
|
#: used to retrieve subject token
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
+
from datetime import datetime, timedelta
|
|
21
22
|
from typing import TYPE_CHECKING, Any, Optional
|
|
22
23
|
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
|
23
24
|
|
|
@@ -72,6 +73,8 @@ class TokenAuth(Authentication):
|
|
|
72
73
|
key to get the access token in the response to the token request
|
|
73
74
|
* :attr:`~eodag.config.PluginConfig.refresh_token_key` (``str``): key to get the refresh
|
|
74
75
|
token in the response to the token request
|
|
76
|
+
* :attr:`~eodag.config.PluginConfig.token_expiration_key` (``str``): key to get expiration time of
|
|
77
|
+
the token (given in s)
|
|
75
78
|
* :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates
|
|
76
79
|
should be verified in the requests; default: ``True``
|
|
77
80
|
* :attr:`~eodag.config.PluginConfig.auth_error_code` (``int``): which error code is
|
|
@@ -91,6 +94,7 @@ class TokenAuth(Authentication):
|
|
|
91
94
|
super(TokenAuth, self).__init__(provider, config)
|
|
92
95
|
self.token = ""
|
|
93
96
|
self.refresh_token = ""
|
|
97
|
+
self.token_expiration = datetime.now()
|
|
94
98
|
|
|
95
99
|
def validate_config_credentials(self) -> None:
|
|
96
100
|
"""Validate configured credentials"""
|
|
@@ -131,7 +135,11 @@ class TokenAuth(Authentication):
|
|
|
131
135
|
def authenticate(self) -> AuthBase:
|
|
132
136
|
"""Authenticate"""
|
|
133
137
|
self.validate_config_credentials()
|
|
134
|
-
|
|
138
|
+
if self.token and self.token_expiration > datetime.now():
|
|
139
|
+
logger.debug("using existing access token")
|
|
140
|
+
return RequestsTokenAuth(
|
|
141
|
+
self.token, "header", headers=getattr(self.config, "headers", {})
|
|
142
|
+
)
|
|
135
143
|
s = requests.Session()
|
|
136
144
|
try:
|
|
137
145
|
# First get the token
|
|
@@ -168,6 +176,12 @@ class TokenAuth(Authentication):
|
|
|
168
176
|
self.token = token
|
|
169
177
|
if getattr(self.config, "refresh_token_key", None):
|
|
170
178
|
self.refresh_token = response.json()[self.config.refresh_token_key]
|
|
179
|
+
if getattr(self.config, "token_expiration_key", None):
|
|
180
|
+
expiration_time = response.json()[self.config.token_expiration_key]
|
|
181
|
+
self.token_expiration = datetime.now() + timedelta(
|
|
182
|
+
seconds=expiration_time
|
|
183
|
+
)
|
|
184
|
+
|
|
171
185
|
if not hasattr(self.config, "headers"):
|
|
172
186
|
raise MisconfiguredError(f"Missing headers configuration for {self}")
|
|
173
187
|
# Return auth class set with obtained token
|
|
@@ -179,6 +193,7 @@ class TokenAuth(Authentication):
|
|
|
179
193
|
self,
|
|
180
194
|
session: requests.Session,
|
|
181
195
|
) -> requests.Response:
|
|
196
|
+
|
|
182
197
|
retry_total = getattr(self.config, "retry_total", REQ_RETRY_TOTAL)
|
|
183
198
|
retry_backoff_factor = getattr(
|
|
184
199
|
self.config, "retry_backoff_factor", REQ_RETRY_BACKOFF_FACTOR
|
eodag/plugins/download/http.py
CHANGED
|
@@ -216,7 +216,7 @@ class HTTPDownload(Download):
|
|
|
216
216
|
product.properties["storageStatus"] = STAGING_STATUS
|
|
217
217
|
except RequestException as e:
|
|
218
218
|
self._check_auth_exception(e)
|
|
219
|
-
msg = f
|
|
219
|
+
msg = f"{product.properties['title']} could not be ordered"
|
|
220
220
|
if e.response is not None and e.response.status_code == 400:
|
|
221
221
|
raise ValidationError.from_error(e, msg) from e
|
|
222
222
|
else:
|
|
@@ -255,6 +255,16 @@ class HTTPDownload(Download):
|
|
|
255
255
|
product.properties.update(
|
|
256
256
|
{k: v for k, v in properties_update.items() if v != NOT_AVAILABLE}
|
|
257
257
|
)
|
|
258
|
+
# the job id becomes the product id for EcmwfSearch products
|
|
259
|
+
if "ORDERABLE" in product.properties.get("id", ""):
|
|
260
|
+
product.properties["id"] = product.properties.get(
|
|
261
|
+
"orderId", product.properties["id"]
|
|
262
|
+
)
|
|
263
|
+
product.properties["title"] = (
|
|
264
|
+
(product.product_type or product.provider).upper()
|
|
265
|
+
+ "_"
|
|
266
|
+
+ product.properties["id"]
|
|
267
|
+
)
|
|
258
268
|
if "downloadLink" in product.properties:
|
|
259
269
|
product.remote_location = product.location = product.properties[
|
|
260
270
|
"downloadLink"
|
|
@@ -390,7 +400,10 @@ class HTTPDownload(Download):
|
|
|
390
400
|
# success and no need to get status response content
|
|
391
401
|
skip_parsing_status_response = True
|
|
392
402
|
except RequestException as e:
|
|
393
|
-
msg =
|
|
403
|
+
msg = (
|
|
404
|
+
f"{product.properties.get('title') or product.properties.get('id') or product} "
|
|
405
|
+
"order status could not be checked"
|
|
406
|
+
)
|
|
394
407
|
if e.response is not None and e.response.status_code == 400:
|
|
395
408
|
raise ValidationError.from_error(e, msg) from e
|
|
396
409
|
else:
|
|
@@ -426,9 +439,14 @@ class HTTPDownload(Download):
|
|
|
426
439
|
f"{product.properties['title']} order status: {status_percent}"
|
|
427
440
|
)
|
|
428
441
|
|
|
429
|
-
|
|
442
|
+
product.properties.update(
|
|
443
|
+
{k: v for k, v in status_dict.items() if v != NOT_AVAILABLE}
|
|
444
|
+
)
|
|
445
|
+
|
|
430
446
|
product.properties["orderStatus"] = status_dict.get("status")
|
|
431
447
|
|
|
448
|
+
status_message = status_dict.get("message")
|
|
449
|
+
|
|
432
450
|
# handle status error
|
|
433
451
|
errors: dict[str, Any] = status_config.get("error", {})
|
|
434
452
|
if errors and errors.items() <= status_dict.items():
|
|
@@ -436,13 +454,14 @@ class HTTPDownload(Download):
|
|
|
436
454
|
f"Provider {product.provider} returned: {status_dict.get('error_message', status_message)}"
|
|
437
455
|
)
|
|
438
456
|
|
|
457
|
+
product.properties["storageStatus"] = STAGING_STATUS
|
|
458
|
+
|
|
439
459
|
success_status: dict[str, Any] = status_config.get("success", {}).get("status")
|
|
440
460
|
# if not success
|
|
441
461
|
if (success_status and success_status != status_dict.get("status")) or (
|
|
442
462
|
success_code and success_code != response.status_code
|
|
443
463
|
):
|
|
444
|
-
|
|
445
|
-
raise error
|
|
464
|
+
return None
|
|
446
465
|
|
|
447
466
|
product.properties["storageStatus"] = ONLINE_STATUS
|
|
448
467
|
|
|
@@ -461,7 +480,11 @@ class HTTPDownload(Download):
|
|
|
461
480
|
product.properties["title"],
|
|
462
481
|
e,
|
|
463
482
|
)
|
|
464
|
-
|
|
483
|
+
msg = f"{product.properties['title']} order status could not be checked"
|
|
484
|
+
if e.response is not None and e.response.status_code == 400:
|
|
485
|
+
raise ValidationError.from_error(e, msg) from e
|
|
486
|
+
else:
|
|
487
|
+
raise DownloadError.from_error(e, msg) from e
|
|
465
488
|
|
|
466
489
|
result_type = config_on_success.get("result_type", "json")
|
|
467
490
|
result_entry = config_on_success.get("results_entry")
|
|
@@ -626,6 +649,8 @@ class HTTPDownload(Download):
|
|
|
626
649
|
if fs_path is not None:
|
|
627
650
|
ext = Path(product.filename).suffix
|
|
628
651
|
path = Path(fs_path).with_suffix(ext)
|
|
652
|
+
if "ORDERABLE" in path.stem and product.properties.get("title"):
|
|
653
|
+
path = path.with_stem(sanitize(product.properties["title"]))
|
|
629
654
|
|
|
630
655
|
with open(path, "wb") as fhandle:
|
|
631
656
|
for chunk in chunk_iterator:
|
|
@@ -961,17 +986,21 @@ class HTTPDownload(Download):
|
|
|
961
986
|
auth = None
|
|
962
987
|
|
|
963
988
|
s = requests.Session()
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
989
|
+
try:
|
|
990
|
+
self.stream = s.request(
|
|
991
|
+
req_method,
|
|
992
|
+
req_url,
|
|
993
|
+
stream=True,
|
|
994
|
+
auth=auth,
|
|
995
|
+
params=params,
|
|
996
|
+
headers=USER_AGENT,
|
|
997
|
+
timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
|
|
998
|
+
verify=ssl_verify,
|
|
999
|
+
**req_kwargs,
|
|
1000
|
+
)
|
|
1001
|
+
except requests.exceptions.MissingSchema:
|
|
1002
|
+
# location is not a valid url -> product is not available yet
|
|
1003
|
+
raise NotAvailableError("Product is not available yet")
|
|
975
1004
|
try:
|
|
976
1005
|
self.stream.raise_for_status()
|
|
977
1006
|
except requests.exceptions.Timeout as exc:
|