provide-foundation 0.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,484 @@
1
+ """Decorators for error handling and resilience patterns.
2
+
3
+ Provides decorators for common error handling patterns like retry,
4
+ fallback, and error suppression.
5
+ """
6
+
7
+ from collections.abc import Callable
8
+ import functools
9
+ import inspect
10
+ import time
11
+ from typing import Any, TypeVar
12
+
13
+ from attrs import define, field
14
+
15
+ from provide.foundation.errors.base import FoundationError
16
+ from provide.foundation.errors.types import RetryPolicy
17
+
18
+ F = TypeVar("F", bound=Callable[..., Any])
19
+
20
+
21
+ def _get_logger():
22
+ """Get logger instance lazily to avoid circular imports."""
23
+ from provide.foundation.logger import logger
24
+
25
+ return logger
26
+
27
+
28
+ def with_error_handling(
29
+ func: F | None = None,
30
+ *,
31
+ fallback: Any = None,
32
+ log_errors: bool = True,
33
+ context_provider: Callable[[], dict[str, Any]] | None = None,
34
+ error_mapper: Callable[[Exception], Exception] | None = None,
35
+ suppress: tuple[type[Exception], ...] | None = None,
36
+ ) -> Callable[[F], F] | F:
37
+ """Decorator for automatic error handling with logging.
38
+
39
+ Args:
40
+ fallback: Value to return when an error occurs.
41
+ log_errors: Whether to log errors.
42
+ context_provider: Function that provides additional logging context.
43
+ error_mapper: Function to transform exceptions before re-raising.
44
+ suppress: Tuple of exception types to suppress (return fallback instead).
45
+
46
+ Returns:
47
+ Decorated function.
48
+
49
+ Examples:
50
+ >>> @with_error_handling(fallback=None, suppress=(KeyError,))
51
+ ... def get_value(data, key):
52
+ ... return data[key]
53
+
54
+ >>> @with_error_handling(
55
+ ... context_provider=lambda: {"request_id": get_request_id()}
56
+ ... )
57
+ ... def process_request():
58
+ ... # errors will be logged with request_id
59
+ ... pass
60
+ """
61
+
62
+ def decorator(func: F) -> F:
63
+ if inspect.iscoroutinefunction(func):
64
+
65
+ @functools.wraps(func)
66
+ async def async_wrapper(*args, **kwargs):
67
+ try:
68
+ return await func(*args, **kwargs)
69
+ except Exception as e:
70
+ # Check if we should suppress this error
71
+ if suppress and isinstance(e, suppress):
72
+ if log_errors:
73
+ context = context_provider() if context_provider else {}
74
+ _get_logger().info(
75
+ f"Suppressed {type(e).__name__} in {func.__name__}",
76
+ function=func.__name__,
77
+ error=str(e),
78
+ **context,
79
+ )
80
+ return fallback
81
+
82
+ # Log the error if configured
83
+ if log_errors:
84
+ context = context_provider() if context_provider else {}
85
+ _get_logger().error(
86
+ f"Error in {func.__name__}: {e}",
87
+ exc_info=True,
88
+ function=func.__name__,
89
+ **context,
90
+ )
91
+
92
+ # Map the error if mapper provided
93
+ if error_mapper and not isinstance(e, FoundationError):
94
+ mapped = error_mapper(e)
95
+ if mapped is not e:
96
+ raise mapped from e
97
+
98
+ # Re-raise the original error
99
+ raise
100
+
101
+ return async_wrapper # type: ignore
102
+ else:
103
+
104
+ @functools.wraps(func)
105
+ def wrapper(*args, **kwargs):
106
+ try:
107
+ return func(*args, **kwargs)
108
+ except Exception as e:
109
+ # Check if we should suppress this error
110
+ if suppress and isinstance(e, suppress):
111
+ if log_errors:
112
+ context = context_provider() if context_provider else {}
113
+ _get_logger().info(
114
+ f"Suppressed {type(e).__name__} in {func.__name__}",
115
+ function=func.__name__,
116
+ error=str(e),
117
+ **context,
118
+ )
119
+ return fallback
120
+
121
+ # Log the error if configured
122
+ if log_errors:
123
+ context = context_provider() if context_provider else {}
124
+ _get_logger().error(
125
+ f"Error in {func.__name__}: {e}",
126
+ exc_info=True,
127
+ function=func.__name__,
128
+ **context,
129
+ )
130
+
131
+ # Map the error if mapper provided
132
+ if error_mapper and not isinstance(e, FoundationError):
133
+ mapped = error_mapper(e)
134
+ if mapped is not e:
135
+ raise mapped from e
136
+
137
+ # Re-raise the original error
138
+ raise
139
+
140
+ return wrapper # type: ignore
141
+
142
+ # Support both @with_error_handling and @with_error_handling(...) forms
143
+ if func is None:
144
+ # Called as @with_error_handling(...) with arguments
145
+ return decorator
146
+ else:
147
+ # Called as @with_error_handling (no parentheses)
148
+ return decorator(func)
149
+
150
+
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
+ def suppress_and_log(
272
+ *exceptions: type[Exception],
273
+ fallback: Any = None,
274
+ log_level: str = "warning",
275
+ ) -> Callable[[F], F]:
276
+ """Decorator to suppress specific exceptions and log them.
277
+
278
+ Args:
279
+ *exceptions: Exception types to suppress.
280
+ fallback: Value to return when exception is suppressed.
281
+ log_level: Log level to use ('debug', 'info', 'warning', 'error').
282
+
283
+ Returns:
284
+ Decorated function.
285
+
286
+ Examples:
287
+ >>> @suppress_and_log(KeyError, AttributeError, fallback={})
288
+ ... def get_nested_value(data):
289
+ ... return data["key"].attribute
290
+ """
291
+
292
+ def decorator(func: F) -> F:
293
+ @functools.wraps(func)
294
+ def wrapper(*args, **kwargs):
295
+ try:
296
+ return func(*args, **kwargs)
297
+ except exceptions as e:
298
+ # Get appropriate log method
299
+ if log_level in ("debug", "info", "warning", "error", "critical"):
300
+ log_method = getattr(_get_logger(), log_level)
301
+ else:
302
+ log_method = _get_logger().warning
303
+
304
+ log_method(
305
+ f"Suppressed {type(e).__name__} in {func.__name__}: {e}",
306
+ function=func.__name__,
307
+ error_type=type(e).__name__,
308
+ error=str(e),
309
+ fallback=fallback,
310
+ )
311
+
312
+ return fallback
313
+
314
+ return wrapper # type: ignore
315
+
316
+ return decorator
317
+
318
+
319
+ def fallback_on_error(
320
+ fallback_func: Callable[..., Any],
321
+ *exceptions: type[Exception],
322
+ log_errors: bool = True,
323
+ ) -> Callable[[F], F]:
324
+ """Decorator to call a fallback function when errors occur.
325
+
326
+ Args:
327
+ fallback_func: Function to call when an error occurs.
328
+ *exceptions: Specific exception types to handle (all if empty).
329
+ log_errors: Whether to log errors before calling fallback.
330
+
331
+ Returns:
332
+ Decorated function.
333
+
334
+ Examples:
335
+ >>> def use_cache():
336
+ ... return cached_value
337
+ ...
338
+ >>> @fallback_on_error(use_cache, NetworkError)
339
+ ... def fetch_from_api():
340
+ ... return api_call()
341
+ """
342
+ catch_types = exceptions if exceptions else (Exception,)
343
+
344
+ def decorator(func: F) -> F:
345
+ @functools.wraps(func)
346
+ def wrapper(*args, **kwargs):
347
+ try:
348
+ return func(*args, **kwargs)
349
+ except catch_types as e:
350
+ if log_errors:
351
+ _get_logger().warning(
352
+ f"Using fallback for {func.__name__} due to {type(e).__name__}",
353
+ function=func.__name__,
354
+ error_type=type(e).__name__,
355
+ error=str(e),
356
+ fallback=fallback_func.__name__,
357
+ )
358
+
359
+ # Call fallback with same arguments
360
+ try:
361
+ return fallback_func(*args, **kwargs)
362
+ except Exception as fallback_error:
363
+ _get_logger().error(
364
+ f"Fallback function {fallback_func.__name__} also failed",
365
+ exc_info=True,
366
+ original_error=str(e),
367
+ fallback_error=str(fallback_error),
368
+ )
369
+ # Re-raise the fallback error
370
+ raise fallback_error from e
371
+
372
+ return wrapper # type: ignore
373
+
374
+ 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