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