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.
- provide/foundation/__init__.py +20 -20
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +90 -91
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +4 -4
- provide/foundation/cli/commands/logs/__init__.py +2 -2
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +3 -3
- provide/foundation/cli/commands/logs/send.py +2 -2
- provide/foundation/cli/commands/logs/tail.py +2 -2
- provide/foundation/cli/decorators.py +0 -1
- provide/foundation/cli/testing.py +0 -5
- provide/foundation/cli/utils.py +1 -2
- provide/foundation/config/__init__.py +19 -19
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +81 -83
- provide/foundation/config/defaults.py +1 -1
- provide/foundation/config/env.py +2 -1
- provide/foundation/config/loader.py +1 -1
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/output.py +7 -7
- provide/foundation/context/core.py +19 -17
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -2
- provide/foundation/errors/decorators.py +0 -3
- provide/foundation/errors/types.py +0 -1
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +1 -1
- provide/foundation/file/lock.py +2 -3
- provide/foundation/hub/components.py +19 -21
- provide/foundation/hub/config.py +25 -19
- provide/foundation/hub/discovery.py +5 -4
- provide/foundation/hub/handlers.py +13 -5
- provide/foundation/hub/lifecycle.py +10 -9
- provide/foundation/hub/manager.py +3 -0
- provide/foundation/hub/processors.py +8 -3
- provide/foundation/integrations/__init__.py +1 -1
- provide/foundation/integrations/openobserve/client.py +2 -2
- provide/foundation/integrations/openobserve/commands.py +9 -9
- provide/foundation/integrations/openobserve/config.py +2 -2
- provide/foundation/integrations/openobserve/otlp.py +2 -2
- provide/foundation/integrations/openobserve/search.py +1 -2
- provide/foundation/integrations/openobserve/streaming.py +1 -1
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +19 -19
- provide/foundation/logger/config/telemetry.py +11 -13
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +38 -24
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +1 -1
- provide/foundation/process/__init__.py +1 -1
- provide/foundation/process/exit.py +6 -5
- provide/foundation/process/lifecycle.py +41 -18
- provide/foundation/resilience/__init__.py +6 -5
- provide/foundation/resilience/circuit.py +32 -30
- provide/foundation/resilience/decorators.py +58 -42
- provide/foundation/resilience/fallback.py +55 -40
- provide/foundation/resilience/retry.py +67 -65
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +8 -9
- provide/foundation/streams/console.py +3 -3
- provide/foundation/streams/core.py +2 -2
- provide/foundation/streams/file.py +1 -1
- provide/foundation/testing/__init__.py +22 -7
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +3 -6
- provide/foundation/testing/common/__init__.py +13 -13
- provide/foundation/testing/common/fixtures.py +27 -30
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +65 -92
- provide/foundation/testing/file/directory_fixtures.py +19 -19
- provide/foundation/testing/file/fixtures.py +14 -17
- provide/foundation/testing/file/special_fixtures.py +34 -42
- provide/foundation/testing/logger.py +28 -23
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +89 -80
- provide/foundation/testing/process/fixtures.py +11 -13
- provide/foundation/testing/process/subprocess_fixtures.py +41 -40
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +21 -17
- provide/foundation/testing/threading/data_fixtures.py +18 -16
- provide/foundation/testing/threading/execution_fixtures.py +67 -52
- provide/foundation/testing/threading/fixtures.py +10 -14
- provide/foundation/testing/threading/sync_fixtures.py +21 -18
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +91 -79
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +62 -69
- provide/foundation/tools/downloader.py +51 -56
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +1 -13
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +11 -13
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +86 -81
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +3 -2
- provide/foundation/utils/parsing.py +7 -7
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev2.dist-info/RECORD +0 -225
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {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
|
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
|
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
|
-
|
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[
|
114
|
+
policy_kwargs["max_attempts"] = max_attempts
|
107
115
|
if base_delay is not None:
|
108
|
-
policy_kwargs[
|
116
|
+
policy_kwargs["base_delay"] = base_delay
|
109
117
|
if backoff is not None:
|
110
|
-
policy_kwargs[
|
118
|
+
policy_kwargs["backoff"] = backoff
|
111
119
|
if max_delay is not None:
|
112
|
-
policy_kwargs[
|
120
|
+
policy_kwargs["max_delay"] = max_delay
|
113
121
|
if jitter is not None:
|
114
|
-
policy_kwargs[
|
122
|
+
policy_kwargs["jitter"] = jitter
|
115
123
|
if exceptions:
|
116
|
-
policy_kwargs[
|
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
|
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,
|
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(
|
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,
|
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,
|
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,
|
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,
|
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(
|
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(
|
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,
|
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,
|
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,
|
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,
|
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(
|
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
|