eodag 2.12.0__py3-none-any.whl → 3.0.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.
Files changed (93) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +654 -538
  3. eodag/api/product/__init__.py +12 -2
  4. eodag/api/product/_assets.py +59 -16
  5. eodag/api/product/_product.py +100 -93
  6. eodag/api/product/drivers/__init__.py +7 -2
  7. eodag/api/product/drivers/base.py +0 -3
  8. eodag/api/product/metadata_mapping.py +192 -96
  9. eodag/api/search_result.py +69 -10
  10. eodag/cli.py +55 -25
  11. eodag/config.py +391 -116
  12. eodag/plugins/apis/base.py +11 -165
  13. eodag/plugins/apis/ecmwf.py +36 -25
  14. eodag/plugins/apis/usgs.py +80 -35
  15. eodag/plugins/authentication/aws_auth.py +13 -4
  16. eodag/plugins/authentication/base.py +10 -1
  17. eodag/plugins/authentication/generic.py +2 -2
  18. eodag/plugins/authentication/header.py +31 -6
  19. eodag/plugins/authentication/keycloak.py +17 -84
  20. eodag/plugins/authentication/oauth.py +3 -3
  21. eodag/plugins/authentication/openid_connect.py +268 -49
  22. eodag/plugins/authentication/qsauth.py +4 -1
  23. eodag/plugins/authentication/sas_auth.py +9 -2
  24. eodag/plugins/authentication/token.py +98 -47
  25. eodag/plugins/authentication/token_exchange.py +122 -0
  26. eodag/plugins/crunch/base.py +3 -1
  27. eodag/plugins/crunch/filter_date.py +3 -9
  28. eodag/plugins/crunch/filter_latest_intersect.py +0 -3
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
  30. eodag/plugins/crunch/filter_overlap.py +4 -8
  31. eodag/plugins/crunch/filter_property.py +5 -11
  32. eodag/plugins/download/aws.py +149 -185
  33. eodag/plugins/download/base.py +88 -97
  34. eodag/plugins/download/creodias_s3.py +1 -1
  35. eodag/plugins/download/http.py +638 -310
  36. eodag/plugins/download/s3rest.py +47 -45
  37. eodag/plugins/manager.py +228 -88
  38. eodag/plugins/search/__init__.py +36 -0
  39. eodag/plugins/search/base.py +239 -30
  40. eodag/plugins/search/build_search_result.py +382 -37
  41. eodag/plugins/search/cop_marine.py +441 -0
  42. eodag/plugins/search/creodias_s3.py +25 -20
  43. eodag/plugins/search/csw.py +5 -7
  44. eodag/plugins/search/data_request_search.py +61 -30
  45. eodag/plugins/search/qssearch.py +713 -255
  46. eodag/plugins/search/static_stac_search.py +106 -40
  47. eodag/resources/ext_product_types.json +1 -1
  48. eodag/resources/product_types.yml +1921 -34
  49. eodag/resources/providers.yml +4091 -3655
  50. eodag/resources/stac.yml +50 -216
  51. eodag/resources/stac_api.yml +71 -25
  52. eodag/resources/stac_provider.yml +5 -0
  53. eodag/resources/user_conf_template.yml +89 -32
  54. eodag/rest/__init__.py +6 -0
  55. eodag/rest/cache.py +70 -0
  56. eodag/rest/config.py +68 -0
  57. eodag/rest/constants.py +26 -0
  58. eodag/rest/core.py +735 -0
  59. eodag/rest/errors.py +178 -0
  60. eodag/rest/server.py +264 -431
  61. eodag/rest/stac.py +442 -836
  62. eodag/rest/types/collections_search.py +44 -0
  63. eodag/rest/types/eodag_search.py +238 -47
  64. eodag/rest/types/queryables.py +164 -0
  65. eodag/rest/types/stac_search.py +273 -0
  66. eodag/rest/utils/__init__.py +216 -0
  67. eodag/rest/utils/cql_evaluate.py +119 -0
  68. eodag/rest/utils/rfc3339.py +64 -0
  69. eodag/types/__init__.py +106 -10
  70. eodag/types/bbox.py +15 -14
  71. eodag/types/download_args.py +40 -0
  72. eodag/types/search_args.py +57 -7
  73. eodag/types/whoosh.py +79 -0
  74. eodag/utils/__init__.py +110 -91
  75. eodag/utils/constraints.py +37 -45
  76. eodag/utils/exceptions.py +39 -22
  77. eodag/utils/import_system.py +0 -4
  78. eodag/utils/logging.py +37 -80
  79. eodag/utils/notebook.py +4 -4
  80. eodag/utils/repr.py +113 -0
  81. eodag/utils/requests.py +128 -0
  82. eodag/utils/rest.py +100 -0
  83. eodag/utils/stac_reader.py +93 -21
  84. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
  85. eodag-3.0.0.dist-info/RECORD +109 -0
  86. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
  87. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
  88. eodag/plugins/apis/cds.py +0 -540
  89. eodag/rest/types/stac_queryables.py +0 -134
  90. eodag/rest/utils.py +0 -1133
  91. eodag-2.12.0.dist-info/RECORD +0 -94
  92. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
  93. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/top_level.txt +0 -0
@@ -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
 
