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.
Files changed (43) hide show
  1. eodag/api/core.py +1 -1
  2. eodag/api/product/drivers/generic.py +5 -1
  3. eodag/api/product/metadata_mapping.py +132 -35
  4. eodag/cli.py +36 -4
  5. eodag/config.py +5 -2
  6. eodag/plugins/apis/ecmwf.py +3 -1
  7. eodag/plugins/apis/usgs.py +2 -1
  8. eodag/plugins/authentication/aws_auth.py +235 -37
  9. eodag/plugins/authentication/base.py +12 -2
  10. eodag/plugins/authentication/oauth.py +5 -0
  11. eodag/plugins/base.py +3 -2
  12. eodag/plugins/download/aws.py +44 -285
  13. eodag/plugins/download/base.py +3 -2
  14. eodag/plugins/download/creodias_s3.py +1 -38
  15. eodag/plugins/download/http.py +111 -103
  16. eodag/plugins/download/s3rest.py +3 -1
  17. eodag/plugins/manager.py +2 -1
  18. eodag/plugins/search/__init__.py +2 -1
  19. eodag/plugins/search/base.py +2 -1
  20. eodag/plugins/search/build_search_result.py +2 -2
  21. eodag/plugins/search/creodias_s3.py +9 -1
  22. eodag/plugins/search/qssearch.py +3 -1
  23. eodag/resources/ext_product_types.json +1 -1
  24. eodag/resources/product_types.yml +220 -30
  25. eodag/resources/providers.yml +633 -88
  26. eodag/resources/stac_provider.yml +5 -2
  27. eodag/resources/user_conf_template.yml +0 -5
  28. eodag/rest/core.py +8 -0
  29. eodag/rest/errors.py +9 -0
  30. eodag/rest/server.py +8 -0
  31. eodag/rest/stac.py +8 -0
  32. eodag/rest/utils/__init__.py +2 -4
  33. eodag/rest/utils/rfc3339.py +1 -1
  34. eodag/utils/__init__.py +69 -54
  35. eodag/utils/dates.py +204 -0
  36. eodag/utils/s3.py +187 -168
  37. {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/METADATA +4 -3
  38. {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/RECORD +42 -42
  39. {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/entry_points.txt +1 -1
  40. eodag/utils/rest.py +0 -100
  41. {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/WHEEL +0 -0
  42. {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/licenses/LICENSE +0 -0
  43. {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
- from typing import TYPE_CHECKING, Optional, cast
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.client import S3Client
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
- Authentication will use the first valid method within the following ones depending on which
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 anonymously using no-sign-request
38
- * auth using ``aws_profile``
39
- * auth using ``aws_access_key_id`` and ``aws_secret_access_key``
40
- (optionally ``aws_session_token``)
41
- * auth using current environment (AWS environment variables and/or ``~/aws/*``),
42
- will be skipped if AWS credentials are filled in eodag conf
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.aws_access_key_id: Optional[str] = None
58
- self.aws_secret_access_key: Optional[str] = None
59
- self.aws_session_token: Optional[str] = None
60
- self.profile_name: Optional[str] = None
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 authenticate(self) -> S3SessionKwargs:
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
- self.aws_access_key_id = credentials.get(
69
- "aws_access_key_id", self.aws_access_key_id
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
- self.aws_secret_access_key = credentials.get(
72
- "aws_secret_access_key", self.aws_secret_access_key
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
- self.aws_session_token = credentials.get(
75
- "aws_session_token", self.aws_session_token
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
- self.profile_name = credentials.get("aws_profile", self.profile_name)
78
-
79
- auth_dict = cast(
80
- S3SessionKwargs,
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
- return auth_dict
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.provider,
71
- self.config.priority,
71
+ getattr(self, "provider", ""),
72
+ config.priority if config else "",
72
73
  self.__class__.mro()[-3].__name__,
73
74
  )