eodag 2.12.1__py3-none-any.whl → 3.0.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.
Files changed (77) hide show
  1. eodag/api/core.py +434 -319
  2. eodag/api/product/__init__.py +5 -1
  3. eodag/api/product/_assets.py +7 -2
  4. eodag/api/product/_product.py +46 -68
  5. eodag/api/product/metadata_mapping.py +181 -66
  6. eodag/api/search_result.py +21 -1
  7. eodag/cli.py +20 -6
  8. eodag/config.py +95 -6
  9. eodag/plugins/apis/base.py +8 -165
  10. eodag/plugins/apis/ecmwf.py +36 -24
  11. eodag/plugins/apis/usgs.py +40 -24
  12. eodag/plugins/authentication/aws_auth.py +2 -2
  13. eodag/plugins/authentication/header.py +31 -6
  14. eodag/plugins/authentication/keycloak.py +13 -84
  15. eodag/plugins/authentication/oauth.py +3 -3
  16. eodag/plugins/authentication/openid_connect.py +256 -46
  17. eodag/plugins/authentication/qsauth.py +3 -0
  18. eodag/plugins/authentication/sas_auth.py +8 -1
  19. eodag/plugins/authentication/token.py +92 -46
  20. eodag/plugins/authentication/token_exchange.py +120 -0
  21. eodag/plugins/download/aws.py +86 -91
  22. eodag/plugins/download/base.py +72 -40
  23. eodag/plugins/download/http.py +607 -264
  24. eodag/plugins/download/s3rest.py +28 -15
  25. eodag/plugins/manager.py +73 -57
  26. eodag/plugins/search/__init__.py +36 -0
  27. eodag/plugins/search/base.py +225 -18
  28. eodag/plugins/search/build_search_result.py +389 -32
  29. eodag/plugins/search/cop_marine.py +378 -0
  30. eodag/plugins/search/creodias_s3.py +15 -14
  31. eodag/plugins/search/csw.py +5 -7
  32. eodag/plugins/search/data_request_search.py +44 -20
  33. eodag/plugins/search/qssearch.py +508 -203
  34. eodag/plugins/search/static_stac_search.py +99 -36
  35. eodag/resources/constraints/climate-dt.json +13 -0
  36. eodag/resources/constraints/extremes-dt.json +8 -0
  37. eodag/resources/ext_product_types.json +1 -1
  38. eodag/resources/product_types.yml +1897 -34
  39. eodag/resources/providers.yml +3539 -3277
  40. eodag/resources/stac.yml +48 -54
  41. eodag/resources/stac_api.yml +71 -25
  42. eodag/resources/stac_provider.yml +5 -0
  43. eodag/resources/user_conf_template.yml +51 -3
  44. eodag/rest/__init__.py +6 -0
  45. eodag/rest/cache.py +70 -0
  46. eodag/rest/config.py +68 -0
  47. eodag/rest/constants.py +27 -0
  48. eodag/rest/core.py +757 -0
  49. eodag/rest/server.py +397 -258
  50. eodag/rest/stac.py +438 -307
  51. eodag/rest/types/collections_search.py +44 -0
  52. eodag/rest/types/eodag_search.py +232 -43
  53. eodag/rest/types/{stac_queryables.py → queryables.py} +81 -43
  54. eodag/rest/types/stac_search.py +277 -0
  55. eodag/rest/utils/__init__.py +216 -0
  56. eodag/rest/utils/cql_evaluate.py +119 -0
  57. eodag/rest/utils/rfc3339.py +65 -0
  58. eodag/types/__init__.py +99 -9
  59. eodag/types/bbox.py +15 -14
  60. eodag/types/download_args.py +31 -0
  61. eodag/types/search_args.py +58 -7
  62. eodag/types/whoosh.py +81 -0
  63. eodag/utils/__init__.py +72 -9
  64. eodag/utils/constraints.py +37 -37
  65. eodag/utils/exceptions.py +23 -17
  66. eodag/utils/requests.py +138 -0
  67. eodag/utils/rest.py +104 -0
  68. eodag/utils/stac_reader.py +100 -16
  69. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/METADATA +64 -44
  70. eodag-3.0.0b1.dist-info/RECORD +109 -0
  71. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/WHEEL +1 -1
  72. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/entry_points.txt +6 -5
  73. eodag/plugins/apis/cds.py +0 -540
  74. eodag/rest/utils.py +0 -1133
  75. eodag-2.12.1.dist-info/RECORD +0 -94
  76. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/LICENSE +0 -0
  77. {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/top_level.txt +0 -0
@@ -18,10 +18,11 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import logging
21
+ import os
21
22
  import shutil
22
23
  import tarfile
23
24
  import zipfile
24
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
25
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
25
26
 
26
27
  import requests
27
28
  from jsonpath_ng.ext import parse
@@ -35,7 +36,7 @@ from eodag.api.product.metadata_mapping import (
35
36
  properties_from_json,
36
37
  )
37
38
  from eodag.plugins.apis.base import Api
38
- from eodag.plugins.download.base import Download
39
+ from eodag.plugins.search import PreparedSearch
39
40
  from eodag.utils import (
40
41
  DEFAULT_DOWNLOAD_TIMEOUT,
41
42
  DEFAULT_DOWNLOAD_WAIT,
@@ -52,17 +53,21 @@ from eodag.utils.exceptions import (
52
53
  NoMatchingProductType,
53
54
  NotAvailableError,
54
55
  RequestError,
56
+ ValidationError,
55
57
  )
56
58
 
57
59
  if TYPE_CHECKING:
60
+ from requests.auth import AuthBase
61
+
58
62
  from eodag.api.search_result import SearchResult
59
63
  from eodag.config import PluginConfig
60
- from eodag.utils import DownloadedCallback
64
+ from eodag.types.download_args import DownloadConf
65
+ from eodag.utils import DownloadedCallback, Unpack
61
66
 
62
67
  logger = logging.getLogger("eodag.apis.usgs")
63
68
 
64
69
 
65
- class UsgsApi(Download, Api):
70
+ class UsgsApi(Api):
66
71
  """A plugin that enables to query and download data on the USGS catalogues"""
67
72
 
68
73
  def __init__(self, provider: str, config: PluginConfig) -> None:
@@ -71,7 +76,7 @@ class UsgsApi(Download, Api):
71
76
  # Same method as in base.py, Search.__init__()
72
77
  # Prepare the metadata mapping
73
78
  # Do a shallow copy, the structure is flat enough for this to be sufficient
74
- metas = DEFAULT_METADATA_MAPPING.copy()
79
+ metas: Dict[str, Any] = DEFAULT_METADATA_MAPPING.copy()
75
80
  # Update the defaults with the mapping value. This will add any new key
76
81
  # added by the provider mapping that is not in the default metadata.
77
82
  metas.update(self.config.metadata_mapping)
@@ -98,25 +103,33 @@ class UsgsApi(Download, Api):
98
103
  except USGSAuthExpiredError:
99
104
  api.logout()
100
105
  continue
101
- except USGSError:
102
- raise AuthenticationError(
103
- "Please check your USGS credentials."
104
- ) from None
106
+ except USGSError as e:
107
+ if i == 0:
108
+ # `.usgs` API file key might be obsolete
109
+ # Remove it and try again
110
+ os.remove(api.TMPFILE)
111
+ continue
112
+ raise AuthenticationError("Please check your USGS credentials.") from e
105
113
 
106
114
  def query(
107
115
  self,
108
- product_type: Optional[str] = None,
109
- items_per_page: int = DEFAULT_ITEMS_PER_PAGE,
110
- page: int = DEFAULT_PAGE,
111
- count: bool = True,
116
+ prep: PreparedSearch = PreparedSearch(),
112
117
  **kwargs: Any,
113
118
  ) -> Tuple[List[EOProduct], Optional[int]]:
114
119
  """Search for data on USGS catalogues"""
120
+ page = prep.page if prep.page is not None else DEFAULT_PAGE
121
+ items_per_page = (
122
+ prep.items_per_page
123
+ if prep.items_per_page is not None
124
+ else DEFAULT_ITEMS_PER_PAGE
125
+ )
115
126
  product_type = kwargs.get("productType")
116
127
  if product_type is None:
117
128
  raise NoMatchingProductType(
118
129
  "Cannot search on USGS without productType specified"
119
130
  )
131
+ if kwargs.get("sortBy"):
132
+ raise ValidationError("USGS does not support sorting feature")
120
133
 
121
134
  self.authenticate()
122
135
 
@@ -230,11 +243,11 @@ class UsgsApi(Download, Api):
230
243
  def download(
231
244
  self,
232
245
  product: EOProduct,
233
- auth: Optional[PluginConfig] = None,
246
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
234
247
  progress_callback: Optional[ProgressCallback] = None,
235
248
  wait: int = DEFAULT_DOWNLOAD_WAIT,
236
249
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
237
- **kwargs: Any,
250
+ **kwargs: Unpack[DownloadConf],
238
251
  ) -> Optional[str]:
239
252
  """Download data from USGS catalogues"""
240
253
 
@@ -250,11 +263,11 @@ class UsgsApi(Download, Api):
250
263
  product.product_type, self.config.products[GENERIC_PRODUCT_TYPE] # type: ignore
251
264
  ).get("outputs_extension", ".tar.gz"),
252
265
  )
