eodag 2.12.1__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.
- eodag/__init__.py +6 -8
- eodag/api/core.py +654 -538
- eodag/api/product/__init__.py +12 -2
- eodag/api/product/_assets.py +59 -16
- eodag/api/product/_product.py +100 -93
- eodag/api/product/drivers/__init__.py +7 -2
- eodag/api/product/drivers/base.py +0 -3
- eodag/api/product/metadata_mapping.py +192 -96
- eodag/api/search_result.py +69 -10
- eodag/cli.py +55 -25
- eodag/config.py +391 -116
- eodag/plugins/apis/base.py +11 -168
- eodag/plugins/apis/ecmwf.py +36 -25
- eodag/plugins/apis/usgs.py +80 -35
- eodag/plugins/authentication/aws_auth.py +13 -4
- eodag/plugins/authentication/base.py +10 -1
- eodag/plugins/authentication/generic.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +17 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +268 -49
- eodag/plugins/authentication/qsauth.py +4 -1
- eodag/plugins/authentication/sas_auth.py +9 -2
- eodag/plugins/authentication/token.py +98 -47
- eodag/plugins/authentication/token_exchange.py +122 -0
- eodag/plugins/crunch/base.py +3 -1
- eodag/plugins/crunch/filter_date.py +3 -9
- eodag/plugins/crunch/filter_latest_intersect.py +0 -3
- eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
- eodag/plugins/crunch/filter_overlap.py +4 -8
- eodag/plugins/crunch/filter_property.py +5 -11
- eodag/plugins/download/aws.py +149 -185
- eodag/plugins/download/base.py +88 -97
- eodag/plugins/download/creodias_s3.py +1 -1
- eodag/plugins/download/http.py +638 -310
- eodag/plugins/download/s3rest.py +47 -45
- eodag/plugins/manager.py +228 -88
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +239 -30
- eodag/plugins/search/build_search_result.py +382 -37
- eodag/plugins/search/cop_marine.py +441 -0
- eodag/plugins/search/creodias_s3.py +25 -20
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +61 -30
- eodag/plugins/search/qssearch.py +713 -255
- eodag/plugins/search/static_stac_search.py +106 -40
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1921 -34
- eodag/resources/providers.yml +4091 -3655
- eodag/resources/stac.yml +50 -216
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +89 -32
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +26 -0
- eodag/rest/core.py +735 -0
- eodag/rest/errors.py +178 -0
- eodag/rest/server.py +264 -431
- eodag/rest/stac.py +442 -836
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +238 -47
- eodag/rest/types/queryables.py +164 -0
- eodag/rest/types/stac_search.py +273 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +64 -0
- eodag/types/__init__.py +106 -10
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +40 -0
- eodag/types/search_args.py +57 -7
- eodag/types/whoosh.py +79 -0
- eodag/utils/__init__.py +110 -91
- eodag/utils/constraints.py +37 -45
- eodag/utils/exceptions.py +39 -22
- eodag/utils/import_system.py +0 -4
- eodag/utils/logging.py +37 -80
- eodag/utils/notebook.py +4 -4
- eodag/utils/repr.py +113 -0
- eodag/utils/requests.py +128 -0
- eodag/utils/rest.py +100 -0
- eodag/utils/stac_reader.py +93 -21
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
- eodag-3.0.0.dist-info/RECORD +109 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/types/stac_queryables.py +0 -134
- eodag/rest/utils.py +0 -1133
- eodag-2.12.1.dist-info/RECORD +0 -94
- {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
- {eodag-2.12.1.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
|
|
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:
|
|
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 = [
|
|
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"
|
|
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) ->
|
|
77
|
+
def authenticate(self) -> HeaderAuth:
|
|
64
78
|
"""Authenticate"""
|
|
65
79
|
self.validate_config_credentials()
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
27
|
-
|
|
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
|
|
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(
|
|
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.
|
|
105
|
+
self.token_info["access_token"] = access_token
|
|
109
106
|
return CodeAuthorizedAuth(
|
|
110
|
-
self.
|
|
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
|
|
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
|
-
|
|
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"""
|