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.
Files changed (161) hide show
  1. provide/foundation/__init__.py +41 -23
  2. provide/foundation/archive/__init__.py +23 -0
  3. provide/foundation/archive/base.py +70 -0
  4. provide/foundation/archive/bzip2.py +157 -0
  5. provide/foundation/archive/gzip.py +159 -0
  6. provide/foundation/archive/operations.py +334 -0
  7. provide/foundation/archive/tar.py +164 -0
  8. provide/foundation/archive/zip.py +203 -0
  9. provide/foundation/cli/__init__.py +2 -2
  10. provide/foundation/cli/commands/deps.py +13 -7
  11. provide/foundation/cli/commands/logs/__init__.py +1 -1
  12. provide/foundation/cli/commands/logs/query.py +1 -1
  13. provide/foundation/cli/commands/logs/send.py +1 -1
  14. provide/foundation/cli/commands/logs/tail.py +1 -1
  15. provide/foundation/cli/decorators.py +11 -10
  16. provide/foundation/cli/main.py +1 -1
  17. provide/foundation/cli/testing.py +2 -35
  18. provide/foundation/cli/utils.py +21 -17
  19. provide/foundation/config/__init__.py +35 -2
  20. provide/foundation/config/base.py +2 -2
  21. provide/foundation/config/converters.py +479 -0
  22. provide/foundation/config/defaults.py +67 -0
  23. provide/foundation/config/env.py +4 -19
  24. provide/foundation/config/loader.py +9 -3
  25. provide/foundation/config/sync.py +19 -4
  26. provide/foundation/console/input.py +5 -5
  27. provide/foundation/console/output.py +35 -13
  28. provide/foundation/context/__init__.py +8 -4
  29. provide/foundation/context/core.py +85 -109
  30. provide/foundation/core.py +1 -2
  31. provide/foundation/crypto/__init__.py +2 -0
  32. provide/foundation/crypto/certificates/__init__.py +34 -0
  33. provide/foundation/crypto/certificates/base.py +173 -0
  34. provide/foundation/crypto/certificates/certificate.py +290 -0
  35. provide/foundation/crypto/certificates/factory.py +213 -0
  36. provide/foundation/crypto/certificates/generator.py +138 -0
  37. provide/foundation/crypto/certificates/loader.py +130 -0
  38. provide/foundation/crypto/certificates/operations.py +198 -0
  39. provide/foundation/crypto/certificates/trust.py +107 -0
  40. provide/foundation/errors/__init__.py +2 -3
  41. provide/foundation/errors/decorators.py +0 -231
  42. provide/foundation/errors/types.py +0 -97
  43. provide/foundation/eventsets/__init__.py +0 -0
  44. provide/foundation/eventsets/display.py +84 -0
  45. provide/foundation/eventsets/registry.py +160 -0
  46. provide/foundation/eventsets/resolver.py +192 -0
  47. provide/foundation/eventsets/sets/das.py +128 -0
  48. provide/foundation/eventsets/sets/database.py +125 -0
  49. provide/foundation/eventsets/sets/http.py +153 -0
  50. provide/foundation/eventsets/sets/llm.py +139 -0
  51. provide/foundation/eventsets/sets/task_queue.py +107 -0
  52. provide/foundation/eventsets/types.py +70 -0
  53. provide/foundation/file/directory.py +13 -22
  54. provide/foundation/file/lock.py +3 -1
  55. provide/foundation/hub/components.py +77 -515
  56. provide/foundation/hub/config.py +151 -0
  57. provide/foundation/hub/discovery.py +62 -0
  58. provide/foundation/hub/handlers.py +81 -0
  59. provide/foundation/hub/lifecycle.py +194 -0
  60. provide/foundation/hub/manager.py +4 -4
  61. provide/foundation/hub/processors.py +44 -0
  62. provide/foundation/integrations/__init__.py +11 -0
  63. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  64. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  65. provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
  66. provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
  67. provide/foundation/integrations/openobserve/config.py +37 -0
  68. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  69. provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
  70. provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
  71. provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
  72. provide/foundation/logger/__init__.py +3 -10
  73. provide/foundation/logger/config/logging.py +68 -298
  74. provide/foundation/logger/config/telemetry.py +41 -121
  75. provide/foundation/logger/core.py +0 -2
  76. provide/foundation/logger/custom_processors.py +1 -0
  77. provide/foundation/logger/factories.py +11 -2
  78. provide/foundation/logger/processors/main.py +20 -84
  79. provide/foundation/logger/setup/__init__.py +5 -1
  80. provide/foundation/logger/setup/coordinator.py +76 -24
  81. provide/foundation/logger/setup/processors.py +2 -9
  82. provide/foundation/logger/trace.py +27 -0
  83. provide/foundation/metrics/otel.py +10 -10
  84. provide/foundation/observability/__init__.py +2 -2
  85. provide/foundation/process/__init__.py +9 -0
  86. provide/foundation/process/exit.py +47 -0
  87. provide/foundation/process/lifecycle.py +115 -59
  88. provide/foundation/resilience/__init__.py +35 -0
  89. provide/foundation/resilience/circuit.py +164 -0
  90. provide/foundation/resilience/decorators.py +220 -0
  91. provide/foundation/resilience/fallback.py +193 -0
  92. provide/foundation/resilience/retry.py +325 -0
  93. provide/foundation/streams/config.py +79 -0
  94. provide/foundation/streams/console.py +7 -8
  95. provide/foundation/streams/core.py +6 -3
  96. provide/foundation/streams/file.py +12 -2
  97. provide/foundation/testing/__init__.py +84 -2
  98. provide/foundation/testing/archive/__init__.py +24 -0
  99. provide/foundation/testing/archive/fixtures.py +217 -0
  100. provide/foundation/testing/cli.py +30 -17
  101. provide/foundation/testing/common/__init__.py +32 -0
  102. provide/foundation/testing/common/fixtures.py +236 -0
  103. provide/foundation/testing/file/__init__.py +40 -0
  104. provide/foundation/testing/file/content_fixtures.py +316 -0
  105. provide/foundation/testing/file/directory_fixtures.py +107 -0
  106. provide/foundation/testing/file/fixtures.py +52 -0
  107. provide/foundation/testing/file/special_fixtures.py +153 -0
  108. provide/foundation/testing/logger.py +117 -11
  109. provide/foundation/testing/mocking/__init__.py +46 -0
  110. provide/foundation/testing/mocking/fixtures.py +331 -0
  111. provide/foundation/testing/process/__init__.py +48 -0
  112. provide/foundation/testing/process/async_fixtures.py +405 -0
  113. provide/foundation/testing/process/fixtures.py +56 -0
  114. provide/foundation/testing/process/subprocess_fixtures.py +209 -0
  115. provide/foundation/testing/threading/__init__.py +38 -0
  116. provide/foundation/testing/threading/basic_fixtures.py +101 -0
  117. provide/foundation/testing/threading/data_fixtures.py +99 -0
  118. provide/foundation/testing/threading/execution_fixtures.py +263 -0
  119. provide/foundation/testing/threading/fixtures.py +54 -0
  120. provide/foundation/testing/threading/sync_fixtures.py +97 -0
  121. provide/foundation/testing/time/__init__.py +32 -0
  122. provide/foundation/testing/time/fixtures.py +409 -0
  123. provide/foundation/testing/transport/__init__.py +30 -0
  124. provide/foundation/testing/transport/fixtures.py +280 -0
  125. provide/foundation/tools/__init__.py +58 -0
  126. provide/foundation/tools/base.py +348 -0
  127. provide/foundation/tools/cache.py +268 -0
  128. provide/foundation/tools/downloader.py +224 -0
  129. provide/foundation/tools/installer.py +254 -0
  130. provide/foundation/tools/registry.py +223 -0
  131. provide/foundation/tools/resolver.py +321 -0
  132. provide/foundation/tools/verifier.py +186 -0
  133. provide/foundation/tracer/otel.py +7 -11
  134. provide/foundation/tracer/spans.py +2 -2
  135. provide/foundation/transport/__init__.py +155 -0
  136. provide/foundation/transport/base.py +171 -0
  137. provide/foundation/transport/client.py +266 -0
  138. provide/foundation/transport/config.py +140 -0
  139. provide/foundation/transport/errors.py +79 -0
  140. provide/foundation/transport/http.py +232 -0
  141. provide/foundation/transport/middleware.py +360 -0
  142. provide/foundation/transport/registry.py +167 -0
  143. provide/foundation/transport/types.py +45 -0
  144. provide/foundation/utils/deps.py +14 -12
  145. provide/foundation/utils/parsing.py +49 -4
  146. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +5 -28
  147. provide_foundation-0.0.0.dev2.dist-info/RECORD +225 -0
  148. provide/foundation/cli/commands/logs/generate_old.py +0 -569
  149. provide/foundation/crypto/certificates.py +0 -896
  150. provide/foundation/logger/emoji/__init__.py +0 -44
  151. provide/foundation/logger/emoji/matrix.py +0 -209
  152. provide/foundation/logger/emoji/sets.py +0 -458
  153. provide/foundation/logger/emoji/types.py +0 -56
  154. provide/foundation/logger/setup/emoji_resolver.py +0 -64
  155. provide_foundation-0.0.0.dev0.dist-info/RECORD +0 -149
  156. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  157. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  158. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
  159. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
  160. {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
  161. {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
@@ -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
- value = RuntimeConfig._auto_parse(attr, value)
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
- value = RuntimeConfig._auto_parse(attr, value)
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
- with open(self.path, encoding=self.encoding) as f:
118
- content = f.read()
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 Context
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() -> Context | None:
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, Context):
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: Context | None = None) -> bool:
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: Context | None = None) -> bool:
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()