266
+ kwargs["outputs_extension"] = kwargs.get("outputs_extension", outputs_extension)
253
267
 
254
268
  fs_path, record_filename = self._prepare_download(
255
269
  product,
256
270
  progress_callback=progress_callback,
257
- outputs_extension=outputs_extension,
258
271
  **kwargs,
259
272
  )
260
273
  if not fs_path or not record_filename:
@@ -308,13 +321,14 @@ class UsgsApi(Download, Api):
308
321
  req_url = req_urls[0]
309
322
  progress_callback.reset()
310
323
  logger.debug(f"Downloading {req_url}")
324
+ ssl_verify = getattr(self.config, "ssl_verify", True)
311
325
 
312
326
  @self._download_retry(product, wait, timeout)
313
327
  def download_request(
314
328
  product: EOProduct,
315
329
  fs_path: str,
316
330
  progress_callback: ProgressCallback,
317
- **kwargs: Any,
331
+ **kwargs: Unpack[DownloadConf],
318
332
  ) -> None:
319
333
  try:
320
334
  with requests.get(
@@ -322,6 +336,7 @@ class UsgsApi(Download, Api):
322
336
  stream=True,
323
337
  headers=USER_AGENT,
324
338
  timeout=wait * 60,
339
+ verify=ssl_verify,
325
340
  ) as stream:
