eodag 3.8.0__py3-none-any.whl → 3.9.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/api/core.py +3 -2
- eodag/api/product/drivers/generic.py +5 -1
- eodag/api/product/metadata_mapping.py +110 -9
- 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 +228 -37
- eodag/plugins/authentication/base.py +12 -2
- eodag/plugins/authentication/oauth.py +5 -0
- eodag/plugins/authentication/sas_auth.py +15 -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 +634 -89
- eodag/resources/stac_provider.yml +6 -3
- 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.0.dist-info → eodag-3.9.0.dist-info}/METADATA +4 -3
- {eodag-3.8.0.dist-info → eodag-3.9.0.dist-info}/RECORD +43 -43
- {eodag-3.8.0.dist-info → eodag-3.9.0.dist-info}/entry_points.txt +1 -1
- eodag/utils/rest.py +0 -100
- {eodag-3.8.0.dist-info → eodag-3.9.0.dist-info}/WHEEL +0 -0
- {eodag-3.8.0.dist-info → eodag-3.9.0.dist-info}/licenses/LICENSE +0 -0
- {eodag-3.8.0.dist-info → eodag-3.9.0.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,189 @@ 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)
|
|
105
|
+
|
|
106
|
+
def _create_s3_session_from_credentials(self) -> boto3.Session:
|
|
107
|
+
credentials = getattr(self.config, "credentials", {}) or {}
|
|
108
|
+
if "aws_profile" in credentials:
|
|
109
|
+
return create_s3_session(profile_name=credentials["aws_profile"])
|
|
110
|
+
# auth using aws keys
|
|
111
|
+
elif credentials:
|
|
112
|
+
s3_session_kwargs: S3SessionKwargs = {
|
|
113
|
+
"aws_access_key_id": credentials["aws_access_key_id"],
|
|
114
|
+
"aws_secret_access_key": credentials["aws_secret_access_key"],
|
|
115
|
+
}
|
|
116
|
+
if credentials.get("aws_session_token"):
|
|
117
|
+
s3_session_kwargs["aws_session_token"] = credentials[
|
|
118
|
+
"aws_session_token"
|
|
119
|
+
]
|
|
120
|
+
return create_s3_session(**s3_session_kwargs)
|
|
121
|
+
else:
|
|
122
|
+
# auth using env variables or ~/.aws
|
|
123
|
+
return create_s3_session()
|
|
124
|
+
|
|
125
|
+
def _create_s3_resource(self) -> S3ServiceResource:
|
|
126
|
+
"""create s3 resource based on s3 session"""
|
|
127
|
+
if not self.s3_session:
|
|
128
|
+
self.s3_session = self._create_s3_session_from_credentials()
|
|
129
|
+
endpoint_url = getattr(self.config, "s3_endpoint", None)
|
|
130
|
+
if self.s3_session.get_credentials():
|
|
131
|
+
return self.s3_session.resource(
|
|
132
|
+
service_name="s3",
|
|
133
|
+
endpoint_url=endpoint_url,
|
|
134
|
+
)
|
|
135
|
+
# could not auth using credentials: use no-sign-request strategy
|
|
136
|
+
s3_resource = boto3.resource(service_name="s3", endpoint_url=endpoint_url)
|
|
137
|
+
s3_resource.meta.client.meta.events.register(
|
|
138
|
+
"choose-signer.s3.*", disable_signing
|
|
139
|
+
)
|
|
140
|
+
return s3_resource
|
|
141
|
+
|
|
142
|
+
def get_s3_client(self) -> S3Client:
|
|
143
|
+
"""Get S3 client from S3 resource
|
|
61
144
|
|
|
62
|
-
|
|
145
|
+
:returns: boto3 client
|
|
146
|
+
"""
|
|
147
|
+
if not self.s3_resource:
|
|
148
|
+
self.s3_resource = self._create_s3_resource()
|
|
149
|
+
return self.s3_resource.meta.client
|
|
150
|
+
|
|
151
|
+
def authenticate(self) -> S3ServiceResource:
|
|
63
152
|
"""Authenticate
|
|
64
153
|
|
|
65
|
-
:returns:
|
|
154
|
+
:returns: S3 Resource created based on an S3 session
|
|
66
155
|
"""
|
|
67
|
-
|
|
68
|
-
self.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
self
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
156
|
+
self.s3_resource = self._create_s3_resource()
|
|
157
|
+
return self.s3_resource
|
|
158
|
+
|
|
159
|
+
def _get_authenticated_objects(
|
|
160
|
+
self, bucket_name: str, prefix: str
|
|
161
|
+
) -> BucketObjectsCollection:
|
|
162
|
+
"""Get boto3 authenticated objects for the given bucket
|
|
163
|
+
|
|
164
|
+
:param bucket_name: Bucket containg objects
|
|
165
|
+
:param prefix: Prefix used to filter objects
|
|
166
|
+
:returns: The boto3 authenticated objects
|
|
167
|
+
"""
|
|
168
|
+
if not self.s3_resource:
|
|
169
|
+
self.s3_resource = self._create_s3_resource()
|
|
170
|
+
try:
|
|
171
|
+
if self.config.requester_pays:
|
|
172
|
+
objects = self.s3_resource.Bucket(bucket_name).objects.filter(
|
|
173
|
+
RequestPayer="requester"
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
objects = self.s3_resource.Bucket(bucket_name).objects
|
|
177
|
+
list(objects.filter(Prefix=prefix).limit(1))
|
|
178
|
+
if objects:
|
|
179
|
+
logger.debug(
|
|
180
|
+
"Authentication for bucket %s succeeded; returning available objects",
|
|
181
|
+
bucket_name,
|
|
182
|
+
)
|
|
183
|
+
return objects
|
|
184
|
+
except ClientError as e:
|
|
185
|
+
if e.response.get("Error", {}).get("Code", {}) in AWS_AUTH_ERROR_MESSAGES:
|
|
186
|
+
pass
|
|
187
|
+
else:
|
|
188
|
+
raise e
|
|
189
|
+
logger.debug(
|
|
190
|
+
"Authentication for bucket %s failed, please check the credentials",
|
|
191
|
+
bucket_name,
|
|
76
192
|
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
k: getattr(self, k)
|
|
83
|
-
for k in S3SessionKwargs.__annotations__
|
|
84
|
-
if getattr(self, k, None)
|
|
85
|
-
},
|
|
193
|
+
|
|
194
|
+
raise AuthenticationError(
|
|
195
|
+
"Unable do authenticate on s3://%s using credendials configuration"
|
|
196
|
+
% bucket_name
|
|
86
197
|
)
|
|
87
|
-
|
|
198
|
+
|
|
199
|
+
def authenticate_objects(
|
|
200
|
+
self,
|
|
201
|
+
bucket_names_and_prefixes: list[tuple[str, Optional[str]]],
|
|
202
|
+
) -> dict[str, BucketObjectsCollection]:
|
|
203
|
+
"""
|
|
204
|
+
Authenticates with s3 and retrieves the available objects
|
|
205
|
+
|
|
206
|
+
:param bucket_names_and_prefixes: list of bucket names and corresponding path prefixes
|
|
207
|
+
:raises AuthenticationError: authentication is not possible
|
|
208
|
+
:return: authenticated objects per bucket
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
authenticated_objects: dict[str, Any] = {}
|
|
212
|
+
auth_error_messages: set[str] = set()
|
|
213
|
+
for _, pack in enumerate(bucket_names_and_prefixes):
|
|
214
|
+
|
|
215
|
+
bucket_name, prefix = pack
|
|
216
|
+
if not prefix:
|
|
217
|
+
continue
|
|
218
|
+
if bucket_name not in authenticated_objects:
|
|
219
|
+
# get Prefixes longest common base path
|
|
220
|
+
common_prefix = ""
|
|
221
|
+
prefix_split = prefix.split("/")
|
|
222
|
+
prefixes_in_bucket = len(
|
|
223
|
+
[p for b, p in bucket_names_and_prefixes if b == bucket_name]
|
|
224
|
+
)
|
|
225
|
+
for i in range(1, len(prefix_split)):
|
|
226
|
+
common_prefix = "/".join(prefix_split[0:i])
|
|
227
|
+
if (
|
|
228
|
+
len(
|
|
229
|
+
[
|
|
230
|
+
p
|
|
231
|
+
for b, p in bucket_names_and_prefixes
|
|
232
|
+
if p and b == bucket_name and common_prefix in p
|
|
233
|
+
]
|
|
234
|
+
)
|
|
235
|
+
< prefixes_in_bucket
|
|
236
|
+
):
|
|
237
|
+
common_prefix = "/".join(prefix_split[0 : i - 1])
|
|
238
|
+
break
|
|
239
|
+
try:
|
|
240
|
+
# connect to aws s3 and get bucket auhenticated objects
|
|
241
|
+
authenticated_objects[
|
|
242
|
+
bucket_name
|
|
243
|
+
] = self._get_authenticated_objects(bucket_name, common_prefix)
|
|
244
|
+
|
|
245
|
+
except AuthenticationError as e:
|
|
246
|
+
logger.warning("Unexpected error: %s" % e)
|
|
247
|
+
logger.warning("Skipping %s/%s" % (bucket_name, prefix))
|
|
248
|
+
auth_error_messages.add(str(e))
|
|
249
|
+
except ClientError as e:
|
|
250
|
+
raise_if_auth_error(e, self.provider)
|
|
251
|
+
logger.warning("Unexpected error: %s" % e)
|
|
252
|
+
logger.warning("Skipping %s/%s" % (bucket_name, prefix))
|
|
253
|
+
auth_error_messages.add(str(e))
|
|
254
|
+
|
|
255
|
+
# could not auth on any bucket
|
|
256
|
+
if not authenticated_objects:
|
|
257
|
+
raise AuthenticationError(", ".join(auth_error_messages))
|
|
258
|
+
return authenticated_objects
|
|
259
|
+
|
|
260
|
+
def get_rio_env(self) -> dict[str, Any]:
|
|
261
|
+
"""Get rasterio environment variables needed for data access authentication.
|
|
262
|
+
|
|
263
|
+
:returns: The rasterio environement variables
|
|
264
|
+
"""
|
|
265
|
+
rio_env_kwargs = {}
|
|
266
|
+
if endpoint_url := getattr(self.config, "s3_endpoint", None):
|
|
267
|
+
rio_env_kwargs["endpoint_url"] = endpoint_url.split("://")[-1]
|
|
268
|
+
|
|
269
|
+
if self.s3_session is None:
|
|
270
|
+
self.authenticate()
|
|
271
|
+
|
|
272
|
+
if self.config.requester_pays:
|
|
273
|
+
rio_env_kwargs["requester_pays"] = True
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
"session": self.s3_session,
|
|
277
|
+
**rio_env_kwargs,
|
|
278
|
+
}
|
|
@@ -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
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
+
import re
|
|
21
22
|
from json import JSONDecodeError
|
|
22
23
|
from typing import TYPE_CHECKING, Optional
|
|
23
24
|
|
|
@@ -29,6 +30,8 @@ from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, deepcopy, format_dict_item
|
|
|
29
30
|
from eodag.utils.exceptions import AuthenticationError, TimeOutError
|
|
30
31
|
|
|
31
32
|
if TYPE_CHECKING:
|
|
33
|
+
from typing import Pattern
|
|
34
|
+
|
|
32
35
|
from requests import PreparedRequest
|
|
33
36
|
|
|
34
37
|
|
|
@@ -44,15 +47,24 @@ class RequestsSASAuth(AuthBase):
|
|
|
44
47
|
signed_url_key: str,
|
|
45
48
|
headers: Optional[dict[str, str]] = None,
|
|
46
49
|
ssl_verify: bool = True,
|
|
50
|
+
matching_url: Optional[Pattern[str]] = None,
|
|
47
51
|
) -> None:
|
|
48
52
|
self.auth_uri = auth_uri
|
|
49
53
|
self.signed_url_key = signed_url_key
|
|
50
54
|
self.headers = headers
|
|
51
55
|
self.signed_urls: dict[str, str] = {}
|
|
52
56
|
self.ssl_verify = ssl_verify
|
|
57
|
+
self.matching_url = matching_url
|
|
53
58
|
|
|
54
59
|
def __call__(self, request: PreparedRequest) -> PreparedRequest:
|
|
55
60
|
"""Perform the actual authentication"""
|
|
61
|
+
# if matching_url is set, check if request.url matches
|
|
62
|
+
if (
|
|
63
|
+
self.matching_url
|
|
64
|
+
and request.url
|
|
65
|
+
and not self.matching_url.match(request.url)
|
|
66
|
+
):
|
|
67
|
+
return request
|
|
56
68
|
|
|
57
69
|
# update headers
|
|
58
70
|
if self.headers and isinstance(self.headers, dict):
|
|
@@ -118,6 +130,8 @@ class SASAuth(Authentication):
|
|
|
118
130
|
# update headers with subscription key if exists
|
|
119
131
|
apikey = getattr(self.config, "credentials", {}).get("apikey")
|
|
120
132
|
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
133
|
+
if matching_url := getattr(self.config, "matching_url", None):
|
|
134
|
+
matching_url = re.compile(matching_url)
|
|
121
135
|
if apikey:
|
|
122
136
|
headers_update = format_dict_items(self.config.headers, apikey=apikey)
|
|
123
137
|
headers.update(headers_update)
|
|
@@ -127,4 +141,5 @@ class SASAuth(Authentication):
|
|
|
127
141
|
signed_url_key=self.config.signed_url_key,
|
|
128
142
|
headers=headers,
|
|
129
143
|
ssl_verify=ssl_verify,
|
|
144
|
+
matching_url=matching_url,
|
|
130
145
|
)
|
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
|
)
|