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,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
|