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.
Files changed (163) hide show
  1. provide/foundation/__init__.py +36 -10
  2. provide/foundation/archive/__init__.py +1 -1
  3. provide/foundation/archive/base.py +15 -14
  4. provide/foundation/archive/bzip2.py +40 -40
  5. provide/foundation/archive/gzip.py +42 -42
  6. provide/foundation/archive/operations.py +93 -96
  7. provide/foundation/archive/tar.py +33 -31
  8. provide/foundation/archive/zip.py +52 -50
  9. provide/foundation/asynctools/__init__.py +20 -0
  10. provide/foundation/asynctools/core.py +126 -0
  11. provide/foundation/cli/__init__.py +2 -2
  12. provide/foundation/cli/commands/deps.py +15 -9
  13. provide/foundation/cli/commands/logs/__init__.py +3 -3
  14. provide/foundation/cli/commands/logs/generate.py +2 -2
  15. provide/foundation/cli/commands/logs/query.py +4 -4
  16. provide/foundation/cli/commands/logs/send.py +3 -3
  17. provide/foundation/cli/commands/logs/tail.py +3 -3
  18. provide/foundation/cli/decorators.py +11 -11
  19. provide/foundation/cli/main.py +1 -1
  20. provide/foundation/cli/testing.py +2 -40
  21. provide/foundation/cli/utils.py +21 -18
  22. provide/foundation/config/__init__.py +35 -2
  23. provide/foundation/config/base.py +2 -2
  24. provide/foundation/config/converters.py +477 -0
  25. provide/foundation/config/defaults.py +67 -0
  26. provide/foundation/config/env.py +6 -20
  27. provide/foundation/config/loader.py +10 -4
  28. provide/foundation/config/sync.py +8 -6
  29. provide/foundation/config/types.py +5 -5
  30. provide/foundation/config/validators.py +4 -4
  31. provide/foundation/console/input.py +5 -5
  32. provide/foundation/console/output.py +36 -14
  33. provide/foundation/context/__init__.py +8 -4
  34. provide/foundation/context/core.py +88 -110
  35. provide/foundation/crypto/certificates/__init__.py +9 -5
  36. provide/foundation/crypto/certificates/base.py +2 -2
  37. provide/foundation/crypto/certificates/certificate.py +48 -19
  38. provide/foundation/crypto/certificates/factory.py +26 -18
  39. provide/foundation/crypto/certificates/generator.py +24 -23
  40. provide/foundation/crypto/certificates/loader.py +24 -16
  41. provide/foundation/crypto/certificates/operations.py +17 -10
  42. provide/foundation/crypto/certificates/trust.py +21 -21
  43. provide/foundation/env/__init__.py +28 -0
  44. provide/foundation/env/core.py +218 -0
  45. provide/foundation/errors/__init__.py +3 -3
  46. provide/foundation/errors/decorators.py +0 -234
  47. provide/foundation/errors/types.py +0 -98
  48. provide/foundation/eventsets/display.py +13 -14
  49. provide/foundation/eventsets/registry.py +61 -31
  50. provide/foundation/eventsets/resolver.py +50 -46
  51. provide/foundation/eventsets/sets/das.py +8 -8
  52. provide/foundation/eventsets/sets/database.py +14 -14
  53. provide/foundation/eventsets/sets/http.py +21 -21
  54. provide/foundation/eventsets/sets/llm.py +16 -16
  55. provide/foundation/eventsets/sets/task_queue.py +13 -13
  56. provide/foundation/eventsets/types.py +7 -7
  57. provide/foundation/file/directory.py +14 -23
  58. provide/foundation/file/lock.py +4 -3
  59. provide/foundation/hub/components.py +75 -389
  60. provide/foundation/hub/config.py +157 -0
  61. provide/foundation/hub/discovery.py +63 -0
  62. provide/foundation/hub/handlers.py +89 -0
  63. provide/foundation/hub/lifecycle.py +195 -0
  64. provide/foundation/hub/manager.py +7 -4
  65. provide/foundation/hub/processors.py +49 -0
  66. provide/foundation/integrations/__init__.py +11 -0
  67. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  68. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  69. provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
  70. provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
  71. provide/foundation/integrations/openobserve/config.py +37 -0
  72. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  73. provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
  74. provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
  75. provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
  76. provide/foundation/logger/__init__.py +0 -1
  77. provide/foundation/logger/config/base.py +1 -1
  78. provide/foundation/logger/config/logging.py +69 -299
  79. provide/foundation/logger/config/telemetry.py +39 -121
  80. provide/foundation/logger/factories.py +2 -2
  81. provide/foundation/logger/processors/main.py +12 -10
  82. provide/foundation/logger/ratelimit/limiters.py +4 -4
  83. provide/foundation/logger/ratelimit/processor.py +1 -1
  84. provide/foundation/logger/setup/coordinator.py +39 -25
  85. provide/foundation/logger/setup/processors.py +3 -3
  86. provide/foundation/logger/setup/testing.py +14 -0
  87. provide/foundation/logger/trace.py +5 -5
  88. provide/foundation/metrics/__init__.py +1 -1
  89. provide/foundation/metrics/otel.py +3 -1
  90. provide/foundation/observability/__init__.py +3 -3
  91. provide/foundation/process/__init__.py +9 -0
  92. provide/foundation/process/exit.py +48 -0
  93. provide/foundation/process/lifecycle.py +69 -46
  94. provide/foundation/resilience/__init__.py +36 -0
  95. provide/foundation/resilience/circuit.py +166 -0
  96. provide/foundation/resilience/decorators.py +236 -0
  97. provide/foundation/resilience/fallback.py +208 -0
  98. provide/foundation/resilience/retry.py +327 -0
  99. provide/foundation/serialization/__init__.py +16 -0
  100. provide/foundation/serialization/core.py +70 -0
  101. provide/foundation/streams/config.py +78 -0
  102. provide/foundation/streams/console.py +4 -5
  103. provide/foundation/streams/core.py +5 -2
  104. provide/foundation/streams/file.py +12 -2
  105. provide/foundation/testing/__init__.py +29 -9
  106. provide/foundation/testing/archive/__init__.py +7 -7
  107. provide/foundation/testing/archive/fixtures.py +58 -54
  108. provide/foundation/testing/cli.py +30 -20
  109. provide/foundation/testing/common/__init__.py +13 -15
  110. provide/foundation/testing/common/fixtures.py +27 -57
  111. provide/foundation/testing/file/__init__.py +15 -15
  112. provide/foundation/testing/file/content_fixtures.py +289 -0
  113. provide/foundation/testing/file/directory_fixtures.py +107 -0
  114. provide/foundation/testing/file/fixtures.py +42 -516
  115. provide/foundation/testing/file/special_fixtures.py +145 -0
  116. provide/foundation/testing/logger.py +89 -8
  117. provide/foundation/testing/mocking/__init__.py +21 -21
  118. provide/foundation/testing/mocking/fixtures.py +80 -67
  119. provide/foundation/testing/process/__init__.py +23 -23
  120. provide/foundation/testing/process/async_fixtures.py +414 -0
  121. provide/foundation/testing/process/fixtures.py +48 -571
  122. provide/foundation/testing/process/subprocess_fixtures.py +210 -0
  123. provide/foundation/testing/threading/__init__.py +17 -17
  124. provide/foundation/testing/threading/basic_fixtures.py +105 -0
  125. provide/foundation/testing/threading/data_fixtures.py +101 -0
  126. provide/foundation/testing/threading/execution_fixtures.py +278 -0
  127. provide/foundation/testing/threading/fixtures.py +32 -502
  128. provide/foundation/testing/threading/sync_fixtures.py +100 -0
  129. provide/foundation/testing/time/__init__.py +11 -11
  130. provide/foundation/testing/time/fixtures.py +95 -83
  131. provide/foundation/testing/transport/__init__.py +9 -9
  132. provide/foundation/testing/transport/fixtures.py +54 -54
  133. provide/foundation/time/__init__.py +18 -0
  134. provide/foundation/time/core.py +63 -0
  135. provide/foundation/tools/__init__.py +2 -2
  136. provide/foundation/tools/base.py +68 -67
  137. provide/foundation/tools/cache.py +69 -74
  138. provide/foundation/tools/downloader.py +68 -62
  139. provide/foundation/tools/installer.py +51 -57
  140. provide/foundation/tools/registry.py +38 -45
  141. provide/foundation/tools/resolver.py +70 -68
  142. provide/foundation/tools/verifier.py +39 -50
  143. provide/foundation/tracer/spans.py +2 -14
  144. provide/foundation/transport/__init__.py +26 -33
  145. provide/foundation/transport/base.py +32 -30
  146. provide/foundation/transport/client.py +44 -49
  147. provide/foundation/transport/config.py +36 -107
  148. provide/foundation/transport/errors.py +13 -27
  149. provide/foundation/transport/http.py +69 -55
  150. provide/foundation/transport/middleware.py +113 -114
  151. provide/foundation/transport/registry.py +29 -27
  152. provide/foundation/transport/types.py +6 -6
  153. provide/foundation/utils/deps.py +17 -14
  154. provide/foundation/utils/parsing.py +49 -4
  155. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
  156. provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
  157. provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
  158. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  159. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  160. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
  161. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
  162. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
  163. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,218 @@
