provide-foundation 0.0.0.dev2__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 (155) hide show
  1. provide/foundation/__init__.py +20 -20
  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 +90 -91
  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 +4 -4
  13. provide/foundation/cli/commands/logs/__init__.py +2 -2
  14. provide/foundation/cli/commands/logs/generate.py +2 -2
  15. provide/foundation/cli/commands/logs/query.py +3 -3
  16. provide/foundation/cli/commands/logs/send.py +2 -2
  17. provide/foundation/cli/commands/logs/tail.py +2 -2
  18. provide/foundation/cli/decorators.py +0 -1
  19. provide/foundation/cli/testing.py +0 -5
  20. provide/foundation/cli/utils.py +1 -2
  21. provide/foundation/config/__init__.py +19 -19
  22. provide/foundation/config/base.py +2 -2
  23. provide/foundation/config/converters.py +81 -83
  24. provide/foundation/config/defaults.py +1 -1
  25. provide/foundation/config/env.py +2 -1
  26. provide/foundation/config/loader.py +1 -1
  27. provide/foundation/config/sync.py +8 -6
  28. provide/foundation/config/types.py +5 -5
  29. provide/foundation/config/validators.py +4 -4
  30. provide/foundation/console/output.py +7 -7
  31. provide/foundation/context/core.py +19 -17
  32. provide/foundation/crypto/certificates/__init__.py +9 -5
  33. provide/foundation/crypto/certificates/base.py +2 -2
  34. provide/foundation/crypto/certificates/certificate.py +48 -19
  35. provide/foundation/crypto/certificates/factory.py +26 -18
  36. provide/foundation/crypto/certificates/generator.py +24 -23
  37. provide/foundation/crypto/certificates/loader.py +24 -16
  38. provide/foundation/crypto/certificates/operations.py +17 -10
  39. provide/foundation/crypto/certificates/trust.py +21 -21
  40. provide/foundation/env/__init__.py +28 -0
  41. provide/foundation/env/core.py +218 -0
  42. provide/foundation/errors/__init__.py +3 -2
  43. provide/foundation/errors/decorators.py +0 -3
  44. provide/foundation/errors/types.py +0 -1
  45. provide/foundation/eventsets/display.py +13 -14
  46. provide/foundation/eventsets/registry.py +61 -31
  47. provide/foundation/eventsets/resolver.py +50 -46
  48. provide/foundation/eventsets/sets/das.py +8 -8
  49. provide/foundation/eventsets/sets/database.py +14 -14
  50. provide/foundation/eventsets/sets/http.py +21 -21
  51. provide/foundation/eventsets/sets/llm.py +16 -16
  52. provide/foundation/eventsets/sets/task_queue.py +13 -13
  53. provide/foundation/eventsets/types.py +7 -7
  54. provide/foundation/file/directory.py +1 -1
  55. provide/foundation/file/lock.py +2 -3
  56. provide/foundation/hub/components.py +19 -21
  57. provide/foundation/hub/config.py +25 -19
  58. provide/foundation/hub/discovery.py +5 -4
  59. provide/foundation/hub/handlers.py +13 -5
  60. provide/foundation/hub/lifecycle.py +10 -9
  61. provide/foundation/hub/manager.py +3 -0
  62. provide/foundation/hub/processors.py +8 -3
  63. provide/foundation/integrations/__init__.py +1 -1
  64. provide/foundation/integrations/openobserve/client.py +2 -2
  65. provide/foundation/integrations/openobserve/commands.py +9 -9
  66. provide/foundation/integrations/openobserve/config.py +2 -2
  67. provide/foundation/integrations/openobserve/otlp.py +2 -2
  68. provide/foundation/integrations/openobserve/search.py +1 -2
  69. provide/foundation/integrations/openobserve/streaming.py +1 -1
  70. provide/foundation/logger/__init__.py +0 -1
  71. provide/foundation/logger/config/base.py +1 -1
  72. provide/foundation/logger/config/logging.py +19 -19
  73. provide/foundation/logger/config/telemetry.py +11 -13
  74. provide/foundation/logger/factories.py +2 -2
  75. provide/foundation/logger/processors/main.py +12 -10
  76. provide/foundation/logger/ratelimit/limiters.py +4 -4
  77. provide/foundation/logger/ratelimit/processor.py +1 -1
  78. provide/foundation/logger/setup/coordinator.py +38 -24
  79. provide/foundation/logger/setup/processors.py +3 -3
  80. provide/foundation/logger/setup/testing.py +14 -0
  81. provide/foundation/logger/trace.py +5 -5
  82. provide/foundation/metrics/__init__.py +1 -1
  83. provide/foundation/metrics/otel.py +3 -1
  84. provide/foundation/observability/__init__.py +1 -1
  85. provide/foundation/process/__init__.py +1 -1
  86. provide/foundation/process/exit.py +6 -5
  87. provide/foundation/process/lifecycle.py +41 -18
  88. provide/foundation/resilience/__init__.py +6 -5
  89. provide/foundation/resilience/circuit.py +32 -30
  90. provide/foundation/resilience/decorators.py +58 -42
  91. provide/foundation/resilience/fallback.py +55 -40
  92. provide/foundation/resilience/retry.py +67 -65
  93. provide/foundation/serialization/__init__.py +16 -0
  94. provide/foundation/serialization/core.py +70 -0
  95. provide/foundation/streams/config.py +8 -9
  96. provide/foundation/streams/console.py +3 -3
  97. provide/foundation/streams/core.py +2 -2
  98. provide/foundation/streams/file.py +1 -1
  99. provide/foundation/testing/__init__.py +22 -7
  100. provide/foundation/testing/archive/__init__.py +7 -7
  101. provide/foundation/testing/archive/fixtures.py +58 -54
  102. provide/foundation/testing/cli.py +3 -6
  103. provide/foundation/testing/common/__init__.py +13 -13
  104. provide/foundation/testing/common/fixtures.py +27 -30
  105. provide/foundation/testing/file/__init__.py +15 -15
  106. provide/foundation/testing/file/content_fixtures.py +65 -92
  107. provide/foundation/testing/file/directory_fixtures.py +19 -19
  108. provide/foundation/testing/file/fixtures.py +14 -17
  109. provide/foundation/testing/file/special_fixtures.py +34 -42
  110. provide/foundation/testing/logger.py +28 -23
  111. provide/foundation/testing/mocking/__init__.py +21 -21
  112. provide/foundation/testing/mocking/fixtures.py +80 -67
  113. provide/foundation/testing/process/__init__.py +23 -23
  114. provide/foundation/testing/process/async_fixtures.py +89 -80
  115. provide/foundation/testing/process/fixtures.py +11 -13
  116. provide/foundation/testing/process/subprocess_fixtures.py +41 -40
  117. provide/foundation/testing/threading/__init__.py +17 -17
  118. provide/foundation/testing/threading/basic_fixtures.py +21 -17
  119. provide/foundation/testing/threading/data_fixtures.py +18 -16
  120. provide/foundation/testing/threading/execution_fixtures.py +67 -52
  121. provide/foundation/testing/threading/fixtures.py +10 -14
  122. provide/foundation/testing/threading/sync_fixtures.py +21 -18
  123. provide/foundation/testing/time/__init__.py +11 -11
  124. provide/foundation/testing/time/fixtures.py +91 -79
  125. provide/foundation/testing/transport/__init__.py +9 -9
  126. provide/foundation/testing/transport/fixtures.py +54 -54
  127. provide/foundation/time/__init__.py +18 -0
  128. provide/foundation/time/core.py +63 -0
  129. provide/foundation/tools/__init__.py +2 -2
  130. provide/foundation/tools/base.py +68 -67
  131. provide/foundation/tools/cache.py +62 -69
  132. provide/foundation/tools/downloader.py +51 -56
  133. provide/foundation/tools/installer.py +51 -57
  134. provide/foundation/tools/registry.py +38 -45
  135. provide/foundation/tools/resolver.py +70 -68
  136. provide/foundation/tools/verifier.py +39 -50
  137. provide/foundation/tracer/spans.py +1 -13
  138. provide/foundation/transport/__init__.py +26 -33
  139. provide/foundation/transport/base.py +32 -30
  140. provide/foundation/transport/client.py +44 -49
  141. provide/foundation/transport/config.py +11 -13
  142. provide/foundation/transport/errors.py +13 -27
  143. provide/foundation/transport/http.py +69 -55
  144. provide/foundation/transport/middleware.py +86 -81
  145. provide/foundation/transport/registry.py +29 -27
  146. provide/foundation/transport/types.py +6 -6
  147. provide/foundation/utils/deps.py +3 -2
  148. provide/foundation/utils/parsing.py +7 -7
  149. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
  150. provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
  151. provide_foundation-0.0.0.dev2.dist-info/RECORD +0 -225
  152. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
  153. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
  154. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
  155. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -4,13 +4,14 @@ Resilience decorators for retry, circuit breaker, and fallback patterns.
