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,220 @@
1
+ """
2
+ Resilience decorators for retry, circuit breaker, and fallback patterns.
3
+ """
4
+
5
+ import asyncio
6
+ import functools
7
+ import time
8
+ from typing import Any, Callable, TypeVar
9
+
10
+ from attrs import define, field
11
+
12
+ from provide.foundation.config.defaults import DEFAULT_CIRCUIT_BREAKER_RECOVERY_TIMEOUT
13
+ from provide.foundation.resilience.retry import BackoffStrategy, RetryExecutor, RetryPolicy
14
+
15
+ F = TypeVar("F", bound=Callable[..., Any])
16
+
17
+
18
+ def _get_logger():
19
+ """Get logger instance lazily to avoid circular imports."""
20
+ from provide.foundation.logger import logger
21
+ return logger
22
+
23
+
24
+ def retry(
25
+ *exceptions: type[Exception],
26
+ policy: RetryPolicy | None = None,
27
+ max_attempts: int | None = None,
28
+ base_delay: float | None = None,
29
+ backoff: BackoffStrategy | None = None,
30
+ max_delay: float | None = None,
31
+ jitter: bool | None = None,
32
+ on_retry: Callable[[int, Exception], None] | None = None,
33
+ ) -> Callable[[F], F]:
34
+ """
35
+ Decorator for retrying operations on errors.
36
+
37
+ Can be used in multiple ways:
38
+
39
+ 1. With a policy object:
40
+ @retry(policy=RetryPolicy(max_attempts=5))
41
+
42
+ 2. With individual parameters:
43
+ @retry(max_attempts=3, base_delay=1.0)
44
+
45
+ 3. With specific exceptions:
46
+ @retry(ConnectionError, TimeoutError, max_attempts=3)
47
+
48
+ 4. Without parentheses (uses defaults):
49
+ @retry
50
+ def my_func(): ...
51
+
52
+ Args:
53
+ *exceptions: Exception types to retry (all if empty)
54
+ policy: Complete retry policy (overrides other params)
55
+ max_attempts: Maximum retry attempts
56
+ base_delay: Base delay between retries
57
+ backoff: Backoff strategy
58
+ max_delay: Maximum delay cap
59
+ jitter: Whether to add jitter
60
+ on_retry: Callback for retry events
61
+
62
+ Returns:
63
+ Decorated function with retry logic
64
+
65
+ Examples:
66
+ >>> @retry(max_attempts=3)
67
+ ... def flaky_operation():
68
+ ... # May fail occasionally
69
+ ... pass
70
+
71
+ >>> @retry(ConnectionError, max_attempts=5, base_delay=2.0)
72
+ ... async def connect_to_service():
73
+ ... # Async function with specific error handling
74
+ ... pass
75
+ """
76
+ # Handle decorator without parentheses
77
+ if len(exceptions) == 1 and callable(exceptions[0]) and not isinstance(exceptions[0], type):
78
+ # Called as @retry without parentheses
79
+ func = exceptions[0]
80
+ executor = RetryExecutor(RetryPolicy())
81
+
82
+ if asyncio.iscoroutinefunction(func):
83
+ @functools.wraps(func)
84
+ async def async_wrapper(*args, **kwargs):
85
+ return await executor.execute_async(func, *args, **kwargs)
86
+ return async_wrapper
87
+ else:
88
+ @functools.wraps(func)
89
+ def sync_wrapper(*args, **kwargs):
90
+ return executor.execute_sync(func, *args, **kwargs)
91
+ return sync_wrapper
92
+
93
+ # Build policy if not provided
94
+ if policy is not None and any(
95
+ p is not None for p in [max_attempts, base_delay, backoff, max_delay, jitter]
96
+ ):
97
+ raise ValueError(
98
+ "Cannot specify both policy and individual retry parameters"
99
+ )
100
+
101
+ if policy is None:
102
+ # Build policy from parameters
103
+ policy_kwargs = {}
104
+
105
+ if max_attempts is not None:
106
+ policy_kwargs['max_attempts'] = max_attempts
107
+ if base_delay is not None:
108
+ policy_kwargs['base_delay'] = base_delay
109
+ if backoff is not None:
110
+ policy_kwargs['backoff'] = backoff
111
+ if max_delay is not None:
112
+ policy_kwargs['max_delay'] = max_delay
113
+ if jitter is not None:
114
+ policy_kwargs['jitter'] = jitter
115
+ if exceptions:
116
+ policy_kwargs['retryable_errors'] = exceptions
117
+
118
+ policy = RetryPolicy(**policy_kwargs)
119
+
120
+ def decorator(func: F) -> F:
121
+ executor = RetryExecutor(policy, on_retry=on_retry)
122
+
123
+ if asyncio.iscoroutinefunction(func):
124
+ @functools.wraps(func)
125
+ async def async_wrapper(*args, **kwargs):
126
+ return await executor.execute_async(func, *args, **kwargs)
127
+ return async_wrapper
128
+ else:
129
+ @functools.wraps(func)
130
+ def sync_wrapper(*args, **kwargs):
131
+ return executor.execute_sync(func, *args, **kwargs)
132
+ return sync_wrapper
133
+
134
+ return decorator
135
+
136
+
137
+ # Import CircuitBreaker from circuit module
138
+ from provide.foundation.resilience.circuit import CircuitBreaker
139
+
140
+
141
+ def circuit_breaker(
142
+ failure_threshold: int = 5,
143
+ recovery_timeout: float = DEFAULT_CIRCUIT_BREAKER_RECOVERY_TIMEOUT,
144
+ expected_exception: tuple[type[Exception], ...] = (Exception,),
145
+ ) -> Callable[[F], F]:
146
+ """Create a circuit breaker decorator.
147
+
148
+ Args:
149
+ failure_threshold: Number of failures before opening circuit.
150
+ recovery_timeout: Seconds to wait before attempting recovery.
151
+ expected_exception: Exception types that trigger the breaker.
152
+
153
+ Returns:
154
+ Circuit breaker decorator.
155
+
156
+ Examples:
157
+ >>> @circuit_breaker(failure_threshold=3, recovery_timeout=30)
158
+ ... def unreliable_service():
159
+ ... return external_api_call()
160
+ """
161
+ breaker = CircuitBreaker(
162
+ failure_threshold=failure_threshold,
163
+ recovery_timeout=recovery_timeout,
164
+ expected_exception=expected_exception,
165
+ )
166
+
167
+ def decorator(func: F) -> F:
168
+ @functools.wraps(func)
169
+ def sync_wrapper(*args, **kwargs):
170
+ return breaker.call(func, *args, **kwargs)
171
+
172
+ @functools.wraps(func)
173
+ async def async_wrapper(*args, **kwargs):
174
+ return await breaker.call_async(func, *args, **kwargs)
175
+
176
+ if asyncio.iscoroutinefunction(func):
177
+ return async_wrapper # type: ignore
178
+ else:
179
+ return sync_wrapper # type: ignore
180
+
181
+ return decorator
182
+
183
+
184
+ def fallback(*fallback_funcs: Callable[..., Any]) -> Callable[[F], F]:
185
+ """
186
+ Fallback decorator using FallbackChain.
187
+
188
+ Args:
189
+ *fallback_funcs: Functions to use as fallbacks, in order of preference
190
+
191
+ Returns:
192
+ Decorated function with fallback chain
193
+
194
+ Examples:
195
+ >>> def backup_api():
196
+ ... return "backup result"
197
+ ...
198
+ >>> @fallback(backup_api)
199
+ ... def primary_api():
200
+ ... return external_api_call()
201
+ """
202
+ from provide.foundation.resilience.fallback import FallbackChain
203
+
204
+ def decorator(func: F) -> F:
205
+ chain = FallbackChain()
206
+ for fallback_func in fallback_funcs:
207
+ chain.add_fallback(fallback_func)
208
+
209
+ if asyncio.iscoroutinefunction(func):
210
+ @functools.wraps(func)
211
+ async def async_wrapper(*args, **kwargs):
212
+ return await chain.execute_async(func, *args, **kwargs)
213
+ return async_wrapper # type: ignore
214
+ else:
215
+ @functools.wraps(func)
216
+ def sync_wrapper(*args, **kwargs):
217
+ return chain.execute(func, *args, **kwargs)
218
+ return sync_wrapper # type: ignore
219
+
220
+ return decorator
@@ -0,0 +1,193 @@
1
+ """
2
+ Fallback implementation for graceful degradation.
3
+ """
4
+
5
+ import asyncio
6
+ import functools
7
+ from typing import Any, Callable, TypeVar
8
+
9
+ from attrs import define, field
10
+
11
+ from provide.foundation.logger import logger
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ @define(kw_only=True, slots=True)
17
+ class FallbackChain:
18
+ """Chain of fallback strategies for graceful degradation.
19
+
20
+ Executes fallback functions in order when primary function fails.
21
+ """
22
+
23
+ fallbacks: list[Callable[..., T]] = field(factory=list)
24
+ expected_exceptions: tuple[type[Exception], ...] = field(
25
+ factory=lambda: (Exception,)
26
+ )
27
+
28
+ def add_fallback(self, fallback_func: Callable[..., T]) -> None:
29
+ """Add a fallback function to the chain."""
30
+ self.fallbacks.append(fallback_func)
31
+ logger.debug(
32
+ "Added fallback to chain",
33
+ fallback_count=len(self.fallbacks),
34
+ fallback_name=getattr(fallback_func, '__name__', 'anonymous')
35
+ )
36
+
37
+ def execute(self, primary_func: Callable[..., T], *args, **kwargs) -> T:
38
+ """Execute primary function with fallback chain (sync)."""
39
+ # Try primary function first
40
+ primary_exception = None
41
+ try:
42
+ result = primary_func(*args, **kwargs)
43
+ logger.trace("Primary function succeeded", func=getattr(primary_func, '__name__', 'anonymous'))
44
+ return result
45
+ except Exception as e:
46
+ primary_exception = e
47
+ if not isinstance(e, self.expected_exceptions):
48
+ # Unexpected exception type, don't use fallbacks
49
+ logger.debug(
50
+ "Primary function failed with unexpected exception type",
51
+ exception_type=type(e).__name__,
52
+ expected_types=[t.__name__ for t in self.expected_exceptions]
53
+ )
54
+ raise
55
+
56
+ logger.warning(
57
+ "Primary function failed, trying fallbacks",
58
+ func=getattr(primary_func, '__name__', 'anonymous'),
59
+ error=str(e),
60
+ fallback_count=len(self.fallbacks)
61
+ )
62
+
63
+ # Try fallbacks in order
64
+ last_exception = None
65
+ for i, fallback_func in enumerate(self.fallbacks):
66
+ try:
67
+ result = fallback_func(*args, **kwargs)
68
+ logger.info(
69
+ "Fallback succeeded",
70
+ fallback_index=i,
71
+ fallback_name=getattr(fallback_func, '__name__', 'anonymous')
72
+ )
73
+ return result
74
+ except Exception as e:
75
+ last_exception = e
76
+ logger.warning(
77
+ "Fallback failed",
78
+ fallback_index=i,
79
+ fallback_name=getattr(fallback_func, '__name__', 'anonymous'),
80
+ error=str(e)
81
+ )
82
+ continue
83
+
84
+ # All fallbacks failed
85
+ logger.error(
86
+ "All fallbacks exhausted",
87
+ primary_func=getattr(primary_func, '__name__', 'anonymous'),
88
+ fallback_count=len(self.fallbacks)
89
+ )
90
+
91
+ # Raise the last exception from fallbacks, or original if no fallbacks
92
+ if last_exception is not None:
93
+ raise last_exception
94
+ if primary_exception is not None:
95
+ raise primary_exception
96
+ # This should never happen but provide fallback
97
+ raise RuntimeError("Fallback chain execution failed with no recorded exceptions")
98
+
99
+ async def execute_async(self, primary_func: Callable[..., T], *args, **kwargs) -> T:
100
+ """Execute primary function with fallback chain (async)."""
101
+ # Try primary function first
102
+ primary_exception = None
103
+ try:
104
+ if asyncio.iscoroutinefunction(primary_func):
105
+ result = await primary_func(*args, **kwargs)
106
+ else:
107
+ result = primary_func(*args, **kwargs)
108
+ logger.trace("Primary function succeeded", func=getattr(primary_func, '__name__', 'anonymous'))
109
+ return result
110
+ except Exception as e:
111
+ primary_exception = e
112
+ if not isinstance(e, self.expected_exceptions):
113
+ # Unexpected exception type, don't use fallbacks
114
+ logger.debug(
115
+ "Primary function failed with unexpected exception type",
116
+ exception_type=type(e).__name__,
117
+ expected_types=[t.__name__ for t in self.expected_exceptions]
118
+ )
119
+ raise
120
+
121
+ logger.warning(
122
+ "Primary function failed, trying fallbacks",
123
+ func=getattr(primary_func, '__name__', 'anonymous'),
124
+ error=str(e),
125
+ fallback_count=len(self.fallbacks)
126
+ )
127
+
128
+ # Try fallbacks in order
129
+ last_exception = None
130
+ for i, fallback_func in enumerate(self.fallbacks):
131
+ try:
132
+ if asyncio.iscoroutinefunction(fallback_func):
133
+ result = await fallback_func(*args, **kwargs)
134
+ else:
135
+ result = fallback_func(*args, **kwargs)
136
+ logger.info(
137
+ "Fallback succeeded",
138
+ fallback_index=i,
139
+ fallback_name=getattr(fallback_func, '__name__', 'anonymous')
140
+ )
141
+ return result
142
+ except Exception as e:
143
+ last_exception = e
144
+ logger.warning(
145
+ "Fallback failed",
146
+ fallback_index=i,
147
+ fallback_name=getattr(fallback_func, '__name__', 'anonymous'),
148
+ error=str(e)
149
+ )
150
+ continue
151
+
152
+ # All fallbacks failed
153
+ logger.error(
154
+ "All fallbacks exhausted",
155
+ primary_func=getattr(primary_func, '__name__', 'anonymous'),
156
+ fallback_count=len(self.fallbacks)
157
+ )
158
+
159
+ # Raise the last exception from fallbacks, or original if no fallbacks
160
+ if last_exception is not None:
161
+ raise last_exception
162
+ if primary_exception is not None:
163
+ raise primary_exception
164
+ # This should never happen but provide fallback
165
+ raise RuntimeError("Fallback chain execution failed with no recorded exceptions")
166
+
167
+
168
+ def fallback(*fallback_funcs: Callable[..., T]) -> Callable:
169
+ """Decorator to add fallback functions to a primary function.
170
+
171
+ Args:
172
+ *fallback_funcs: Functions to use as fallbacks, in order of preference
173
+
174
+ Returns:
175
+ Decorated function that uses fallback chain
176
+ """
177
+ def decorator(primary_func: Callable[..., T]) -> Callable[..., T]:
178
+ chain = FallbackChain()
179
+ for fallback_func in fallback_funcs:
180
+ chain.add_fallback(fallback_func)
181
+
182
+ if asyncio.iscoroutinefunction(primary_func):
183
+ @functools.wraps(primary_func)
184
+ async def async_wrapper(*args, **kwargs):
185
+ return await chain.execute_async(primary_func, *args, **kwargs)
186
+ return async_wrapper
187
+ else:
188
+ @functools.wraps(primary_func)
189
+ def sync_wrapper(*args, **kwargs):
190
+ return chain.execute(primary_func, *args, **kwargs)
191
+ return sync_wrapper
192
+
193
+ return decorator