eodag 3.3.0__py3-none-any.whl → 3.3.2__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/product/_product.py +73 -36
- eodag/api/search_result.py +5 -7
- eodag/config.py +3 -0
- eodag/plugins/authentication/token.py +97 -60
- eodag/plugins/search/base.py +9 -7
- eodag/plugins/search/qssearch.py +15 -14
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/providers.yml +3 -3
- eodag/rest/errors.py +18 -3
- eodag/types/queryables.py +3 -0
- {eodag-3.3.0.dist-info → eodag-3.3.2.dist-info}/METADATA +2 -2
- {eodag-3.3.0.dist-info → eodag-3.3.2.dist-info}/RECORD +16 -16
- {eodag-3.3.0.dist-info → eodag-3.3.2.dist-info}/WHEEL +1 -1
- {eodag-3.3.0.dist-info → eodag-3.3.2.dist-info}/entry_points.txt +0 -0
- {eodag-3.3.0.dist-info → eodag-3.3.2.dist-info}/licenses/LICENSE +0 -0
- {eodag-3.3.0.dist-info → eodag-3.3.2.dist-info}/top_level.txt +0 -0
eodag/api/product/_product.py
CHANGED
|
@@ -313,7 +313,6 @@ class EOProduct:
|
|
|
313
313
|
"EO product is unable to download itself due to lacking of a "
|
|
314
314
|
"download plugin"
|
|
315
315
|
)
|
|
316
|
-
|
|
317
316
|
auth = (
|
|
318
317
|
self.downloader_auth.authenticate()
|
|
319
318
|
if self.downloader_auth is not None
|
|
@@ -323,7 +322,6 @@ class EOProduct:
|
|
|
323
322
|
progress_callback, close_progress_callback = self._init_progress_bar(
|
|
324
323
|
progress_callback
|
|
325
324
|
)
|
|
326
|
-
|
|
327
325
|
fs_path = self.downloader.download(
|
|
328
326
|
self,
|
|
329
327
|
auth=auth,
|
|
@@ -369,6 +367,47 @@ class EOProduct:
|
|
|
369
367
|
progress_callback.refresh()
|
|
370
368
|
return (progress_callback, close_progress_callback)
|
|
371
369
|
|
|
370
|
+
def _download_quicklook(
|
|
371
|
+
self,
|
|
372
|
+
quicklook_file: str,
|
|
373
|
+
progress_callback: ProgressCallback,
|
|
374
|
+
ssl_verify: Optional[bool] = None,
|
|
375
|
+
auth: Optional[AuthBase] = None,
|
|
376
|
+
):
|
|
377
|
+
|
|
378
|
+
"""Download the quicklook image from the EOProduct's quicklook URL.
|
|
379
|
+
|
|
380
|
+
This method performs an HTTP GET request to retrieve the quicklook image and saves it
|
|
381
|
+
locally at the specified path. It optionally verifies SSL certificates, uses HTTP
|
|
382
|
+
authentication, and can display a download progress if a callback is provided.
|
|
383
|
+
|
|
384
|
+
:param quicklook_file: The full path (including filename) where the quicklook will be saved.
|
|
385
|
+
:param progress_callback: A callable that accepts the current and total download sizes
|
|
386
|
+
to display or log the download progress. It must support `reset(total)`
|
|
387
|
+
and be callable with downloaded chunk sizes.
|
|
388
|
+
:param ssl_verify: (optional) Whether to verify SSL certificates. Defaults to True.
|
|
389
|
+
:param auth: (optional) Authentication credentials (e.g., tuple or object) used for the
|
|
390
|
+
HTTP request if the resource requires authentication.
|
|
391
|
+
:raises HTTPError: If the HTTP request to the quicklook URL fails.
|
|
392
|
+
"""
|
|
393
|
+
with requests.get(
|
|
394
|
+
self.properties["quicklook"],
|
|
395
|
+
stream=True,
|
|
396
|
+
auth=auth,
|
|
397
|
+
headers=USER_AGENT,
|
|
398
|
+
timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
|
|
399
|
+
verify=ssl_verify,
|
|
400
|
+
) as stream:
|
|
401
|
+
stream.raise_for_status()
|
|
402
|
+
stream_size = int(stream.headers.get("content-length", 0))
|
|
403
|
+
progress_callback.reset(stream_size)
|
|
404
|
+
with open(quicklook_file, "wb") as fhandle:
|
|
405
|
+
for chunk in stream.iter_content(chunk_size=64 * 1024):
|
|
406
|
+
if chunk:
|
|
407
|
+
fhandle.write(chunk)
|
|
408
|
+
progress_callback(len(chunk))
|
|
409
|
+
logger.info("Download recorded in %s", quicklook_file)
|
|
410
|
+
|
|
372
411
|
def get_quicklook(
|
|
373
412
|
self,
|
|
374
413
|
filename: Optional[str] = None,
|
|
@@ -378,6 +417,9 @@ class EOProduct:
|
|
|
378
417
|
"""Download the quicklook image of a given EOProduct from its provider if it
|
|
379
418
|
exists.
|
|
380
419
|
|
|
420
|
+
This method retrieves the quicklook URL from the EOProduct metadata and delegates
|
|
421
|
+
the download to the internal `download_quicklook` method.
|
|
422
|
+
|
|
381
423
|
:param filename: (optional) The name to give to the downloaded quicklook. If not
|
|
382
424
|
given, it defaults to the product's ID (without file extension).
|
|
383
425
|
:param output_dir: (optional) The absolute path of the directory where to store
|
|
@@ -403,18 +445,6 @@ class EOProduct:
|
|
|
403
445
|
}
|
|
404
446
|
)
|
|
405
447
|
|
|
406
|
-
# progress bar init
|
|
407
|
-
if progress_callback is None:
|
|
408
|
-
progress_callback = ProgressCallback()
|
|
409
|
-
# one shot progress callback to close after download
|
|
410
|
-
close_progress_callback = True
|
|
411
|
-
else:
|
|
412
|
-
close_progress_callback = False
|
|
413
|
-
# update units as bar may have been previously used for extraction
|
|
414
|
-
progress_callback.unit = "B"
|
|
415
|
-
progress_callback.unit_scale = True
|
|
416
|
-
progress_callback.desc = "quicklooks/%s" % self.properties.get("id", "")
|
|
417
|
-
|
|
418
448
|
if self.properties.get("quicklook", None) is None:
|
|
419
449
|
logger.warning(
|
|
420
450
|
"Missing information to retrieve quicklook for EO product: %s",
|
|
@@ -442,6 +472,18 @@ class EOProduct:
|
|
|
442
472
|
)
|
|
443
473
|
|
|
444
474
|
if not os.path.isfile(quicklook_file):
|
|
475
|
+
|
|
476
|
+
# progress bar init
|
|
477
|
+
if progress_callback is None:
|
|
478
|
+
progress_callback = ProgressCallback()
|
|
479
|
+
# one shot progress callback to close after download
|
|
480
|
+
close_progress_callback = True
|
|
481
|
+
else:
|
|
482
|
+
close_progress_callback = False
|
|
483
|
+
# update units as bar may have been previously used for extraction
|
|
484
|
+
progress_callback.unit = "B"
|
|
485
|
+
progress_callback.unit_scale = True
|
|
486
|
+
progress_callback.desc = "quicklooks/%s" % self.properties.get("id", "")
|
|
445
487
|
# VERY SPECIAL CASE (introduced by the onda provider): first check if
|
|
446
488
|
# it is a HTTP URL. If not, we assume it is a base64 string, in which case
|
|
447
489
|
# we just decode the content, write it into the quicklook_file and return it.
|
|
@@ -468,30 +510,25 @@ class EOProduct:
|
|
|
468
510
|
if self.downloader
|
|
469
511
|
else True
|
|
470
512
|
)
|
|
471
|
-
|
|
472
|
-
self.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
timeout=DEFAULT_STREAM_REQUESTS_TIMEOUT,
|
|
477
|
-
verify=ssl_verify,
|
|
478
|
-
) as stream:
|
|
479
|
-
try:
|
|
480
|
-
stream.raise_for_status()
|
|
481
|
-
except RequestException as e:
|
|
482
|
-
import traceback as tb
|
|
513
|
+
try:
|
|
514
|
+
self._download_quicklook(
|
|
515
|
+
quicklook_file, progress_callback, ssl_verify, auth
|
|
516
|
+
)
|
|
517
|
+
except RequestException as e:
|
|
483
518
|
|
|
484
|
-
|
|
519
|
+
logger.debug(
|
|
520
|
+
f"Error while getting resource with authentication. {e} \nTrying without authentication..."
|
|
521
|
+
)
|
|
522
|
+
try:
|
|
523
|
+
self._download_quicklook(
|
|
524
|
+
quicklook_file, progress_callback, ssl_verify, None
|
|
525
|
+
)
|
|
526
|
+
except RequestException as e_no_auth:
|
|
527
|
+
logger.error(
|
|
528
|
+
f"Failed to get resource with authentication: {e} \n \
|
|
529
|
+
Failed to get resource even without authentication. {e_no_auth}"
|
|
530
|
+
)
|
|
485
531
|
return str(e)
|
|
486
|
-
else:
|
|
487
|
-
stream_size = int(stream.headers.get("content-length", 0))
|
|
488
|
-
progress_callback.reset(stream_size)
|
|
489
|
-
with open(quicklook_file, "wb") as fhandle:
|
|
490
|
-
for chunk in stream.iter_content(chunk_size=64 * 1024):
|
|
491
|
-
if chunk:
|
|
492
|
-
fhandle.write(chunk)
|
|
493
|
-
progress_callback(len(chunk))
|
|
494
|
-
logger.info("Download recorded in %s", quicklook_file)
|
|
495
532
|
|
|
496
533
|
# close progress bar if needed
|
|
497
534
|
if close_progress_callback:
|
eodag/api/search_result.py
CHANGED
|
@@ -36,7 +36,7 @@ if TYPE_CHECKING:
|
|
|
36
36
|
from eodag.plugins.crunch.base import Crunch
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
class SearchResult(UserList):
|
|
39
|
+
class SearchResult(UserList[EOProduct]):
|
|
40
40
|
"""An object representing a collection of :class:`~eodag.api.product._product.EOProduct` resulting from a search.
|
|
41
41
|
|
|
42
42
|
:param products: A list of products resulting from a search
|
|
@@ -46,8 +46,6 @@ class SearchResult(UserList):
|
|
|
46
46
|
:ivar number_matched: Estimated total number of matching results
|
|
47
47
|
"""
|
|
48
48
|
|
|
49
|
-
data: list[EOProduct]
|
|
50
|
-
|
|
51
49
|
errors: Annotated[
|
|
52
50
|
list[tuple[str, Exception]], Doc("Tuple of provider name, exception")
|
|
53
51
|
]
|
|
@@ -56,11 +54,11 @@ class SearchResult(UserList):
|
|
|
56
54
|
self,
|
|
57
55
|
products: list[EOProduct],
|
|
58
56
|
number_matched: Optional[int] = None,
|
|
59
|
-
errors: list[tuple[str, Exception]] =
|
|
57
|
+
errors: Optional[list[tuple[str, Exception]]] = None,
|
|
60
58
|
) -> None:
|
|
61
59
|
super().__init__(products)
|
|
62
60
|
self.number_matched = number_matched
|
|
63
|
-
self.errors = errors
|
|
61
|
+
self.errors = errors if errors is not None else []
|
|
64
62
|
|
|
65
63
|
def crunch(self, cruncher: Crunch, **search_params: Any) -> SearchResult:
|
|
66
64
|
"""Do some crunching with the underlying EO products.
|
|
@@ -193,8 +191,8 @@ class SearchResult(UserList):
|
|
|
193
191
|
<details><summary style='color: grey; font-family: monospace;'>
|
|
194
192
|
{i} 
|
|
195
193
|
{type(p).__name__}(id=<span style='color: black;'>{
|
|
196
|
-
|
|
197
|
-
|
|
194
|
+
p.properties["id"]
|
|
195
|
+
}</span>, provider={p.provider})
|
|
198
196
|
</summary>
|
|
199
197
|
{p._repr_html_()}
|
|
200
198
|
</details>
|
eodag/config.py
CHANGED
|
@@ -618,6 +618,9 @@ class PluginConfig(yaml.YAMLObject):
|
|
|
618
618
|
#: :class:`~eodag.plugins.authentication.token.TokenAuth`
|
|
619
619
|
#: key to get the expiration time of the token
|
|
620
620
|
token_expiration_key: str
|
|
621
|
+
#: :class:`~eodag.plugins.authentication.token.TokenAuth`
|
|
622
|
+
#: HTTP method to use
|
|
623
|
+
request_method: str
|
|
621
624
|
#: :class:`~eodag.plugins.authentication.token_exchange.OIDCTokenExchangeAuth`
|
|
622
625
|
#: The full :class:`~eodag.plugins.authentication.openid_connect.OIDCAuthorizationCodeFlowAuth` plugin configuration
|
|
623
626
|
#: used to retrieve subject token
|
|
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
from datetime import datetime, timedelta
|
|
22
|
+
from threading import Lock
|
|
22
23
|
from typing import TYPE_CHECKING, Any, Optional
|
|
23
24
|
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
|
24
25
|
|
|
@@ -77,6 +78,7 @@ class TokenAuth(Authentication):
|
|
|
77
78
|
the token (given in s)
|
|
78
79
|
* :attr:`~eodag.config.PluginConfig.ssl_verify` (``bool``): if the ssl certificates
|
|
79
80
|
should be verified in the requests; default: ``True``
|
|
81
|
+
* :attr:`~eodag.config.PluginConfig.request_method` (``str``): HTTP method to use; default: ``POST``
|
|
80
82
|
* :attr:`~eodag.config.PluginConfig.auth_error_code` (``int``): which error code is
|
|
81
83
|
returned in case of an authentication error
|
|
82
84
|
* :attr:`~eodag.config.PluginConfig.req_data` (``dict[str, Any]``): if the credentials
|
|
@@ -95,6 +97,19 @@ class TokenAuth(Authentication):
|
|
|
95
97
|
self.token = ""
|
|
96
98
|
self.refresh_token = ""
|
|
97
99
|
self.token_expiration = datetime.now()
|
|
100
|
+
self.auth_lock = Lock()
|
|
101
|
+
|
|
102
|
+
def __getstate__(self):
|
|
103
|
+
"""Exclude attributes that can't be pickled from serialization."""
|
|
104
|
+
state = dict(self.__dict__)
|
|
105
|
+
del state["auth_lock"]
|
|
106
|
+
return state
|
|
107
|
+
|
|
108
|
+
def __setstate__(self, state):
|
|
109
|
+
"""Exclude attributes that can't be pickled from deserialization."""
|
|
110
|
+
self.__dict__.update(state)
|
|
111
|
+
# Init them manually
|
|
112
|
+
self.auth_lock = Lock()
|
|
98
113
|
|
|
99
114
|
def validate_config_credentials(self) -> None:
|
|
100
115
|
"""Validate configured credentials"""
|
|
@@ -134,61 +149,67 @@ class TokenAuth(Authentication):
|
|
|
134
149
|
|
|
135
150
|
def authenticate(self) -> AuthBase:
|
|
136
151
|
"""Authenticate"""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
152
|
+
|
|
153
|
+
# Use a thread lock to avoid several threads requesting the token at the same time
|
|
154
|
+
with self.auth_lock:
|
|
155
|
+
|
|
156
|
+
self.validate_config_credentials()
|
|
157
|
+
if self.token and self.token_expiration > datetime.now():
|
|
158
|
+
logger.debug("using existing access token")
|
|
159
|
+
return RequestsTokenAuth(
|
|
160
|
+
self.token, "header", headers=getattr(self.config, "headers", {})
|
|
161
|
+
)
|
|
162
|
+
s = requests.Session()
|
|
163
|
+
try:
|
|
164
|
+
# First get the token
|
|
165
|
+
response = self._token_request(session=s)
|
|
166
|
+
response.raise_for_status()
|
|
167
|
+
except requests.exceptions.Timeout as exc:
|
|
168
|
+
raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
|
|
169
|
+
except RequestException as e:
|
|
170
|
+
response_text = getattr(e.response, "text", "").strip()
|
|
171
|
+
# check if error is identified as auth_error in provider conf
|
|
172
|
+
auth_errors = getattr(self.config, "auth_error_code", [None])
|
|
173
|
+
if not isinstance(auth_errors, list):
|
|
174
|
+
auth_errors = [auth_errors]
|
|
175
|
+
if (
|
|
176
|
+
e.response is not None
|
|
177
|
+
and getattr(e.response, "status_code", None)
|
|
178
|
+
and e.response.status_code in auth_errors
|
|
179
|
+
):
|
|
180
|
+
raise AuthenticationError(
|
|
181
|
+
f"Please check your credentials for {self.provider}.",
|
|
182
|
+
f"HTTP Error {e.response.status_code} returned.",
|
|
183
|
+
response_text,
|
|
184
|
+
) from e
|
|
185
|
+
# other error
|
|
186
|
+
else:
|
|
187
|
+
raise AuthenticationError(
|
|
188
|
+
"Could no get authentication token", str(e), response_text
|
|
189
|
+
) from e
|
|
174
190
|
else:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
if getattr(self.config, "token_type", "text") == "json":
|
|
192
|
+
token = response.json()[self.config.token_key]
|
|
193
|
+
else:
|
|
194
|
+
token = response.text
|
|
195
|
+
self.token = token
|
|
196
|
+
if getattr(self.config, "refresh_token_key", None):
|
|
197
|
+
self.refresh_token = response.json()[self.config.refresh_token_key]
|
|
198
|
+
if getattr(self.config, "token_expiration_key", None):
|
|
199
|
+
expiration_time = response.json()[self.config.token_expiration_key]
|
|
200
|
+
self.token_expiration = datetime.now() + timedelta(
|
|
201
|
+
seconds=expiration_time
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if not hasattr(self.config, "headers"):
|
|
205
|
+
raise MisconfiguredError(
|
|
206
|
+
f"Missing headers configuration for {self}"
|
|
207
|
+
)
|
|
208
|
+
# Return auth class set with obtained token
|
|
209
|
+
return RequestsTokenAuth(
|
|
210
|
+
token, "header", headers=getattr(self.config, "headers", {})
|
|
183
211
|
)
|
|
184
212
|
|
|
185
|
-
if not hasattr(self.config, "headers"):
|
|
186
|
-
raise MisconfiguredError(f"Missing headers configuration for {self}")
|
|
187
|
-
# Return auth class set with obtained token
|
|
188
|
-
return RequestsTokenAuth(
|
|
189
|
-
token, "header", headers=getattr(self.config, "headers", {})
|
|
190
|
-
)
|
|
191
|
-
|
|
192
213
|
def _token_request(
|
|
193
214
|
self,
|
|
194
215
|
session: requests.Session,
|
|
@@ -217,14 +238,36 @@ class TokenAuth(Authentication):
|
|
|
217
238
|
# append headers to req if some are specified in config
|
|
218
239
|
req_kwargs: dict[str, Any] = {"headers": dict(headers, **USER_AGENT)}
|
|
219
240
|
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
241
|
+
method = getattr(self.config, "request_method", "POST")
|
|
242
|
+
|
|
243
|
+
def set_request_data(call_refresh: bool) -> None:
|
|
244
|
+
"""Set the request data contents for POST requests"""
|
|
245
|
+
if method != "POST":
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
# append req_data to credentials if specified in config
|
|
249
|
+
data = dict(getattr(self.config, "req_data", {}), **self.config.credentials)
|
|
250
|
+
|
|
251
|
+
# when refreshing the token, we pass only the client_id/secret if present,
|
|
252
|
+
# not other parameters (username/password, scope, ...)
|
|
253
|
+
if call_refresh:
|
|
254
|
+
data = {
|
|
255
|
+
k: v for k, v in data.items() if k in ["client_id", "client_secret"]
|
|
256
|
+
}
|
|
257
|
+
# the grant type is always refresh_token
|
|
258
|
+
data["grant_type"] = "refresh_token"
|
|
259
|
+
# and we add the old refresh token value to the request
|
|
260
|
+
data["refresh_token"] = self.refresh_token
|
|
261
|
+
|
|
262
|
+
req_kwargs["data"] = data
|
|
220
263
|
|
|
221
264
|
if self.refresh_token:
|
|
222
265
|
logger.debug("fetching access token with refresh token")
|
|
223
266
|
session.mount(self.config.refresh_uri, HTTPAdapter(max_retries=retries))
|
|
267
|
+
set_request_data(call_refresh=True)
|
|
224
268
|
try:
|
|
225
269
|
response = session.post(
|
|
226
270
|
self.config.refresh_uri,
|
|
227
|
-
data={"refresh_token": self.refresh_token},
|
|
228
271
|
timeout=HTTP_REQ_TIMEOUT,
|
|
229
272
|
verify=ssl_verify,
|
|
230
273
|
**req_kwargs,
|
|
@@ -237,14 +280,8 @@ class TokenAuth(Authentication):
|
|
|
237
280
|
logger.debug("fetching access token from %s", self.config.auth_uri)
|
|
238
281
|
# append headers to req if some are specified in config
|
|
239
282
|
session.mount(self.config.auth_uri, HTTPAdapter(max_retries=retries))
|
|
240
|
-
method = getattr(self.config, "request_method", "POST")
|
|
241
283
|
|
|
242
|
-
|
|
243
|
-
if method == "POST":
|
|
244
|
-
# append req_data to credentials if specified in config
|
|
245
|
-
req_kwargs["data"] = dict(
|
|
246
|
-
getattr(self.config, "req_data", {}), **self.config.credentials
|
|
247
|
-
)
|
|
284
|
+
set_request_data(call_refresh=False)
|
|
248
285
|
|
|
249
286
|
# credentials as auth tuple if possible
|
|
250
287
|
req_kwargs["auth"] = (
|
eodag/plugins/search/base.py
CHANGED
|
@@ -337,7 +337,9 @@ class Search(PluginTopic):
|
|
|
337
337
|
try:
|
|
338
338
|
filters["productType"] = product_type
|
|
339
339
|
queryables = self.discover_queryables(**{**default_values, **filters}) or {}
|
|
340
|
-
except NotImplementedError:
|
|
340
|
+
except NotImplementedError as e:
|
|
341
|
+
if str(e):
|
|
342
|
+
logger.debug(str(e))
|
|
341
343
|
queryables = self.queryables_from_metadata_mapping(product_type, alias)
|
|
342
344
|
|
|
343
345
|
return QueryablesDict(**queryables)
|
|
@@ -381,12 +383,12 @@ class Search(PluginTopic):
|
|
|
381
383
|
for pt in available_product_types:
|
|
382
384
|
self.config.product_type_config = product_type_configs[pt]
|
|
383
385
|
pt_queryables = self._get_product_type_queryables(pt, None, filters)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
386
|
+
all_queryables.update(pt_queryables)
|
|
387
|
+
# reset defaults because they may vary between product types
|
|
388
|
+
for k, v in all_queryables.items():
|
|
389
|
+
v.__metadata__[0].default = getattr(
|
|
390
|
+
Queryables.model_fields.get(k, Field(None)), "default", None
|
|
391
|
+
)
|
|
390
392
|
return QueryablesDict(
|
|
391
393
|
additional_properties=True,
|
|
392
394
|
additional_information=additional_info,
|
eodag/plugins/search/qssearch.py
CHANGED
|
@@ -1810,8 +1810,7 @@ class StacSearch(PostJsonSearch):
|
|
|
1810
1810
|
not self.config.discover_queryables["fetch_url"]
|
|
1811
1811
|
and not self.config.discover_queryables["product_type_fetch_url"]
|
|
1812
1812
|
):
|
|
1813
|
-
|
|
1814
|
-
return None
|
|
1813
|
+
raise NotImplementedError()
|
|
1815
1814
|
|
|
1816
1815
|
product_type = kwargs.get("productType", None)
|
|
1817
1816
|
provider_product_type = (
|
|
@@ -1823,18 +1822,16 @@ class StacSearch(PostJsonSearch):
|
|
|
1823
1822
|
provider_product_type
|
|
1824
1823
|
and not self.config.discover_queryables["product_type_fetch_url"]
|
|
1825
1824
|
):
|
|
1826
|
-
|
|
1825
|
+
raise NotImplementedError(
|
|
1827
1826
|
f"Cannot fetch queryables for a specific product type with {self.provider}"
|
|
1828
1827
|
)
|
|
1829
|
-
return None
|
|
1830
1828
|
if (
|
|
1831
1829
|
not provider_product_type
|
|
1832
1830
|
and not self.config.discover_queryables["fetch_url"]
|
|
1833
1831
|
):
|
|
1834
|
-
|
|
1835
|
-
f"Cannot fetch global queryables
|
|
1832
|
+
raise ValidationError(
|
|
1833
|
+
f"Cannot fetch global queryables for {self.provider}. A product type must be specified"
|
|
1836
1834
|
)
|
|
1837
|
-
return None
|
|
1838
1835
|
|
|
1839
1836
|
try:
|
|
1840
1837
|
unparsed_fetch_url = (
|
|
@@ -1843,7 +1840,9 @@ class StacSearch(PostJsonSearch):
|
|
|
1843
1840
|
else self.config.discover_queryables["fetch_url"]
|
|
1844
1841
|
)
|
|
1845
1842
|
if unparsed_fetch_url is None:
|
|
1846
|
-
|
|
1843
|
+
raise PluginImplementationError(
|
|
1844
|
+
f"Cannot fetch queryables for {self.provider}: missing url"
|
|
1845
|
+
)
|
|
1847
1846
|
|
|
1848
1847
|
fetch_url = unparsed_fetch_url.format(
|
|
1849
1848
|
provider_product_type=provider_product_type,
|
|
@@ -1864,9 +1863,12 @@ class StacSearch(PostJsonSearch):
|
|
|
1864
1863
|
"{} {} instance:".format(self.provider, self.__class__.__name__),
|
|
1865
1864
|
),
|
|
1866
1865
|
)
|
|
1867
|
-
except (
|
|
1868
|
-
|
|
1869
|
-
|
|
1866
|
+
except (KeyError, AttributeError) as e:
|
|
1867
|
+
raise PluginImplementationError(
|
|
1868
|
+
"failure in queryables discovery: %s", e
|
|
1869
|
+
) from e
|
|
1870
|
+
except RequestError as e:
|
|
1871
|
+
raise RequestError("failure in queryables discovery: %s", e) from e
|
|
1870
1872
|
else:
|
|
1871
1873
|
json_queryables = dict()
|
|
1872
1874
|
try:
|
|
@@ -1875,17 +1877,16 @@ class StacSearch(PostJsonSearch):
|
|
|
1875
1877
|
# extract results from response json
|
|
1876
1878
|
results_entry = self.config.discover_queryables["results_entry"]
|
|
1877
1879
|
if not isinstance(results_entry, JSONPath):
|
|
1878
|
-
|
|
1880
|
+
raise MisconfiguredError(
|
|
1879
1881
|
f"Could not parse {self.provider} discover_queryables.results_entry"
|
|
1880
1882
|
f" as JSONPath: {results_entry}"
|
|
1881
1883
|
)
|
|
1882
|
-
return None
|
|
1883
1884
|
json_queryables = [
|
|
1884
1885
|
match.value for match in results_entry.find(resp_as_json)
|
|
1885
1886
|
][0]
|
|
1886
1887
|
|
|
1887
1888
|
except KeyError as e:
|
|
1888
|
-
|
|
1889
|
+
raise MisconfiguredError(
|
|
1889
1890
|
"Incomplete %s discover_queryables configuration: %s",
|
|
1890
1891
|
self.provider,
|
|
1891
1892
|
e,
|