4
4
 
5
5
  import asyncio
6
6
  import functools
7
- import time
8
7
  from typing import Any, Callable, TypeVar
9
8
 
10
- from attrs import define, field
11
-
12
9
  from provide.foundation.config.defaults import DEFAULT_CIRCUIT_BREAKER_RECOVERY_TIMEOUT
13
- from provide.foundation.resilience.retry import BackoffStrategy, RetryExecutor, RetryPolicy
10
+ from provide.foundation.resilience.retry import (
11
+ BackoffStrategy,
12
+ RetryExecutor,
13
+ RetryPolicy,
14
+ )
14
15
 
15
16
  F = TypeVar("F", bound=Callable[..., Any])
16
17
 
@@ -18,6 +19,7 @@ F = TypeVar("F", bound=Callable[..., Any])
18
19
  def _get_logger():
19
20
  """Get logger instance lazily to avoid circular imports."""
20
21
  from provide.foundation.logger import logger
22
+
21
23
  return logger
22
24
 
23
25
 
@@ -33,22 +35,22 @@ def retry(
33
35
  ) -> Callable[[F], F]:
34
36
  """
35
37
  Decorator for retrying operations on errors.
36
-
38
+
37
39
  Can be used in multiple ways:
38
-
40
+
39
41
  1. With a policy object:
40
42
  @retry(policy=RetryPolicy(max_attempts=5))
41
-
43
+
42
44
  2. With individual parameters:
43
45
  @retry(max_attempts=3, base_delay=1.0)
44
-
46
+
45
47
  3. With specific exceptions:
46
48
  @retry(ConnectionError, TimeoutError, max_attempts=3)
47
-
49
+
48
50
  4. Without parentheses (uses defaults):
49
51
  @retry
50
52
  def my_func(): ...
51
-
53
+
52
54
  Args:
53
55
  *exceptions: Exception types to retry (all if empty)
54
56
  policy: Complete retry policy (overrides other params)
@@ -58,79 +60,89 @@ def retry(
58
60
  max_delay: Maximum delay cap
59
61
  jitter: Whether to add jitter
60
62
  on_retry: Callback for retry events
61
-
63
+
62
64
  Returns:
63
65
  Decorated function with retry logic
64
-
66
+
65
67
  Examples:
66
68
  >>> @retry(max_attempts=3)
67
69
  ... def flaky_operation():
68
70
  ... # May fail occasionally
69
71
  ... pass
70
-
72
+
71
73
  >>> @retry(ConnectionError, max_attempts=5, base_delay=2.0)
72
74
  ... async def connect_to_service():
73
75
  ... # Async function with specific error handling
74
76
  ... pass
75
77
  """
