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.
Files changed (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. 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)