provide-foundation 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev3__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/foundation/__init__.py +36 -10
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +93 -96
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +15 -9
- provide/foundation/cli/commands/logs/__init__.py +3 -3
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +4 -4
- provide/foundation/cli/commands/logs/send.py +3 -3
- provide/foundation/cli/commands/logs/tail.py +3 -3
- provide/foundation/cli/decorators.py +11 -11
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -40
- provide/foundation/cli/utils.py +21 -18
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +477 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +6 -20
- provide/foundation/config/loader.py +10 -4
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +36 -14
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +88 -110
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -3
- provide/foundation/errors/decorators.py +0 -234
- provide/foundation/errors/types.py +0 -98
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +14 -23
- provide/foundation/file/lock.py +4 -3
- provide/foundation/hub/components.py +75 -389
- provide/foundation/hub/config.py +157 -0
- provide/foundation/hub/discovery.py +63 -0
- provide/foundation/hub/handlers.py +89 -0
- provide/foundation/hub/lifecycle.py +195 -0
- provide/foundation/hub/manager.py +7 -4
- provide/foundation/hub/processors.py +49 -0
- provide/foundation/integrations/__init__.py +11 -0
- provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
- provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
- provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
- provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +69 -299
- provide/foundation/logger/config/telemetry.py +39 -121
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +39 -25
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +3 -3
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +48 -0
- provide/foundation/process/lifecycle.py +69 -46
- provide/foundation/resilience/__init__.py +36 -0
- provide/foundation/resilience/circuit.py +166 -0
- provide/foundation/resilience/decorators.py +236 -0
- provide/foundation/resilience/fallback.py +208 -0
- provide/foundation/resilience/retry.py +327 -0
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +78 -0
- provide/foundation/streams/console.py +4 -5
- provide/foundation/streams/core.py +5 -2
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +29 -9
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +30 -20
- provide/foundation/testing/common/__init__.py +13 -15
- provide/foundation/testing/common/fixtures.py +27 -57
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +289 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +42 -516
- provide/foundation/testing/file/special_fixtures.py +145 -0
- provide/foundation/testing/logger.py +89 -8
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +414 -0
- provide/foundation/testing/process/fixtures.py +48 -571
- provide/foundation/testing/process/subprocess_fixtures.py +210 -0
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +105 -0
- provide/foundation/testing/threading/data_fixtures.py +101 -0
- provide/foundation/testing/threading/execution_fixtures.py +278 -0
- provide/foundation/testing/threading/fixtures.py +32 -502
- provide/foundation/testing/threading/sync_fixtures.py +100 -0
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +95 -83
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +69 -74
- provide/foundation/tools/downloader.py +68 -62
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +2 -14
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +36 -107
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +113 -114
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +17 -14
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,477 @@
|
|
1
|
+
"""
|
2
|
+
Configuration field converters for parsing environment variables.
|
3
|
+
|
4
|
+
These converters are used with the field() decorator to automatically
|
5
|
+
parse and validate environment variable values into the correct types.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
from typing import Any
|
10
|
+
|
11
|
+
# Error handling decorator temporarily removed to break circular import
|
12
|
+
|
13
|
+
# Type definitions to avoid circular imports
|
14
|
+
LogLevelStr = str
|
15
|
+
ConsoleFormatterStr = str
|
16
|
+
|
17
|
+
_VALID_LOG_LEVEL_TUPLE = (
|
18
|
+
"TRACE",
|
19
|
+
"DEBUG",
|
20
|
+
"INFO",
|
21
|
+
"WARNING",
|
22
|
+
"ERROR",
|
23
|
+
"CRITICAL",
|
24
|
+
)
|
25
|
+
|
26
|
+
_VALID_FORMATTER_TUPLE = (
|
27
|
+
"key_value",
|
28
|
+
"json",
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
def parse_log_level(value: str) -> LogLevelStr:
|
33
|
+
"""
|
34
|
+
Parse and validate log level string.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
value: Log level string (case-insensitive)
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Valid log level string in uppercase
|
41
|
+
|
42
|
+
Raises:
|
43
|
+
ValueError: If the log level is invalid
|
44
|
+
"""
|
45
|
+
level = value.upper()
|
46
|
+
if level not in _VALID_LOG_LEVEL_TUPLE:
|
47
|
+
raise ValueError(
|
48
|
+
f"Invalid log level '{value}'. Valid options: {', '.join(_VALID_LOG_LEVEL_TUPLE)}"
|
49
|
+
)
|
50
|
+
return level
|
51
|
+
|
52
|
+
|
53
|
+
def parse_console_formatter(value: str) -> ConsoleFormatterStr:
|
54
|
+
"""
|
55
|
+
Parse and validate console formatter string.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
value: Formatter string (case-insensitive)
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Valid formatter string in lowercase
|
62
|
+
|
63
|
+
Raises:
|
64
|
+
ValueError: If the formatter is invalid
|
65
|
+
"""
|
66
|
+
formatter = value.lower()
|
67
|
+
if formatter not in _VALID_FORMATTER_TUPLE:
|
68
|
+
raise ValueError(
|
69
|
+
f"Invalid console formatter '{value}'. Valid options: {', '.join(_VALID_FORMATTER_TUPLE)}"
|
70
|
+
)
|
71
|
+
return formatter
|
72
|
+
|
73
|
+
|
74
|
+
# Temporarily remove error handling to break circular import
|
75
|
+
# @with_error_handling(...)
|
76
|
+
def parse_module_levels(value: str | dict[str, str]) -> dict[str, LogLevelStr]:
|
77
|
+
"""
|
78
|
+
Parse module-specific log levels from string format.
|
79
|
+
|
80
|
+
Format: "module1:LEVEL,module2:LEVEL"
|
81
|
+
Example: "auth.service:DEBUG,database:ERROR"
|
82
|
+
|
83
|
+
Args:
|
84
|
+
value: Comma-separated module:level pairs or dict
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
Dictionary mapping module names to log levels
|
88
|
+
"""
|
89
|
+
# If already a dict, validate and return
|
90
|
+
if isinstance(value, dict):
|
91
|
+
result = {}
|
92
|
+
for module, level in value.items():
|
93
|
+
try:
|
94
|
+
result[module] = parse_log_level(level)
|
95
|
+
except ValueError:
|
96
|
+
# Skip invalid levels silently
|
97
|
+
continue
|
98
|
+
return result
|
99
|
+
|
100
|
+
if not value or not value.strip():
|
101
|
+
return {}
|
102
|
+
|
103
|
+
result = {}
|
104
|
+
for pair in value.split(","):
|
105
|
+
pair = pair.strip()
|
106
|
+
if not pair:
|
107
|
+
continue
|
108
|
+
|
109
|
+
if ":" not in pair:
|
110
|
+
# Skip invalid entries silently
|
111
|
+
continue
|
112
|
+
|
113
|
+
module, level = pair.split(":", 1)
|
114
|
+
module = module.strip()
|
115
|
+
level = level.strip()
|
116
|
+
|
117
|
+
if module:
|
118
|
+
try:
|
119
|
+
result[module] = parse_log_level(level)
|
120
|
+
except ValueError:
|
121
|
+
# Skip invalid log levels silently
|
122
|
+
continue
|
123
|
+
|
124
|
+
return result
|
125
|
+
|
126
|
+
|
127
|
+
def parse_rate_limits(value: str) -> dict[str, tuple[float, float]]:
|
128
|
+
"""
|
129
|
+
Parse per-logger rate limits from string format.
|
130
|
+
|
131
|
+
Format: "logger1:rate:capacity,logger2:rate:capacity"
|
132
|
+
Example: "api:10.0:100.0,worker:5.0:50.0"
|
133
|
+
|
134
|
+
Args:
|
135
|
+
value: Comma-separated logger:rate:capacity triplets
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
Dictionary mapping logger names to (rate, capacity) tuples
|
139
|
+
"""
|
140
|
+
if not value or not value.strip():
|
141
|
+
return {}
|
142
|
+
|
143
|
+
result = {}
|
144
|
+
for item in value.split(","):
|
145
|
+
item = item.strip()
|
146
|
+
if not item:
|
147
|
+
continue
|
148
|
+
|
149
|
+
parts = item.split(":")
|
150
|
+
if len(parts) != 3:
|
151
|
+
# Skip invalid entries silently
|
152
|
+
continue
|
153
|
+
|
154
|
+
logger, rate_str, capacity_str = parts
|
155
|
+
logger = logger.strip()
|
156
|
+
|
157
|
+
if logger:
|
158
|
+
try:
|
159
|
+
rate = float(rate_str.strip())
|
160
|
+
capacity = float(capacity_str.strip())
|
161
|
+
result[logger] = (rate, capacity)
|
162
|
+
except (ValueError, TypeError):
|
163
|
+
# Skip invalid numbers silently
|
164
|
+
continue
|
165
|
+
|
166
|
+
return result
|
167
|
+
|
168
|
+
|
169
|
+
def parse_foundation_log_output(value: str) -> str:
|
170
|
+
"""
|
171
|
+
Parse and validate foundation log output destination.
|
172
|
+
|
173
|
+
Args:
|
174
|
+
value: Output destination string
|
175
|
+
|
176
|
+
Returns:
|
177
|
+
Valid output destination (stderr, stdout, main)
|
178
|
+
|
179
|
+
Raises:
|
180
|
+
ValueError: If the value is invalid
|
181
|
+
"""
|
182
|
+
if not value:
|
183
|
+
return "stderr"
|
184
|
+
|
185
|
+
normalized = value.lower().strip()
|
186
|
+
valid_options = ("stderr", "stdout", "main")
|
187
|
+
|
188
|
+
if normalized in valid_options:
|
189
|
+
return normalized
|
190
|
+
else:
|
191
|
+
raise ValueError(
|
192
|
+
f"Invalid foundation log output '{value}'. Valid options: {', '.join(valid_options)}"
|
193
|
+
)
|
194
|
+
|
195
|
+
|
196
|
+
def parse_comma_list(value: str) -> list[str]:
|
197
|
+
"""
|
198
|
+
Parse comma-separated list of strings.
|
199
|
+
|
200
|
+
Args:
|
201
|
+
value: Comma-separated string
|
202
|
+
|
203
|
+
Returns:
|
204
|
+
List of trimmed non-empty strings
|
205
|
+
"""
|
206
|
+
if not value or not value.strip():
|
207
|
+
return []
|
208
|
+
|
209
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
210
|
+
|
211
|
+
|
212
|
+
def parse_bool_extended(value: str | bool) -> bool:
|
213
|
+
"""
|
214
|
+
Parse boolean from string with extended format support.
|
215
|
+
|
216
|
+
Recognizes: true/false, yes/no, 1/0, on/off (case-insensitive)
|
217
|
+
|
218
|
+
Args:
|
219
|
+
value: Boolean string representation or bool
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
Boolean value
|
223
|
+
"""
|
224
|
+
# If already a bool, return as-is
|
225
|
+
if isinstance(value, bool):
|
226
|
+
return value
|
227
|
+
|
228
|
+
# Convert to string and parse
|
229
|
+
value_lower = str(value).lower().strip()
|
230
|
+
return value_lower in ("true", "yes", "1", "on")
|
231
|
+
|
232
|
+
|
233
|
+
def parse_bool_strict(value: str | bool) -> bool:
|
234
|
+
"""
|
235
|
+
Parse boolean from string with strict validation.
|
236
|
+
|
237
|
+
Recognizes: true/false, yes/no, 1/0, on/off (case-insensitive)
|
238
|
+
|
239
|
+
Args:
|
240
|
+
value: Boolean string representation or bool
|
241
|
+
|
242
|
+
Returns:
|
243
|
+
Boolean value
|
244
|
+
|
245
|
+
Raises:
|
246
|
+
TypeError: If value is not a string or bool
|
247
|
+
ValueError: If the value cannot be parsed as boolean
|
248
|
+
"""
|
249
|
+
# Check type first
|
250
|
+
if not isinstance(value, (str, bool)):
|
251
|
+
raise TypeError(
|
252
|
+
f"Boolean field requires str or bool, got {type(value).__name__}"
|
253
|
+
)
|
254
|
+
|
255
|
+
# If already a bool, return as-is
|
256
|
+
if isinstance(value, bool):
|
257
|
+
return value
|
258
|
+
|
259
|
+
# Convert to string and parse
|
260
|
+
value_lower = value.lower().strip()
|
261
|
+
|
262
|
+
if value_lower in ("true", "yes", "1", "on"):
|
263
|
+
return True
|
264
|
+
elif value_lower in ("false", "no", "0", "off"):
|
265
|
+
return False
|
266
|
+
else:
|
267
|
+
raise ValueError(
|
268
|
+
f"Invalid boolean value '{value}'. Valid options: true/false, yes/no, 1/0, on/off"
|
269
|
+
)
|
270
|
+
|
271
|
+
|
272
|
+
# Temporarily remove error handling to break circular import
|
273
|
+
# @with_error_handling(...)
|
274
|
+
def parse_float_with_validation(
|
275
|
+
value: str, min_val: float | None = None, max_val: float | None = None
|
276
|
+
) -> float:
|
277
|
+
"""
|
278
|
+
Parse float with optional range validation.
|
279
|
+
|
280
|
+
Args:
|
281
|
+
value: String representation of float
|
282
|
+
min_val: Minimum allowed value (inclusive)
|
283
|
+
max_val: Maximum allowed value (inclusive)
|
284
|
+
|
285
|
+
Returns:
|
286
|
+
Parsed float value
|
287
|
+
|
288
|
+
Raises:
|
289
|
+
ValueError: If value is not a valid float or out of range
|
290
|
+
"""
|
291
|
+
try:
|
292
|
+
result = float(value)
|
293
|
+
except (ValueError, TypeError) as e:
|
294
|
+
raise ValueError(f"Invalid float value '{value}': {e}")
|
295
|
+
|
296
|
+
if min_val is not None and result < min_val:
|
297
|
+
raise ValueError(f"Value {result} is below minimum {min_val}")
|
298
|
+
|
299
|
+
if max_val is not None and result > max_val:
|
300
|
+
raise ValueError(f"Value {result} is above maximum {max_val}")
|
301
|
+
|
302
|
+
return result
|
303
|
+
|
304
|
+
|
305
|
+
def parse_sample_rate(value: str) -> float:
|
306
|
+
"""
|
307
|
+
Parse sampling rate (0.0 to 1.0).
|
308
|
+
|
309
|
+
Args:
|
310
|
+
value: String representation of sampling rate
|
311
|
+
|
312
|
+
Returns:
|
313
|
+
Float between 0.0 and 1.0
|
314
|
+
|
315
|
+
Raises:
|
316
|
+
ValueError: If value is not valid or out of range
|
317
|
+
"""
|
318
|
+
return parse_float_with_validation(value, min_val=0.0, max_val=1.0)
|
319
|
+
|
320
|
+
|
321
|
+
def parse_json_dict(value: str) -> dict[str, Any]:
|
322
|
+
"""
|
323
|
+
Parse JSON string into dictionary.
|
324
|
+
|
325
|
+
Args:
|
326
|
+
value: JSON string
|
327
|
+
|
328
|
+
Returns:
|
329
|
+
Parsed dictionary
|
330
|
+
|
331
|
+
Raises:
|
332
|
+
ValueError: If JSON is invalid
|
333
|
+
"""
|
334
|
+
if not value or not value.strip():
|
335
|
+
return {}
|
336
|
+
|
337
|
+
try:
|
338
|
+
result = json.loads(value)
|
339
|
+
if not isinstance(result, dict):
|
340
|
+
raise ValueError(f"Expected JSON object, got {type(result).__name__}")
|
341
|
+
return result
|
342
|
+
except json.JSONDecodeError as e:
|
343
|
+
raise ValueError(f"Invalid JSON: {e}")
|
344
|
+
|
345
|
+
|
346
|
+
def parse_json_list(value: str) -> list[Any]:
|
347
|
+
"""
|
348
|
+
Parse JSON string into list.
|
349
|
+
|
350
|
+
Args:
|
351
|
+
value: JSON string
|
352
|
+
|
353
|
+
Returns:
|
354
|
+
Parsed list
|
355
|
+
|
356
|
+
Raises:
|
357
|
+
ValueError: If JSON is invalid
|
358
|
+
"""
|
359
|
+
if not value or not value.strip():
|
360
|
+
return []
|
361
|
+
|
362
|
+
try:
|
363
|
+
result = json.loads(value)
|
364
|
+
if not isinstance(result, list):
|
365
|
+
raise ValueError(f"Expected JSON array, got {type(result).__name__}")
|
366
|
+
return result
|
367
|
+
except json.JSONDecodeError as e:
|
368
|
+
raise ValueError(f"Invalid JSON: {e}")
|
369
|
+
|
370
|
+
|
371
|
+
def parse_headers(value: str) -> dict[str, str]:
|
372
|
+
"""
|
373
|
+
Parse HTTP headers from string format.
|
374
|
+
|
375
|
+
Format: "key1=value1,key2=value2"
|
376
|
+
Example: "Authorization=Bearer token,Content-Type=application/json"
|
377
|
+
|
378
|
+
Args:
|
379
|
+
value: Comma-separated key=value pairs
|
380
|
+
|
381
|
+
Returns:
|
382
|
+
Dictionary of headers
|
383
|
+
"""
|
384
|
+
if not value or not value.strip():
|
385
|
+
return {}
|
386
|
+
|
387
|
+
result = {}
|
388
|
+
for pair in value.split(","):
|
389
|
+
pair = pair.strip()
|
390
|
+
if not pair:
|
391
|
+
continue
|
392
|
+
|
393
|
+
if "=" not in pair:
|
394
|
+
# Skip invalid entries
|
395
|
+
continue
|
396
|
+
|
397
|
+
key, val = pair.split("=", 1)
|
398
|
+
key = key.strip()
|
399
|
+
val = val.strip()
|
400
|
+
|
401
|
+
if key:
|
402
|
+
result[key] = val
|
403
|
+
|
404
|
+
return result
|
405
|
+
|
406
|
+
|
407
|
+
# Validators (used with validator parameter in field())
|
408
|
+
|
409
|
+
|
410
|
+
def validate_log_level(instance: Any, attribute: Any, value: str) -> None:
|
411
|
+
"""Validate that a log level is valid."""
|
412
|
+
if value not in _VALID_LOG_LEVEL_TUPLE:
|
413
|
+
raise ValueError(
|
414
|
+
f"Invalid log level '{value}' for {attribute.name}. "
|
415
|
+
f"Valid options: {', '.join(_VALID_LOG_LEVEL_TUPLE)}"
|
416
|
+
)
|
417
|
+
|
418
|
+
|
419
|
+
def validate_sample_rate(instance: Any, attribute: Any, value: float) -> None:
|
420
|
+
"""Validate that a sample rate is between 0.0 and 1.0."""
|
421
|
+
if not 0.0 <= value <= 1.0:
|
422
|
+
raise ValueError(
|
423
|
+
f"Sample rate {value} for {attribute.name} must be between 0.0 and 1.0"
|
424
|
+
)
|
425
|
+
|
426
|
+
|
427
|
+
def validate_port(instance: Any, attribute: Any, value: int) -> None:
|
428
|
+
"""Validate that a port number is valid."""
|
429
|
+
if not 1 <= value <= 65535:
|
430
|
+
raise ValueError(
|
431
|
+
f"Port {value} for {attribute.name} must be between 1 and 65535"
|
432
|
+
)
|
433
|
+
|
434
|
+
|
435
|
+
def validate_positive(instance: Any, attribute: Any, value: float | int) -> None:
|
436
|
+
"""Validate that a value is positive."""
|
437
|
+
if value <= 0:
|
438
|
+
raise ValueError(f"Value {value} for {attribute.name} must be positive")
|
439
|
+
|
440
|
+
|
441
|
+
def validate_non_negative(instance: Any, attribute: Any, value: float | int) -> None:
|
442
|
+
"""Validate that a value is non-negative."""
|
443
|
+
if value < 0:
|
444
|
+
raise ValueError(f"Value {value} for {attribute.name} must be non-negative")
|
445
|
+
|
446
|
+
|
447
|
+
def validate_overflow_policy(instance: Any, attribute: Any, value: str) -> None:
|
448
|
+
"""Validate rate limit overflow policy."""
|
449
|
+
valid_policies = ("drop_oldest", "drop_newest", "block")
|
450
|
+
if value not in valid_policies:
|
451
|
+
raise ValueError(
|
452
|
+
f"Invalid overflow policy '{value}' for {attribute.name}. "
|
453
|
+
f"Valid options: {', '.join(valid_policies)}"
|
454
|
+
)
|
455
|
+
|
456
|
+
|
457
|
+
__all__ = [
|
458
|
+
# Parsers/Converters
|
459
|
+
"parse_log_level",
|
460
|
+
"parse_console_formatter",
|
461
|
+
"parse_module_levels",
|
462
|
+
"parse_rate_limits",
|
463
|
+
"parse_comma_list",
|
464
|
+
"parse_bool_extended",
|
465
|
+
"parse_float_with_validation",
|
466
|
+
"parse_sample_rate",
|
467
|
+
"parse_json_dict",
|
468
|
+
"parse_json_list",
|
469
|
+
"parse_headers",
|
470
|
+
# Validators
|
471
|
+
"validate_log_level",
|
472
|
+
"validate_sample_rate",
|
473
|
+
"validate_port",
|
474
|
+
"validate_positive",
|
475
|
+
"validate_non_negative",
|
476
|
+
"validate_overflow_policy",
|
477
|
+
]
|
@@ -0,0 +1,67 @@
|
|
1
|
+
"""
|
2
|
+
Centralized default values for Foundation configuration.
|
3
|
+
All defaults are defined here instead of inline in field definitions.
|
4
|
+
"""
|
5
|
+
|
6
|
+
# =================================
|
7
|
+
# Logging defaults
|
8
|
+
# =================================
|
9
|
+
DEFAULT_LOG_LEVEL = "WARNING"
|
10
|
+
DEFAULT_CONSOLE_FORMATTER = "key_value"
|
11
|
+
DEFAULT_LOGGER_NAME_EMOJI_ENABLED = True
|
12
|
+
DEFAULT_DAS_EMOJI_ENABLED = True
|
13
|
+
DEFAULT_OMIT_TIMESTAMP = False
|
14
|
+
DEFAULT_FOUNDATION_SETUP_LOG_LEVEL = "INFO"
|
15
|
+
DEFAULT_FOUNDATION_LOG_OUTPUT = "stderr"
|
16
|
+
DEFAULT_RATE_LIMIT_ENABLED = False
|
17
|
+
DEFAULT_RATE_LIMIT_EMIT_WARNINGS = True
|
18
|
+
DEFAULT_RATE_LIMIT_GLOBAL = 5.0
|
19
|
+
DEFAULT_RATE_LIMIT_GLOBAL_CAPACITY = 1000
|
20
|
+
DEFAULT_RATE_LIMIT_OVERFLOW_POLICY = "drop_oldest"
|
21
|
+
|
22
|
+
# =================================
|
23
|
+
# Telemetry defaults
|
24
|
+
# =================================
|
25
|
+
DEFAULT_TELEMETRY_GLOBALLY_DISABLED = False
|
26
|
+
DEFAULT_TRACING_ENABLED = True
|
27
|
+
DEFAULT_METRICS_ENABLED = True
|
28
|
+
DEFAULT_OTLP_PROTOCOL = "http/protobuf"
|
29
|
+
DEFAULT_TRACE_SAMPLE_RATE = 1.0
|
30
|
+
|
31
|
+
# =================================
|
32
|
+
# Process defaults
|
33
|
+
# =================================
|
34
|
+
DEFAULT_PROCESS_READLINE_TIMEOUT = 2.0
|
35
|
+
DEFAULT_PROCESS_READCHAR_TIMEOUT = 1.0
|
36
|
+
DEFAULT_PROCESS_TERMINATE_TIMEOUT = 7.0
|
37
|
+
DEFAULT_PROCESS_WAIT_TIMEOUT = 10.0
|
38
|
+
|
39
|
+
# =================================
|
40
|
+
# File/Lock defaults
|
41
|
+
# =================================
|
42
|
+
DEFAULT_FILE_LOCK_TIMEOUT = 10.0
|
43
|
+
|
44
|
+
# =================================
|
45
|
+
# Resilience defaults
|
46
|
+
# =================================
|
47
|
+
DEFAULT_CIRCUIT_BREAKER_RECOVERY_TIMEOUT = 60.0
|
48
|
+
|
49
|
+
# =================================
|
50
|
+
# Integration defaults (OpenObserve)
|
51
|
+
# =================================
|
52
|
+
DEFAULT_OPENOBSERVE_TIMEOUT = 30
|
53
|
+
DEFAULT_OPENOBSERVE_MAX_RETRIES = 3
|
54
|
+
|
55
|
+
# =================================
|
56
|
+
# Testing defaults
|
57
|
+
# =================================
|
58
|
+
DEFAULT_TEST_WAIT_TIMEOUT = 5.0
|
59
|
+
DEFAULT_TEST_PARALLEL_TIMEOUT = 10.0
|
60
|
+
DEFAULT_TEST_CHECKPOINT_TIMEOUT = 5.0
|
61
|
+
|
62
|
+
# =================================
|
63
|
+
# Exit codes
|
64
|
+
# =================================
|
65
|
+
EXIT_SUCCESS = 0
|
66
|
+
EXIT_ERROR = 1
|
67
|
+
EXIT_SIGINT = 130 # Standard exit code for SIGINT
|
provide/foundation/config/env.py
CHANGED
@@ -17,9 +17,6 @@ from attrs import fields
|
|
17
17
|
|
18
18
|
from provide.foundation.config.base import BaseConfig, field
|
19
19
|
from provide.foundation.config.types import ConfigSource
|
20
|
-
from provide.foundation.utils.parsing import (
|
21
|
-
auto_parse,
|
22
|
-
)
|
23
20
|
|
24
21
|
T = TypeVar("T")
|
25
22
|
|
@@ -205,7 +202,9 @@ class RuntimeConfig(BaseConfig):
|
|
205
202
|
raise ValueError(f"Failed to parse {env_var}: {e}")
|
206
203
|
else:
|
207
204
|
# Try to infer parser from type
|
208
|
-
|
205
|
+
from provide.foundation.utils.parsing import auto_parse
|
206
|
+
|
207
|
+
value = auto_parse(attr, value)
|
209
208
|
|
210
209
|
data[attr.name] = value
|
211
210
|
|
@@ -285,7 +284,9 @@ class RuntimeConfig(BaseConfig):
|
|
285
284
|
raise ValueError(f"Failed to parse {env_var}: {e}")
|
286
285
|
else:
|
287
286
|
# Try to infer parser from type
|
288
|
-
|
287
|
+
from provide.foundation.utils.parsing import auto_parse
|
288
|
+
|
289
|
+
value = auto_parse(attr, value)
|
289
290
|
|
290
291
|
data[field_name] = value
|
291
292
|
|
@@ -307,21 +308,6 @@ class RuntimeConfig(BaseConfig):
|
|
307
308
|
except Exception as e:
|
308
309
|
raise ValueError(f"Failed to read secret from file '{file_path}': {e}")
|
309
310
|
|
310
|
-
@staticmethod
|
311
|
-
def _auto_parse(attr: Any, value: str) -> Any:
|
312
|
-
"""
|
313
|
-
Automatically parse value based on field type.
|
314
|
-
|
315
|
-
Args:
|
316
|
-
attr: Field attribute
|
317
|
-
value: String value to parse
|
318
|
-
|
319
|
-
Returns:
|
320
|
-
Parsed value
|
321
|
-
"""
|
322
|
-
# Use the utility function from utils.parsing
|
323
|
-
return auto_parse(attr, value)
|
324
|
-
|
325
311
|
def to_env_dict(self, prefix: str = "", delimiter: str = "_") -> dict[str, str]:
|
326
312
|
"""
|
327
313
|
Convert configuration to environment variable dictionary.
|
@@ -21,6 +21,7 @@ from provide.foundation.config.types import ConfigDict, ConfigFormat, ConfigSour
|
|
21
21
|
from provide.foundation.errors.config import ConfigurationError
|
22
22
|
from provide.foundation.errors.decorators import with_error_handling
|
23
23
|
from provide.foundation.errors.resources import NotFoundError
|
24
|
+
from provide.foundation.file.safe import safe_read_text
|
24
25
|
|
25
26
|
T = TypeVar("T", bound=BaseConfig)
|
26
27
|
|
@@ -113,9 +114,14 @@ class FileConfigLoader(ConfigLoader):
|
|
113
114
|
async with aiofiles.open(self.path, encoding=self.encoding) as f:
|
114
115
|
content = await f.read()
|
115
116
|
else:
|
116
|
-
# Fallback to synchronous read
|
117
|
-
|
118
|
-
|
117
|
+
# Fallback to synchronous read using Foundation's safe file operations
|
118
|
+
content = safe_read_text(self.path, encoding=self.encoding)
|
119
|
+
if not content:
|
120
|
+
raise ConfigurationError(
|
121
|
+
f"Failed to read config file: {self.path}",
|
122
|
+
code="CONFIG_READ_ERROR",
|
123
|
+
path=str(self.path),
|
124
|
+
)
|
119
125
|
|
120
126
|
if self.format == ConfigFormat.JSON:
|
121
127
|
return json.loads(content)
|
@@ -144,7 +150,7 @@ class FileConfigLoader(ConfigLoader):
|
|
144
150
|
format=str(self.format),
|
145
151
|
)
|
146
152
|
|
147
|
-
def _ini_to_dict(self, parser) -> ConfigDict:
|
153
|
+
def _ini_to_dict(self, parser: object) -> ConfigDict:
|
148
154
|
"""Convert INI parser to dictionary."""
|
149
155
|
result = {}
|
150
156
|
for section in parser.sections():
|
@@ -14,18 +14,18 @@ from typing import Any, TypeVar
|
|
14
14
|
from provide.foundation.config.base import BaseConfig
|
15
15
|
from provide.foundation.config.env import RuntimeConfig
|
16
16
|
from provide.foundation.config.loader import (
|
17
|
+
ConfigLoader,
|
17
18
|
DictConfigLoader,
|
18
19
|
FileConfigLoader,
|
19
20
|
MultiSourceLoader,
|
20
21
|
)
|
21
22
|
from provide.foundation.config.manager import ConfigManager
|
22
23
|
from provide.foundation.config.types import ConfigDict, ConfigSource
|
23
|
-
from provide.foundation.config.loader import ConfigLoader
|
24
24
|
|
25
25
|
T = TypeVar("T", bound=BaseConfig)
|
26
26
|
|
27
27
|
|
28
|
-
def run_async(coro):
|
28
|
+
def run_async(coro: Any) -> Any:
|
29
29
|
"""
|
30
30
|
Run an async coroutine in a sync context.
|
31
31
|
|
@@ -230,7 +230,7 @@ class SyncConfigManager:
|
|
230
230
|
|
231
231
|
def __init__(self, loader: ConfigLoader | None = None) -> None:
|
232
232
|
"""Initialize sync config manager.
|
233
|
-
|
233
|
+
|
234
234
|
Args:
|
235
235
|
loader: Optional config loader for loading configurations.
|
236
236
|
"""
|
@@ -245,14 +245,16 @@ class SyncConfigManager:
|
|
245
245
|
"""Get a configuration by name (sync)."""
|
246
246
|
return run_async(self._async_manager.get(name))
|
247
247
|
|
248
|
-
def load(
|
248
|
+
def load(
|
249
|
+
self, name: str, config_class: type[T], loader: ConfigLoader | None = None
|
250
|
+
) -> T:
|
249
251
|
"""Load a configuration (sync).
|
250
|
-
|
252
|
+
|
251
253
|
Args:
|
252
254
|
name: Configuration name
|
253
255
|
config_class: Configuration class
|
254
256
|
loader: Optional loader (uses registered if None)
|
255
|
-
|
257
|
+
|
256
258
|
Returns:
|
257
259
|
Configuration instance
|
258
260
|
"""
|