76
78
  # Handle decorator without parentheses
77
- if len(exceptions) == 1 and callable(exceptions[0]) and not isinstance(exceptions[0], type):
79
+ if (
80
+ len(exceptions) == 1
81
+ and callable(exceptions[0])
82
+ and not isinstance(exceptions[0], type)
83
+ ):
78
84
  # Called as @retry without parentheses
79
85
  func = exceptions[0]
80
86
  executor = RetryExecutor(RetryPolicy())
81
-
87
+
82
88
  if asyncio.iscoroutinefunction(func):
89
+
83
90
  @functools.wraps(func)
84
91
  async def async_wrapper(*args, **kwargs):
85
92
  return await executor.execute_async(func, *args, **kwargs)
93
+
86
94
  return async_wrapper
87
95
  else:
96
+
88
97
  @functools.wraps(func)
89
98
  def sync_wrapper(*args, **kwargs):
90
99
  return executor.execute_sync(func, *args, **kwargs)
100
+
91
101
  return sync_wrapper
92
-
102
+
93
103
  # Build policy if not provided
94
104
  if policy is not None and any(
95
105
  p is not None for p in [max_attempts, base_delay, backoff, max_delay, jitter]
96
106
  ):
97
- raise ValueError(
98
- "Cannot specify both policy and individual retry parameters"
99
- )
100
-
107
+ raise ValueError("Cannot specify both policy and individual retry parameters")
108
+
101
109
  if policy is None:
102
110
  # Build policy from parameters
103
111
  policy_kwargs = {}
104
-
112
+
105
113
  if max_attempts is not None:
106
- policy_kwargs['max_attempts'] = max_attempts
114
+ policy_kwargs["max_attempts"] = max_attempts
107
115
  if base_delay is not None:
108
- policy_kwargs['base_delay'] = base_delay
116
+ policy_kwargs["base_delay"] = base_delay
109
117
  if backoff is not None:
110
- policy_kwargs['backoff'] = backoff
118
+ policy_kwargs["backoff"] = backoff
111
119
  if max_delay is not None:
112
- policy_kwargs['max_delay'] = max_delay
120
+ policy_kwargs["max_delay"] = max_delay
113
121
  if jitter is not None:
114
- policy_kwargs['jitter'] = jitter
122
+ policy_kwargs["jitter"] = jitter
115
123
  if exceptions:
116
- policy_kwargs['retryable_errors'] = exceptions
117
-
124
+ policy_kwargs["retryable_errors"] = exceptions
125
+
118
126
  policy = RetryPolicy(**policy_kwargs)
119
-
127
+
120
128
  def decorator(func: F) -> F:
121
129
  executor = RetryExecutor(policy, on_retry=on_retry)
122
-
130
+
123
131
  if asyncio.iscoroutinefunction(func):
132
+
124
133
  @functools.wraps(func)
125
134
  async def async_wrapper(*args, **kwargs):
126
135
  return await executor.execute_async(func, *args, **kwargs)
136
+
127
137
  return async_wrapper
128
138
  else:
139
+
129
140
  @functools.wraps(func)
130
141
  def sync_wrapper(*args, **kwargs):
131
142
  return executor.execute_sync(func, *args, **kwargs)
143
+
132
144
  return sync_wrapper
133
-
145
+
134
146
  return decorator
135
147
 
136
148
 
@@ -163,34 +175,34 @@ def circuit_breaker(
163
175
  recovery_timeout=recovery_timeout,
164
176
  expected_exception=expected_exception,
165
177
  )
166
-
178
+
167
179
  def decorator(func: F) -> F:
168
180
  @functools.wraps(func)
169
181
  def sync_wrapper(*args, **kwargs):
170
182
  return breaker.call(func, *args, **kwargs)
171
-
183
+
172
184
  @functools.wraps(func)
173
185
  async def async_wrapper(*args, **kwargs):
174
186
  return await breaker.call_async(func, *args, **kwargs)
175
-
187
+
176
188
  if asyncio.iscoroutinefunction(func):
177
189
  return async_wrapper # type: ignore
178
190
  else:
179
191
  return sync_wrapper # type: ignore
180
-
192
+
181
193
  return decorator
182
194
 
183
195
 
184
196
  def fallback(*fallback_funcs: Callable[..., Any]) -> Callable[[F], F]:
185
197
  """
186
198
  Fallback decorator using FallbackChain.
187
-
199
+
188
200
  Args:
189
201
  *fallback_funcs: Functions to use as fallbacks, in order of preference
190
-
202
+
191
203
  Returns:
192
204
  Decorated function with fallback chain
193
-
205
+
194
206
  Examples:
195
207
  >>> def backup_api():
196
208
  ... return "backup result"
@@ -200,21 +212,25 @@ def fallback(*fallback_funcs: Callable[..., Any]) -> Callable[[F], F]:
200
212
  ... return external_api_call()
201
213
  """
202
214
  from provide.foundation.resilience.fallback import FallbackChain
203
-
215
+
204
216
  def decorator(func: F) -> F:
205
217
  chain = FallbackChain()
206
218
  for fallback_func in fallback_funcs:
207
219
  chain.add_fallback(fallback_func)
208
-
220
+
209
221
  if asyncio.iscoroutinefunction(func):
222
+
210
223
  @functools.wraps(func)
211
224
  async def async_wrapper(*args, **kwargs):
212
225
  return await chain.execute_async(func, *args, **kwargs)
226
+
213
227
  return async_wrapper # type: ignore
214
228
  else:
229
+
215
230
  @functools.wraps(func)
216
231
  def sync_wrapper(*args, **kwargs):
217
232
  return chain.execute(func, *args, **kwargs)
233
+
218
234
  return sync_wrapper # type: ignore
219
-
220
- return decorator
235
+
236
+ return decorator
@@ -4,7 +4,7 @@ Fallback implementation for graceful degradation.
4
4
 
5
5
  import asyncio
6
6
  import functools
7
- from typing import Any, Callable, TypeVar
7
+ from typing import Callable, TypeVar
8
8
 
9
9
  from attrs import define, field
10
10
 
@@ -16,31 +16,34 @@ T = TypeVar("T")
16
16
  @define(kw_only=True, slots=True)