326
341
  try:
327
342
  stream.raise_for_status()
@@ -334,7 +349,9 @@ class UsgsApi(Download, Api):
334
349
  error_message = str(e)
335
350
  raise NotAvailableError(error_message)
336
351
  else:
337
- stream_size = int(stream.headers.get("content-length", 0))
352
+ stream_size = (
353
+ int(stream.headers.get("content-length", 0)) or None
354
+ )
338
355
  progress_callback.reset(total=stream_size)
339
356
  with open(fs_path, "wb") as fhandle:
340
357
  for chunk in stream.iter_content(chunk_size=64 * 1024):
@@ -357,13 +374,12 @@ class UsgsApi(Download, Api):
357
374
  api.logout()
358
375
 
359
376
  # Check downloaded file format
360
- if (outputs_extension == ".tar.gz" and tarfile.is_tarfile(fs_path)) or (
361
- outputs_extension == ".zip" and zipfile.is_zipfile(fs_path)
362
- ):
377
+ if (
378
+ kwargs["outputs_extension"] == ".tar.gz" and tarfile.is_tarfile(fs_path)
379
+ ) or (kwargs["outputs_extension"] == ".zip" and zipfile.is_zipfile(fs_path)):
363
380
  product_path = self._finalize(
364
381
  fs_path,
365
382
  progress_callback=progress_callback,
366
- outputs_extension=outputs_extension,
367
383
  **kwargs,
368
384
  )
369
385
  product.location = path_to_uri(product_path)
@@ -396,12 +412,12 @@ class UsgsApi(Download, Api):
396
412
  def download_all(
397
413
  self,
398
414
  products: SearchResult,
399
- auth: Optional[PluginConfig] = None,
415
+ auth: Optional[Union[AuthBase, Dict[str, str]]] = None,
400
416
  downloaded_callback: Optional[DownloadedCallback] = None,
401
417
  progress_callback: Optional[ProgressCallback] = None,
402
418
  wait: int = DEFAULT_DOWNLOAD_WAIT,
403
419
  timeout: int = DEFAULT_DOWNLOAD_TIMEOUT,
404
- **kwargs: Any,
420
+ **kwargs: Unpack[DownloadConf],
405
421
  ) -> List[str]:
406
422
  """
407
423
  Download all using parent (base plugin) method
@@ -22,7 +22,7 @@ from typing import TYPE_CHECKING, Dict
22
22
  from eodag.plugins.authentication.base import Authentication
23
23
 
24
24
  if TYPE_CHECKING:
25
- from botocore.client import S3
25
+ from mypy_boto3_s3.client import S3Client
26
26
 
27
27
  from eodag.config import PluginConfig
28
28
 
@@ -39,7 +39,7 @@ class AwsAuth(Authentication):
39
39
  will be skipped if AWS credentials are filled in eodag conf
40
40
  """
41
41
 
42
- s3_client: S3
42
+ s3_client: S3Client
43
43
 
44
44
  def __init__(self, provider: str, config: PluginConfig) -> None:
45
45
  super(AwsAuth, self).__init__(provider, config)
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING, Dict
22
22
  from requests.auth import AuthBase
23
23
 
24
24
  from eodag.plugins.authentication import Authentication
25
+ from eodag.utils.exceptions import MisconfiguredError
25
26
 
26
27
  if TYPE_CHECKING:
27
28
  from requests import PreparedRequest
@@ -58,16 +59,40 @@ class HTTPHeaderAuth(Authentication):
58
59
  oh-my-another-user-input: YYY
59
60
 
60
61
  Expect an undefined behaviour if you use empty braces in header value strings.
62
+
63
+ The plugin also accepts headers to be passed directly through credentials::
64
+
65
+ provider:
66
+ ...
67
+ auth:
68
+ plugin: HTTPHeaderAuth
69
+ credentials:
70
+ Authorization: "Something XXX"
71
+ X-Special-Header: "Fixed value"
72
+ X-Another-Special-Header: "YYY"
73
+ ...
74
+ ...
61
75
  """
62
76
 
63
- def authenticate(self) -> AuthBase:
77
+ def authenticate(self) -> HeaderAuth:
64
78
  """Authenticate"""
65
79
  self.validate_config_credentials()
66
- headers = {
67
- header: value.format(**self.config.credentials)
68
- for header, value in self.config.headers.items()
69
- }
70
- return HeaderAuth(headers)
80
+ try:
81
+ headers = (
82
+ {
83
+ header: value.format(**self.config.credentials)
84
+ for header, value in self.config.headers.items()
85
+ }
86
+ if getattr(self.config, "headers", None)
87
+ else self.config.credentials
88
+ )
89
+ return HeaderAuth(headers)
90
+ except KeyError as e:
91
+ raise MisconfiguredError(
92
+ "The following credentials are missing for provider {}: {}".format(
93
+ self.provider, ", ".join(e.args)
94
+ )
95
+ )
71
96
 
72
97
 
73
98
  class HeaderAuth(AuthBase):
@@ -18,15 +18,16 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import logging
21
- from datetime import datetime
22
- from typing import TYPE_CHECKING, Dict, Union
21
+ from typing import TYPE_CHECKING, Any, Dict
23
22
 
24
23
  import requests
25
24
 
26
- from eodag.plugins.authentication import Authentication
27
- from eodag.plugins.authentication.openid_connect import CodeAuthorizedAuth
25
+ from eodag.plugins.authentication.openid_connect import (
26
+ CodeAuthorizedAuth,
27
+ OIDCRefreshTokenBase,
28
+ )
28
29
  from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT
29
- from eodag.utils.exceptions import AuthenticationError, MisconfiguredError
30
+ from eodag.utils.exceptions import MisconfiguredError, TimeOutError
30
31
 
31
32
  if TYPE_CHECKING:
32
33
  from requests.auth import AuthBase
@@ -37,7 +38,7 @@ if TYPE_CHECKING:
37
38
  logger = logging.getLogger("eodag.auth.keycloak")
38
39
 
39
40
 
40
- class KeycloakOIDCPasswordAuth(Authentication):
41
+ class KeycloakOIDCPasswordAuth(OIDCRefreshTokenBase):
41
42
  """Authentication plugin using Keycloak and OpenId Connect.
42
43
 
43
44
  This plugin request a token and use it through a query-string or a header.
@@ -80,13 +81,9 @@ class KeycloakOIDCPasswordAuth(Authentication):
80
81
  GRANT_TYPE = "password"
81
82
  TOKEN_URL_TEMPLATE = "{auth_base_uri}/realms/{realm}/protocol/openid-connect/token"
82
83
  REQUIRED_PARAMS = ["auth_base_uri", "client_id", "client_secret", "token_provision"]
83
- # already retrieved token store, to be used if authenticate() fails (OTP use-case)
84
- retrieved_token: str = ""
85
- token_info: Dict[str, Union[str, datetime]] = {}
86
84
 
87
85
  def __init__(self, provider: str, config: PluginConfig) -> None:
88
86
  super(KeycloakOIDCPasswordAuth, self).__init__(provider, config)
89
- self.session = requests.Session()
90
87
 
91
88
  def validate_config_credentials(self) -> None:
92
89
  """Validate configured credentials"""
