eodag 4.0.0a2__py3-none-any.whl → 4.0.0a4__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/collection.py +10 -9
- eodag/api/core.py +233 -335
- eodag/api/product/_product.py +3 -3
- eodag/api/provider.py +990 -0
- eodag/cli.py +1 -3
- eodag/config.py +73 -444
- eodag/plugins/authentication/token.py +0 -1
- eodag/plugins/download/http.py +0 -1
- eodag/plugins/manager.py +24 -34
- eodag/resources/ext_collections.json +1 -1
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/providers.yml +4 -4
- {eodag-4.0.0a2.dist-info → eodag-4.0.0a4.dist-info}/METADATA +1 -1
- {eodag-4.0.0a2.dist-info → eodag-4.0.0a4.dist-info}/RECORD +18 -17
- {eodag-4.0.0a2.dist-info → eodag-4.0.0a4.dist-info}/WHEEL +0 -0
- {eodag-4.0.0a2.dist-info → eodag-4.0.0a4.dist-info}/entry_points.txt +0 -0
- {eodag-4.0.0a2.dist-info → eodag-4.0.0a4.dist-info}/licenses/LICENSE +0 -0
- {eodag-4.0.0a2.dist-info → eodag-4.0.0a4.dist-info}/top_level.txt +0 -0
eodag/api/provider.py
ADDED
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2025, CS GROUP - France, https://www.csgroup.eu/
|
|
3
|
+
#
|
|
4
|
+
# This file is part of EODAG project
|
|
5
|
+
# https://www.github.com/CS-SI/EODAG
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import tempfile
|
|
23
|
+
import traceback
|
|
24
|
+
from collections import UserDict
|
|
25
|
+
from inspect import isclass
|
|
26
|
+
from textwrap import shorten
|
|
27
|
+
from typing import (
|
|
28
|
+
TYPE_CHECKING,
|
|
29
|
+
Any,
|
|
30
|
+
Iterator,
|
|
31
|
+
Mapping,
|
|
32
|
+
Optional,
|
|
33
|
+
Union,
|
|
34
|
+
get_type_hints,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
import yaml
|
|
38
|
+
|
|
39
|
+
from eodag.api.collection import Collection
|
|
40
|
+
from eodag.api.product.metadata_mapping import (
|
|
41
|
+
NOT_AVAILABLE,
|
|
42
|
+
mtd_cfg_as_conversion_and_querypath,
|
|
43
|
+
)
|
|
44
|
+
from eodag.config import PluginConfig, credentials_in_auth, load_stac_provider_config
|
|
45
|
+
from eodag.utils import (
|
|
46
|
+
GENERIC_COLLECTION,
|
|
47
|
+
STAC_SEARCH_PLUGINS,
|
|
48
|
+
cast_scalar_value,
|
|
49
|
+
deepcopy,
|
|
50
|
+
merge_mappings,
|
|
51
|
+
slugify,
|
|
52
|
+
update_nested_dict,
|
|
53
|
+
)
|
|
54
|
+
from eodag.utils.exceptions import (
|
|
55
|
+
UnsupportedCollection,
|
|
56
|
+
UnsupportedProvider,
|
|
57
|
+
ValidationError,
|
|
58
|
+
)
|
|
59
|
+
from eodag.utils.free_text_search import compile_free_text_query
|
|
60
|
+
from eodag.utils.repr import dict_to_html_table, str_as_href
|
|
61
|
+
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
from typing_extensions import Self
|
|
64
|
+
|
|
65
|
+
from eodag.api.core import EODataAccessGateway
|
|
66
|
+
|
|
67
|
+
logger = logging.getLogger("eodag.provider")
|
|
68
|
+
|
|
69
|
+
AUTH_TOPIC_KEYS = ("auth", "search_auth", "download_auth")
|
|
70
|
+
PLUGINS_TOPICS_KEYS = ("api", "search", "download") + AUTH_TOPIC_KEYS
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ProviderConfig(yaml.YAMLObject):
|
|
74
|
+
"""EODAG configuration for a provider.
|
|
75
|
+
|
|
76
|
+
:param name: The name of the provider
|
|
77
|
+
:param priority: (optional) The priority of the provider while searching a product.
|
|
78
|
+
Lower value means lower priority. (Default: 0)
|
|
79
|
+
:param roles: The roles of the provider (e.g. "host", "producer", "licensor", "processor")
|
|
80
|
+
:param description: (optional) A short description of the provider
|
|
81
|
+
:param url: URL to the webpage representing the provider
|
|
82
|
+
:param api: (optional) The configuration of a plugin of type Api
|
|
83
|
+
:param search: (optional) The configuration of a plugin of type Search
|
|
84
|
+
:param products: (optional) The collections supported by the provider
|
|
85
|
+
:param download: (optional) The configuration of a plugin of type Download
|
|
86
|
+
:param auth: (optional) The configuration of a plugin of type Authentication
|
|
87
|
+
:param search_auth: (optional) The configuration of a plugin of type Authentication for search
|
|
88
|
+
:param download_auth: (optional) The configuration of a plugin of type Authentication for download
|
|
89
|
+
:param kwargs: Additional configuration variables for this provider
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
name: str
|
|
93
|
+
group: str
|
|
94
|
+
priority: int = 0
|
|
95
|
+
roles: list[str]
|
|
96
|
+
description: str
|
|
97
|
+
url: str
|
|
98
|
+
api: PluginConfig
|
|
99
|
+
search: PluginConfig
|
|
100
|
+
products: dict[str, Any]
|
|
101
|
+
download: PluginConfig
|
|
102
|
+
auth: PluginConfig
|
|
103
|
+
search_auth: PluginConfig
|
|
104
|
+
download_auth: PluginConfig
|
|
105
|
+
|
|
106
|
+
yaml_loader = yaml.Loader
|
|
107
|
+
yaml_dumper = yaml.SafeDumper
|
|
108
|
+
yaml_tag = "!provider"
|
|
109
|
+
|
|
110
|
+
def __setstate__(self, state: dict[str, Any]) -> None:
|
|
111
|
+
"""Apply defaults when building from yaml."""
|
|
112
|
+
self.__dict__.update(state)
|
|
113
|
+
self._apply_defaults()
|
|
114
|
+
|
|
115
|
+
def __contains__(self, key):
|
|
116
|
+
"""Check if a key is in the ProviderConfig."""
|
|
117
|
+
return key in self.__dict__
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def from_yaml(cls, loader: yaml.Loader, node: Any) -> Iterator[Self]:
|
|
121
|
+
"""Build a :class:`~eodag.api.provider.ProviderConfig` from Yaml"""
|
|
122
|
+
cls.validate(tuple(node_key.value for node_key, _ in node.value))
|
|
123
|
+
for node_key, node_value in node.value:
|
|
124
|
+
if node_key.value == "name":
|
|
125
|
+
node_value.value = slugify(node_value.value).replace("-", "_")
|
|
126
|
+
break
|
|
127
|
+
return loader.construct_yaml_object(node, cls)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_mapping(cls, mapping: dict[str, Any]) -> Self:
|
|
131
|
+
"""Build a :class:`~eodag.api.provider.ProviderConfig` from a mapping"""
|
|
132
|
+
cls.validate(mapping)
|
|
133
|
+
# Create a deep copy to avoid modifying the input dict or its nested structures
|
|
134
|
+
mapping_copy = deepcopy(mapping)
|
|
135
|
+
for key in PLUGINS_TOPICS_KEYS:
|
|
136
|
+
if not (_mapping := mapping_copy.get(key)):
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
if not isinstance(_mapping, dict):
|
|
140
|
+
_mapping = _mapping.__dict__
|
|
141
|
+
|
|
142
|
+
mapping_copy[key] = PluginConfig.from_mapping(_mapping)
|
|
143
|
+
c = cls()
|
|
144
|
+
c.__dict__.update(mapping_copy)
|
|
145
|
+
c._apply_defaults()
|
|
146
|
+
return c
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def validate(config_keys: Union[tuple[str, ...], dict[str, Any]]) -> None:
|
|
150
|
+
"""Validate a :class:`~eodag.api.provider.ProviderConfig`
|
|
151
|
+
|
|
152
|
+
:param config_keys: The configurations keys to validate
|
|
153
|
+
"""
|
|
154
|
+
if "name" not in config_keys:
|
|
155
|
+
raise ValidationError("Provider config must have name key")
|
|
156
|
+
if not any(k in config_keys for k in PLUGINS_TOPICS_KEYS):
|
|
157
|
+
raise ValidationError("A provider must implement at least one plugin")
|
|
158
|
+
non_api_keys = [k for k in PLUGINS_TOPICS_KEYS if k != "api"]
|
|
159
|
+
if "api" in config_keys and any(k in config_keys for k in non_api_keys):
|
|
160
|
+
raise ValidationError(
|
|
161
|
+
"A provider implementing an Api plugin must not implement any other "
|
|
162
|
+
"type of plugin"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def update(self, config: Union[Self, dict[str, Any]]) -> None:
|
|
166
|
+
"""Update the configuration parameters with values from `mapping`
|
|
167
|
+
|
|
168
|
+
:param config: The config from which to override configuration parameters
|
|
169
|
+
"""
|
|
170
|
+
source = config if isinstance(config, dict) else config.__dict__
|
|
171
|
+
|
|
172
|
+
merge_mappings(
|
|
173
|
+
self.__dict__,
|
|
174
|
+
{
|
|
175
|
+
key: value
|
|
176
|
+
for key, value in source.items()
|
|
177
|
+
if key not in PLUGINS_TOPICS_KEYS and value is not None
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
for key in PLUGINS_TOPICS_KEYS:
|
|
181
|
+
current_value: Optional[PluginConfig] = getattr(self, key, None)
|
|
182
|
+
config_value = source.get(key, {})
|
|
183
|
+
if current_value is not None:
|
|
184
|
+
current_value |= config_value
|
|
185
|
+
elif isinstance(config_value, PluginConfig):
|
|
186
|
+
setattr(self, key, config_value)
|
|
187
|
+
elif config_value:
|
|
188
|
+
try:
|
|
189
|
+
setattr(self, key, PluginConfig.from_mapping(config_value))
|
|
190
|
+
except ValidationError as e:
|
|
191
|
+
logger.warning(
|
|
192
|
+
(
|
|
193
|
+
"Could not add %s Plugin config to %s configuration: %s. "
|
|
194
|
+
"Try updating existing %s Plugin configs instead."
|
|
195
|
+
),
|
|
196
|
+
key,
|
|
197
|
+
self.name,
|
|
198
|
+
str(e),
|
|
199
|
+
", ".join([k for k in PLUGINS_TOPICS_KEYS if hasattr(self, k)]),
|
|
200
|
+
)
|
|
201
|
+
self._apply_defaults()
|
|
202
|
+
|
|
203
|
+
def with_name(self, new_name: str) -> Self:
|
|
204
|
+
"""Create a copy of this :class:`~eodag.api.provider.ProviderConfig` with a different name.
|
|
205
|
+
|
|
206
|
+
:param new_name: The new name for the provider config.
|
|
207
|
+
:return: A new ProviderConfig instance with the updated name.
|
|
208
|
+
"""
|
|
209
|
+
config_dict = self.__dict__.copy()
|
|
210
|
+
config_dict["name"] = new_name
|
|
211
|
+
|
|
212
|
+
for key in PLUGINS_TOPICS_KEYS:
|
|
213
|
+
if key in config_dict and isinstance(config_dict[key], PluginConfig):
|
|
214
|
+
config_dict[key] = config_dict[key].__dict__
|
|
215
|
+
|
|
216
|
+
return self.__class__.from_mapping(config_dict)
|
|
217
|
+
|
|
218
|
+
def _apply_defaults(self: Self) -> None:
|
|
219
|
+
"""Applies some default values to provider config."""
|
|
220
|
+
stac_search_default_conf = load_stac_provider_config()
|
|
221
|
+
|
|
222
|
+
# For the provider, set the default output_dir of its download plugin
|
|
223
|
+
# as tempdir in a portable way
|
|
224
|
+
for download_topic_key in ("download", "api"):
|
|
225
|
+
if download_topic_key in vars(self):
|
|
226
|
+
download_conf = getattr(self, download_topic_key)
|
|
227
|
+
if not getattr(download_conf, "output_dir", None):
|
|
228
|
+
download_conf.output_dir = tempfile.gettempdir()
|
|
229
|
+
if not getattr(download_conf, "delete_archive", None):
|
|
230
|
+
download_conf.delete_archive = True
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
if (
|
|
234
|
+
stac_search_default_conf is not None
|
|
235
|
+
and self.search
|
|
236
|
+
and self.search.type in STAC_SEARCH_PLUGINS
|
|
237
|
+
):
|
|
238
|
+
# search config set to stac defaults overriden with provider config
|
|
239
|
+
per_provider_stac_provider_config = deepcopy(stac_search_default_conf)
|
|
240
|
+
self.search.__dict__ = update_nested_dict(
|
|
241
|
+
per_provider_stac_provider_config["search"],
|
|
242
|
+
self.search.__dict__,
|
|
243
|
+
allow_empty_values=True,
|
|
244
|
+
)
|
|
245
|
+
except AttributeError:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class Provider:
|
|
250
|
+
"""
|
|
251
|
+
Represents a data provider with its configuration and utility methods.
|
|
252
|
+
|
|
253
|
+
:param config: Provider configuration as :meth:`~eodag.api.provider.ProviderConfig` instance or :class:`dict`
|
|
254
|
+
:param collections_fetched: Flag indicating whether collections have been fetched
|
|
255
|
+
|
|
256
|
+
Example
|
|
257
|
+
-------
|
|
258
|
+
|
|
259
|
+
>>> from eodag.api.provider import Provider
|
|
260
|
+
>>> config = {
|
|
261
|
+
... 'name': 'example_provider',
|
|
262
|
+
... 'description': 'Example provider for testing',
|
|
263
|
+
... 'search': {'type': 'StacSearch'},
|
|
264
|
+
... 'products': {'S2_MSI_L1C': {'_collection': 'S2_MSI_L1C'}}
|
|
265
|
+
... }
|
|
266
|
+
>>> provider = Provider(config)
|
|
267
|
+
>>> provider.name
|
|
268
|
+
'example_provider'
|
|
269
|
+
>>> 'S2_MSI_L1C' in provider.collections_config
|
|
270
|
+
True
|
|
271
|
+
>>> provider.priority # Default priority
|
|
272
|
+
0
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
_name: str
|
|
276
|
+
_config: ProviderConfig
|
|
277
|
+
collections_fetched: bool
|
|
278
|
+
|
|
279
|
+
def __init__(self, config: Union[ProviderConfig, dict[str, Any]]):
|
|
280
|
+
"""Initialize provider with configuration."""
|
|
281
|
+
if isinstance(config, dict):
|
|
282
|
+
self._config = ProviderConfig.from_mapping(config)
|
|
283
|
+
elif isinstance(config, ProviderConfig):
|
|
284
|
+
self._config = config
|
|
285
|
+
else:
|
|
286
|
+
msg = f"Unsupported config type: {type(config)}. Expected ProviderConfig or dict."
|
|
287
|
+
raise ValidationError(msg)
|
|
288
|
+
|
|
289
|
+
self._name = self._config.name
|
|
290
|
+
self.collections_fetched = False
|
|
291
|
+
|
|
292
|
+
def __str__(self) -> str:
|
|
293
|
+
"""Return the provider's name as string."""
|
|
294
|
+
return self.name
|
|
295
|
+
|
|
296
|
+
def __repr__(self) -> str:
|
|
297
|
+
"""Return a string representation of the Provider."""
|
|
298
|
+
return f"Provider('{self.name}')"
|
|
299
|
+
|
|
300
|
+
def __eq__(self, other: object):
|
|
301
|
+
"""Compare providers by name or with a string."""
|
|
302
|
+
if isinstance(other, Provider):
|
|
303
|
+
return self.name == other.name
|
|
304
|
+
|
|
305
|
+
elif isinstance(other, str):
|
|
306
|
+
return self.name == other
|
|
307
|
+
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
def __hash__(self):
|
|
311
|
+
"""Hash based on provider name, for use in sets/dicts."""
|
|
312
|
+
return hash(self.name)
|
|
313
|
+
|
|
314
|
+
def _repr_html_(self, embedded: bool = False) -> str:
|
|
315
|
+
"""HTML representation for Jupyter/IPython display."""
|
|
316
|
+
group_display = f" ({self.group})" if self.group else ""
|
|
317
|
+
thead = (
|
|
318
|
+
f"""<thead><tr><td style='text-align: left; color: grey;'>
|
|
319
|
+
{type(self).__name__}("<span style='color: black'>{self.name}{group_display}</span>")</td></tr></thead>
|
|
320
|
+
"""
|
|
321
|
+
if not embedded
|
|
322
|
+
else ""
|
|
323
|
+
)
|
|
324
|
+
tr_style = "style='background-color: transparent;'" if embedded else ""
|
|
325
|
+
|
|
326
|
+
summaries = {
|
|
327
|
+
"name": self.name,
|
|
328
|
+
"title": self.config.description or "",
|
|
329
|
+
"url": self.config.url or "",
|
|
330
|
+
"priority": self.priority,
|
|
331
|
+
}
|
|
332
|
+
if self.group:
|
|
333
|
+
summaries["group"] = self.group
|
|
334
|
+
|
|
335
|
+
col_html_table = dict_to_html_table(summaries, depth=1, brackets=False)
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
f"<table>{thead}<tbody>"
|
|
339
|
+
f"<tr {tr_style}><td style='text-align: left;'>"
|
|
340
|
+
f"{col_html_table}</td></tr>"
|
|
341
|
+
"</tbody></table>"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def config(self) -> ProviderConfig:
|
|
346
|
+
"""
|
|
347
|
+
Provider configuration (read-only assignment).
|
|
348
|
+
|
|
349
|
+
To update configuration safely, use :meth:`~eodag.api.provider.Provider.update_from_config`
|
|
350
|
+
which handles metadata mapping and other provider-specific logic.
|
|
351
|
+
|
|
352
|
+
Note: Direct config modification (``config.update()``, ``config.name = ...``)
|
|
353
|
+
bypasses important provider validation.
|
|
354
|
+
"""
|
|
355
|
+
return self._config
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def name(self) -> str:
|
|
359
|
+
"""The name of the provider."""
|
|
360
|
+
return self._name
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def title(self) -> Optional[str]:
|
|
364
|
+
"""The title of the provider."""
|
|
365
|
+
return getattr(self.config, "description", None)
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def url(self) -> Optional[str]:
|
|
369
|
+
"""The url of the provider."""
|
|
370
|
+
return getattr(self.config, "url", None)
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def collections_config(self) -> dict[str, Any]:
|
|
374
|
+
"""Return the collections configuration dictionary for this provider."""
|
|
375
|
+
return getattr(self.config, "products", {})
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def priority(self) -> int:
|
|
379
|
+
"""Return the provider's priority (default: 0)."""
|
|
380
|
+
return self.config.priority
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def group(self) -> Optional[str]:
|
|
384
|
+
"""Return the provider's group, if any."""
|
|
385
|
+
return getattr(self.config, "group", None)
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def search_config(self) -> Optional[PluginConfig]:
|
|
389
|
+
"""Return the search plugin config, if any."""
|
|
390
|
+
return getattr(self.config, "search", None) or getattr(self.config, "api", None)
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def fetchable(self) -> bool:
|
|
394
|
+
"""Return True if the provider can fetch collections."""
|
|
395
|
+
return bool(
|
|
396
|
+
getattr(self.search_config, "discover_collections", {}).get("fetch_url")
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def unparsable_properties(self) -> set[str]:
|
|
401
|
+
"""Return set of unparsable properties from
|
|
402
|
+
:attr:`~eodag.config.PluginConfig.DiscoverCollections.generic_collection_unparsable_properties`, if any.
|
|
403
|
+
"""
|
|
404
|
+
if not self.fetchable or self.search_config is None:
|
|
405
|
+
return set()
|
|
406
|
+
|
|
407
|
+
props = getattr(
|
|
408
|
+
getattr(self.search_config, "discover_collections", None),
|
|
409
|
+
"generic_collection_unparsable_properties",
|
|
410
|
+
{},
|
|
411
|
+
)
|
|
412
|
+
return set(props.keys()) if isinstance(props, dict) else set()
|
|
413
|
+
|
|
414
|
+
@property
|
|
415
|
+
def api_config(self) -> Optional[PluginConfig]:
|
|
416
|
+
"""Return the api plugin config, if any."""
|
|
417
|
+
return getattr(self.config, "api", None)
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def download_config(self) -> Optional[PluginConfig]:
|
|
421
|
+
"""Return the download plugin config, if any."""
|
|
422
|
+
return getattr(self.config, "download", None)
|
|
423
|
+
|
|
424
|
+
def _get_auth_confs_with_credentials(self) -> list[PluginConfig]:
|
|
425
|
+
"""
|
|
426
|
+
Collect all auth configs from the provider that have credentials.
|
|
427
|
+
|
|
428
|
+
:return: List of auth plugin configs with credentials.
|
|
429
|
+
"""
|
|
430
|
+
return [
|
|
431
|
+
getattr(self.config, auth_key)
|
|
432
|
+
for auth_key in AUTH_TOPIC_KEYS
|
|
433
|
+
if hasattr(self.config, auth_key)
|
|
434
|
+
and credentials_in_auth(getattr(self.config, auth_key))
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
def _copy_matching_credentials(
|
|
438
|
+
self,
|
|
439
|
+
auth_confs_with_creds: list[PluginConfig],
|
|
440
|
+
) -> None:
|
|
441
|
+
"""
|
|
442
|
+
Copy credentials from matching auth configs to the target auth config.
|
|
443
|
+
|
|
444
|
+
:param auth_confs_with_creds: Auth configs with credentials.
|
|
445
|
+
"""
|
|
446
|
+
for key in AUTH_TOPIC_KEYS:
|
|
447
|
+
provider_auth_config = getattr(self.config, key, None)
|
|
448
|
+
if provider_auth_config and not credentials_in_auth(provider_auth_config):
|
|
449
|
+
for conf_with_creds in auth_confs_with_creds:
|
|
450
|
+
if conf_with_creds.matches_target_auth(provider_auth_config):
|
|
451
|
+
getattr(
|
|
452
|
+
self.config, key
|
|
453
|
+
).credentials = conf_with_creds.credentials
|
|
454
|
+
|
|
455
|
+
def delete_collection(self, name: str) -> None:
|
|
456
|
+
"""Remove a collection from this provider.
|
|
457
|
+
|
|
458
|
+
:param name: The collection name.
|
|
459
|
+
|
|
460
|
+
:raises UnsupportedCollection: If the collection is not found.
|
|
461
|
+
"""
|
|
462
|
+
try:
|
|
463
|
+
del self.collections_config[name]
|
|
464
|
+
except KeyError:
|
|
465
|
+
msg = f"Collection '{name}' not found in provider '{self.name}'."
|
|
466
|
+
raise UnsupportedCollection(msg)
|
|
467
|
+
|
|
468
|
+
def sync_collections(
|
|
469
|
+
self,
|
|
470
|
+
dag: EODataAccessGateway,
|
|
471
|
+
strict_mode: bool,
|
|
472
|
+
) -> None:
|
|
473
|
+
"""
|
|
474
|
+
Synchronize collections for a provider based on strict or permissive mode.
|
|
475
|
+
|
|
476
|
+
In strict mode, removes collections not in :attr:`~eodag.api.core.EODataAccessGateway.collections_config`.
|
|
477
|
+
In permissive mode, adds empty collection to config for missing types.
|
|
478
|
+
|
|
479
|
+
:param dag: The gateway instance to use to list existing collections and to create new collection instances.
|
|
480
|
+
:param strict_mode: If ``True``, remove unknown collections; if ``False``, add empty configs for them.
|
|
481
|
+
"""
|
|
482
|
+
products_to_remove: list[str] = []
|
|
483
|
+
products_to_add: list[str] = []
|
|
484
|
+
|
|
485
|
+
for product_id in self.collections_config:
|
|
486
|
+
if product_id == GENERIC_COLLECTION:
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
if product_id not in dag.collections_config:
|
|
490
|
+
if strict_mode:
|
|
491
|
+
products_to_remove.append(product_id)
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
empty_product = Collection.create_with_dag(
|
|
495
|
+
dag, id=product_id, title=product_id, description=NOT_AVAILABLE
|
|
496
|
+
)
|
|
497
|
+
dag.collections_config[product_id] = empty_product
|
|
498
|
+
products_to_add.append(product_id)
|
|
499
|
+
|
|
500
|
+
if products_to_add:
|
|
501
|
+
logger.debug(
|
|
502
|
+
"Collections permissive mode, %s added (provider %s)",
|
|
503
|
+
", ".join(products_to_add),
|
|
504
|
+
self,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
if products_to_remove:
|
|
508
|
+
logger.debug(
|
|
509
|
+
"Collections strict mode, ignoring %s (provider %s)",
|
|
510
|
+
", ".join(products_to_remove),
|
|
511
|
+
self,
|
|
512
|
+
)
|
|
513
|
+
for id in products_to_remove:
|
|
514
|
+
self.delete_collection(id)
|
|
515
|
+
|
|
516
|
+
def _mm_already_built(self) -> bool:
|
|
517
|
+
"""Check if metadata mapping is already built (converted to querypaths/conversion)."""
|
|
518
|
+
mm = getattr(self.search_config, "metadata_mapping", None)
|
|
519
|
+
if not mm:
|
|
520
|
+
return False
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
first = next(iter(mm.values()))
|
|
524
|
+
except StopIteration:
|
|
525
|
+
return False
|
|
526
|
+
|
|
527
|
+
# Consider it built if it's a tuple, or a list with second element as tuple
|
|
528
|
+
if isinstance(first, tuple):
|
|
529
|
+
return True
|
|
530
|
+
if isinstance(first, list) and len(first) > 1 and isinstance(first[1], tuple):
|
|
531
|
+
return True
|
|
532
|
+
|
|
533
|
+
return False
|
|
534
|
+
|
|
535
|
+
def update_from_config(self, config: Union[ProviderConfig, dict[str, Any]]) -> None:
|
|
536
|
+
"""Update the provider's configuration from a given config.
|
|
537
|
+
|
|
538
|
+
:param config: The new configuration to update from.
|
|
539
|
+
:raises ValidationError: If the config attempts to change the provider name.
|
|
540
|
+
"""
|
|
541
|
+
# Prevent name changes to maintain provider identity
|
|
542
|
+
source = config if isinstance(config, dict) else config.__dict__
|
|
543
|
+
if (new_name := source.get("name")) and new_name != self._name:
|
|
544
|
+
raise ValidationError(
|
|
545
|
+
f"Cannot change provider name from '{self._name}' to '{new_name}'. "
|
|
546
|
+
"Provider names are immutable after creation."
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
# check if metadata mapping is already built for that provider
|
|
550
|
+
# this happens when the provider search plugin has already been used
|
|
551
|
+
search_key = "search" if "search" in self.config else "api"
|
|
552
|
+
new_conf_search = source.get(search_key, {}) or {}
|
|
553
|
+
|
|
554
|
+
if "metadata_mapping" in new_conf_search and self._mm_already_built():
|
|
555
|
+
mtd_cfg_as_conversion_and_querypath(
|
|
556
|
+
deepcopy(new_conf_search["metadata_mapping"]),
|
|
557
|
+
new_conf_search["metadata_mapping"],
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
self.config.update(config)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
class ProvidersDict(UserDict[str, Provider]):
|
|
564
|
+
"""
|
|
565
|
+
A dictionary-like collection of :class:`~eodag.api.provider.Provider` objects, keyed by provider name.
|
|
566
|
+
|
|
567
|
+
:param providers: Initial providers to populate the dictionary.
|
|
568
|
+
"""
|
|
569
|
+
|
|
570
|
+
def __contains__(self, item: object) -> bool:
|
|
571
|
+
"""
|
|
572
|
+
Check if a provider is in the dictionary by name or :class:`~eodag.api.provider.Provider` instance.
|
|
573
|
+
|
|
574
|
+
:param item: Provider name or Provider instance to check.
|
|
575
|
+
:return: True if the provider is in the dictionary, False otherwise.
|
|
576
|
+
"""
|
|
577
|
+
if isinstance(item, Provider):
|
|
578
|
+
return item.name in self.data
|
|
579
|
+
return item in self.data
|
|
580
|
+
|
|
581
|
+
def __setitem__(self, key: str, value: Provider) -> None:
|
|
582
|
+
"""
|
|
583
|
+
Add a :class:`~eodag.api.provider.Provider` to the dictionary.
|
|
584
|
+
|
|
585
|
+
:param key: The name of the provider.
|
|
586
|
+
:param value: The Provider instance to add.
|
|
587
|
+
:raises ValueError: If the provider key already exists.
|
|
588
|
+
"""
|
|
589
|
+
if key in self.data:
|
|
590
|
+
msg = f"Provider '{key}' already exists."
|
|
591
|
+
raise ValueError(msg)
|
|
592
|
+
super().__setitem__(key, value)
|
|
593
|
+
|
|
594
|
+
def __delitem__(self, key: str) -> None:
|
|
595
|
+
"""
|
|
596
|
+
Delete a provider by name.
|
|
597
|
+
|
|
598
|
+
:param key: The name of the provider to delete.
|
|
599
|
+
:raises UnsupportedProvider: If the provider key is not found.
|
|
600
|
+
"""
|
|
601
|
+
if key not in self.data:
|
|
602
|
+
msg = f"Provider '{key}' not found."
|
|
603
|
+
raise UnsupportedProvider(msg)
|
|
604
|
+
super().__delitem__(key)
|
|
605
|
+
|
|
606
|
+
def __repr__(self) -> str:
|
|
607
|
+
"""
|
|
608
|
+
String representation of :class:`~eodag.api.provider.ProvidersDict`.
|
|
609
|
+
|
|
610
|
+
:return: String listing provider names.
|
|
611
|
+
"""
|
|
612
|
+
return f"ProvidersDict({list(self.data.keys())})"
|
|
613
|
+
|
|
614
|
+
def _repr_html_(self, embeded=False) -> str:
|
|
615
|
+
"""
|
|
616
|
+
HTML representation for Jupyter/IPython display.
|
|
617
|
+
|
|
618
|
+
:return: HTML string representation of the :class:`~eodag.api.provider.ProvidersDict`.
|
|
619
|
+
"""
|
|
620
|
+
longest_name = max([len(k) for k in self.keys()])
|
|
621
|
+
thead = (
|
|
622
|
+
f"""<thead><tr><td style='text-align: left; color: grey;'>
|
|
623
|
+
{type(self).__name__} ({len(self)})
|
|
624
|
+
</td></tr></thead>
|
|
625
|
+
"""
|
|
626
|
+
if not embeded
|
|
627
|
+
else ""
|
|
628
|
+
)
|
|
629
|
+
tr_style = "style='background-color: transparent;'" if embeded else ""
|
|
630
|
+
return (
|
|
631
|
+
f"<table>{thead}"
|
|
632
|
+
+ "".join(
|
|
633
|
+
[
|
|
634
|
+
f"""<tr {tr_style}><td style='text-align: left;'>
|
|
635
|
+
<details><summary style='color: grey;'>
|
|
636
|
+
<span style='color: black; font-family: monospace;'>{k}:{' ' * (longest_name - len(k))}</span>
|
|
637
|
+
Provider(
|
|
638
|
+
{"'priority': '<span style='color: black'>" + str(v.priority) + "</span>', "
|
|
639
|
+
if v.priority is not None else ""}
|
|
640
|
+
{"'title': '<span style='color: black'>"
|
|
641
|
+
+ shorten(v.title, width=70, placeholder="[...]") + "</span>', "
|
|
642
|
+
if v.title else ""}
|
|
643
|
+
{"'url': '" + str_as_href(v.url) + "'" if v.url else ""}
|
|
644
|
+
)
|
|
645
|
+
</summary>
|
|
646
|
+
{v._repr_html_(embedded=True)}
|
|
647
|
+
</details>
|
|
648
|
+
</td></tr>
|
|
649
|
+
"""
|
|
650
|
+
for k, v in self.items()
|
|
651
|
+
]
|
|
652
|
+
)
|
|
653
|
+
+ "</table>"
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
@property
|
|
657
|
+
def names(self) -> list[str]:
|
|
658
|
+
"""
|
|
659
|
+
List of provider names.
|
|
660
|
+
|
|
661
|
+
:return: List of provider names.
|
|
662
|
+
"""
|
|
663
|
+
return [provider.name for provider in self.data.values()]
|
|
664
|
+
|
|
665
|
+
@property
|
|
666
|
+
def groups(self) -> list[str]:
|
|
667
|
+
"""
|
|
668
|
+
List of provider groups if exist or names.
|
|
669
|
+
|
|
670
|
+
:return: List of provider groups if exist or names.
|
|
671
|
+
"""
|
|
672
|
+
return list(
|
|
673
|
+
set(provider.group or provider.name for provider in self.data.values())
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
@property
|
|
677
|
+
def configs(self) -> dict[str, ProviderConfig]:
|
|
678
|
+
"""
|
|
679
|
+
Dictionary of provider configs keyed by provider name.
|
|
680
|
+
|
|
681
|
+
:return: Dictionary mapping provider name to :class:`~eodag.api.provider.ProviderConfig`.
|
|
682
|
+
"""
|
|
683
|
+
return {provider.name: provider.config for provider in self.data.values()}
|
|
684
|
+
|
|
685
|
+
@property
|
|
686
|
+
def priorities(self) -> dict[str, int]:
|
|
687
|
+
"""
|
|
688
|
+
Dictionary of provider priorities keyed by provider name.
|
|
689
|
+
|
|
690
|
+
:return: Dictionary mapping provider name to priority integer.
|
|
691
|
+
"""
|
|
692
|
+
return {
|
|
693
|
+
provider.name: provider.config.priority for provider in self.data.values()
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
def get_config(self, provider: str) -> Optional[ProviderConfig]:
|
|
697
|
+
"""
|
|
698
|
+
Get a :class:`~eodag.api.provider.ProviderConfig` from provider name.
|
|
699
|
+
|
|
700
|
+
:param provider: The provider name.
|
|
701
|
+
:return: The :class:`~eodag.api.provider.ProviderConfig` if found, otherwise None.
|
|
702
|
+
"""
|
|
703
|
+
prov = self.get(provider)
|
|
704
|
+
return prov.config if prov else None
|
|
705
|
+
|
|
706
|
+
def filter(self, q: Optional[str] = None) -> ProvidersDict:
|
|
707
|
+
"""
|
|
708
|
+
Return providers whose name, group, description, URL or collection matches the free-text query.
|
|
709
|
+
|
|
710
|
+
Supports logical operators with parenthesis (``AND``/``OR``/``NOT``), quoted phrases (``"exact phrase"``),
|
|
711
|
+
``*`` and ``?`` wildcards.
|
|
712
|
+
|
|
713
|
+
If no query is provided, returns all providers.
|
|
714
|
+
|
|
715
|
+
:param q: Free-text parameter to filter providers. If None, returns all providers.
|
|
716
|
+
:return: matching Provider objects in a :class:`~eodag.api.provider.ProvidersDict`.
|
|
717
|
+
|
|
718
|
+
Example
|
|
719
|
+
-------
|
|
720
|
+
|
|
721
|
+
>>> from eodag.api.provider import ProvidersDict, Provider
|
|
722
|
+
>>> providers = ProvidersDict()
|
|
723
|
+
>>> providers['test1'] = Provider({
|
|
724
|
+
... 'name': 'test1',
|
|
725
|
+
... 'description': 'Satellite data',
|
|
726
|
+
... 'search': {'type': 'StacSearch'}
|
|
727
|
+
... })
|
|
728
|
+
>>> providers['test2'] = Provider({
|
|
729
|
+
... 'name': 'test2',
|
|
730
|
+
... 'description': 'Weather data',
|
|
731
|
+
... 'search': {'type': 'StacSearch'}
|
|
732
|
+
... })
|
|
733
|
+
>>> # Filter by description content
|
|
734
|
+
>>> providers.filter('Satellite')
|
|
735
|
+
ProvidersDict(['test1'])
|
|
736
|
+
>>> # Filter with logical operators
|
|
737
|
+
>>> providers['test3'] = Provider({
|
|
738
|
+
... 'name': 'test3',
|
|
739
|
+
... 'description': 'Satellite weather data',
|
|
740
|
+
... 'search': {'type': 'StacSearch'}
|
|
741
|
+
... })
|
|
742
|
+
>>> providers.filter('Satellite AND weather')
|
|
743
|
+
ProvidersDict(['test3'])
|
|
744
|
+
>>> # Get all providers when no filter
|
|
745
|
+
>>> len(providers.filter())
|
|
746
|
+
3
|
|
747
|
+
"""
|
|
748
|
+
if not q:
|
|
749
|
+
# yield from self.data.values()
|
|
750
|
+
return self
|
|
751
|
+
|
|
752
|
+
free_text_query = compile_free_text_query(q)
|
|
753
|
+
searchable_attributes = {"name", "group", "description", "products"}
|
|
754
|
+
|
|
755
|
+
filtered = ProvidersDict()
|
|
756
|
+
for p in self.data.values():
|
|
757
|
+
searchables = {
|
|
758
|
+
k: v for k, v in p.config.__dict__.items() if k in searchable_attributes
|
|
759
|
+
}
|
|
760
|
+
if free_text_query(searchables):
|
|
761
|
+
# yield p
|
|
762
|
+
filtered[p.name] = p
|
|
763
|
+
return filtered
|
|
764
|
+
|
|
765
|
+
def filter_by_name_or_group(
|
|
766
|
+
self, name_or_group: Optional[str] = None
|
|
767
|
+
) -> Iterator[Provider]:
|
|
768
|
+
"""
|
|
769
|
+
Yield providers whose name or group matches the given ``name_or_group``.
|
|
770
|
+
|
|
771
|
+
If ``name_or_group`` is ``None``, yields all providers.
|
|
772
|
+
|
|
773
|
+
:param name_or_group: The provider name or group to filter by. If None, yields all providers.
|
|
774
|
+
:return: Iterator of matching :class:`~eodag.api.provider.Provider` objects.
|
|
775
|
+
|
|
776
|
+
Example
|
|
777
|
+
-------
|
|
778
|
+
|
|
779
|
+
>>> from eodag.api.provider import ProvidersDict, Provider
|
|
780
|
+
>>> providers = ProvidersDict()
|
|
781
|
+
>>> providers['sentinel'] = Provider({'name': 'sentinel', 'group': 'esa', 'search': {'type': 'StacSearch'}})
|
|
782
|
+
>>> providers['landsat'] = Provider({'name': 'landsat', 'group': 'usgs', 'search': {'type': 'StacSearch'}})
|
|
783
|
+
>>> providers['modis'] = Provider({'name': 'modis', 'group': 'nasa', 'search': {'type': 'StacSearch'}})
|
|
784
|
+
>>>
|
|
785
|
+
>>> # Filter by exact provider name
|
|
786
|
+
>>> list(p.name for p in providers.filter_by_name_or_group('sentinel'))
|
|
787
|
+
['sentinel']
|
|
788
|
+
>>>
|
|
789
|
+
>>> # Filter by group (case-insensitive)
|
|
790
|
+
>>> list(p.name for p in providers.filter_by_name_or_group('ESA'))
|
|
791
|
+
['sentinel']
|
|
792
|
+
>>>
|
|
793
|
+
>>> # Get all providers when no filter
|
|
794
|
+
>>> len(list(providers.filter_by_name_or_group()))
|
|
795
|
+
3
|
|
796
|
+
"""
|
|
797
|
+
if name_or_group is None:
|
|
798
|
+
yield from self.data.values()
|
|
799
|
+
return
|
|
800
|
+
|
|
801
|
+
name_or_group_lower = name_or_group.lower()
|
|
802
|
+
for provider in self.data.values():
|
|
803
|
+
if provider.name.lower() == name_or_group_lower or (
|
|
804
|
+
provider.group and provider.group.lower() == name_or_group_lower
|
|
805
|
+
):
|
|
806
|
+
yield provider
|
|
807
|
+
|
|
808
|
+
def delete_collection(self, provider: str, collection: str) -> None:
|
|
809
|
+
"""
|
|
810
|
+
Delete a collection from a provider.
|
|
811
|
+
|
|
812
|
+
:param provider: The provider's name.
|
|
813
|
+
:param product_ID: The collection to delete.
|
|
814
|
+
:raises UnsupportedProvider: If the provider or product is not found.
|
|
815
|
+
"""
|
|
816
|
+
if provider_obj := self.get(provider):
|
|
817
|
+
if collection in provider_obj.collections_config:
|
|
818
|
+
provider_obj.delete_collection(collection)
|
|
819
|
+
else:
|
|
820
|
+
msg = f"Collection '{collection}' not found for provider '{provider}'."
|
|
821
|
+
raise UnsupportedCollection(msg)
|
|
822
|
+
else:
|
|
823
|
+
msg = f"Provider '{provider}' not found."
|
|
824
|
+
raise UnsupportedProvider(msg)
|
|
825
|
+
|
|
826
|
+
def _share_credentials(self) -> None:
|
|
827
|
+
"""
|
|
828
|
+
Share credentials between plugins with matching criteria
|
|
829
|
+
across all providers in this dictionary.
|
|
830
|
+
"""
|
|
831
|
+
auth_confs_with_creds: list[PluginConfig] = []
|
|
832
|
+
for provider in self.values():
|
|
833
|
+
auth_confs_with_creds.extend(provider._get_auth_confs_with_credentials())
|
|
834
|
+
|
|
835
|
+
if not auth_confs_with_creds:
|
|
836
|
+
return
|
|
837
|
+
|
|
838
|
+
for provider in self.values():
|
|
839
|
+
provider._copy_matching_credentials(auth_confs_with_creds)
|
|
840
|
+
|
|
841
|
+
@staticmethod
|
|
842
|
+
def _get_whitelisted_configs(
|
|
843
|
+
configs: Mapping[str, Union[ProviderConfig, dict[str, Any]]],
|
|
844
|
+
) -> Mapping[str, Union[ProviderConfig, dict[str, Any]]]:
|
|
845
|
+
"""
|
|
846
|
+
Filter configs according to the EODAG_PROVIDERS_WHITELIST environment variable, if set.
|
|
847
|
+
|
|
848
|
+
:param configs: The dictionary of provider configurations.
|
|
849
|
+
:return: Filtered configurations.
|
|
850
|
+
"""
|
|
851
|
+
whitelist = set(os.getenv("EODAG_PROVIDERS_WHITELIST", "").split(","))
|
|
852
|
+
if not whitelist or whitelist == {""}:
|
|
853
|
+
return configs
|
|
854
|
+
return {name: conf for name, conf in configs.items() if name in whitelist}
|
|
855
|
+
|
|
856
|
+
def update_from_configs(
|
|
857
|
+
self,
|
|
858
|
+
configs: Mapping[str, Union[ProviderConfig, dict[str, Any]]],
|
|
859
|
+
) -> None:
|
|
860
|
+
"""
|
|
861
|
+
Update providers from a dictionary of configurations.
|
|
862
|
+
|
|
863
|
+
:param configs: A dictionary mapping provider names to configurations.
|
|
864
|
+
"""
|
|
865
|
+
configs = self._get_whitelisted_configs(configs)
|
|
866
|
+
for name, conf in configs.items():
|
|
867
|
+
if isinstance(conf, dict) and conf.get("name") != name:
|
|
868
|
+
if "name" in conf:
|
|
869
|
+
logger.debug(
|
|
870
|
+
"%s: config name '%s' overridden by dict key",
|
|
871
|
+
name,
|
|
872
|
+
conf["name"],
|
|
873
|
+
)
|
|
874
|
+
conf = {**conf, "name": name}
|
|
875
|
+
elif isinstance(conf, ProviderConfig) and conf.name != name:
|
|
876
|
+
raise ValidationError(
|
|
877
|
+
f"ProviderConfig name '{conf.name}' must match dict key '{name}'"
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
try:
|
|
881
|
+
if name in self.data:
|
|
882
|
+
self.data[name].update_from_config(conf)
|
|
883
|
+
else:
|
|
884
|
+
self.data[name] = Provider(conf)
|
|
885
|
+
|
|
886
|
+
self.data[name].collections_fetched = False
|
|
887
|
+
|
|
888
|
+
except Exception:
|
|
889
|
+
operation = "updating" if name in self.data else "creating"
|
|
890
|
+
logger.warning("%s: skipped %s due to invalid config", name, operation)
|
|
891
|
+
logger.debug("Traceback:\n%s", traceback.format_exc())
|
|
892
|
+
|
|
893
|
+
self._share_credentials()
|
|
894
|
+
|
|
895
|
+
def update_from_config_file(self, file_path: str) -> None:
|
|
896
|
+
"""
|
|
897
|
+
Override provider configurations with values loaded from a YAML file.
|
|
898
|
+
|
|
899
|
+
:param file_path: The path to the configuration file.
|
|
900
|
+
:raises yaml.parser.ParserError: If the YAML file cannot be parsed.
|
|
901
|
+
"""
|
|
902
|
+
logger.info("Loading user configuration from: %s", os.path.abspath(file_path))
|
|
903
|
+
with open(os.path.abspath(os.path.realpath(file_path)), "r") as fh:
|
|
904
|
+
try:
|
|
905
|
+
config_in_file = yaml.safe_load(fh)
|
|
906
|
+
if config_in_file is None:
|
|
907
|
+
return
|
|
908
|
+
except yaml.parser.ParserError as e:
|
|
909
|
+
logger.error("Unable to load configuration file %s", file_path)
|
|
910
|
+
raise e
|
|
911
|
+
|
|
912
|
+
self.update_from_configs(config_in_file)
|
|
913
|
+
|
|
914
|
+
def update_from_env(self) -> None:
|
|
915
|
+
"""
|
|
916
|
+
Override provider configurations with environment variables values.
|
|
917
|
+
|
|
918
|
+
Environment variables must start with ``EODAG__`` and follow a nested key
|
|
919
|
+
pattern separated by double underscores ``__``.
|
|
920
|
+
"""
|
|
921
|
+
|
|
922
|
+
def build_mapping_from_env(
|
|
923
|
+
env_var: str, env_value: str, mapping: dict[str, Any]
|
|
924
|
+
) -> None:
|
|
925
|
+
"""
|
|
926
|
+
Recursively build a dictionary from an environment variable.
|
|
927
|
+
|
|
928
|
+
The environment variable must respect the pattern: ``KEY1__KEY2__[...]__KEYN``.
|
|
929
|
+
It will be transformed into a nested dictionary.
|
|
930
|
+
|
|
931
|
+
:param env_var: The environment variable key (nested keys separated by ``__``).
|
|
932
|
+
:param env_value: The value from environment variable.
|
|
933
|
+
:param mapping: The dictionary where the nested mapping is built.
|
|
934
|
+
"""
|
|
935
|
+
parts = env_var.split("__")
|
|
936
|
+
iter_parts = iter(parts)
|
|
937
|
+
env_type = get_type_hints(PluginConfig).get(next(iter_parts, ""), str)
|
|
938
|
+
child_env_type = (
|
|
939
|
+
get_type_hints(env_type).get(next(iter_parts, ""))
|
|
940
|
+
if isclass(env_type)
|
|
941
|
+
else None
|
|
942
|
+
)
|
|
943
|
+
if len(parts) == 2 and child_env_type:
|
|
944
|
+
try:
|
|
945
|
+
env_value = cast_scalar_value(env_value, child_env_type)
|
|
946
|
+
except TypeError:
|
|
947
|
+
logger.warning(
|
|
948
|
+
f"Could not convert {parts} value {env_value} to {child_env_type}"
|
|
949
|
+
)
|
|
950
|
+
mapping.setdefault(parts[0], {})
|
|
951
|
+
mapping[parts[0]][parts[1]] = env_value
|
|
952
|
+
elif len(parts) == 1:
|
|
953
|
+
try:
|
|
954
|
+
env_value = cast_scalar_value(env_value, env_type)
|
|
955
|
+
except TypeError:
|
|
956
|
+
logger.warning(
|
|
957
|
+
f"Could not convert {parts[0]} value {env_value} to {env_type}"
|
|
958
|
+
)
|
|
959
|
+
mapping[parts[0]] = env_value
|
|
960
|
+
else:
|
|
961
|
+
new_map = mapping.setdefault(parts[0], {})
|
|
962
|
+
build_mapping_from_env("__".join(parts[1:]), env_value, new_map)
|
|
963
|
+
|
|
964
|
+
logger.debug("Loading configuration from environment variables")
|
|
965
|
+
|
|
966
|
+
mapping_from_env: dict[str, dict[str, Any]] = {}
|
|
967
|
+
for env_var in os.environ:
|
|
968
|
+
if env_var.startswith("EODAG__"):
|
|
969
|
+
build_mapping_from_env(
|
|
970
|
+
env_var[len("EODAG__") :].lower(), # noqa
|
|
971
|
+
os.environ[env_var],
|
|
972
|
+
mapping_from_env,
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
self.update_from_configs(mapping_from_env)
|
|
976
|
+
|
|
977
|
+
@classmethod
|
|
978
|
+
def from_configs(
|
|
979
|
+
cls, configs: Mapping[str, Union[ProviderConfig, dict[str, Any]]]
|
|
980
|
+
) -> Self:
|
|
981
|
+
"""
|
|
982
|
+
Build a ProvidersDict from a configuration mapping.
|
|
983
|
+
|
|
984
|
+
:param configs: A dictionary mapping provider names to configuration dicts or
|
|
985
|
+
:class:`~eodag.api.provider.ProviderConfig` instances.
|
|
986
|
+
:return: An instance of :class:`~eodag.api.provider.ProvidersDict` populated with the given configurations.
|
|
987
|
+
"""
|
|
988
|
+
providers = cls()
|
|
989
|
+
providers.update_from_configs(configs)
|
|
990
|
+
return providers
|