17
17
  class FallbackChain:
18
18
  """Chain of fallback strategies for graceful degradation.
19
-
19
+
20
20
  Executes fallback functions in order when primary function fails.
21
21
  """
22
-
22
+
23
23
  fallbacks: list[Callable[..., T]] = field(factory=list)
24
24
  expected_exceptions: tuple[type[Exception], ...] = field(
25
25
  factory=lambda: (Exception,)
26
26
  )
27
-
27
+
28
28
  def add_fallback(self, fallback_func: Callable[..., T]) -> None:
29
29
  """Add a fallback function to the chain."""
30
30
  self.fallbacks.append(fallback_func)
31
31
  logger.debug(
32
32
  "Added fallback to chain",
33
33
  fallback_count=len(self.fallbacks),
34
- fallback_name=getattr(fallback_func, '__name__', 'anonymous')
34
+ fallback_name=getattr(fallback_func, "__name__", "anonymous"),
35
35
  )
36
-
36
+
37
37
  def execute(self, primary_func: Callable[..., T], *args, **kwargs) -> T:
38
38
  """Execute primary function with fallback chain (sync)."""
39
39
  # Try primary function first
40
40
  primary_exception = None
41
41
  try:
42
42
  result = primary_func(*args, **kwargs)
43
- logger.trace("Primary function succeeded", func=getattr(primary_func, '__name__', 'anonymous'))
43
+ logger.trace(
44
+ "Primary function succeeded",
45
+ func=getattr(primary_func, "__name__", "anonymous"),
46
+ )
44
47
  return result
45
48
  except Exception as e:
46
49
  primary_exception = e
@@ -49,17 +52,17 @@ class FallbackChain:
49
52
  logger.debug(
50
53
  "Primary function failed with unexpected exception type",
51
54
  exception_type=type(e).__name__,
52
- expected_types=[t.__name__ for t in self.expected_exceptions]
55
+ expected_types=[t.__name__ for t in self.expected_exceptions],
53
56
  )
54
57
  raise
55
-
58
+
56
59
  logger.warning(
57
60
  "Primary function failed, trying fallbacks",
58
- func=getattr(primary_func, '__name__', 'anonymous'),
61
+ func=getattr(primary_func, "__name__", "anonymous"),
59
62
  error=str(e),
60
- fallback_count=len(self.fallbacks)
63
+ fallback_count=len(self.fallbacks),
61
64
  )
62
-
65
+
63
66
  # Try fallbacks in order
64
67
  last_exception = None
65
68
  for i, fallback_func in enumerate(self.fallbacks):
@@ -68,7 +71,7 @@ class FallbackChain:
68
71
  logger.info(
69
72
  "Fallback succeeded",
70
73
  fallback_index=i,
71
- fallback_name=getattr(fallback_func, '__name__', 'anonymous')
74
+ fallback_name=getattr(fallback_func, "__name__", "anonymous"),
72
75
  )
73
76
  return result
74
77
  except Exception as e:
@@ -76,26 +79,28 @@ class FallbackChain:
76
79
  logger.warning(
77
80
  "Fallback failed",
78
81
  fallback_index=i,
79
- fallback_name=getattr(fallback_func, '__name__', 'anonymous'),
80
- error=str(e)
82
+ fallback_name=getattr(fallback_func, "__name__", "anonymous"),
83
+ error=str(e),
81
84
  )
82
85
  continue
83
-
86
+
84
87
  # All fallbacks failed
85
88
  logger.error(
86
89
  "All fallbacks exhausted",
87
- primary_func=getattr(primary_func, '__name__', 'anonymous'),
88
- fallback_count=len(self.fallbacks)
90
+ primary_func=getattr(primary_func, "__name__", "anonymous"),
91
+ fallback_count=len(self.fallbacks),
89
92
  )
90
-
93
+
91
94
  # Raise the last exception from fallbacks, or original if no fallbacks
92
95
  if last_exception is not None:
93
96
  raise last_exception
94
97
  if primary_exception is not None:
95
98
  raise primary_exception
96
99
  # This should never happen but provide fallback
97
- raise RuntimeError("Fallback chain execution failed with no recorded exceptions")
98
-
100
+ raise RuntimeError(
101
+ "Fallback chain execution failed with no recorded exceptions"
102
+ )
103
+
99
104
  async def execute_async(self, primary_func: Callable[..., T], *args, **kwargs) -> T:
100
105
  """Execute primary function with fallback chain (async)."""
101
106
  # Try primary function first