@@ -105,51 +102,14 @@ class KeycloakOIDCPasswordAuth(Authentication):
105
102
  """
106
103
  self.validate_config_credentials()
107
104
  access_token = self._get_access_token()
108
- self.retrieved_token = access_token
105
+ self.token_info["access_token"] = access_token
109
106
  return CodeAuthorizedAuth(
110
- self.retrieved_token,
107
+ self.token_info["access_token"],
111
108
  self.config.token_provision,
112
109
  key=getattr(self.config, "token_qs_key", None),
113
110
  )
114
111
 
115
- def _get_access_token(self) -> str:
116
- current_time = datetime.now()
117
- if (
118
- not self.token_info
119
- or (
120
- "refresh_token" in self.token_info
121
- and (current_time - self.token_info["token_time"]).seconds
122
- >= self.token_info["refresh_token_expiration"]
123
- )
124
- or (
125
- "refresh_token" not in self.token_info
126
- and (current_time - self.token_info["token_time"]).seconds
127
- >= self.token_info["access_token_expiration"]
128
- )
129
- ):
130
- # Request new TOKEN on first attempt or if token expired
131
- res = self._request_new_token()
132
- self.token_info["token_time"] = current_time
133
- self.token_info["access_token_expiration"] = res["expires_in"]
134
- if "refresh_token" in res:
135
- self.token_info["refresh_time"] = current_time
136
- self.token_info["refresh_token_expiration"] = res["refresh_expires_in"]
137
- self.token_info["refresh_token"] = res["refresh_token"]
138
- return res["access_token"]
139
- elif (
140
- "refresh_token" in self.token_info
141
- and (current_time - self.token_info["refresh_time"]).seconds
142
- >= self.token_info["access_token_expiration"]
143
- ):
144
- # Use refresh token
145
- res = self._get_token_with_refresh_token()
146
- self.token_info["refresh_token"] = res["refresh_token"]
147
- self.token_info["refresh_time"] = current_time
148
- return res["access_token"]
149
- logger.debug("using already retrieved access token")
150
- return self.retrieved_token
151
-
152
- def _request_new_token(self) -> Dict[str, str]:
112
+ def _request_new_token(self) -> Dict[str, Any]:
153
113
  logger.debug("fetching new access token")
154
114
  req_data = {
155
115
  "client_id": self.config.client_id,
@@ -168,41 +128,10 @@ class KeycloakOIDCPasswordAuth(Authentication):
168
128
  timeout=HTTP_REQ_TIMEOUT,
169
129
  )
170
130
  response.raise_for_status()
131
+ except requests.exceptions.Timeout as exc:
132
+ raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
171
133
  except requests.RequestException as e:
172
- if self.retrieved_token:
173
- # try using already retrieved token if authenticate() fails (OTP use-case)
174
- if "access_token_expiration" in self.token_info:
175
- return {
176
- "access_token": self.retrieved_token,
177
- "expires_in": self.token_info["access_token_expiration"],
178
- }
179
- else:
180
- return {"access_token": self.retrieved_token, "expires_in": 0}
181
- response_text = getattr(e.response, "text", "").strip()
182
- # check if error is identified as auth_error in provider conf
183
- auth_errors = getattr(self.config, "auth_error_code", [None])
184
- if not isinstance(auth_errors, list):
185
- auth_errors = [auth_errors]
186
- if (
187
- hasattr(e.response, "status_code")
188
- and e.response.status_code in auth_errors
189
- ):
190
- raise AuthenticationError(
191
- "HTTP Error %s returned, %s\nPlease check your credentials for %s"
192
- % (e.response.status_code, response_text, self.provider)
193
- )
194
- # other error
195
- else:
196
- import traceback as tb
197
-
198
- logger.error(
199
- f"Provider {self.provider} returned {e.response.status_code}: {response_text}"
200
- )
201
- raise AuthenticationError(
202
- "Something went wrong while trying to get access token:\n{}".format(
203
- tb.format_exc()
204
- )
205
- )
134
+ return self._request_new_token_error(e)
206
135
  return response.json()
207
136
 
208
137
  def _get_token_with_refresh_token(self) -> Dict[str, str]:
@@ -17,7 +17,7 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import TYPE_CHECKING, Dict
20
+ from typing import TYPE_CHECKING, Dict, Optional
21
21
 
22
22
  from eodag.plugins.authentication.base import Authentication
23
23
 
@@ -30,8 +30,8 @@ class OAuth(Authentication):
30
30
 
31
31
  def __init__(self, provider: str, config: PluginConfig) -> None:
32
32
  super(OAuth, self).__init__(provider, config)
33
- self.access_key = None
34
- self.secret_key = None
33
+ self.access_key: Optional[str] = None
34
+ self.secret_key: Optional[str] = None
35
35
 
36
36
  def authenticate(self) -> Dict[str, str]:
37
37
  """Authenticate"""