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.
- provide/foundation/__init__.py +41 -23
- provide/foundation/archive/__init__.py +23 -0
- provide/foundation/archive/base.py +70 -0
- provide/foundation/archive/bzip2.py +157 -0
- provide/foundation/archive/gzip.py +159 -0
- provide/foundation/archive/operations.py +334 -0
- provide/foundation/archive/tar.py +164 -0
- provide/foundation/archive/zip.py +203 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +13 -7
- provide/foundation/cli/commands/logs/__init__.py +1 -1
- provide/foundation/cli/commands/logs/query.py +1 -1
- provide/foundation/cli/commands/logs/send.py +1 -1
- provide/foundation/cli/commands/logs/tail.py +1 -1
- provide/foundation/cli/decorators.py +11 -10
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -35
- provide/foundation/cli/utils.py +21 -17
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +479 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +4 -19
- provide/foundation/config/loader.py +9 -3
- provide/foundation/config/sync.py +19 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +35 -13
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +85 -109
- provide/foundation/core.py +1 -2
- provide/foundation/crypto/__init__.py +2 -0
- provide/foundation/crypto/certificates/__init__.py +34 -0
- provide/foundation/crypto/certificates/base.py +173 -0
- provide/foundation/crypto/certificates/certificate.py +290 -0
- provide/foundation/crypto/certificates/factory.py +213 -0
- provide/foundation/crypto/certificates/generator.py +138 -0
- provide/foundation/crypto/certificates/loader.py +130 -0
- provide/foundation/crypto/certificates/operations.py +198 -0
- provide/foundation/crypto/certificates/trust.py +107 -0
- provide/foundation/errors/__init__.py +2 -3
- provide/foundation/errors/decorators.py +0 -231
- provide/foundation/errors/types.py +0 -97
- provide/foundation/eventsets/__init__.py +0 -0
- provide/foundation/eventsets/display.py +84 -0
- provide/foundation/eventsets/registry.py +160 -0
- provide/foundation/eventsets/resolver.py +192 -0
- provide/foundation/eventsets/sets/das.py +128 -0
- provide/foundation/eventsets/sets/database.py +125 -0
- provide/foundation/eventsets/sets/http.py +153 -0
- provide/foundation/eventsets/sets/llm.py +139 -0
- provide/foundation/eventsets/sets/task_queue.py +107 -0
- provide/foundation/eventsets/types.py +70 -0
- provide/foundation/file/directory.py +13 -22
- provide/foundation/file/lock.py +3 -1
- provide/foundation/hub/components.py +77 -515
- provide/foundation/hub/config.py +151 -0
- provide/foundation/hub/discovery.py +62 -0
- provide/foundation/hub/handlers.py +81 -0
- provide/foundation/hub/lifecycle.py +194 -0
- provide/foundation/hub/manager.py +4 -4
- provide/foundation/hub/processors.py +44 -0
- provide/foundation/integrations/__init__.py +11 -0
- provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
- provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/client.py +12 -12
- provide/foundation/{observability → integrations}/openobserve/commands.py +3 -3
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/streaming.py +4 -4
- provide/foundation/logger/__init__.py +3 -10
- provide/foundation/logger/config/logging.py +68 -298
- provide/foundation/logger/config/telemetry.py +41 -121
- provide/foundation/logger/core.py +0 -2
- provide/foundation/logger/custom_processors.py +1 -0
- provide/foundation/logger/factories.py +11 -2
- provide/foundation/logger/processors/main.py +20 -84
- provide/foundation/logger/setup/__init__.py +5 -1
- provide/foundation/logger/setup/coordinator.py +76 -24
- provide/foundation/logger/setup/processors.py +2 -9
- provide/foundation/logger/trace.py +27 -0
- provide/foundation/metrics/otel.py +10 -10
- provide/foundation/observability/__init__.py +2 -2
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +47 -0
- provide/foundation/process/lifecycle.py +115 -59
- provide/foundation/resilience/__init__.py +35 -0
- provide/foundation/resilience/circuit.py +164 -0
- provide/foundation/resilience/decorators.py +220 -0
- provide/foundation/resilience/fallback.py +193 -0
- provide/foundation/resilience/retry.py +325 -0
- provide/foundation/streams/config.py +79 -0
- provide/foundation/streams/console.py +7 -8
- provide/foundation/streams/core.py +6 -3
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +84 -2
- provide/foundation/testing/archive/__init__.py +24 -0
- provide/foundation/testing/archive/fixtures.py +217 -0
- provide/foundation/testing/cli.py +30 -17
- provide/foundation/testing/common/__init__.py +32 -0
- provide/foundation/testing/common/fixtures.py +236 -0
- provide/foundation/testing/file/__init__.py +40 -0
- provide/foundation/testing/file/content_fixtures.py +316 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +52 -0
- provide/foundation/testing/file/special_fixtures.py +153 -0
- provide/foundation/testing/logger.py +117 -11
- provide/foundation/testing/mocking/__init__.py +46 -0
- provide/foundation/testing/mocking/fixtures.py +331 -0
- provide/foundation/testing/process/__init__.py +48 -0
- provide/foundation/testing/process/async_fixtures.py +405 -0
- provide/foundation/testing/process/fixtures.py +56 -0
- provide/foundation/testing/process/subprocess_fixtures.py +209 -0
- provide/foundation/testing/threading/__init__.py +38 -0
- provide/foundation/testing/threading/basic_fixtures.py +101 -0
- provide/foundation/testing/threading/data_fixtures.py +99 -0
- provide/foundation/testing/threading/execution_fixtures.py +263 -0
- provide/foundation/testing/threading/fixtures.py +54 -0
- provide/foundation/testing/threading/sync_fixtures.py +97 -0
- provide/foundation/testing/time/__init__.py +32 -0
- provide/foundation/testing/time/fixtures.py +409 -0
- provide/foundation/testing/transport/__init__.py +30 -0
- provide/foundation/testing/transport/fixtures.py +280 -0
- provide/foundation/tools/__init__.py +58 -0
- provide/foundation/tools/base.py +348 -0
- provide/foundation/tools/cache.py +268 -0
- provide/foundation/tools/downloader.py +224 -0
- provide/foundation/tools/installer.py +254 -0
- provide/foundation/tools/registry.py +223 -0
- provide/foundation/tools/resolver.py +321 -0
- provide/foundation/tools/verifier.py +186 -0
- provide/foundation/tracer/otel.py +7 -11
- provide/foundation/tracer/spans.py +2 -2
- provide/foundation/transport/__init__.py +155 -0
- provide/foundation/transport/base.py +171 -0
- provide/foundation/transport/client.py +266 -0
- provide/foundation/transport/config.py +140 -0
- provide/foundation/transport/errors.py +79 -0
- provide/foundation/transport/http.py +232 -0
- provide/foundation/transport/middleware.py +360 -0
- provide/foundation/transport/registry.py +167 -0
- provide/foundation/transport/types.py +45 -0
- provide/foundation/utils/deps.py +14 -12
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/METADATA +5 -28
- provide_foundation-0.0.0.dev2.dist-info/RECORD +225 -0
- provide/foundation/cli/commands/logs/generate_old.py +0 -569
- provide/foundation/crypto/certificates.py +0 -896
- provide/foundation/logger/emoji/__init__.py +0 -44
- provide/foundation/logger/emoji/matrix.py +0 -209
- provide/foundation/logger/emoji/sets.py +0 -458
- provide/foundation/logger/emoji/types.py +0 -56
- provide/foundation/logger/setup/emoji_resolver.py +0 -64
- provide_foundation-0.0.0.dev0.dist-info/RECORD +0 -149
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev0.dist-info → provide_foundation-0.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
- {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
|