1
+ """Core environment variable utilities for Foundation."""
2
+
3
+ import os
4
+
5
+ from provide.foundation.errors import ValidationError
6
+
7
+
8
+ def get_env(key: str, default: str | None = None) -> str | None:
9
+ """
10
+ Get environment variable with Foundation tracking.
11
+
12
+ Args:
13
+ key: Environment variable name
14
+ default: Default value if variable not found
15
+
16
+ Returns:
17
+ Environment variable value or default
18
+
19
+ Example:
20
+ >>> get_env("HOME") # doctest: +SKIP
21
+ '/Users/username'
22
+ >>> get_env("NONEXISTENT", "fallback")
23
+ 'fallback'
24
+ """
25
+ return os.environ.get(key, default)
26
+
27
+
28
+ def set_env(key: str, value: str) -> None:
29
+ """
30
+ Set environment variable with validation.
31
+
32
+ Args:
33
+ key: Environment variable name
34
+ value: Value to set
35
+
36
+ Raises:
37
+ ValidationError: If key or value is invalid
38
+
39
+ Example:
40
+ >>> set_env("TEST_VAR", "test_value")
41
+ >>> get_env("TEST_VAR")
42
+ 'test_value'
43
+ """
44
+ if not isinstance(key, str) or not key:
45
+ raise ValidationError("Environment variable key must be a non-empty string")
46
+ if not isinstance(value, str):
47
+ raise ValidationError("Environment variable value must be a string")
48
+
49
+ os.environ[key] = value
50
+
51
+
52
+ def unset_env(key: str) -> None:
53
+ """
54
+ Remove environment variable if it exists.
55
+
56
+ Args:
57
+ key: Environment variable name to remove
58
+
59
+ Example:
60
+ >>> set_env("TEMP_VAR", "value")
61
+ >>> unset_env("TEMP_VAR")
62
+ >>> has_env("TEMP_VAR")
63
+ False
64
+ """
65
+ os.environ.pop(key, None)
66
+
67
+
68
+ def has_env(key: str) -> bool:
69
+ """
70
+ Check if environment variable exists.
71
+
72
+ Args:
73
+ key: Environment variable name
74
+
75
+ Returns:
76
+ True if variable exists, False otherwise
77
+
78
+ Example:
79
+ >>> has_env("PATH")
80
+ True
81
+ >>> has_env("DEFINITELY_NOT_SET")
82
+ False
83
+ """
84
+ return key in os.environ
85
+
86
+
87
+ def get_env_int(key: str, default: int | None = None) -> int | None:
88
+ """
89
+ Get environment variable as integer.
90
+
91
+ Args:
92
+ key: Environment variable name
93
+ default: Default value if variable not found or invalid
94
+
95
+ Returns:
96
+ Integer value or default
97
+
98
+ Raises:
99
+ ValidationError: If value exists but cannot be converted to int
100
+
101
+ Example:
102
+ >>> set_env("PORT", "8080")
103
+ >>> get_env_int("PORT")
104
+ 8080
105
+ >>> get_env_int("MISSING_PORT", 3000)
106
+ 3000
107
+ """
108
+ value = os.environ.get(key)
109
+ if value is None:
110
+ return default
111
+
112
+ try:
113
+ return int(value)
114
+ except ValueError as e:
115
+ raise ValidationError(
116
+ f"Environment variable {key}='{value}' cannot be converted to int"
117
+ ) from e
118
+
119
+
120
+ def get_env_bool(key: str, default: bool | None = None) -> bool | None:
121
+ """
122
+ Get environment variable as boolean.
123
+
124
+ Recognizes: true/false, yes/no, 1/0, on/off (case insensitive)
125
+
126
+ Args:
127
+ key: Environment variable name
128
+ default: Default value if variable not found
129
+
130
+ Returns:
131
+ Boolean value or default
132
+
133
+ Raises:
134
+ ValidationError: If value exists but cannot be converted to bool
135
+
136
+ Example:
137
+ >>> set_env("DEBUG", "true")
138
+ >>> get_env_bool("DEBUG")
139
+ True
140
+ >>> set_env("VERBOSE", "no")
141
+ >>> get_env_bool("VERBOSE")
142
+ False
143
+ """
144
+ value = os.environ.get(key)
145
+ if value is None:
146
+ return default
147
+
148
+ value_lower = value.lower().strip()
149
+ if value_lower in ("true", "yes", "1", "on"):
150
+ return True
151
+ elif value_lower in ("false", "no", "0", "off"):
152
+ return False
153
+ else:
154
+ raise ValidationError(
155
+ f"Environment variable {key}='{value}' cannot be converted to bool"
156
+ )
157
+
158
+
159
+ def get_env_float(key: str, default: float | None = None) -> float | None:
160
+ """
161
+ Get environment variable as float.
162
+
163
+ Args:
164
+ key: Environment variable name
165
+ default: Default value if variable not found or invalid
166
+
167
+ Returns:
168
+ Float value or default
169
+
170
+ Raises:
171
+ ValidationError: If value exists but cannot be converted to float
172
+
173
+ Example:
174
+ >>> set_env("TIMEOUT", "30.5")
175
+ >>> get_env_float("TIMEOUT")
176
+ 30.5
177
+ """
178
+ value = os.environ.get(key)
179
+ if value is None:
180
+ return default
181
+
182
+ try:
183
+ return float(value)
184
+ except ValueError as e:
185
+ raise ValidationError(
186
+ f"Environment variable {key}='{value}' cannot be converted to float"
187
+ ) from e
188
+
189
+
190
+ def get_env_list(
191
+ key: str, separator: str = ",", default: list[str] | None = None
192
+ ) -> list[str] | None:
193
+ """
194
+ Get environment variable as list of strings.
195
+
196
+ Args:
197
+ key: Environment variable name
198
+ separator: Character to split on (default: comma)
199
+ default: Default value if variable not found
200
+
201
+ Returns:
202
+ List of strings or default
203
+
204
+ Example:
205
+ >>> set_env("ALLOWED_HOSTS", "localhost,127.0.0.1,example.com")
206
+ >>> get_env_list("ALLOWED_HOSTS")
207
+ ['localhost', '127.0.0.1', 'example.com']
208
+ >>> get_env_list("MISSING", default=["fallback"])
209
+ ['fallback']
210
+ """
211
+ value = os.environ.get(key)
212
+ if value is None:
213
+ return default
214
+
215
+ if not value.strip():
216
+ return []
217
+
218
+ return [item.strip() for item in value.split(separator) if item.strip()]
@@ -20,7 +20,6 @@ from provide.foundation.errors.context import (
20
20
  )