@@ -105,7 +110,10 @@ class FallbackChain:
105
110
  result = await primary_func(*args, **kwargs)
106
111
  else:
107
112
  result = primary_func(*args, **kwargs)
108
- logger.trace("Primary function succeeded", func=getattr(primary_func, '__name__', 'anonymous'))
113
+ logger.trace(
114
+ "Primary function succeeded",
115
+ func=getattr(primary_func, "__name__", "anonymous"),
116
+ )
109
117
  return result
110
118
  except Exception as e:
111
119
  primary_exception = e
@@ -114,17 +122,17 @@ class FallbackChain:
114
122
  logger.debug(
115
123
  "Primary function failed with unexpected exception type",
116
124
  exception_type=type(e).__name__,
117
- expected_types=[t.__name__ for t in self.expected_exceptions]
125
+ expected_types=[t.__name__ for t in self.expected_exceptions],
118
126
  )
119
127
  raise
120
-
128
+
121
129
  logger.warning(
122
130
  "Primary function failed, trying fallbacks",
123
- func=getattr(primary_func, '__name__', 'anonymous'),
131
+ func=getattr(primary_func, "__name__", "anonymous"),
124
132
  error=str(e),
125
- fallback_count=len(self.fallbacks)
133
+ fallback_count=len(self.fallbacks),
126
134
  )
127
-
135
+
128
136
  # Try fallbacks in order
129
137
  last_exception = None
130
138
  for i, fallback_func in enumerate(self.fallbacks):
@@ -136,7 +144,7 @@ class FallbackChain:
136
144
  logger.info(
137
145
  "Fallback succeeded",
138
146
  fallback_index=i,
139
- fallback_name=getattr(fallback_func, '__name__', 'anonymous')
147
+ fallback_name=getattr(fallback_func, "__name__", "anonymous"),
140
148
  )
141
149
  return result
142
150
  except Exception as e:
@@ -144,50 +152,57 @@ class FallbackChain:
144
152
  logger.warning(
145
153
  "Fallback failed",
146
154
  fallback_index=i,
147
- fallback_name=getattr(fallback_func, '__name__', 'anonymous'),
148
- error=str(e)
155
+ fallback_name=getattr(fallback_func, "__name__", "anonymous"),
156
+ error=str(e),
149
157
  )
150
158
  continue
151
-
159
+
152
160
  # All fallbacks failed
153
161
  logger.error(
154
162
  "All fallbacks exhausted",
155
- primary_func=getattr(primary_func, '__name__', 'anonymous'),
156
- fallback_count=len(self.fallbacks)
163
+ primary_func=getattr(primary_func, "__name__", "anonymous"),
164
+ fallback_count=len(self.fallbacks),
157
165
  )
158
-
166
+
159
167
  # Raise the last exception from fallbacks, or original if no fallbacks
160
168
  if last_exception is not None:
161
169
  raise last_exception
162
170
  if primary_exception is not None:
163
171
  raise primary_exception
164
172
  # This should never happen but provide fallback
165
- raise RuntimeError("Fallback chain execution failed with no recorded exceptions")
173
+ raise RuntimeError(
174
+ "Fallback chain execution failed with no recorded exceptions"
175
+ )
166
176
 
167
177
 
168
178
  def fallback(*fallback_funcs: Callable[..., T]) -> Callable:
169
179
  """Decorator to add fallback functions to a primary function.
170
-
180
+
171
181
  Args:
172
182
  *fallback_funcs: Functions to use as fallbacks, in order of preference
173
-
183
+
174
184
  Returns:
175
185
  Decorated function that uses fallback chain
176
186
  """
187
+
177
188
  def decorator(primary_func: Callable[..., T]) -> Callable[..., T]:
178
189
  chain = FallbackChain()
179
190
  for fallback_func in fallback_funcs:
180
191
  chain.add_fallback(fallback_func)
181
-
192
+
182
193
  if asyncio.iscoroutinefunction(primary_func):
194
+
183
195
  @functools.wraps(primary_func)
184
196
  async def async_wrapper(*args, **kwargs):
185
197
  return await chain.execute_async(primary_func, *args, **kwargs)
198
+
186
199
  return async_wrapper
187
200
  else:
201
+
188
202
  @functools.wraps(primary_func)
189
203
  def sync_wrapper(*args, **kwargs):
190
204
  return chain.execute(primary_func, *args, **kwargs)
205
+
191
206
  return sync_wrapper
192
-
193
- return decorator
207
+
208
+ return decorator