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,284 @@
1
+ """
2
+ Configuration schema and validation.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ from collections.abc import Awaitable, Callable
9
+ from typing import Any
10
+
11
+ from attrs import Attribute, define, fields
12
+
13
+ from provide.foundation.config.base import BaseConfig
14
+ from provide.foundation.config.types import ConfigDict
15
+ from provide.foundation.errors import ConfigValidationError
16
+
17
+
18
+ @define
19
+ class SchemaField:
20
+ """Schema definition for a configuration field."""
21
+
22
+ name: str
23
+ type: type | None = None
24
+ required: bool = False
25
+ default: Any = None
26
+ description: str | None = None
27
+ validator: Callable[[Any], bool | Awaitable[bool]] | None = None
28
+ choices: list[Any] | None = None
29
+ min_value: Any = None
30
+ max_value: Any = None
31
+ pattern: str | None = None
32
+ sensitive: bool = False
33
+
34
+ async def validate(self, value: Any) -> None:
35
+ """
36
+ Validate a value against this schema field.
37
+
38
+ Args:
39
+ value: Value to validate
40
+
41
+ Raises:
42
+ ConfigValidationError: If validation fails
43
+ """
44
+ # Check required
45
+ if self.required and value is None:
46
+ raise ConfigValidationError(
47
+ "Field is required", field=self.name, value=value
48
+ )
49
+
50
+ # Skip further validation for None values
51
+ if value is None:
52
+ return
53
+
54
+ # Check type
55
+ if self.type is not None and not isinstance(value, self.type):
56
+ raise ConfigValidationError(
57
+ f"Expected type {self.type.__name__}, got {type(value).__name__}",
58
+ field=self.name,
59
+ value=value,
60
+ )
61
+
62
+ # Check choices
63
+ if self.choices is not None and value not in self.choices:
64
+ raise ConfigValidationError(
65
+ f"Value must be one of {self.choices}", field=self.name, value=value
66
+ )
67
+
68
+ # Check min/max
69
+ if self.min_value is not None and value < self.min_value:
70
+ raise ConfigValidationError(
71
+ f"Value must be >= {self.min_value}", field=self.name, value=value
72
+ )
73
+
74
+ if self.max_value is not None and value > self.max_value:
75
+ raise ConfigValidationError(
76
+ f"Value must be <= {self.max_value}", field=self.name, value=value
77
+ )
78
+
79
+ # Check pattern
80
+ if self.pattern is not None and isinstance(value, str):
81
+ import re
82
+
83
+ if not re.match(self.pattern, value):
84
+ raise ConfigValidationError(
85
+ f"Value does not match pattern: {self.pattern}",
86
+ field=self.name,
87
+ value=value,
88
+ )
89
+
90
+ # Custom validator
91
+ if self.validator is not None:
92
+ try:
93
+ result = self.validator(value)
94
+ # Handle both sync and async validators
95
+ if asyncio.iscoroutine(result) or asyncio.isfuture(result):
96
+ result = await result
97
+ if not result:
98
+ raise ConfigValidationError(
99
+ "Custom validation failed", field=self.name, value=value
100
+ )
101
+ except ConfigValidationError:
102
+ raise
103
+ except Exception as e:
104
+ raise ConfigValidationError(
105
+ f"Validation error: {e}", field=self.name, value=value
106
+ )
107
+
108
+
109
+ class ConfigSchema:
110
+ """Schema definition for configuration classes."""
111
+
112
+ def __init__(self, fields: list[SchemaField] | None = None) -> None:
113
+ """
114
+ Initialize configuration schema.
115
+
116
+ Args:
117
+ fields: List of schema fields
118
+ """
119
+ self.fields = fields or []
120
+ self._field_map = {field.name: field for field in self.fields}
121
+
122
+ def add_field(self, field: SchemaField) -> None:
123
+ """Add a field to the schema."""
124
+ self.fields.append(field)
125
+ self._field_map[field.name] = field
126
+
127
+ async def validate(self, data: ConfigDict) -> None:
128
+ """
129
+ Validate configuration data against schema.
130
+
131
+ Args:
132
+ data: Configuration data to validate
133
+
134
+ Raises:
135
+ ConfigValidationError: If validation fails
136
+ """
137
+ # Check required fields
138
+ for field in self.fields:
139
+ if field.required and field.name not in data:
140
+ raise ConfigValidationError("Required field missing", field=field.name)
141
+
142
+ # Validate each field
143
+ for key, value in data.items():
144
+ if key in self._field_map:
145
+ await self._field_map[key].validate(value)
146
+
147
+ def apply_defaults(self, data: ConfigDict) -> ConfigDict:
148
+ """
149
+ Apply default values to configuration data.
150
+
151
+ Args:
152
+ data: Configuration data
153
+
154
+ Returns:
155
+ Data with defaults applied
156
+ """
157
+ result = data.copy()
158
+
159
+ for field in self.fields:
160
+ if field.name not in result and field.default is not None:
161
+ result[field.name] = field.default
162
+
163
+ return result
164
+
165
+ def filter_extra_fields(self, data: ConfigDict) -> ConfigDict:
166
+ """
167
+ Remove fields not defined in schema.
168
+
169
+ Args:
170
+ data: Configuration data
171
+
172
+ Returns:
173
+ Filtered data
174
+ """
175
+ return {k: v for k, v in data.items() if k in self._field_map}
176
+
177
+ @classmethod
178
+ def from_config_class(cls, config_class: type[BaseConfig]) -> ConfigSchema:
179
+ """
180
+ Generate schema from configuration class.
181
+
182
+ Args:
183
+ config_class: Configuration class
184
+
185
+ Returns:
186
+ Generated schema
187
+ """
188
+ schema_fields = []
189
+
190
+ for attr in fields(config_class):
191
+ schema_field = cls._attr_to_schema_field(attr)
192
+ schema_fields.append(schema_field)
193
+
194
+ return cls(schema_fields)
195
+
196
+ @staticmethod
197
+ def _attr_to_schema_field(attr: Attribute) -> SchemaField:
198
+ """Convert attrs attribute to schema field."""
199
+ # Determine if required
200
+ required = attr.default is None and attr.factory is None
201
+
202
+ # Get type from attribute
203
+ field_type = getattr(attr, "type", None)
204
+
205
+ # Extract metadata
206
+ description = attr.metadata.get("description")
207
+ sensitive = attr.metadata.get("sensitive", False)
208
+
209
+ # Create schema field
210
+ return SchemaField(
211
+ name=attr.name,
212
+ type=field_type,
213
+ required=required,
214
+ default=attr.default if attr.default is not None else None,
215
+ description=description,
216
+ sensitive=sensitive,
217
+ )
218
+
219
+
220
+ async def validate_schema(config: BaseConfig, schema: ConfigSchema) -> None:
221
+ """
222
+ Validate configuration instance against schema.
223
+
224
+ Args:
225
+ config: Configuration instance
226
+ schema: Schema to validate against
227
+
228
+ Raises:
229
+ ConfigValidationError: If validation fails
230
+ """
231
+ data = config.to_dict(include_sensitive=True)
232
+ await schema.validate(data)
233
+
234
+
235
+ # Common validators (all sync since they're simple checks)
236
+ def validate_port(value: int) -> bool:
237
+ """Validate port number."""
238
+ return 1 <= value <= 65535
239
+
240
+
241
+ def validate_url(value: str) -> bool:
242
+ """Validate URL format."""
243
+ from urllib.parse import urlparse
244
+
245
+ try:
246
+ result = urlparse(value)
247
+ return all([result.scheme, result.netloc])
248
+ except Exception:
249
+ return False
250
+
251
+
252
+ def validate_email(value: str) -> bool:
253
+ """Validate email format."""
254
+ import re
255
+
256
+ pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
257
+ return bool(re.match(pattern, value))
258
+
259
+
260
+ def validate_path(value: str) -> bool:
261
+ """Validate file path."""
262
+ from pathlib import Path
263
+
264
+ try:
265
+ Path(value)
266
+ return True
267
+ except Exception:
268
+ return False
269
+
270
+
271
+ def validate_version(value: str) -> bool:
272
+ """Validate semantic version."""
273
+ import re
274
+
275
+ pattern = r"^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$"
276
+ return bool(re.match(pattern, value))
277
+
278
+
279
+ # Example async validator for complex checks
280
+ async def validate_url_accessible(value: str) -> bool:
281
+ """Validate URL is accessible (example async validator)."""
282
+ # This is just an example - in real use you'd use aiohttp or similar
283
+ # For now, just do basic URL validation
284
+ return validate_url(value)
@@ -0,0 +1,281 @@
1
+ """
2
+ Synchronous wrappers for the async configuration system.
3
+
4
+ These wrappers allow using the async config system in synchronous contexts
5
+ like CLI tools, scripts, and frameworks that don't support async.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ from pathlib import Path
12
+ from typing import Any, TypeVar
13
+
14
+ from provide.foundation.config.base import BaseConfig
15
+ from provide.foundation.config.env import RuntimeConfig
16
+ from provide.foundation.config.loader import (
17
+ DictConfigLoader,
18
+ FileConfigLoader,
19
+ MultiSourceLoader,
20
+ )
21
+ from provide.foundation.config.manager import ConfigManager
22
+ from provide.foundation.config.types import ConfigDict, ConfigSource
23
+
24
+ T = TypeVar("T", bound=BaseConfig)
25
+
26
+
27
+ def run_async(coro):
28
+ """
29
+ Run an async coroutine in a sync context.
30
+
31
+ Creates a new event loop if needed or uses the existing one.
32
+ """
33
+ try:
34
+ # Try to get the current event loop
35
+ asyncio.get_running_loop()
36
+ # If we're here, we're already in an async context
37
+ # This shouldn't happen in sync code, but handle it gracefully
38
+ import concurrent.futures
39
+
40
+ with concurrent.futures.ThreadPoolExecutor() as executor:
41
+ future = executor.submit(asyncio.run, coro)
42
+ return future.result()
43
+ except RuntimeError:
44
+ # No event loop, create one
45
+ return asyncio.run(coro)
46
+
47
+
48
+ def load_config(
49
+ config_class: type[T],
50
+ data: ConfigDict | None = None,
51
+ source: ConfigSource = ConfigSource.RUNTIME,
52
+ ) -> T:
53
+ """
54
+ Load configuration from dictionary (sync wrapper).
55
+
56
+ Args:
57
+ config_class: Configuration class
58
+ data: Configuration data
59
+ source: Source of the configuration
60
+
61
+ Returns:
62
+ Configuration instance
63
+ """
64
+ if data is None:
65
+ data = {}
66
+ return run_async(config_class.from_dict(data, source))
67
+
68
+
69
+ def load_config_from_env(
70
+ config_class: type[T],
71
+ prefix: str = "",
72
+ delimiter: str = "_",
73
+ case_sensitive: bool = False,
74
+ ) -> T:
75
+ """
76
+ Load configuration from environment variables (sync wrapper).
77
+
78
+ Args:
79
+ config_class: Configuration class (must inherit from RuntimeConfig)
80
+ prefix: Prefix for environment variables
81
+ delimiter: Delimiter between prefix and field name
82
+ case_sensitive: Whether variable names are case-sensitive
83
+
84
+ Returns:
85
+ Configuration instance
86
+ """
87
+ if not issubclass(config_class, RuntimeConfig):
88
+ raise TypeError(f"{config_class.__name__} must inherit from RuntimeConfig")
89
+
90
+ return run_async(
91
+ config_class.from_env(
92
+ prefix=prefix,
93
+ delimiter=delimiter,
94
+ case_sensitive=case_sensitive,
95
+ use_async_secrets=False, # Use sync I/O in sync context
96
+ )
97
+ )
98
+
99
+
100
+ def load_config_from_file(
101
+ path: str | Path,
102
+ config_class: type[T],
103
+ format: str | None = None,
104
+ encoding: str = "utf-8",
105
+ ) -> T:
106
+ """
107
+ Load configuration from file (sync wrapper).
108
+
109
+ Args:
110
+ path: Path to configuration file
111
+ config_class: Configuration class
112
+ format: File format (auto-detected if None)
113
+ encoding: File encoding
114
+
115
+ Returns:
116
+ Configuration instance
117
+ """
118
+ loader = FileConfigLoader(path, format=format, encoding=encoding)
119
+ return run_async(loader.load(config_class))
120
+
121
+
122
+ def load_config_from_multiple(
123
+ config_class: type[T],
124
+ *sources: tuple[str, Any],
125
+ ) -> T:
126
+ """
127
+ Load configuration from multiple sources (sync wrapper).
128
+
129
+ Args:
130
+ config_class: Configuration class
131
+ *sources: Tuples of (source_type, source_data) where:
132
+ - source_type: "file", "env", "dict"
133
+ - source_data: Path for file, prefix for env, dict for dict
134
+
135
+ Returns:
136
+ Configuration instance merged from all sources
137
+ """
138
+ loaders = []
139
+
140
+ for source_type, source_data in sources:
141
+ if source_type == "file":
142
+ loaders.append(FileConfigLoader(source_data))
143
+ elif source_type == "env":
144
+ from provide.foundation.config.loader import RuntimeConfigLoader
145
+
146
+ loaders.append(RuntimeConfigLoader(prefix=source_data))
147
+ elif source_type == "dict":
148
+ loaders.append(DictConfigLoader(source_data))
149
+ else:
150
+ raise ValueError(f"Unknown source type: {source_type}")
151
+
152
+ multi_loader = MultiSourceLoader(*loaders)
153
+ return run_async(multi_loader.load(config_class))
154
+
155
+
156
+ def validate_config(config: BaseConfig) -> None:
157
+ """
158
+ Validate a configuration instance (sync wrapper).
159
+
160
+ Args:
161
+ config: Configuration instance to validate
162
+ """
163
+ run_async(config.validate())
164
+
165
+
166
+ def update_config(
167
+ config: BaseConfig, updates: ConfigDict, source: ConfigSource = ConfigSource.RUNTIME
168
+ ) -> None:
169
+ """
170
+ Update configuration with new values (sync wrapper).
171
+
172
+ Args:
173
+ config: Configuration instance
174
+ updates: Dictionary of updates
175
+ source: Source of the updates
176
+ """
177
+ run_async(config.update(updates, source))
178
+
179
+
180
+ def config_to_dict(config: BaseConfig, include_sensitive: bool = False) -> ConfigDict:
181
+ """
182
+ Convert configuration to dictionary (sync wrapper).
183
+
184
+ Args:
185
+ config: Configuration instance
186
+ include_sensitive: Whether to include sensitive fields
187
+
188
+ Returns:
189
+ Dictionary representation
190
+ """
191
+ return run_async(config.to_dict(include_sensitive))
192
+
193
+
194
+ def clone_config(config: T) -> T:
195
+ """
196
+ Create a deep copy of configuration (sync wrapper).
197
+
198
+ Args:
199
+ config: Configuration instance
200
+
201
+ Returns:
202
+ Cloned configuration
203
+ """
204
+ return run_async(config.clone())
205
+
206
+
207
+ def diff_configs(
208
+ config1: BaseConfig, config2: BaseConfig
209
+ ) -> dict[str, tuple[Any, Any]]:
210
+ """
211
+ Compare two configurations (sync wrapper).
212
+
213
+ Args:
214
+ config1: First configuration
215
+ config2: Second configuration
216
+
217
+ Returns:
218
+ Dictionary of differences
219
+ """
220
+ return run_async(config1.diff(config2))
221
+
222
+
223
+ class SyncConfigManager:
224
+ """
225
+ Synchronous wrapper for ConfigManager.
226
+
227
+ Provides a sync interface to the async ConfigManager.
228
+ """
229
+
230
+ def __init__(self) -> None:
231
+ """Initialize sync config manager."""
232
+ self._async_manager = ConfigManager()
233
+
234
+ def register(self, name: str, config: BaseConfig | None = None, **kwargs) -> None:
235
+ """Register a configuration (sync)."""
236
+ run_async(self._async_manager.register(name, config, **kwargs))
237
+
238
+ def get(self, name: str) -> BaseConfig | None:
239
+ """Get a configuration by name (sync)."""
240
+ return run_async(self._async_manager.get(name))
241
+
242
+ def load(self, name: str, config_class: type[T], loader=None) -> T:
243
+ """Load a configuration (sync)."""
244
+ return run_async(self._async_manager.load(name, config_class, loader))
245
+
246
+ def update(
247
+ self,
248
+ name: str,
249
+ updates: ConfigDict,
250
+ source: ConfigSource = ConfigSource.RUNTIME,
251
+ ) -> None:
252
+ """Update a configuration (sync)."""
253
+ run_async(self._async_manager.update(name, updates, source))
254
+
255
+ def export(self, name: str, include_sensitive: bool = False) -> ConfigDict:
256
+ """Export a configuration as dictionary (sync)."""
257
+ return run_async(self._async_manager.export(name, include_sensitive))
258
+
259
+ def export_all(self, include_sensitive: bool = False) -> dict[str, ConfigDict]:
260
+ """Export all configurations (sync)."""
261
+ return run_async(self._async_manager.export_all(include_sensitive))
262
+
263
+
264
+ # Global sync manager instance
265
+ sync_manager = SyncConfigManager()
266
+
267
+
268
+ # Convenience functions using the global sync manager
269
+ def get_config(name: str) -> BaseConfig | None:
270
+ """Get a configuration from the global sync manager."""
271
+ return sync_manager.get(name)
272
+
273
+
274
+ def set_config(name: str, config: BaseConfig) -> None:
275
+ """Set a configuration in the global sync manager."""
276
+ sync_manager.register(name, config=config)
277
+
278
+
279
+ def register_config(name: str, **kwargs) -> None:
280
+ """Register a configuration with the global sync manager."""
281
+ sync_manager.register(name, **kwargs)
@@ -0,0 +1,78 @@
1
+ """
2
+ Type definitions for the configuration system.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from enum import Enum
8
+ from typing import Any
9
+
10
+ # Basic type aliases
11
+ ConfigValue = str | int | float | bool | None | list[Any] | dict[str, Any]
12
+ ConfigDict = dict[str, ConfigValue]
13
+
14
+
15
+ class ConfigSource(Enum):
16
+ """Sources for configuration values with precedence order."""
17
+
18
+ DEFAULT = 0 # Lowest precedence
19
+ FILE = 10
20
+ ENV = 20
21
+ RUNTIME = 30 # Highest precedence
22
+
23
+ def __lt__(self, other):
24
+ """Enable comparison for precedence."""
25
+ if not isinstance(other, ConfigSource):
26
+ return NotImplemented
27
+ return self.value < other.value
28
+
29
+ def __le__(self, other):
30
+ """Enable <= comparison for precedence."""
31
+ if not isinstance(other, ConfigSource):
32
+ return NotImplemented
33
+ return self.value <= other.value
34
+
35
+ def __gt__(self, other):
36
+ """Enable > comparison for precedence."""
37
+ if not isinstance(other, ConfigSource):
38
+ return NotImplemented
39
+ return self.value > other.value
40
+
41
+ def __ge__(self, other):
42
+ """Enable >= comparison for precedence."""
43
+ if not isinstance(other, ConfigSource):
44
+ return NotImplemented
45
+ return self.value >= other.value
46
+
47
+ def __eq__(self, other):
48
+ """Enable == comparison for precedence."""
49
+ if not isinstance(other, ConfigSource):
50
+ return NotImplemented
51
+ return self.value == other.value
52
+
53
+
54
+ class ConfigFormat(Enum):
55
+ """Supported configuration file formats."""
56
+
57
+ JSON = "json"
58
+ YAML = "yaml"
59
+ TOML = "toml"
60
+ INI = "ini"
61
+ ENV = "env" # .env files
62
+
63
+ @classmethod
64
+ def from_extension(cls, filename: str) -> ConfigFormat | None:
65
+ """Determine format from file extension."""
66
+ ext_map = {
67
+ ".json": cls.JSON,
68
+ ".yaml": cls.YAML,
69
+ ".yml": cls.YAML,
70
+ ".toml": cls.TOML,
71
+ ".ini": cls.INI,
72
+ ".env": cls.ENV,
73
+ }
74
+
75
+ for ext, format_type in ext_map.items():
76
+ if filename.lower().endswith(ext):
77
+ return format_type
78
+ return None