21
21
  from provide.foundation.errors.decorators import (
22
22
  fallback_on_error,
23
- retry_on_error,
24
23
  suppress_and_log,
25
24
  with_error_handling,
26
25
  )
@@ -50,9 +49,11 @@ from provide.foundation.errors.safe_decorators import log_only_error_context
50
49
  from provide.foundation.errors.types import (
51
50
  ErrorCode,
52
51
  ErrorMetadata,
53
- RetryPolicy,
54
52
  )
55
53
 
54
+ # Re-export from resilience module for compatibility
55
+ from provide.foundation.resilience.decorators import retry as retry_on_error
56
+
56
57
  __all__ = [
57
58
  "AlreadyExistsError",
58
59
  "AuthenticationError",
@@ -77,7 +78,6 @@ __all__ = [
77
78
  "ProcessError",
78
79
  "ProcessTimeoutError",
79
80
  "ResourceError",
80
- "RetryPolicy",
81
81
  "RuntimeError",
82
82
  "StateError",
83
83
  "TimeoutError",
@@ -7,13 +7,9 @@ fallback, and error suppression.
7
7
  from collections.abc import Callable
8
8
  import functools
9
9
  import inspect
10
- import time
11
10
  from typing import Any, TypeVar
12
11
 
13
- from attrs import define, field
14
-
15
12
  from provide.foundation.errors.base import FoundationError
16
- from provide.foundation.errors.types import RetryPolicy
17
13
 
18
14
  F = TypeVar("F", bound=Callable[..., Any])
19
15
 
@@ -148,126 +144,6 @@ def with_error_handling(
148
144
  return decorator(func)
149
145
 
150
146
 
151
- def retry_on_error(
152
- *exceptions: type[Exception],
153
- policy: RetryPolicy | None = None,
154
- max_attempts: int | None = None,
155
- delay: float | None = None,
156
- backoff: float | None = None,
157
- on_retry: Callable[[int, Exception], None] | None = None,
158
- ) -> Callable[[F], F]:
159
- """Decorator for retrying operations on specific errors.
160
-
161
- Args:
162
- *exceptions: Exception types to retry on (all if empty).
163
- policy: Complete retry policy (overrides other retry params).
164
- max_attempts: Maximum retry attempts (ignored if policy provided).
165
- delay: Base delay between retries in seconds.
166
- backoff: Backoff multiplier for delays.
167
- on_retry: Callback function called before each retry.
168
-
169
- Returns:
170
- Decorated function.
171
-
172
- Examples:
173
- >>> @retry_on_error(ConnectionError, TimeoutError, max_attempts=3)
174
- ... def fetch_data():
175
- ... return api_call()
176
-
177
- >>> @retry_on_error(
178
- ... policy=RetryPolicy(max_attempts=5, backoff="exponential")
179
- ... )
180
- ... def unreliable_operation():
181
- ... pass
182
- """
183
- # Use provided policy or create one from parameters
184
- if policy is None:
185
- from provide.foundation.errors.types import BackoffStrategy
186
-
187
- # Determine backoff strategy
188
- if backoff is not None and backoff > 1:
189
- backoff_strategy = BackoffStrategy.EXPONENTIAL
190
- elif backoff == 1:
191
- backoff_strategy = BackoffStrategy.FIXED
192
- else:
193
- backoff_strategy = BackoffStrategy.EXPONENTIAL
194
-
195
- policy = RetryPolicy(
196
- max_attempts=max_attempts or 3,
197
- base_delay=delay or 1.0,
198
- backoff=backoff_strategy,
199
- retryable_errors=exceptions if exceptions else None,
200
- )
201
-
202
- def decorator(func: F) -> F:
203
- @functools.wraps(func)
204
- def wrapper(*args, **kwargs):
205
- last_exception = None
206
-
207
- for attempt in range(1, policy.max_attempts + 1):
208
- try:
209
- return func(*args, **kwargs)
210
- except Exception as e:
211
- last_exception = e
212
-
213
- # Check if we should retry this error
214
- if not policy.should_retry(e, attempt):
215
- if attempt > 1: # Only log if we've actually retried
216
- _get_logger().error(
217
- f"All {attempt} retry attempts failed for {func.__name__}",
218
- attempts=attempt,
219
- error=str(e),
220
- error_type=type(e).__name__,
221
- )
222
- raise
223
-
224
- # Don't retry on last attempt
225
- if attempt >= policy.max_attempts:
226
- _get_logger().error(
227
- f"All {policy.max_attempts} retry attempts failed for {func.__name__}",
228
- attempts=policy.max_attempts,
229
- error=str(e),
230
- error_type=type(e).__name__,
231
- )
232
- raise
233
-
234
- # Calculate delay
235
- retry_delay = policy.calculate_delay(attempt)
236
-
237
- # Log retry attempt
238
- _get_logger().warning(
239
- f"Retry {attempt}/{policy.max_attempts} for {func.__name__} after {retry_delay:.2f}s",
240
- function=func.__name__,
241
- attempt=attempt,
242
- max_attempts=policy.max_attempts,
243
- delay=retry_delay,
244
- error=str(e),
245
- error_type=type(e).__name__,
246
- )
247
-
248
- # Call retry callback if provided
249
- if on_retry:
250
- try:
251
- on_retry(attempt, e)
252
- except Exception as callback_error:
253
- _get_logger().warning(
254
- f"Retry callback failed: {callback_error}",
255
- function=func.__name__,
256
- attempt=attempt,
257
- )
258
-
259
- # Wait before retry
260
- time.sleep(retry_delay)
261
-
262
- # Should never reach here, but just in case
263
- if last_exception:
264
- raise last_exception
265
-
266
- return wrapper # type: ignore
267
-
268
- return decorator
269
-
270
-
271
147
  def suppress_and_log(
272
148
  *exceptions: type[Exception],
273
149
  fallback: Any = None,
@@ -372,113 +248,3 @@ def fallback_on_error(
372
248
  return wrapper # type: ignore
373
249
 
374
250
  return decorator
375
-
376
-
377
- @define(kw_only=True, slots=True)
378
- class CircuitBreaker:
379
- """Circuit breaker pattern for preventing cascading failures.
380
-
381
- Attributes:
382
- failure_threshold: Number of failures before opening circuit.
383
- recovery_timeout: Seconds to wait before attempting recovery.
384
- expected_exception: Exception types that trigger the breaker.
385
- """
386
-
387
- failure_threshold: int = 5
388
- recovery_timeout: float = 60.0
389
- expected_exception: tuple[type[Exception], ...] = field(default=(Exception,))
390
-
391
- # Internal state
392
- _failure_count: int = field(init=False, default=0)
393
- _last_failure_time: float | None = field(init=False, default=None)
394
- _state: str = field(init=False, default="closed") # closed, open, half_open
395
-
396
- def __call__(self, func: F) -> F:
397
- """Decorator to apply circuit breaker to a function."""
398
-
399
- @functools.wraps(func)
400
- def wrapper(*args, **kwargs):
401
- # Check circuit state
402
- if self._state == "open":
403
- # Check if we should try half-open
404
- if (
405
- self._last_failure_time
406
- and (time.time() - self._last_failure_time) > self.recovery_timeout
407
- ):
408
- self._state = "half_open"
409
- _get_logger().info(
410
- f"Circuit breaker for {func.__name__} entering half-open state",
411
- function=func.__name__,
412
- )
413
- else:
414
- raise RuntimeError(f"Circuit breaker is open for {func.__name__}")
415
-
416
- try:
417
- result = func(*args, **kwargs)
418
-
419
- # Success - reset on half-open or reduce failure count
420
- if self._state == "half_open":
421
- self._state = "closed"
422
- self._failure_count = 0
423
- _get_logger().info(
424
- f"Circuit breaker for {func.__name__} closed after successful recovery",
425
- function=func.__name__,
426
- )
427
- elif self._failure_count > 0:
428
- self._failure_count = max(0, self._failure_count - 1)
429
-
430
- return result
431
-
432
- except self.expected_exception as e:
433
- self._failure_count += 1
434
- self._last_failure_time = time.time()
435
-
436
- # Check if we should open the circuit
437
- if self._failure_count >= self.failure_threshold:
438
- self._state = "open"
439
- _get_logger().error(
440
- f"Circuit breaker for {func.__name__} opened after {self._failure_count} failures",
441
- function=func.__name__,
442
- failures=self._failure_count,
443
- error=str(e),
444
- )
445
- else:
446
- _get_logger().warning(
447
- f"Circuit breaker for {func.__name__} failure {self._failure_count}/{self.failure_threshold}",
448
- function=func.__name__,
449
- failures=self._failure_count,
450
- threshold=self.failure_threshold,
451
- error=str(e),
452
- )
453
-
454
- raise
455
-
456
- return wrapper # type: ignore
457
-
458
-
459
- def circuit_breaker(
460
- failure_threshold: int = 5,
461
- recovery_timeout: float = 60.0,
462
- expected_exception: tuple[type[Exception], ...] = (Exception,),
463
- ) -> Callable[[F], F]:
464
- """Create a circuit breaker decorator.
465
-
466
- Args:
467
- failure_threshold: Number of failures before opening circuit.
468
- recovery_timeout: Seconds to wait before attempting recovery.
469
- expected_exception: Exception types that trigger the breaker.
470
-
471
- Returns:
472
- Circuit breaker decorator.
473
-
474
- Examples:
475
- >>> @circuit_breaker(failure_threshold=3, recovery_timeout=30)
476
- ... def unreliable_service():
477
- ... return external_api_call()
478
- """
479
- breaker = CircuitBreaker(
480
- failure_threshold=failure_threshold,
481
- recovery_timeout=recovery_timeout,
482
- expected_exception=expected_exception,
483
- )
484
- return breaker
@@ -118,104 +118,6 @@ class ErrorMetadata:
118
118
  return result
119
119
 
120
120
 
121
- class BackoffStrategy(str, Enum):
122
- """Backoff strategies for retry policies."""
123
-
124
- FIXED = "fixed" # Fixed delay between retries
125
- LINEAR = "linear" # Linear increase (delay * attempt)
126
- EXPONENTIAL = "exponential" # Exponential increase (delay * 2^attempt)
127
- FIBONACCI = "fibonacci" # Fibonacci sequence delays
128
-
129
-
130
- @define(kw_only=True, slots=True)
131
- class RetryPolicy:
132
- """Configuration for retry behavior on errors.
133
-
134
- Attributes:
135
- max_attempts: Maximum number of retry attempts.
136
- backoff: Backoff strategy to use.
137
- base_delay: Base delay in seconds between retries.
138
- max_delay: Maximum delay in seconds (caps exponential growth).
139
- jitter: Whether to add random jitter to delays.
140
- retryable_errors: Optional tuple of exception types to retry on.
141
-
142
- Examples:
143
- >>> policy = RetryPolicy(
144
- ... max_attempts=5,
145
- ... backoff=BackoffStrategy.EXPONENTIAL,
146
- ... base_delay=1.0,
147
- ... max_delay=30.0
148
- ... )
149
- """
150
-
151
- max_attempts: int = 3
152
- backoff: BackoffStrategy = BackoffStrategy.EXPONENTIAL
153
- base_delay: float = 1.0
154
- max_delay: float = 60.0
155
- jitter: bool = True
156
- retryable_errors: tuple[type[Exception], ...] | None = None
157
-
158
- def calculate_delay(self, attempt: int) -> float:
159
- """Calculate delay for a given attempt number.
160
-
161
- Args:
162
- attempt: Attempt number (1-based).
163
-
164
- Returns:
165
- Delay in seconds.
166
- """
167
- if attempt <= 0:
168
- return 0
169
-
170
- if self.backoff == BackoffStrategy.FIXED:
171
- delay = self.base_delay
172
- elif self.backoff == BackoffStrategy.LINEAR:
173
- delay = self.base_delay * attempt
174
- elif self.backoff == BackoffStrategy.EXPONENTIAL:
175
- delay = self.base_delay * (2 ** (attempt - 1))
176
- elif self.backoff == BackoffStrategy.FIBONACCI:
177
- # Calculate fibonacci number for attempt
178
- a, b = 0, 1
179
- for _ in range(attempt):
180
- a, b = b, a + b
181
- delay = self.base_delay * a
182
- else:
183
- delay = self.base_delay
184
-
185
- # Cap at max delay
186
- delay = min(delay, self.max_delay)
187
-
188
- # Add jitter if configured (±25% random variation)
189
- if self.jitter:
190
- import random
191
-
192
- jitter_factor = 0.75 + (random.random() * 0.5)
193
- delay *= jitter_factor
194
-
195
- return delay
196
-
197
- def should_retry(self, error: Exception, attempt: int) -> bool:
198
- """Determine if an error should be retried.
199
-
200
- Args:
201
- error: The exception that occurred.
202
- attempt: Current attempt number (1-based).
203
-
204
- Returns:
205
- True if should retry, False otherwise.
206
- """
207
- # Check attempt limit
208
- if attempt >= self.max_attempts:
209
- return False
210
-
211
- # Check error type if filter is configured
212
- if self.retryable_errors is not None:
213
- return isinstance(error, self.retryable_errors)
214
-
215
- # Default to retry for any error
216
- return True
217
-
218
-
219
121
  @define(kw_only=True, slots=True)
220
122
  class ErrorResponse:
221
123
  """Structured error response for APIs and external interfaces.
@@ -2,10 +2,9 @@
2
2
  Event set display utilities for Foundation.
3
3
  """
4
4
 
5
- from provide.foundation.logger import get_logger
6
-
7
- from provide.foundation.eventsets.registry import get_registry, discover_event_sets
5
+ from provide.foundation.eventsets.registry import discover_event_sets, get_registry
8
6
  from provide.foundation.eventsets.resolver import get_resolver
7
+ from provide.foundation.logger import get_logger
9
8
 
10
9
  logger = get_logger(__name__)
11
10
 
@@ -17,16 +16,16 @@ def show_event_matrix() -> None:
17
16
  """
18
17
  # Ensure event sets are discovered
19
18
  discover_event_sets()
20
-
19
+
21
20
  registry = get_registry()
22
21
  resolver = get_resolver()
23
-
22
+
24
23
  # Force resolution to ensure everything is loaded
25
24
  resolver.resolve()
26
-
25
+
27
26
  lines: list[str] = ["Foundation Event Sets: Active Configuration"]
28
27
  lines.append("=" * 70)
29
-
28
+
30
29
  # Show registered event sets
31
30
  event_sets = registry.list_event_sets()
32
31
  if event_sets:
@@ -35,7 +34,7 @@ def show_event_matrix() -> None:
35
34
  lines.append(f"\n {config.name} (priority: {config.priority})")
36
35
  if config.description:
37
36
  lines.append(f" {config.description}")
38
-
37
+
39
38
  # Show field mappings
40
39
  if config.field_mappings:
41
40
  lines.append(f" Field Mappings ({len(config.field_mappings)}):")
@@ -43,7 +42,7 @@ def show_event_matrix() -> None:
43
42
  lines.append(f" - {mapping.log_key}")
44
43
  if len(config.field_mappings) > 5:
45
44
  lines.append(f" ... and {len(config.field_mappings) - 5} more")
46
-
45
+
47
46
  # Show event sets
48
47
  if config.event_sets:
49
48
  lines.append(f" Event Sets ({len(config.event_sets)}):")
@@ -59,15 +58,15 @@ def show_event_matrix() -> None:
59
58
  )
60
59
  else:
61
60
  lines.append("\n (No event sets registered)")
62
-
61
+
63
62
  lines.append("\n" + "=" * 70)
64
-
63
+
65
64
  # Show resolved state
66
65
  if resolver._resolved:
67
66
  lines.append("\nResolver State:")
68
67
  lines.append(f" Total Field Mappings: {len(resolver._field_mappings)}")
69
68
  lines.append(f" Total Event Sets: {len(resolver._event_sets)}")
70
-
69
+
71
70
  # Show sample visual markers
72
71
  if resolver._event_sets:
73
72
  lines.append("\n Sample Visual Markers:")
@@ -79,6 +78,6 @@ def show_event_matrix() -> None:
79
78
  lines.append(f" {marker} -> {key}")
80
79
  else:
81
80
  lines.append("\n (Resolver not yet initialized)")
82
-
81
+
83
82
  # Log the complete display
84
- logger.info("\n".join(lines))
83
+ logger.info("\n".join(lines))