provide-foundation 0.0.0.dev0__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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,311 @@
|
|
1
|
+
"""
|
2
|
+
Configuration loaders for various sources.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
from abc import ABC, abstractmethod
|
8
|
+
import json
|
9
|
+
import os
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import TypeVar
|
12
|
+
|
13
|
+
try:
|
14
|
+
import aiofiles
|
15
|
+
except ImportError:
|
16
|
+
aiofiles = None
|
17
|
+
|
18
|
+
from provide.foundation.config.base import BaseConfig
|
19
|
+
from provide.foundation.config.env import RuntimeConfig
|
20
|
+
from provide.foundation.config.types import ConfigDict, ConfigFormat, ConfigSource
|
21
|
+
from provide.foundation.errors.config import ConfigurationError
|
22
|
+
from provide.foundation.errors.decorators import with_error_handling
|
23
|
+
from provide.foundation.errors.resources import NotFoundError
|
24
|
+
|
25
|
+
T = TypeVar("T", bound=BaseConfig)
|
26
|
+
|
27
|
+
|
28
|
+
class ConfigLoader(ABC):
|
29
|
+
"""Abstract base class for configuration loaders."""
|
30
|
+
|
31
|
+
@abstractmethod
|
32
|
+
async def load(self, config_class: type[T]) -> T:
|
33
|
+
"""
|
34
|
+
Load configuration.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
config_class: Configuration class to instantiate
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Configuration instance
|
41
|
+
"""
|
42
|
+
pass
|
43
|
+
|
44
|
+
@abstractmethod
|
45
|
+
def exists(self) -> bool:
|
46
|
+
"""
|
47
|
+
Check if the configuration source exists.
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
True if source exists
|
51
|
+
"""
|
52
|
+
pass
|
53
|
+
|
54
|
+
|
55
|
+
class FileConfigLoader(ConfigLoader):
|
56
|
+
"""Load configuration from files."""
|
57
|
+
|
58
|
+
def __init__(
|
59
|
+
self,
|
60
|
+
path: str | Path,
|
61
|
+
format: ConfigFormat | None = None,
|
62
|
+
encoding: str = "utf-8",
|
63
|
+
) -> None:
|
64
|
+
"""
|
65
|
+
Initialize file configuration loader.
|
66
|
+
|
67
|
+
Args:
|
68
|
+
path: Path to configuration file
|
69
|
+
format: File format (auto-detected if None)
|
70
|
+
encoding: File encoding
|
71
|
+
"""
|
72
|
+
self.path = Path(path)
|
73
|
+
self.encoding = encoding
|
74
|
+
|
75
|
+
if format is None:
|
76
|
+
format = ConfigFormat.from_extension(str(self.path))
|
77
|
+
if format is None:
|
78
|
+
raise ConfigurationError(
|
79
|
+
f"Cannot determine format for file: {self.path}",
|
80
|
+
code="CONFIG_FORMAT_UNKNOWN",
|
81
|
+
path=str(self.path),
|
82
|
+
)
|
83
|
+
|
84
|
+
self.format = format
|
85
|
+
|
86
|
+
def exists(self) -> bool:
|
87
|
+
"""Check if configuration file exists."""
|
88
|
+
return self.path.exists()
|
89
|
+
|
90
|
+
@with_error_handling(
|
91
|
+
context_provider=lambda: {"loader": "FileLoader"},
|
92
|
+
error_mapper=lambda e: ConfigurationError(
|
93
|
+
f"Failed to load configuration: {e}", code="CONFIG_LOAD_ERROR", cause=e
|
94
|
+
)
|
95
|
+
if not isinstance(e, ConfigurationError | NotFoundError)
|
96
|
+
else e,
|
97
|
+
)
|
98
|
+
async def load(self, config_class: type[T]) -> T:
|
99
|
+
"""Load configuration from file."""
|
100
|
+
if not self.exists():
|
101
|
+
raise NotFoundError(
|
102
|
+
f"Configuration file not found: {self.path}",
|
103
|
+
code="CONFIG_FILE_NOT_FOUND",
|
104
|
+
path=str(self.path),
|
105
|
+
)
|
106
|
+
|
107
|
+
data = await self._read_file()
|
108
|
+
return config_class.from_dict(data, source=ConfigSource.FILE)
|
109
|
+
|
110
|
+
async def _read_file(self) -> ConfigDict:
|
111
|
+
"""Read and parse configuration file."""
|
112
|
+
if aiofiles:
|
113
|
+
async with aiofiles.open(self.path, encoding=self.encoding) as f:
|
114
|
+
content = await f.read()
|
115
|
+
else:
|
116
|
+
# Fallback to synchronous read
|
117
|
+
with open(self.path, encoding=self.encoding) as f:
|
118
|
+
content = f.read()
|
119
|
+
|
120
|
+
if self.format == ConfigFormat.JSON:
|
121
|
+
return json.loads(content)
|
122
|
+
elif self.format == ConfigFormat.YAML:
|
123
|
+
import yaml
|
124
|
+
|
125
|
+
return yaml.safe_load(content)
|
126
|
+
elif self.format == ConfigFormat.TOML:
|
127
|
+
try:
|
128
|
+
import tomllib
|
129
|
+
except ImportError:
|
130
|
+
import tomli as tomllib
|
131
|
+
return tomllib.loads(content)
|
132
|
+
elif self.format == ConfigFormat.INI:
|
133
|
+
import configparser
|
134
|
+
|
135
|
+
parser = configparser.ConfigParser()
|
136
|
+
parser.read_string(content)
|
137
|
+
return self._ini_to_dict(parser)
|
138
|
+
elif self.format == ConfigFormat.ENV:
|
139
|
+
return self._parse_env_file(content)
|
140
|
+
else:
|
141
|
+
raise ConfigurationError(
|
142
|
+
f"Unsupported format: {self.format}",
|
143
|
+
code="CONFIG_FORMAT_UNSUPPORTED",
|
144
|
+
format=str(self.format),
|
145
|
+
)
|
146
|
+
|
147
|
+
def _ini_to_dict(self, parser) -> ConfigDict:
|
148
|
+
"""Convert INI parser to dictionary."""
|
149
|
+
result = {}
|
150
|
+
for section in parser.sections():
|
151
|
+
result[section] = dict(parser.items(section))
|
152
|
+
|
153
|
+
# Include DEFAULT section if present
|
154
|
+
if parser.defaults():
|
155
|
+
result["DEFAULT"] = dict(parser.defaults())
|
156
|
+
|
157
|
+
return result
|
158
|
+
|
159
|
+
def _parse_env_file(self, content: str) -> ConfigDict:
|
160
|
+
"""Parse .env file format."""
|
161
|
+
result = {}
|
162
|
+
|
163
|
+
for line in content.splitlines():
|
164
|
+
line = line.strip()
|
165
|
+
|
166
|
+
# Skip comments and empty lines
|
167
|
+
if not line or line.startswith("#"):
|
168
|
+
continue
|
169
|
+
|
170
|
+
# Parse key=value
|
171
|
+
if "=" in line:
|
172
|
+
key, value = line.split("=", 1)
|
173
|
+
key = key.strip().lower() # Convert to lowercase for compatibility
|
174
|
+
value = value.strip()
|
175
|
+
|
176
|
+
# Remove quotes if present
|
177
|
+
if (value.startswith('"') and value.endswith('"')) or (
|
178
|
+
value.startswith("'") and value.endswith("'")
|
179
|
+
):
|
180
|
+
value = value[1:-1]
|
181
|
+
|
182
|
+
result[key] = value
|
183
|
+
|
184
|
+
return result
|
185
|
+
|
186
|
+
|
187
|
+
class RuntimeConfigLoader(ConfigLoader):
|
188
|
+
"""Load configuration from environment variables."""
|
189
|
+
|
190
|
+
def __init__(
|
191
|
+
self, prefix: str = "", delimiter: str = "_", case_sensitive: bool = False
|
192
|
+
) -> None:
|
193
|
+
"""
|
194
|
+
Initialize environment configuration loader.
|
195
|
+
|
196
|
+
Args:
|
197
|
+
prefix: Prefix for environment variables
|
198
|
+
delimiter: Delimiter between prefix and field name
|
199
|
+
case_sensitive: Whether variable names are case-sensitive
|
200
|
+
"""
|
201
|
+
self.prefix = prefix
|
202
|
+
self.delimiter = delimiter
|
203
|
+
self.case_sensitive = case_sensitive
|
204
|
+
|
205
|
+
def exists(self) -> bool:
|
206
|
+
"""Check if any relevant environment variables exist."""
|
207
|
+
if self.prefix:
|
208
|
+
prefix_with_delim = f"{self.prefix}{self.delimiter}"
|
209
|
+
return any(key.startswith(prefix_with_delim) for key in os.environ)
|
210
|
+
return bool(os.environ)
|
211
|
+
|
212
|
+
async def load(self, config_class: type[T]) -> T:
|
213
|
+
"""Load configuration from environment variables."""
|
214
|
+
if not issubclass(config_class, RuntimeConfig):
|
215
|
+
raise TypeError(f"{config_class.__name__} must inherit from RuntimeConfig")
|
216
|
+
|
217
|
+
return config_class.from_env(
|
218
|
+
prefix=self.prefix,
|
219
|
+
delimiter=self.delimiter,
|
220
|
+
case_sensitive=self.case_sensitive,
|
221
|
+
)
|
222
|
+
|
223
|
+
|
224
|
+
class DictConfigLoader(ConfigLoader):
|
225
|
+
"""Load configuration from a dictionary."""
|
226
|
+
|
227
|
+
def __init__(
|
228
|
+
self, data: ConfigDict, source: ConfigSource = ConfigSource.RUNTIME
|
229
|
+
) -> None:
|
230
|
+
"""
|
231
|
+
Initialize dictionary configuration loader.
|
232
|
+
|
233
|
+
Args:
|
234
|
+
data: Configuration data
|
235
|
+
source: Source of the configuration
|
236
|
+
"""
|
237
|
+
self.data = data
|
238
|
+
self.source = source
|
239
|
+
|
240
|
+
def exists(self) -> bool:
|
241
|
+
"""Check if configuration data exists."""
|
242
|
+
return self.data is not None
|
243
|
+
|
244
|
+
async def load(self, config_class: type[T]) -> T:
|
245
|
+
"""Load configuration from dictionary."""
|
246
|
+
return config_class.from_dict(self.data, source=self.source)
|
247
|
+
|
248
|
+
|
249
|
+
class MultiSourceLoader(ConfigLoader):
|
250
|
+
"""Load configuration from multiple sources with precedence."""
|
251
|
+
|
252
|
+
def __init__(self, *loaders: ConfigLoader) -> None:
|
253
|
+
"""
|
254
|
+
Initialize multi-source configuration loader.
|
255
|
+
|
256
|
+
Args:
|
257
|
+
*loaders: Configuration loaders in order of precedence (later overrides earlier)
|
258
|
+
"""
|
259
|
+
self.loaders = loaders
|
260
|
+
|
261
|
+
def exists(self) -> bool:
|
262
|
+
"""Check if any configuration source exists."""
|
263
|
+
return any(loader.exists() for loader in self.loaders)
|
264
|
+
|
265
|
+
async def load(self, config_class: type[T]) -> T:
|
266
|
+
"""Load and merge configuration from multiple sources."""
|
267
|
+
if not self.exists():
|
268
|
+
raise ValueError("No configuration sources available")
|
269
|
+
|
270
|
+
config = None
|
271
|
+
|
272
|
+
for loader in self.loaders:
|
273
|
+
if loader.exists():
|
274
|
+
if config is None:
|
275
|
+
config = await loader.load(config_class)
|
276
|
+
else:
|
277
|
+
# Load and merge
|
278
|
+
new_config = await loader.load(config_class)
|
279
|
+
new_dict = new_config.to_dict(include_sensitive=True)
|
280
|
+
# Update each field with its proper source
|
281
|
+
for key, value in new_dict.items():
|
282
|
+
source = new_config.get_source(key)
|
283
|
+
if source is not None:
|
284
|
+
config.update({key: value}, source=source)
|
285
|
+
|
286
|
+
return config
|
287
|
+
|
288
|
+
|
289
|
+
class ChainedLoader(ConfigLoader):
|
290
|
+
"""Try multiple loaders until one succeeds."""
|
291
|
+
|
292
|
+
def __init__(self, *loaders: ConfigLoader) -> None:
|
293
|
+
"""
|
294
|
+
Initialize chained configuration loader.
|
295
|
+
|
296
|
+
Args:
|
297
|
+
*loaders: Configuration loaders to try in order
|
298
|
+
"""
|
299
|
+
self.loaders = loaders
|
300
|
+
|
301
|
+
def exists(self) -> bool:
|
302
|
+
"""Check if any configuration source exists."""
|
303
|
+
return any(loader.exists() for loader in self.loaders)
|
304
|
+
|
305
|
+
async def load(self, config_class: type[T]) -> T:
|
306
|
+
"""Load configuration from first available source."""
|
307
|
+
for loader in self.loaders:
|
308
|
+
if loader.exists():
|
309
|
+
return await loader.load(config_class)
|
310
|
+
|
311
|
+
raise ValueError("No configuration source available")
|
@@ -0,0 +1,387 @@
|
|
1
|
+
"""
|
2
|
+
Configuration manager for centralized configuration management.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from __future__ import annotations
|
6
|
+
|
7
|
+
from typing import TypeVar
|
8
|
+
|
9
|
+
from provide.foundation.config.base import BaseConfig
|
10
|
+
from provide.foundation.config.loader import ConfigLoader
|
11
|
+
from provide.foundation.config.schema import ConfigSchema
|
12
|
+
from provide.foundation.config.types import ConfigDict, ConfigSource
|
13
|
+
|
14
|
+
T = TypeVar("T", bound=BaseConfig)
|
15
|
+
|
16
|
+
|
17
|
+
class ConfigManager:
|
18
|
+
"""
|
19
|
+
Centralized configuration manager.
|
20
|
+
|
21
|
+
Manages multiple configuration objects and provides a unified interface.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self) -> None:
|
25
|
+
"""Initialize configuration manager."""
|
26
|
+
self._configs: dict[str, BaseConfig] = {}
|
27
|
+
self._schemas: dict[str, ConfigSchema] = {}
|
28
|
+
self._loaders: dict[str, ConfigLoader] = {}
|
29
|
+
self._defaults: dict[str, ConfigDict] = {}
|
30
|
+
|
31
|
+
async def register(
|
32
|
+
self,
|
33
|
+
name: str,
|
34
|
+
config: BaseConfig | None = None,
|
35
|
+
schema: ConfigSchema | None = None,
|
36
|
+
loader: ConfigLoader | None = None,
|
37
|
+
defaults: ConfigDict | None = None,
|
38
|
+
) -> None:
|
39
|
+
"""
|
40
|
+
Register a configuration.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
name: Configuration name
|
44
|
+
config: Configuration instance
|
45
|
+
schema: Configuration schema
|
46
|
+
loader: Configuration loader
|
47
|
+
defaults: Default configuration values
|
48
|
+
"""
|
49
|
+
if config is not None:
|
50
|
+
self._configs[name] = config
|
51
|
+
|
52
|
+
if schema is not None:
|
53
|
+
self._schemas[name] = schema
|
54
|
+
|
55
|
+
if loader is not None:
|
56
|
+
self._loaders[name] = loader
|
57
|
+
|
58
|
+
if defaults is not None:
|
59
|
+
self._defaults[name] = defaults
|
60
|
+
|
61
|
+
def unregister(self, name: str) -> None:
|
62
|
+
"""
|
63
|
+
Unregister a configuration.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
name: Configuration name
|
67
|
+
"""
|
68
|
+
self._configs.pop(name, None)
|
69
|
+
self._schemas.pop(name, None)
|
70
|
+
self._loaders.pop(name, None)
|
71
|
+
self._defaults.pop(name, None)
|
72
|
+
|
73
|
+
# Alias for unregister
|
74
|
+
def remove(self, name: str) -> None:
|
75
|
+
"""Remove a configuration. Alias for unregister."""
|
76
|
+
self.unregister(name)
|
77
|
+
|
78
|
+
async def get(self, name: str) -> BaseConfig | None:
|
79
|
+
"""
|
80
|
+
Get a configuration by name.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
name: Configuration name
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
Configuration instance or None
|
87
|
+
"""
|
88
|
+
return self._configs.get(name)
|
89
|
+
|
90
|
+
async def set(self, name: str, config: BaseConfig) -> None:
|
91
|
+
"""
|
92
|
+
Set a configuration.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
name: Configuration name
|
96
|
+
config: Configuration instance
|
97
|
+
"""
|
98
|
+
self._configs[name] = config
|
99
|
+
|
100
|
+
async def load(
|
101
|
+
self, name: str, config_class: type[T], loader: ConfigLoader | None = None
|
102
|
+
) -> T:
|
103
|
+
"""
|
104
|
+
Load a configuration.
|
105
|
+
|
106
|
+
Args:
|
107
|
+
name: Configuration name
|
108
|
+
config_class: Configuration class
|
109
|
+
loader: Optional loader (uses registered if None)
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
Configuration instance
|
113
|
+
"""
|
114
|
+
# Use provided loader or registered one
|
115
|
+
if loader is None:
|
116
|
+
loader = self._loaders.get(name)
|
117
|
+
if loader is None:
|
118
|
+
raise ValueError(f"No loader registered for configuration: {name}")
|
119
|
+
|
120
|
+
# Load configuration
|
121
|
+
config = await loader.load(config_class)
|
122
|
+
|
123
|
+
# Apply defaults if available
|
124
|
+
if name in self._defaults:
|
125
|
+
defaults_dict = self._defaults[name]
|
126
|
+
for key, value in defaults_dict.items():
|
127
|
+
if not hasattr(config, key) or getattr(config, key) is None:
|
128
|
+
setattr(config, key, value)
|
129
|
+
|
130
|
+
# Validate against schema if available
|
131
|
+
if name in self._schemas:
|
132
|
+
schema = self._schemas[name]
|
133
|
+
config_dict = config.to_dict(include_sensitive=True)
|
134
|
+
await schema.validate(config_dict)
|
135
|
+
|
136
|
+
# Store configuration
|
137
|
+
self._configs[name] = config
|
138
|
+
|
139
|
+
return config
|
140
|
+
|
141
|
+
async def reload(self, name: str) -> BaseConfig:
|
142
|
+
"""
|
143
|
+
Reload a configuration.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
name: Configuration name
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
Reloaded configuration instance
|
150
|
+
"""
|
151
|
+
if name not in self._configs:
|
152
|
+
raise ValueError(f"Configuration not found: {name}")
|
153
|
+
|
154
|
+
config = self._configs[name]
|
155
|
+
loader = self._loaders.get(name)
|
156
|
+
|
157
|
+
if loader is None:
|
158
|
+
raise ValueError(f"No loader registered for configuration: {name}")
|
159
|
+
|
160
|
+
# Reload from loader
|
161
|
+
new_config = await loader.load(config.__class__)
|
162
|
+
|
163
|
+
# Apply defaults
|
164
|
+
if name in self._defaults:
|
165
|
+
defaults_dict = self._defaults[name]
|
166
|
+
for key, value in defaults_dict.items():
|
167
|
+
if not hasattr(new_config, key) or getattr(new_config, key) is None:
|
168
|
+
setattr(new_config, key, value)
|
169
|
+
|
170
|
+
# Validate
|
171
|
+
if name in self._schemas:
|
172
|
+
schema = self._schemas[name]
|
173
|
+
config_dict = new_config.to_dict(include_sensitive=True)
|
174
|
+
await schema.validate(config_dict)
|
175
|
+
|
176
|
+
# Update stored configuration
|
177
|
+
self._configs[name] = new_config
|
178
|
+
|
179
|
+
return new_config
|
180
|
+
|
181
|
+
async def update(
|
182
|
+
self,
|
183
|
+
name: str,
|
184
|
+
updates: ConfigDict,
|
185
|
+
source: ConfigSource = ConfigSource.RUNTIME,
|
186
|
+
) -> None:
|
187
|
+
"""
|
188
|
+
Update a configuration.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
name: Configuration name
|
192
|
+
updates: Configuration updates
|
193
|
+
source: Source of updates
|
194
|
+
"""
|
195
|
+
if name not in self._configs:
|
196
|
+
raise ValueError(f"Configuration not found: {name}")
|
197
|
+
|
198
|
+
config = self._configs[name]
|
199
|
+
|
200
|
+
# Validate updates against schema if available
|
201
|
+
if name in self._schemas:
|
202
|
+
schema = self._schemas[name]
|
203
|
+
# Validate only the updated fields
|
204
|
+
for key, value in updates.items():
|
205
|
+
if key in schema._field_map:
|
206
|
+
await schema._field_map[key].validate(value)
|
207
|
+
|
208
|
+
# Apply updates
|
209
|
+
config.update(updates, source)
|
210
|
+
|
211
|
+
async def reset(self, name: str) -> None:
|
212
|
+
"""
|
213
|
+
Reset a configuration to defaults.
|
214
|
+
|
215
|
+
Args:
|
216
|
+
name: Configuration name
|
217
|
+
"""
|
218
|
+
if name not in self._configs:
|
219
|
+
raise ValueError(f"Configuration not found: {name}")
|
220
|
+
|
221
|
+
config = self._configs[name]
|
222
|
+
config.reset_to_defaults()
|
223
|
+
|
224
|
+
# Apply registered defaults
|
225
|
+
if name in self._defaults:
|
226
|
+
config.update(self._defaults[name], ConfigSource.DEFAULT)
|
227
|
+
|
228
|
+
def list_configs(self) -> list[str]:
|
229
|
+
"""
|
230
|
+
List all registered configurations.
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
List of configuration names
|
234
|
+
"""
|
235
|
+
return list(self._configs.keys())
|
236
|
+
|
237
|
+
def get_all(self) -> dict[str, BaseConfig]:
|
238
|
+
"""Get all registered configurations."""
|
239
|
+
return self._configs.copy()
|
240
|
+
|
241
|
+
def clear(self) -> None:
|
242
|
+
"""Clear all configurations."""
|
243
|
+
self._configs.clear()
|
244
|
+
self._schemas.clear()
|
245
|
+
self._loaders.clear()
|
246
|
+
self._defaults.clear()
|
247
|
+
|
248
|
+
async def export(self, name: str, include_sensitive: bool = False) -> ConfigDict:
|
249
|
+
"""
|
250
|
+
Export a configuration as dictionary.
|
251
|
+
|
252
|
+
Args:
|
253
|
+
name: Configuration name
|
254
|
+
include_sensitive: Whether to include sensitive fields
|
255
|
+
|
256
|
+
Returns:
|
257
|
+
Configuration dictionary
|
258
|
+
"""
|
259
|
+
if name not in self._configs:
|
260
|
+
raise ValueError(f"Configuration not found: {name}")
|
261
|
+
|
262
|
+
return self._configs[name].to_dict(include_sensitive)
|
263
|
+
|
264
|
+
async def export_all(
|
265
|
+
self, include_sensitive: bool = False
|
266
|
+
) -> dict[str, ConfigDict]:
|
267
|
+
"""
|
268
|
+
Export all configurations.
|
269
|
+
|
270
|
+
Args:
|
271
|
+
include_sensitive: Whether to include sensitive fields
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
Dictionary of all configurations
|
275
|
+
"""
|
276
|
+
result = {}
|
277
|
+
for name, config in self._configs.items():
|
278
|
+
result[name] = config.to_dict(include_sensitive)
|
279
|
+
return result
|
280
|
+
|
281
|
+
# Alias for export_all
|
282
|
+
async def export_to_dict(
|
283
|
+
self, include_sensitive: bool = False
|
284
|
+
) -> dict[str, ConfigDict]:
|
285
|
+
"""Export all configs to dict. Alias for export_all."""
|
286
|
+
return await self.export_all(include_sensitive)
|
287
|
+
|
288
|
+
async def load_from_dict(
|
289
|
+
self, name: str, config_class: type[T], data: ConfigDict
|
290
|
+
) -> T:
|
291
|
+
"""Load config from dictionary."""
|
292
|
+
config = config_class.from_dict(data)
|
293
|
+
self._configs[name] = config
|
294
|
+
return config
|
295
|
+
|
296
|
+
def add_loader(self, name: str, loader: ConfigLoader) -> None:
|
297
|
+
"""Add a loader for a configuration."""
|
298
|
+
self._loaders[name] = loader
|
299
|
+
|
300
|
+
async def validate_all(self) -> None:
|
301
|
+
"""Validate all configurations."""
|
302
|
+
for name, config in self._configs.items():
|
303
|
+
if hasattr(config, "validate"):
|
304
|
+
await config.validate()
|
305
|
+
if name in self._schemas:
|
306
|
+
schema = self._schemas[name]
|
307
|
+
config_dict = config.to_dict(include_sensitive=True)
|
308
|
+
if hasattr(schema, "validate"):
|
309
|
+
await schema.validate(config_dict)
|
310
|
+
|
311
|
+
async def get_or_create(
|
312
|
+
self, name: str, config_class: type[T], defaults: ConfigDict | None = None
|
313
|
+
) -> T:
|
314
|
+
"""Get existing config or create new one with defaults."""
|
315
|
+
existing = await self.get(name)
|
316
|
+
if existing is not None:
|
317
|
+
return existing
|
318
|
+
|
319
|
+
# Create new config with defaults
|
320
|
+
config = config_class.from_dict(defaults or {})
|
321
|
+
self._configs[name] = config
|
322
|
+
return config
|
323
|
+
|
324
|
+
|
325
|
+
# Global configuration manager instance
|
326
|
+
_manager = ConfigManager()
|
327
|
+
|
328
|
+
|
329
|
+
async def get_config(name: str) -> BaseConfig | None:
|
330
|
+
"""
|
331
|
+
Get a configuration from the global manager.
|
332
|
+
|
333
|
+
Args:
|
334
|
+
name: Configuration name
|
335
|
+
|
336
|
+
Returns:
|
337
|
+
Configuration instance or None
|
338
|
+
"""
|
339
|
+
return await _manager.get(name)
|
340
|
+
|
341
|
+
|
342
|
+
async def set_config(name: str, config: BaseConfig) -> None:
|
343
|
+
"""
|
344
|
+
Set a configuration in the global manager.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
name: Configuration name
|
348
|
+
config: Configuration instance
|
349
|
+
"""
|
350
|
+
await _manager.set(name, config)
|
351
|
+
|
352
|
+
|
353
|
+
async def register_config(
|
354
|
+
name: str,
|
355
|
+
config: BaseConfig | None = None,
|
356
|
+
schema: ConfigSchema | None = None,
|
357
|
+
loader: ConfigLoader | None = None,
|
358
|
+
defaults: ConfigDict | None = None,
|
359
|
+
) -> None:
|
360
|
+
"""
|
361
|
+
Register a configuration with the global manager.
|
362
|
+
|
363
|
+
Args:
|
364
|
+
name: Configuration name
|
365
|
+
config: Configuration instance
|
366
|
+
schema: Configuration schema
|
367
|
+
loader: Configuration loader
|
368
|
+
defaults: Default configuration values
|
369
|
+
"""
|
370
|
+
await _manager.register(name, config, schema, loader, defaults)
|
371
|
+
|
372
|
+
|
373
|
+
async def load_config(
|
374
|
+
name: str, config_class: type[T], loader: ConfigLoader | None = None
|
375
|
+
) -> T:
|
376
|
+
"""
|
377
|
+
Load a configuration using the global manager.
|
378
|
+
|
379
|
+
Args:
|
380
|
+
name: Configuration name
|
381
|
+
config_class: Configuration class
|
382
|
+
loader: Optional loader
|
383
|
+
|
384
|
+
Returns:
|
385
|
+
Configuration instance
|
386
|
+
"""
|
387
|
+
return await _manager.load(name, config_class, loader)
|