eodag 3.8.1__py3-none-any.whl → 3.9.1__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 +1 -1
- eodag/api/product/drivers/generic.py +5 -1
- eodag/api/product/metadata_mapping.py +132 -35
- eodag/cli.py +36 -4
- eodag/config.py +5 -2
- eodag/plugins/apis/ecmwf.py +3 -1
- eodag/plugins/apis/usgs.py +2 -1
- eodag/plugins/authentication/aws_auth.py +235 -37
- eodag/plugins/authentication/base.py +12 -2
- eodag/plugins/authentication/oauth.py +5 -0
- eodag/plugins/base.py +3 -2
- eodag/plugins/download/aws.py +44 -285
- eodag/plugins/download/base.py +3 -2
- eodag/plugins/download/creodias_s3.py +1 -38
- eodag/plugins/download/http.py +111 -103
- eodag/plugins/download/s3rest.py +3 -1
- eodag/plugins/manager.py +2 -1
- eodag/plugins/search/__init__.py +2 -1
- eodag/plugins/search/base.py +2 -1
- eodag/plugins/search/build_search_result.py +2 -2
- eodag/plugins/search/creodias_s3.py +9 -1
- eodag/plugins/search/qssearch.py +3 -1
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +220 -30
- eodag/resources/providers.yml +633 -88
- eodag/resources/stac_provider.yml +5 -2
- eodag/resources/user_conf_template.yml +0 -5
- eodag/rest/core.py +8 -0
- eodag/rest/errors.py +9 -0
- eodag/rest/server.py +8 -0
- eodag/rest/stac.py +8 -0
- eodag/rest/utils/__init__.py +2 -4
- eodag/rest/utils/rfc3339.py +1 -1
- eodag/utils/__init__.py +69 -54
- eodag/utils/dates.py +204 -0
- eodag/utils/s3.py +187 -168
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/METADATA +4 -3
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/RECORD +42 -42
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/entry_points.txt +1 -1
- eodag/utils/rest.py +0 -100
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/WHEEL +0 -0
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/licenses/LICENSE +0 -0
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/top_level.txt +0 -0
|
@@ -17,29 +17,72 @@
|
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
import logging
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Optional, cast
|
|
22
|
+
|
|
23
|
+
import boto3
|
|
24
|
+
from botocore.exceptions import ClientError, ProfileNotFound
|
|
25
|
+
from botocore.handlers import disable_signing
|
|
21
26
|
|
|
22
27
|
from eodag.plugins.authentication.base import Authentication
|
|
23
28
|
from eodag.types import S3SessionKwargs
|
|
29
|
+
from eodag.utils.exceptions import AuthenticationError
|
|
24
30
|
|
|
25
31
|
if TYPE_CHECKING:
|
|
26
|
-
from mypy_boto3_s3
|
|
32
|
+
from mypy_boto3_s3 import S3Client, S3ServiceResource
|
|
33
|
+
from mypy_boto3_s3.service_resource import BucketObjectsCollection
|
|
27
34
|
|
|
28
35
|
from eodag.config import PluginConfig
|
|
29
36
|
|
|
30
37
|
|
|
38
|
+
logger = logging.getLogger("eodag.download.aws_auth")
|
|
39
|
+
|
|
40
|
+
AWS_AUTH_ERROR_MESSAGES = [
|
|
41
|
+
"AccessDenied",
|
|
42
|
+
"InvalidAccessKeyId",
|
|
43
|
+
"SignatureDoesNotMatch",
|
|
44
|
+
"InvalidRequest",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def raise_if_auth_error(exception: ClientError, provider: str) -> None:
|
|
49
|
+
"""Raises an error if given exception is an authentication error"""
|
|
50
|
+
err = cast(dict[str, str], exception.response["Error"])
|
|
51
|
+
if err["Code"] in AWS_AUTH_ERROR_MESSAGES and "key" in err["Message"].lower():
|
|
52
|
+
raise AuthenticationError(
|
|
53
|
+
f"Please check your credentials for {provider}.",
|
|
54
|
+
f"HTTP Error {exception.response['ResponseMetadata']['HTTPStatusCode']} returned.",
|
|
55
|
+
err["Code"] + ": " + err["Message"],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_s3_session(**kwargs) -> boto3.Session:
|
|
60
|
+
"""create s3 session based on available credentials
|
|
61
|
+
|
|
62
|
+
:param kwargs: keyword arguments containing credentials
|
|
63
|
+
:returns: boto3 Session
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
s3_session = boto3.Session(**kwargs)
|
|
67
|
+
except ProfileNotFound:
|
|
68
|
+
raise AuthenticationError(
|
|
69
|
+
f"AWS profile {kwargs['profile_name']} not found, please check your credentials configuration"
|
|
70
|
+
)
|
|
71
|
+
return s3_session
|
|
72
|
+
|
|
73
|
+
|
|
31
74
|
class AwsAuth(Authentication):
|
|
32
75
|
"""AWS authentication plugin
|
|
33
76
|
|
|
34
|
-
|
|
35
|
-
parameters are available in the configuration:
|
|
77
|
+
The authentication method will be chosen depending on which parameters are available in the configuration:
|
|
36
78
|
|
|
37
|
-
* auth
|
|
38
|
-
* auth using ``
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
79
|
+
* auth using ``profile_name`` (if credentials are given and contain ``aws_profile``)
|
|
80
|
+
* auth using ``aws_access_key_id``, ``aws_secret_access_key`` and optionally ``aws_session_token``
|
|
81
|
+
(if credentials are given but no ``aws_profile``)
|
|
82
|
+
* auth using current environment - AWS environment variables and/or ``~/.aws/*``
|
|
83
|
+
(if no credentials are given in config)
|
|
84
|
+
* auth anonymously using no-sign-request if no credentials are given in config and
|
|
85
|
+
auth using current environment failed
|
|
43
86
|
|
|
44
87
|
:param provider: provider name
|
|
45
88
|
:param config: Authentication plugin configuration:
|
|
@@ -47,41 +90,196 @@ class AwsAuth(Authentication):
|
|
|
47
90
|
* :attr:`~eodag.config.PluginConfig.type` (``str``) (**mandatory**): AwsAuth
|
|
48
91
|
* :attr:`~eodag.config.PluginConfig.auth_error_code` (``int``) (mandatory for ``creodias_s3``):
|
|
49
92
|
which error code is returned in case of an authentication error
|
|
93
|
+
* :attr:`~eodag.config.PluginConfig.s3_endpoint` (``str``): s3 endpoint url
|
|
94
|
+
* :attr:`~eodag.config.PluginConfig.requester_pays` (``bool``): whether download is done
|
|
95
|
+
from a requester-pays bucket or not; default: ``False``
|
|
50
96
|
|
|
51
97
|
"""
|
|
52
98
|
|
|
53
|
-
s3_client: S3Client
|
|
54
|
-
|
|
55
99
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
56
100
|
super(AwsAuth, self).__init__(provider, config)
|
|
57
|
-
self.
|
|
58
|
-
self.
|
|
59
|
-
|
|
60
|
-
self.
|
|
101
|
+
self.s3_session: Optional[boto3.Session] = None
|
|
102
|
+
self.s3_resource: Optional[S3ServiceResource] = None
|
|
103
|
+
# set default for requester_pays if not given
|
|
104
|
+
self.config.__dict__.setdefault("requester_pays", False)
|
|
61
105
|
|
|
62
|
-
def
|
|
63
|
-
"""Authenticate
|
|
64
|
-
|
|
65
|
-
:returns: dict containing AWS/boto3 non-empty credentials
|
|
66
|
-
"""
|
|
106
|
+
def _create_s3_session_from_credentials(self) -> boto3.Session:
|
|
67
107
|
credentials = getattr(self.config, "credentials", {}) or {}
|
|
68
|
-
|
|
69
|
-
"
|
|
108
|
+
if "aws_profile" in credentials:
|
|
109
|
+
logger.debug("Authentication using AWS profile")
|
|
110
|
+
return create_s3_session(profile_name=credentials["aws_profile"])
|
|
111
|
+
# auth using aws keys
|
|
112
|
+
elif credentials.get("aws_access_key_id") and credentials.get(
|
|
113
|
+
"aws_secret_access_key"
|
|
114
|
+
):
|
|
115
|
+
s3_session_kwargs: S3SessionKwargs = {
|
|
116
|
+
"aws_access_key_id": credentials["aws_access_key_id"],
|
|
117
|
+
"aws_secret_access_key": credentials["aws_secret_access_key"],
|
|
118
|
+
}
|
|
119
|
+
if credentials.get("aws_session_token"):
|
|
120
|
+
s3_session_kwargs["aws_session_token"] = credentials[
|
|
121
|
+
"aws_session_token"
|
|
122
|
+
]
|
|
123
|
+
return create_s3_session(**s3_session_kwargs)
|
|
124
|
+
else:
|
|
125
|
+
# auth using env variables or ~/.aws
|
|
126
|
+
logger.debug("Authentication using AWS environment")
|
|
127
|
+
return create_s3_session()
|
|
128
|
+
|
|
129
|
+
def _create_s3_resource(self) -> S3ServiceResource:
|
|
130
|
+
"""create s3 resource based on s3 session"""
|
|
131
|
+
if not self.s3_session:
|
|
132
|
+
self.s3_session = self._create_s3_session_from_credentials()
|
|
133
|
+
endpoint_url = getattr(self.config, "s3_endpoint", None)
|
|
134
|
+
if self.s3_session.get_credentials():
|
|
135
|
+
return self.s3_session.resource(
|
|
136
|
+
service_name="s3",
|
|
137
|
+
endpoint_url=endpoint_url,
|
|
138
|
+
)
|
|
139
|
+
# could not auth using credentials: use no-sign-request strategy
|
|
140
|
+
logger.debug(
|
|
141
|
+
"Authentication using AWS no-sign-request strategy (no credentials found)"
|
|
70
142
|
)
|
|
71
|
-
|
|
72
|
-
|
|
143
|
+
s3_resource = boto3.resource(service_name="s3", endpoint_url=endpoint_url)
|
|
144
|
+
s3_resource.meta.client.meta.events.register(
|
|
145
|
+
"choose-signer.s3.*", disable_signing
|
|
73
146
|
)
|
|
74
|
-
|
|
75
|
-
|
|
147
|
+
return s3_resource
|
|
148
|
+
|
|
149
|
+
def get_s3_client(self) -> S3Client:
|
|
150
|
+
"""Get S3 client from S3 resource
|
|
151
|
+
|
|
152
|
+
:returns: boto3 client
|
|
153
|
+
"""
|
|
154
|
+
if not self.s3_resource:
|
|
155
|
+
self.s3_resource = self._create_s3_resource()
|
|
156
|
+
return self.s3_resource.meta.client
|
|
157
|
+
|
|
158
|
+
def authenticate(self) -> S3ServiceResource:
|
|
159
|
+
"""Authenticate
|
|
160
|
+
|
|
161
|
+
:returns: S3 Resource created based on an S3 session
|
|
162
|
+
"""
|
|
163
|
+
self.s3_resource = self._create_s3_resource()
|
|
164
|
+
return self.s3_resource
|
|
165
|
+
|
|
166
|
+
def _get_authenticated_objects(
|
|
167
|
+
self, bucket_name: str, prefix: str
|
|
168
|
+
) -> BucketObjectsCollection:
|
|
169
|
+
"""Get boto3 authenticated objects for the given bucket
|
|
170
|
+
|
|
171
|
+
:param bucket_name: Bucket containg objects
|
|
172
|
+
:param prefix: Prefix used to filter objects
|
|
173
|
+
:returns: The boto3 authenticated objects
|
|
174
|
+
"""
|
|
175
|
+
if not self.s3_resource:
|
|
176
|
+
self.s3_resource = self._create_s3_resource()
|
|
177
|
+
try:
|
|
178
|
+
if self.config.requester_pays:
|
|
179
|
+
objects = self.s3_resource.Bucket(bucket_name).objects.filter(
|
|
180
|
+
RequestPayer="requester"
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
objects = self.s3_resource.Bucket(bucket_name).objects
|
|
184
|
+
list(objects.filter(Prefix=prefix).limit(1))
|
|
185
|
+
if objects:
|
|
186
|
+
logger.debug(
|
|
187
|
+
"Authentication for bucket %s succeeded; returning available objects",
|
|
188
|
+
bucket_name,
|
|
189
|
+
)
|
|
190
|
+
return objects
|
|
191
|
+
except ClientError as e:
|
|
192
|
+
if e.response.get("Error", {}).get("Code", {}) in AWS_AUTH_ERROR_MESSAGES:
|
|
193
|
+
pass
|
|
194
|
+
else:
|
|
195
|
+
raise e
|
|
196
|
+
logger.debug(
|
|
197
|
+
"Authentication for bucket %s failed, please check the credentials",
|
|
198
|
+
bucket_name,
|
|
76
199
|
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
k: getattr(self, k)
|
|
83
|
-
for k in S3SessionKwargs.__annotations__
|
|
84
|
-
if getattr(self, k, None)
|
|
85
|
-
},
|
|
200
|
+
|
|
201
|
+
raise AuthenticationError(
|
|
202
|
+
"Unable do authenticate on s3://%s using credendials configuration"
|
|
203
|
+
% bucket_name
|
|
86
204
|
)
|
|
87
|
-
|
|
205
|
+
|
|
206
|
+
def authenticate_objects(
|
|
207
|
+
self,
|
|
208
|
+
bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
|
|
209
|
+
) -> dict[str, BucketObjectsCollection]:
|
|
210
|
+
"""
|
|
211
|
+
Authenticates with s3 and retrieves the available objects
|
|
212
|
+
|
|
213
|
+
:param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
|
|
214
|
+
:raises AuthenticationError: authentication is not possible
|
|
215
|
+
:return: authenticated objects per bucket
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
authenticated_objects: dict[str, Any] = {}
|
|
219
|
+
auth_error_messages: set[str] = set()
|
|
220
|
+
for _, pack in enumerate(bucket_names_and_prefixes):
|
|
221
|
+
|
|
222
|
+
bucket_name, prefix = pack
|
|
223
|
+
if not prefix:
|
|
224
|
+
continue
|
|
225
|
+
if bucket_name not in authenticated_objects:
|
|
226
|
+
# get Prefixes longest common base path
|
|
227
|
+
common_prefix = ""
|
|
228
|
+
prefix_split = prefix.split("/")
|
|
229
|
+
prefixes_in_bucket = len(
|
|
230
|
+
[p for b, p in bucket_names_and_prefixes if b == bucket_name]
|
|
231
|
+
)
|
|
232
|
+
for i in range(1, len(prefix_split)):
|
|
233
|
+
common_prefix = "/".join(prefix_split[0:i])
|
|
234
|
+
if (
|
|
235
|
+
len(
|
|
236
|
+
[
|
|
237
|
+
p
|
|
238
|
+
for b, p in bucket_names_and_prefixes
|
|
239
|
+
if p and b == bucket_name and common_prefix in p
|
|
240
|
+
]
|
|
241
|
+
)
|
|
242
|
+
< prefixes_in_bucket
|
|
243
|
+
):
|
|
244
|
+
common_prefix = "/".join(prefix_split[0 : i - 1])
|
|
245
|
+
break
|
|
246
|
+
try:
|
|
247
|
+
# connect to aws s3 and get bucket auhenticated objects
|
|
248
|
+
authenticated_objects[
|
|
249
|
+
bucket_name
|
|
250
|
+
] = self._get_authenticated_objects(bucket_name, common_prefix)
|
|
251
|
+
|
|
252
|
+
except AuthenticationError as e:
|
|
253
|
+
logger.warning("Unexpected error: %s" % e)
|
|
254
|
+
logger.warning("Skipping %s/%s" % (bucket_name, prefix))
|
|
255
|
+
auth_error_messages.add(str(e))
|
|
256
|
+
except ClientError as e:
|
|
257
|
+
raise_if_auth_error(e, self.provider)
|
|
258
|
+
logger.warning("Unexpected error: %s" % e)
|
|
259
|
+
logger.warning("Skipping %s/%s" % (bucket_name, prefix))
|
|
260
|
+
auth_error_messages.add(str(e))
|
|
261
|
+
|
|
262
|
+
# could not auth on any bucket
|
|
263
|
+
if not authenticated_objects:
|
|
264
|
+
raise AuthenticationError(", ".join(auth_error_messages))
|
|
265
|
+
return authenticated_objects
|
|
266
|
+
|
|
267
|
+
def get_rio_env(self) -> dict[str, Any]:
|
|
268
|
+
"""Get rasterio environment variables needed for data access authentication.
|
|
269
|
+
|
|
270
|
+
:returns: The rasterio environement variables
|
|
271
|
+
"""
|
|
272
|
+
rio_env_kwargs = {}
|
|
273
|
+
if endpoint_url := getattr(self.config, "s3_endpoint", None):
|
|
274
|
+
rio_env_kwargs["endpoint_url"] = endpoint_url.split("://")[-1]
|
|
275
|
+
|
|
276
|
+
if self.s3_session is None:
|
|
277
|
+
self.authenticate()
|
|
278
|
+
|
|
279
|
+
if self.config.requester_pays:
|
|
280
|
+
rio_env_kwargs["requester_pays"] = True
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"session": self.s3_session,
|
|
284
|
+
**rio_env_kwargs,
|
|
285
|
+
}
|
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
from typing import TYPE_CHECKING, Union
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
21
21
|
|
|
22
22
|
from eodag.plugins.base import PluginTopic
|
|
23
23
|
from eodag.utils.exceptions import MisconfiguredError
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
+
from mypy_boto3_s3 import S3ServiceResource
|
|
26
27
|
from requests.auth import AuthBase
|
|
27
28
|
|
|
28
29
|
from eodag.types import S3SessionKwargs
|
|
@@ -40,7 +41,7 @@ class Authentication(PluginTopic):
|
|
|
40
41
|
configuration that needs authentication and helps identifying it
|
|
41
42
|
"""
|
|
42
43
|
|
|
43
|
-
def authenticate(self) -> Union[AuthBase, S3SessionKwargs]:
|
|
44
|
+
def authenticate(self) -> Union[AuthBase, S3SessionKwargs, S3ServiceResource]:
|
|
44
45
|
"""Authenticate"""
|
|
45
46
|
raise NotImplementedError
|
|
46
47
|
|
|
@@ -70,3 +71,12 @@ class Authentication(PluginTopic):
|
|
|
70
71
|
self.provider, ", ".join(missing_credentials)
|
|
71
72
|
)
|
|
72
73
|
)
|
|
74
|
+
|
|
75
|
+
def authenticate_objects(
|
|
76
|
+
self,
|
|
77
|
+
bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
"""
|
|
80
|
+
Authenticates with s3 and retrieves the available objects
|
|
81
|
+
"""
|
|
82
|
+
raise NotImplementedError
|
|
@@ -20,12 +20,17 @@ from __future__ import annotations
|
|
|
20
20
|
from typing import TYPE_CHECKING, Optional
|
|
21
21
|
|
|
22
22
|
from eodag.plugins.authentication.base import Authentication
|
|
23
|
+
from eodag.utils import _deprecated
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
26
|
from eodag.config import PluginConfig
|
|
26
27
|
from eodag.types import S3SessionKwargs
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
@_deprecated(
|
|
31
|
+
reason="Plugin was used to authenticate using S3 credentials, use AwsAuth instead",
|
|
32
|
+
version="3.8.1",
|
|
33
|
+
)
|
|
29
34
|
class OAuth(Authentication):
|
|
30
35
|
"""OAuth authentication plugin
|
|
31
36
|
|
eodag/plugins/base.py
CHANGED
|
@@ -65,9 +65,10 @@ class PluginTopic(metaclass=EODAGPluginMount):
|
|
|
65
65
|
self.provider = provider
|
|
66
66
|
|
|
67
67
|
def __repr__(self) -> str:
|
|
68
|
+
config = getattr(self, "config", None)
|
|
68
69
|
return "{}(provider={}, priority={}, topic={})".format(
|
|
69
70
|
self.__class__.__name__,
|
|
70
|
-
self
|
|
71
|
-
|
|
71
|
+
getattr(self, "provider", ""),
|
|
72
|
+
config.priority if config else "",
|
|
72
73
|
self.__class__.mro()[-3].__name__,
|
|
73
74
|
)
|