flowyml 1.1.0__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.
- flowyml/__init__.py +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Error handling utilities for robust pipeline execution."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from enum import Enum
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CircuitState(Enum):
|
|
12
|
+
"""Circuit breaker states."""
|
|
13
|
+
|
|
14
|
+
CLOSED = "closed" # Normal operation
|
|
15
|
+
OPEN = "open" # Failures exceed threshold, rejecting requests
|
|
16
|
+
HALF_OPEN = "half_open" # Testing if service recovered
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CircuitBreakerConfig:
|
|
21
|
+
"""Configuration for circuit breaker."""
|
|
22
|
+
|
|
23
|
+
failure_threshold: int = 5
|
|
24
|
+
"""Number of failures before opening circuit"""
|
|
25
|
+
|
|
26
|
+
timeout: float = 60
|
|
27
|
+
"""Time to wait before trying again (seconds)"""
|
|
28
|
+
|
|
29
|
+
recovery_timeout: float = 300
|
|
30
|
+
"""Time to wait before fully closing circuit (seconds)"""
|
|
31
|
+
|
|
32
|
+
expected_exceptions: list[type[Exception]] = field(default_factory=lambda: [Exception])
|
|
33
|
+
"""Exceptions that trigger circuit breaker"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CircuitBreaker:
|
|
37
|
+
"""Circuit breaker pattern implementation.
|
|
38
|
+
|
|
39
|
+
Prevents cascading failures by failing fast when a service is down.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
```python
|
|
43
|
+
@step(circuit_breaker=CircuitBreaker(failure_threshold=3, timeout=60))
|
|
44
|
+
def call_api(url):
|
|
45
|
+
return requests.get(url).json()
|
|
46
|
+
```
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
failure_threshold: int = 5,
|
|
52
|
+
timeout: float = 60,
|
|
53
|
+
recovery_timeout: float = 300,
|
|
54
|
+
expected_exceptions: list[type[Exception]] | None = None,
|
|
55
|
+
):
|
|
56
|
+
"""Initialize circuit breaker.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
failure_threshold: Number of failures before opening circuit
|
|
60
|
+
timeout: Seconds to wait before trying again
|
|
61
|
+
recovery_timeout: Seconds to wait before fully closing circuit
|
|
62
|
+
expected_exceptions: Exceptions that trigger the breaker
|
|
63
|
+
"""
|
|
64
|
+
self.config = CircuitBreakerConfig(
|
|
65
|
+
failure_threshold=failure_threshold,
|
|
66
|
+
timeout=timeout,
|
|
67
|
+
recovery_timeout=recovery_timeout,
|
|
68
|
+
expected_exceptions=expected_exceptions or [Exception],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self.state = CircuitState.CLOSED
|
|
72
|
+
self.failure_count = 0
|
|
73
|
+
self.last_failure_time: datetime | None = None
|
|
74
|
+
self.success_count = 0
|
|
75
|
+
|
|
76
|
+
def call(self, func: Callable, *args, **kwargs) -> Any:
|
|
77
|
+
"""Call function with circuit breaker protection.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
func: Function to call
|
|
81
|
+
*args: Positional arguments
|
|
82
|
+
**kwargs: Keyword arguments
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Function result
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
CircuitOpenError: If circuit is open
|
|
89
|
+
"""
|
|
90
|
+
if self.state == CircuitState.OPEN:
|
|
91
|
+
if self._should_attempt_reset():
|
|
92
|
+
self.state = CircuitState.HALF_OPEN
|
|
93
|
+
else:
|
|
94
|
+
raise CircuitOpenError(
|
|
95
|
+
f"Circuit breaker is open. Wait {self.config.timeout}s before retry.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
result = func(*args, **kwargs)
|
|
100
|
+
self._on_success()
|
|
101
|
+
return result
|
|
102
|
+
|
|
103
|
+
except Exception as e:
|
|
104
|
+
if self._is_expected_exception(e):
|
|
105
|
+
self._on_failure()
|
|
106
|
+
raise
|
|
107
|
+
|
|
108
|
+
def _should_attempt_reset(self) -> bool:
|
|
109
|
+
"""Check if enough time has passed to attempt reset."""
|
|
110
|
+
if self.last_failure_time is None:
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
time_since_failure = (datetime.now() - self.last_failure_time).total_seconds()
|
|
114
|
+
return time_since_failure >= self.config.timeout
|
|
115
|
+
|
|
116
|
+
def _is_expected_exception(self, exception: Exception) -> bool:
|
|
117
|
+
"""Check if exception should trigger circuit breaker."""
|
|
118
|
+
return any(isinstance(exception, exc_type) for exc_type in self.config.expected_exceptions)
|
|
119
|
+
|
|
120
|
+
def _on_success(self) -> None:
|
|
121
|
+
"""Handle successful call."""
|
|
122
|
+
self.failure_count = 0
|
|
123
|
+
|
|
124
|
+
if self.state == CircuitState.HALF_OPEN:
|
|
125
|
+
self.success_count += 1
|
|
126
|
+
# Fully close circuit after successful recovery period
|
|
127
|
+
if (
|
|
128
|
+
self.last_failure_time
|
|
129
|
+
and (datetime.now() - self.last_failure_time).total_seconds() >= self.config.recovery_timeout
|
|
130
|
+
):
|
|
131
|
+
self.state = CircuitState.CLOSED
|
|
132
|
+
self.success_count = 0
|
|
133
|
+
|
|
134
|
+
def _on_failure(self) -> None:
|
|
135
|
+
"""Handle failed call."""
|
|
136
|
+
self.failure_count += 1
|
|
137
|
+
self.last_failure_time = datetime.now()
|
|
138
|
+
|
|
139
|
+
if self.failure_count >= self.config.failure_threshold:
|
|
140
|
+
self.state = CircuitState.OPEN
|
|
141
|
+
|
|
142
|
+
def reset(self) -> None:
|
|
143
|
+
"""Reset circuit breaker to closed state."""
|
|
144
|
+
self.state = CircuitState.CLOSED
|
|
145
|
+
self.failure_count = 0
|
|
146
|
+
self.success_count = 0
|
|
147
|
+
self.last_failure_time = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class CircuitOpenError(Exception):
|
|
151
|
+
"""Exception raised when circuit breaker is open."""
|
|
152
|
+
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class FallbackConfig:
|
|
158
|
+
"""Configuration for fallback handler."""
|
|
159
|
+
|
|
160
|
+
fallback_func: Callable
|
|
161
|
+
"""Fallback function to call on error"""
|
|
162
|
+
|
|
163
|
+
fallback_on: list[type[Exception]] = field(default_factory=lambda: [Exception])
|
|
164
|
+
"""Exceptions that trigger fallback"""
|
|
165
|
+
|
|
166
|
+
max_fallback_attempts: int = 1
|
|
167
|
+
"""Maximum number of fallback attempts"""
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class FallbackHandler:
|
|
171
|
+
"""Fallback handler for graceful degradation.
|
|
172
|
+
|
|
173
|
+
Example:
|
|
174
|
+
```python
|
|
175
|
+
@step(fallback=lambda: load_cached_data(), fallback_on=[TimeoutError])
|
|
176
|
+
def fetch_live_data():
|
|
177
|
+
return requests.get(url).json()
|
|
178
|
+
```
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
fallback_func: Callable,
|
|
184
|
+
fallback_on: list[type[Exception]] | None = None,
|
|
185
|
+
max_attempts: int = 1,
|
|
186
|
+
):
|
|
187
|
+
"""Initialize fallback handler.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
fallback_func: Function to call as fallback
|
|
191
|
+
fallback_on: Exceptions that trigger fallback
|
|
192
|
+
max_attempts: Maximum fallback attempts
|
|
193
|
+
"""
|
|
194
|
+
self.config = FallbackConfig(
|
|
195
|
+
fallback_func=fallback_func,
|
|
196
|
+
fallback_on=fallback_on or [Exception],
|
|
197
|
+
max_fallback_attempts=max_attempts,
|
|
198
|
+
)
|
|
199
|
+
self.fallback_attempts = 0
|
|
200
|
+
|
|
201
|
+
def call(self, func: Callable, *args, **kwargs) -> Any:
|
|
202
|
+
"""Call function with fallback protection.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
func: Primary function to call
|
|
206
|
+
*args: Positional arguments
|
|
207
|
+
**kwargs: Keyword arguments
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Function result or fallback result
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
return func(*args, **kwargs)
|
|
214
|
+
|
|
215
|
+
except Exception as e:
|
|
216
|
+
if self._should_fallback(e) and self.fallback_attempts < self.config.max_fallback_attempts:
|
|
217
|
+
self.fallback_attempts += 1
|
|
218
|
+
return self.config.fallback_func()
|
|
219
|
+
raise
|
|
220
|
+
|
|
221
|
+
def _should_fallback(self, exception: Exception) -> bool:
|
|
222
|
+
"""Check if exception should trigger fallback."""
|
|
223
|
+
return any(isinstance(exception, exc_type) for exc_type in self.config.fallback_on)
|
|
224
|
+
|
|
225
|
+
def reset(self) -> None:
|
|
226
|
+
"""Reset fallback attempts counter."""
|
|
227
|
+
self.fallback_attempts = 0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class ExponentialBackoff:
|
|
231
|
+
"""Exponential backoff retry strategy.
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
```python
|
|
235
|
+
from flowyml import step, retry, ExponentialBackoff
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@step(
|
|
239
|
+
retry=retry(
|
|
240
|
+
max_attempts=5, backoff=ExponentialBackoff(initial=1, max=60, multiplier=2), on=[NetworkError, TimeoutError]
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
def fetch_data():
|
|
244
|
+
return api.get_data()
|
|
245
|
+
```
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
def __init__(
|
|
249
|
+
self,
|
|
250
|
+
initial: float = 1.0,
|
|
251
|
+
max_delay: float = 60.0,
|
|
252
|
+
multiplier: float = 2.0,
|
|
253
|
+
jitter: bool = True,
|
|
254
|
+
):
|
|
255
|
+
"""Initialize exponential backoff.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
initial: Initial delay in seconds
|
|
259
|
+
max_delay: Maximum delay in seconds
|
|
260
|
+
multiplier: Backoff multiplier
|
|
261
|
+
jitter: Add random jitter to delays
|
|
262
|
+
"""
|
|
263
|
+
self.initial = initial
|
|
264
|
+
self.max_delay = max_delay
|
|
265
|
+
self.multiplier = multiplier
|
|
266
|
+
self.jitter = jitter
|
|
267
|
+
self.attempt = 0
|
|
268
|
+
|
|
269
|
+
def get_delay(self) -> float:
|
|
270
|
+
"""Get delay for current attempt.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Delay in seconds
|
|
274
|
+
"""
|
|
275
|
+
delay = min(self.initial * (self.multiplier**self.attempt), self.max_delay)
|
|
276
|
+
|
|
277
|
+
if self.jitter:
|
|
278
|
+
import random
|
|
279
|
+
|
|
280
|
+
delay = delay * (0.5 + random.random())
|
|
281
|
+
|
|
282
|
+
self.attempt += 1
|
|
283
|
+
return delay
|
|
284
|
+
|
|
285
|
+
def reset(self) -> None:
|
|
286
|
+
"""Reset attempt counter."""
|
|
287
|
+
self.attempt = 0
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass
|
|
291
|
+
class RetryConfig:
|
|
292
|
+
"""Configuration for retry logic."""
|
|
293
|
+
|
|
294
|
+
max_attempts: int = 3
|
|
295
|
+
"""Maximum number of retry attempts"""
|
|
296
|
+
|
|
297
|
+
backoff: ExponentialBackoff | None = None
|
|
298
|
+
"""Backoff strategy"""
|
|
299
|
+
|
|
300
|
+
retry_on: list[type[Exception]] = field(default_factory=lambda: [Exception])
|
|
301
|
+
"""Exceptions to retry on"""
|
|
302
|
+
|
|
303
|
+
not_retry_on: list[type[Exception]] = field(default_factory=list)
|
|
304
|
+
"""Exceptions NOT to retry on"""
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def retry(
|
|
308
|
+
max_attempts: int = 3,
|
|
309
|
+
backoff: ExponentialBackoff | None = None,
|
|
310
|
+
on: list[type[Exception]] | None = None,
|
|
311
|
+
not_on: list[type[Exception]] | None = None,
|
|
312
|
+
) -> RetryConfig:
|
|
313
|
+
"""Create retry configuration.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
max_attempts: Maximum retry attempts
|
|
317
|
+
backoff: Backoff strategy
|
|
318
|
+
on: Exceptions to retry on
|
|
319
|
+
not_on: Exceptions not to retry on
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
RetryConfig instance
|
|
323
|
+
"""
|
|
324
|
+
return RetryConfig(
|
|
325
|
+
max_attempts=max_attempts,
|
|
326
|
+
backoff=backoff or ExponentialBackoff(),
|
|
327
|
+
retry_on=on or [Exception],
|
|
328
|
+
not_retry_on=not_on or [],
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def execute_with_retry(
|
|
333
|
+
func: Callable,
|
|
334
|
+
retry_config: RetryConfig,
|
|
335
|
+
*args,
|
|
336
|
+
**kwargs,
|
|
337
|
+
) -> Any:
|
|
338
|
+
"""Execute function with retry logic.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
func: Function to execute
|
|
342
|
+
retry_config: Retry configuration
|
|
343
|
+
*args: Positional arguments
|
|
344
|
+
**kwargs: Keyword arguments
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Function result
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
Last exception if all retries fail
|
|
351
|
+
"""
|
|
352
|
+
last_exception = None
|
|
353
|
+
|
|
354
|
+
for attempt in range(retry_config.max_attempts):
|
|
355
|
+
try:
|
|
356
|
+
return func(*args, **kwargs)
|
|
357
|
+
|
|
358
|
+
except Exception as e:
|
|
359
|
+
# Don't retry if in not_retry_on list
|
|
360
|
+
if any(isinstance(e, exc_type) for exc_type in retry_config.not_retry_on):
|
|
361
|
+
raise
|
|
362
|
+
|
|
363
|
+
# Only retry if in retry_on list
|
|
364
|
+
if not any(isinstance(e, exc_type) for exc_type in retry_config.retry_on):
|
|
365
|
+
raise
|
|
366
|
+
|
|
367
|
+
last_exception = e
|
|
368
|
+
|
|
369
|
+
# Don't sleep on last attempt
|
|
370
|
+
if attempt < retry_config.max_attempts - 1 and retry_config.backoff:
|
|
371
|
+
delay = retry_config.backoff.get_delay()
|
|
372
|
+
time.sleep(delay)
|
|
373
|
+
|
|
374
|
+
# All retries failed
|
|
375
|
+
if last_exception:
|
|
376
|
+
raise last_exception
|
|
377
|
+
raise RuntimeError("Retry failed with no exception captured")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@dataclass
|
|
381
|
+
class OnFailureConfig:
|
|
382
|
+
"""Configuration for failure handling."""
|
|
383
|
+
|
|
384
|
+
action: str = "log"
|
|
385
|
+
"""Action to take (log, email, slack, webhook)"""
|
|
386
|
+
|
|
387
|
+
recipients: list[str] = field(default_factory=list)
|
|
388
|
+
"""Recipients for notifications"""
|
|
389
|
+
|
|
390
|
+
include_logs: bool = True
|
|
391
|
+
"""Include logs in notification"""
|
|
392
|
+
|
|
393
|
+
include_traceback: bool = True
|
|
394
|
+
"""Include full traceback"""
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def on_failure(
|
|
398
|
+
action: str = "log",
|
|
399
|
+
recipients: list[str] | None = None,
|
|
400
|
+
include_logs: bool = True,
|
|
401
|
+
include_traceback: bool = True,
|
|
402
|
+
) -> OnFailureConfig:
|
|
403
|
+
"""Create failure handling configuration.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
action: Action to take on failure
|
|
407
|
+
recipients: Recipients for notifications
|
|
408
|
+
include_logs: Include logs in notification
|
|
409
|
+
include_traceback: Include full traceback
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
OnFailureConfig instance
|
|
413
|
+
"""
|
|
414
|
+
return OnFailureConfig(
|
|
415
|
+
action=action,
|
|
416
|
+
recipients=recipients or [],
|
|
417
|
+
include_logs=include_logs,
|
|
418
|
+
include_traceback=include_traceback,
|
|
419
|
+
)
|