@@ -35,23 +35,24 @@ class AwsAuth(Authentication):
35
35
  - auth anonymously using no-sign-request
36
36
  - auth using ``aws_profile``
37
37
  - auth using ``aws_access_key_id`` and ``aws_secret_access_key``
38
+ (optionally ``aws_session_token``)
38
39
  - auth using current environment (AWS environment variables and/or ``~/aws/*``),
39
40
  will be skipped if AWS credentials are filled in eodag conf
40
41
  """
41
42
 
42
- s3_client: S3
43
+ s3_client: S3Client
43
44
 
44
45
  def __init__(self, provider: str, config: PluginConfig) -> None:
45
46
  super(AwsAuth, self).__init__(provider, config)
46
47
  self.aws_access_key_id = None
47
48
  self.aws_secret_access_key = None
49
+ self.aws_session_token = None
48
50
  self.profile_name = None
49
51
 
50
52
  def authenticate(self) -> Dict[str, str]:
51
53
  """Authenticate
52
54
 
53
55
  :returns: dict containing AWS/boto3 non-empty credentials
54
- :rtype: dict
55
56
  """
56
57
  credentials = getattr(self.config, "credentials", {}) or {}
57
58
  self.aws_access_key_id = credentials.get(
@@ -60,7 +61,15 @@ class AwsAuth(Authentication):
60
61
  self.aws_secret_access_key = credentials.get(
61
62
  "aws_secret_access_key", self.aws_secret_access_key
62
63
  )
64
+ self.aws_session_token = credentials.get(
65
+ "aws_session_token", self.aws_session_token
66
+ )
63
67
  self.profile_name = credentials.get("aws_profile", self.profile_name)
64
68
 
65
- auth_keys = ["aws_access_key_id", "aws_secret_access_key", "profile_name"]
69
+ auth_keys = [
70
+ "aws_access_key_id",
71
+ "aws_secret_access_key",
72
+ "aws_session_token",
73
+ "profile_name",
74
+ ]
66
75
  return {k: getattr(self, k) for k in auth_keys if getattr(self, k)}
@@ -27,7 +27,16 @@ if TYPE_CHECKING:
27
27
 
28
28
 
29
29
  class Authentication(PluginTopic):
30
- """Plugins authentication Base plugin"""
30
+ """Plugins authentication Base plugin
31
+
32
+ :param provider: provider name
33
+ :param config: Authentication plugin configuration:
34
+
35
+ * :attr:`~eodag.config.PluginConfig.matching_url` (``str``): URL pattern to match with search plugin endpoint or
36
+ download link
37
+ * :attr:`~eodag.config.PluginConfig.matching_conf` (``Dict[str, Any]``): Part of the search or download plugin
38
+ configuration that needs authentication and helps identifying it
39
+ """
31
40
 
32
41
  def authenticate(self) -> Union[AuthBase, Dict[str, str]]:
33
42
  """Authenticate"""
@@ -48,6 +48,6 @@ class GenericAuth(Authentication):
48
48
  )
49
49
  else:
50
50
  raise MisconfiguredError(
51
- f"Cannot authenticate with {self.provider}:",
52
- f"method {method} is not supported. Must be one of digest or basic",
51
+ f"Cannot authenticate with {self.provider}",
52
+ f"Method {method} is not supported; it must be one of 'digest' or 'basic'.",
53
53
  )
@@ -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,
@@ -157,6 +117,7 @@ class KeycloakOIDCPasswordAuth(Authentication):
157
117
  "grant_type": self.GRANT_TYPE,
158
118
  }
159
119
  credentials = {k: v for k, v in self.config.credentials.items()}
120
+ ssl_verify = getattr(self.config, "ssl_verify", True)
160
121
  try:
161
122
  response = self.session.post(
162
123
  self.TOKEN_URL_TEMPLATE.format(
@@ -166,43 +127,13 @@ class KeycloakOIDCPasswordAuth(Authentication):
166
127
  data=dict(req_data, **credentials),
167
128
  headers=USER_AGENT,
168
129
  timeout=HTTP_REQ_TIMEOUT,
130
+ verify=ssl_verify,
169
131
  )
170
132
  response.raise_for_status()
133
+ except requests.exceptions.Timeout as exc:
134
+ raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
171
135
  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
- )
136
+ return self._request_new_token_error(e)
206
137
  return response.json()
207
138
 
208
139
  def _get_token_with_refresh_token(self) -> Dict[str, str]:
@@ -213,6 +144,7 @@ class KeycloakOIDCPasswordAuth(Authentication):
213
144
  "grant_type": "refresh_token",
214
145
  "refresh_token": self.token_info["refresh_token"],
215
146
  }
147
+ ssl_verify = getattr(self.config, "ssl_verify", True)
216
148
  try:
217
149
  response = self.session.post(
218
150
  self.TOKEN_URL_TEMPLATE.format(
@@ -222,6 +154,7 @@ class KeycloakOIDCPasswordAuth(Authentication):
222
154
  data=req_data,
223
155
  headers=USER_AGENT,
224
156
  timeout=HTTP_REQ_TIMEOUT,
157
+ verify=ssl_verify,
225
158
  )
226
159
  response.raise_for_status()
227
160
  except requests.RequestException as e:
@@ -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"""