localqueue 0.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.
- localqueue/__init__.py +67 -0
- localqueue/cli.py +744 -0
- localqueue/queue.py +261 -0
- localqueue/retry/__init__.py +39 -0
- localqueue/retry/store.py +182 -0
- localqueue/retry/tenacity.py +685 -0
- localqueue/store.py +1197 -0
- localqueue/worker.py +192 -0
- localqueue-0.1.0.dist-info/METADATA +356 -0
- localqueue-0.1.0.dist-info/RECORD +13 -0
- localqueue-0.1.0.dist-info/WHEEL +4 -0
- localqueue-0.1.0.dist-info/entry_points.txt +2 -0
- localqueue-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import inspect
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Callable, TypeVar, cast
|
|
11
|
+
|
|
12
|
+
from tenacity import DoAttempt, DoSleep
|
|
13
|
+
from tenacity import AsyncRetrying, RetryCallState, Retrying
|
|
14
|
+
from tenacity import _utils
|
|
15
|
+
from tenacity.after import after_nothing
|
|
16
|
+
from tenacity.asyncio import _portable_async_sleep
|
|
17
|
+
from tenacity.before import before_nothing
|
|
18
|
+
from tenacity.nap import sleep as tenacity_sleep
|
|
19
|
+
from tenacity.retry import retry_if_exception_type
|
|
20
|
+
from tenacity.stop import stop_after_attempt
|
|
21
|
+
from tenacity.wait import wait_exponential
|
|
22
|
+
|
|
23
|
+
from .store import AttemptStore, LMDBAttemptStore, RetryRecord, SQLiteAttemptStore
|
|
24
|
+
|
|
25
|
+
WrappedFn = TypeVar("WrappedFn", bound=Callable[..., Any])
|
|
26
|
+
_UNSET = object()
|
|
27
|
+
_default_store_local = threading.local()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _sqlite_default_store_factory() -> AttemptStore:
|
|
31
|
+
return SQLiteAttemptStore("localqueue_retries.sqlite3")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_default_store_factory: Callable[[], AttemptStore] = _sqlite_default_store_factory
|
|
35
|
+
_default_store_factory_lock = threading.Lock()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PersistentRetryExhausted(RuntimeError):
|
|
39
|
+
key: str
|
|
40
|
+
attempts: int
|
|
41
|
+
|
|
42
|
+
def __init__(self, key: str, attempts: int) -> None:
|
|
43
|
+
super().__init__(
|
|
44
|
+
f"retry budget already exhausted for key={key!r} after {attempts} attempts"
|
|
45
|
+
)
|
|
46
|
+
self.key = key
|
|
47
|
+
self.attempts = attempts
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(slots=True)
|
|
51
|
+
class PersistentCallContext:
|
|
52
|
+
key: str
|
|
53
|
+
record: RetryRecord
|
|
54
|
+
starting_attempts: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PersistentRetryState(RetryCallState):
|
|
58
|
+
_retry_state: RetryCallState
|
|
59
|
+
_record: RetryRecord
|
|
60
|
+
_starting_attempts: int
|
|
61
|
+
|
|
62
|
+
def __init__( # pyright: ignore[reportMissingSuperCall]
|
|
63
|
+
self, retry_state: RetryCallState, record: RetryRecord, starting_attempts: int
|
|
64
|
+
) -> None:
|
|
65
|
+
self._retry_state = retry_state
|
|
66
|
+
self._record = record
|
|
67
|
+
self._starting_attempts = starting_attempts
|
|
68
|
+
|
|
69
|
+
def __getattr__(self, name: str) -> Any:
|
|
70
|
+
return getattr(self._retry_state, name)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def attempt_number(self) -> int: # pyright: ignore[reportIncompatibleVariableOverride]
|
|
74
|
+
return self._starting_attempts + self._retry_state.attempt_number
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def start_time(self) -> float:
|
|
78
|
+
return self._record.first_attempt_at
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def seconds_since_start(self) -> float | None:
|
|
82
|
+
if self._retry_state.outcome_timestamp is None:
|
|
83
|
+
return None
|
|
84
|
+
return max(time.time() - self._record.first_attempt_at, 0.0)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_default_store() -> AttemptStore:
|
|
88
|
+
store = getattr(_default_store_local, "store", None)
|
|
89
|
+
if store is None:
|
|
90
|
+
with _default_store_factory_lock:
|
|
91
|
+
factory = _default_store_factory
|
|
92
|
+
store = factory()
|
|
93
|
+
_default_store_local.store = store
|
|
94
|
+
return cast(AttemptStore, store)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def configure_default_store(store: AttemptStore | None = None) -> None:
|
|
98
|
+
if store is None:
|
|
99
|
+
existing_store = getattr(_default_store_local, "store", None)
|
|
100
|
+
close = getattr(existing_store, "close", None)
|
|
101
|
+
if callable(close):
|
|
102
|
+
close()
|
|
103
|
+
if existing_store is not None:
|
|
104
|
+
del _default_store_local.store
|
|
105
|
+
return
|
|
106
|
+
_default_store_local.store = store
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def configure_default_store_factory(factory: Callable[[], AttemptStore]) -> None:
|
|
110
|
+
global _default_store_factory
|
|
111
|
+
with _default_store_factory_lock:
|
|
112
|
+
_default_store_factory = factory
|
|
113
|
+
configure_default_store(None)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _validate_store_args(
|
|
117
|
+
store: AttemptStore | None, store_path: str | Path | None
|
|
118
|
+
) -> None:
|
|
119
|
+
if store is not None and store_path is not None:
|
|
120
|
+
raise ValueError("pass either store= or store_path=, not both")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _default_key_fn(
|
|
124
|
+
fn: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
125
|
+
) -> str:
|
|
126
|
+
raise ValueError("persistent retries require an explicit key= or key_fn=")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def key_from_argument(
|
|
130
|
+
name: str,
|
|
131
|
+
) -> Callable[[Callable[..., Any], tuple[Any, ...], dict[str, Any]], str]:
|
|
132
|
+
if not name:
|
|
133
|
+
raise ValueError("argument name cannot be empty")
|
|
134
|
+
|
|
135
|
+
def resolve_key(
|
|
136
|
+
fn: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
137
|
+
) -> str:
|
|
138
|
+
signature = inspect.signature(fn)
|
|
139
|
+
bound = signature.bind_partial(*args, **kwargs)
|
|
140
|
+
if name not in bound.arguments:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"could not derive a persistent retry key from argument {name!r}"
|
|
143
|
+
)
|
|
144
|
+
return str(bound.arguments[name])
|
|
145
|
+
|
|
146
|
+
return resolve_key
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def key_from_attr(
|
|
150
|
+
argument_name: str, attribute_name: str, *, prefix: str | None = None
|
|
151
|
+
) -> Callable[[Callable[..., Any], tuple[Any, ...], dict[str, Any]], str]:
|
|
152
|
+
if not argument_name:
|
|
153
|
+
raise ValueError("argument name cannot be empty")
|
|
154
|
+
if not attribute_name:
|
|
155
|
+
raise ValueError("attribute name cannot be empty")
|
|
156
|
+
if prefix == "":
|
|
157
|
+
raise ValueError("prefix cannot be empty")
|
|
158
|
+
|
|
159
|
+
def resolve_key(
|
|
160
|
+
fn: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
161
|
+
) -> str:
|
|
162
|
+
signature = inspect.signature(fn)
|
|
163
|
+
bound = signature.bind_partial(*args, **kwargs)
|
|
164
|
+
if argument_name not in bound.arguments:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"could not derive a persistent retry key from argument "
|
|
167
|
+
f"{argument_name!r}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
value = bound.arguments[argument_name]
|
|
171
|
+
if not hasattr(value, attribute_name):
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"could not derive a persistent retry key from "
|
|
174
|
+
f"{argument_name}.{attribute_name}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
key = str(getattr(value, attribute_name))
|
|
178
|
+
if prefix is None:
|
|
179
|
+
return key
|
|
180
|
+
return f"{prefix}:{key}"
|
|
181
|
+
|
|
182
|
+
return resolve_key
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def idempotency_key_from_id(
|
|
186
|
+
argument_name: str, *, prefix: str | None = None
|
|
187
|
+
) -> Callable[[Callable[..., Any], tuple[Any, ...], dict[str, Any]], str]:
|
|
188
|
+
return key_from_attr(argument_name, "id", prefix=prefix)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _build_effective_tenacity_kwargs(
|
|
192
|
+
base_cls: type[Any], tenacity_kwargs: dict[str, Any]
|
|
193
|
+
) -> dict[str, Any]:
|
|
194
|
+
signature = inspect.signature(base_cls.__init__)
|
|
195
|
+
effective: dict[str, Any] = {}
|
|
196
|
+
remaining = dict(tenacity_kwargs)
|
|
197
|
+
has_var_keyword = False
|
|
198
|
+
|
|
199
|
+
for name, parameter in signature.parameters.items():
|
|
200
|
+
if name == "self":
|
|
201
|
+
continue
|
|
202
|
+
if parameter.kind is inspect.Parameter.VAR_KEYWORD:
|
|
203
|
+
has_var_keyword = True
|
|
204
|
+
continue
|
|
205
|
+
if name in remaining:
|
|
206
|
+
effective[name] = remaining.pop(name)
|
|
207
|
+
continue
|
|
208
|
+
if parameter.default is not inspect.Parameter.empty:
|
|
209
|
+
effective[name] = parameter.default
|
|
210
|
+
|
|
211
|
+
if remaining:
|
|
212
|
+
if not has_var_keyword:
|
|
213
|
+
unknown = ", ".join(sorted(remaining))
|
|
214
|
+
raise TypeError(f"unexpected Tenacity arguments: {unknown}")
|
|
215
|
+
effective.update(remaining)
|
|
216
|
+
|
|
217
|
+
return effective
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class _PersistentMixin:
|
|
221
|
+
_store: AttemptStore | None
|
|
222
|
+
_store_path: Path | None
|
|
223
|
+
_local: threading.local
|
|
224
|
+
_retrying: Retrying | AsyncRetrying
|
|
225
|
+
key: str | None
|
|
226
|
+
key_fn: Callable[[Callable[..., Any], tuple[Any, ...], dict[str, Any]], str]
|
|
227
|
+
clear_on_success: bool
|
|
228
|
+
_base_cls: type[Any]
|
|
229
|
+
_tenacity_kwargs: dict[str, Any]
|
|
230
|
+
_user_stop: Any
|
|
231
|
+
_user_wait: Any
|
|
232
|
+
_user_retry: Any
|
|
233
|
+
_user_before: Any
|
|
234
|
+
_user_after: Any
|
|
235
|
+
_user_before_sleep: Any
|
|
236
|
+
_user_retry_error_callback: Any
|
|
237
|
+
|
|
238
|
+
def __init__(
|
|
239
|
+
self,
|
|
240
|
+
*,
|
|
241
|
+
base_cls: type[Any],
|
|
242
|
+
store: AttemptStore | None = None,
|
|
243
|
+
store_path: str | Path | None = None,
|
|
244
|
+
key: str | None = None,
|
|
245
|
+
key_fn: Callable[[Callable[..., Any], tuple[Any, ...], dict[str, Any]], str]
|
|
246
|
+
| None = None,
|
|
247
|
+
clear_on_success: bool = True,
|
|
248
|
+
max_tries: int | None = None,
|
|
249
|
+
**tenacity_kwargs: Any,
|
|
250
|
+
) -> None:
|
|
251
|
+
_validate_store_args(store, store_path)
|
|
252
|
+
if max_tries is not None and "stop" in tenacity_kwargs:
|
|
253
|
+
raise ValueError("pass either max_tries= or stop=, not both")
|
|
254
|
+
if max_tries is not None:
|
|
255
|
+
tenacity_kwargs["stop"] = stop_after_attempt(max_tries)
|
|
256
|
+
|
|
257
|
+
effective_kwargs = _build_effective_tenacity_kwargs(base_cls, tenacity_kwargs)
|
|
258
|
+
|
|
259
|
+
self._store = store
|
|
260
|
+
self._store_path = Path(store_path) if store_path is not None else None
|
|
261
|
+
self.key = key
|
|
262
|
+
self.key_fn = key_fn or _default_key_fn
|
|
263
|
+
self.clear_on_success = clear_on_success
|
|
264
|
+
self._base_cls = base_cls
|
|
265
|
+
self._tenacity_kwargs = dict(tenacity_kwargs)
|
|
266
|
+
self._user_stop = effective_kwargs["stop"]
|
|
267
|
+
self._user_wait = effective_kwargs["wait"]
|
|
268
|
+
self._user_retry = effective_kwargs["retry"]
|
|
269
|
+
self._user_before = effective_kwargs["before"]
|
|
270
|
+
self._user_after = effective_kwargs["after"]
|
|
271
|
+
self._user_before_sleep = effective_kwargs["before_sleep"]
|
|
272
|
+
self._user_retry_error_callback = effective_kwargs["retry_error_callback"]
|
|
273
|
+
|
|
274
|
+
forwarded_kwargs = dict(effective_kwargs)
|
|
275
|
+
forwarded_kwargs["stop"] = self._wrap_stop()
|
|
276
|
+
forwarded_kwargs["wait"] = self._wrap_wait()
|
|
277
|
+
forwarded_kwargs["retry"] = self._wrap_retry()
|
|
278
|
+
forwarded_kwargs["before"] = self._wrap_callback(self._user_before)
|
|
279
|
+
forwarded_kwargs["after"] = self._wrap_callback(self._user_after)
|
|
280
|
+
forwarded_kwargs["before_sleep"] = self._wrap_callback(self._user_before_sleep)
|
|
281
|
+
forwarded_kwargs["retry_error_callback"] = self._wrap_retry_error_callback()
|
|
282
|
+
|
|
283
|
+
self._local = threading.local()
|
|
284
|
+
self._retrying = base_cls(**forwarded_kwargs)
|
|
285
|
+
|
|
286
|
+
def _get_store(self) -> AttemptStore:
|
|
287
|
+
if self._store is None:
|
|
288
|
+
if self._store_path is not None:
|
|
289
|
+
self._store = LMDBAttemptStore(self._store_path)
|
|
290
|
+
else:
|
|
291
|
+
self._store = _get_default_store()
|
|
292
|
+
return self._store
|
|
293
|
+
|
|
294
|
+
def _current_context(self) -> PersistentCallContext:
|
|
295
|
+
context = getattr(self._local, "persistent_context", None)
|
|
296
|
+
if context is None:
|
|
297
|
+
raise RuntimeError("persistent retry context not initialized")
|
|
298
|
+
return cast(PersistentCallContext, context)
|
|
299
|
+
|
|
300
|
+
def _translate_state(self, retry_state: RetryCallState) -> PersistentRetryState:
|
|
301
|
+
context = self._current_context()
|
|
302
|
+
return PersistentRetryState(
|
|
303
|
+
retry_state, context.record, context.starting_attempts
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def _resolve_key(
|
|
307
|
+
self, fn: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
308
|
+
) -> str:
|
|
309
|
+
if self.key is not None:
|
|
310
|
+
return self.key
|
|
311
|
+
return self.key_fn(fn, args, kwargs)
|
|
312
|
+
|
|
313
|
+
def _load_context(
|
|
314
|
+
self, fn: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
315
|
+
) -> PersistentCallContext:
|
|
316
|
+
key = self._resolve_key(fn, args, kwargs)
|
|
317
|
+
record = self._get_store().load(key) or RetryRecord.new()
|
|
318
|
+
return PersistentCallContext(
|
|
319
|
+
key=key,
|
|
320
|
+
record=record,
|
|
321
|
+
starting_attempts=record.attempts,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def _persist_attempt(self, retry_state: RetryCallState, *, exhausted: bool) -> None:
|
|
325
|
+
context = self._current_context()
|
|
326
|
+
context.record.attempts = self._translate_state(retry_state).attempt_number
|
|
327
|
+
context.record.exhausted = exhausted
|
|
328
|
+
self._get_store().save(context.key, context.record)
|
|
329
|
+
|
|
330
|
+
def _clear_if_success(self, retry_state: RetryCallState) -> None:
|
|
331
|
+
if not self.clear_on_success:
|
|
332
|
+
return
|
|
333
|
+
outcome = retry_state.outcome
|
|
334
|
+
if outcome is not None and not outcome.failed:
|
|
335
|
+
self._get_store().delete(self._current_context().key)
|
|
336
|
+
|
|
337
|
+
def reset(self, key: str) -> None:
|
|
338
|
+
self._get_store().delete(key)
|
|
339
|
+
|
|
340
|
+
def get_record(self, key: str) -> RetryRecord | None:
|
|
341
|
+
return self._get_store().load(key)
|
|
342
|
+
|
|
343
|
+
def _wrap_retry(self) -> Callable[[RetryCallState], bool]:
|
|
344
|
+
def wrapped_retry(retry_state: RetryCallState) -> bool:
|
|
345
|
+
return bool(self._user_retry(self._translate_state(retry_state)))
|
|
346
|
+
|
|
347
|
+
return wrapped_retry
|
|
348
|
+
|
|
349
|
+
def _wrap_wait(self) -> Callable[[RetryCallState], float]:
|
|
350
|
+
def wrapped_wait(retry_state: RetryCallState) -> float:
|
|
351
|
+
if self._user_wait is None:
|
|
352
|
+
return 0.0
|
|
353
|
+
return float(self._user_wait(self._translate_state(retry_state)))
|
|
354
|
+
|
|
355
|
+
return wrapped_wait
|
|
356
|
+
|
|
357
|
+
def _wrap_stop(self) -> Callable[[RetryCallState], bool]:
|
|
358
|
+
def wrapped_stop(retry_state: RetryCallState) -> bool:
|
|
359
|
+
should_stop = bool(self._user_stop(self._translate_state(retry_state)))
|
|
360
|
+
self._persist_attempt(retry_state, exhausted=should_stop)
|
|
361
|
+
return should_stop
|
|
362
|
+
|
|
363
|
+
return wrapped_stop
|
|
364
|
+
|
|
365
|
+
def _wrap_callback(
|
|
366
|
+
self, callback: Callable[[RetryCallState], Any] | None
|
|
367
|
+
) -> Callable[[RetryCallState], Any] | None:
|
|
368
|
+
if callback is None:
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
def wrapped_callback(retry_state: RetryCallState) -> Any:
|
|
372
|
+
return callback(self._translate_state(retry_state))
|
|
373
|
+
|
|
374
|
+
return wrapped_callback
|
|
375
|
+
|
|
376
|
+
def _wrap_retry_error_callback(self) -> Callable[[RetryCallState], Any] | None:
|
|
377
|
+
if self._user_retry_error_callback is None:
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
def wrapped_retry_error_callback(retry_state: RetryCallState) -> Any:
|
|
381
|
+
self._persist_attempt(retry_state, exhausted=True)
|
|
382
|
+
return self._user_retry_error_callback(self._translate_state(retry_state))
|
|
383
|
+
|
|
384
|
+
return wrapped_retry_error_callback
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def statistics(self) -> dict[str, Any]:
|
|
388
|
+
return cast(dict[str, Any], self._retrying.statistics)
|
|
389
|
+
|
|
390
|
+
def begin(self) -> None:
|
|
391
|
+
self._retrying.begin()
|
|
392
|
+
|
|
393
|
+
def iter(self, retry_state: RetryCallState) -> Any:
|
|
394
|
+
return self._retrying.iter(retry_state=retry_state)
|
|
395
|
+
|
|
396
|
+
def wraps(self, f: WrappedFn) -> WrappedFn:
|
|
397
|
+
@functools.wraps(
|
|
398
|
+
f, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__")
|
|
399
|
+
)
|
|
400
|
+
def wrapped_f(*args: Any, **kwargs: Any) -> Any:
|
|
401
|
+
copy = self.copy()
|
|
402
|
+
wrapped_f.statistics = copy.statistics # type: ignore[attr-defined]
|
|
403
|
+
return cast(Any, copy)(f, *args, **kwargs)
|
|
404
|
+
|
|
405
|
+
def retry_with(*args: Any, **kwargs: Any) -> WrappedFn:
|
|
406
|
+
return self.copy(*args, **kwargs).wraps(f)
|
|
407
|
+
|
|
408
|
+
wrapped_f.retry = self # type: ignore[attr-defined]
|
|
409
|
+
wrapped_f.retry_with = retry_with # type: ignore[attr-defined]
|
|
410
|
+
wrapped_f.statistics = {} # type: ignore[attr-defined]
|
|
411
|
+
return cast(WrappedFn, wrapped_f)
|
|
412
|
+
|
|
413
|
+
def copy(self, **kwargs: Any) -> "_PersistentMixin":
|
|
414
|
+
store = kwargs.pop("store", self._store)
|
|
415
|
+
store_path = kwargs.pop("store_path", _UNSET)
|
|
416
|
+
key = kwargs.pop("key", self.key)
|
|
417
|
+
key_fn = kwargs.pop("key_fn", self.key_fn)
|
|
418
|
+
clear_on_success = kwargs.pop("clear_on_success", self.clear_on_success)
|
|
419
|
+
max_tries = kwargs.pop("max_tries", _UNSET)
|
|
420
|
+
tenacity_kwargs = dict(self._tenacity_kwargs)
|
|
421
|
+
tenacity_kwargs.update(kwargs)
|
|
422
|
+
if max_tries is not _UNSET:
|
|
423
|
+
_ = tenacity_kwargs.pop("stop", None)
|
|
424
|
+
|
|
425
|
+
return self.__class__(
|
|
426
|
+
store=cast(AttemptStore | None, store),
|
|
427
|
+
store_path=self._store_path
|
|
428
|
+
if store_path is _UNSET
|
|
429
|
+
else cast(str | Path | None, store_path),
|
|
430
|
+
key=cast(str | None, key),
|
|
431
|
+
key_fn=cast(
|
|
432
|
+
Callable[[Callable[..., Any], tuple[Any, ...], dict[str, Any]], str]
|
|
433
|
+
| None,
|
|
434
|
+
key_fn,
|
|
435
|
+
),
|
|
436
|
+
clear_on_success=cast(bool, clear_on_success),
|
|
437
|
+
**({} if max_tries is _UNSET else {"max_tries": max_tries}),
|
|
438
|
+
**tenacity_kwargs,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class PersistentRetrying(_PersistentMixin):
|
|
443
|
+
def __init__(
|
|
444
|
+
self,
|
|
445
|
+
*,
|
|
446
|
+
store: AttemptStore | None = None,
|
|
447
|
+
store_path: str | Path | None = None,
|
|
448
|
+
key: str | None = None,
|
|
449
|
+
key_fn: Callable[[Callable[..., Any], tuple[Any, ...], dict[str, Any]], str]
|
|
450
|
+
| None = None,
|
|
451
|
+
clear_on_success: bool = True,
|
|
452
|
+
max_tries: int | None = None,
|
|
453
|
+
**tenacity_kwargs: Any,
|
|
454
|
+
) -> None:
|
|
455
|
+
tenacity_kwargs.setdefault("sleep", tenacity_sleep)
|
|
456
|
+
tenacity_kwargs.setdefault(
|
|
457
|
+
"wait", wait_exponential(multiplier=1, min=2, max=10)
|
|
458
|
+
)
|
|
459
|
+
tenacity_kwargs.setdefault("retry", retry_if_exception_type())
|
|
460
|
+
tenacity_kwargs.setdefault("before", before_nothing)
|
|
461
|
+
tenacity_kwargs.setdefault("after", after_nothing)
|
|
462
|
+
tenacity_kwargs.setdefault("reraise", True)
|
|
463
|
+
super().__init__(
|
|
464
|
+
base_cls=Retrying,
|
|
465
|
+
store=store,
|
|
466
|
+
store_path=store_path,
|
|
467
|
+
key=key,
|
|
468
|
+
key_fn=key_fn,
|
|
469
|
+
clear_on_success=clear_on_success,
|
|
470
|
+
max_tries=max_tries,
|
|
471
|
+
**tenacity_kwargs,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
def __call__(self, fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
|
475
|
+
self.begin()
|
|
476
|
+
context = self._load_context(fn, args, kwargs)
|
|
477
|
+
if context.record.exhausted:
|
|
478
|
+
raise PersistentRetryExhausted(context.key, context.record.attempts)
|
|
479
|
+
|
|
480
|
+
self._local.persistent_context = context
|
|
481
|
+
try:
|
|
482
|
+
retry_state = RetryCallState(
|
|
483
|
+
retry_object=cast(Any, self._retrying), fn=fn, args=args, kwargs=kwargs
|
|
484
|
+
)
|
|
485
|
+
while True:
|
|
486
|
+
do = self.iter(retry_state=retry_state)
|
|
487
|
+
if isinstance(do, DoAttempt):
|
|
488
|
+
try:
|
|
489
|
+
result = fn(*args, **kwargs)
|
|
490
|
+
except BaseException: # noqa: B902
|
|
491
|
+
exc_info = sys.exc_info()
|
|
492
|
+
if exc_info[0] is None:
|
|
493
|
+
raise
|
|
494
|
+
retry_state.set_exception(
|
|
495
|
+
cast(
|
|
496
|
+
"tuple[type[BaseException], BaseException, Any]",
|
|
497
|
+
exc_info,
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
else:
|
|
501
|
+
retry_state.set_result(result)
|
|
502
|
+
elif isinstance(do, DoSleep):
|
|
503
|
+
retry_state.prepare_for_next_attempt()
|
|
504
|
+
cast(Any, self._retrying).sleep(do)
|
|
505
|
+
else:
|
|
506
|
+
self._clear_if_success(retry_state)
|
|
507
|
+
return do
|
|
508
|
+
finally:
|
|
509
|
+
if hasattr(self._local, "persistent_context"):
|
|
510
|
+
del self._local.persistent_context
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class PersistentAsyncRetrying(_PersistentMixin):
|
|
514
|
+
def __init__(
|
|
515
|
+
self,
|
|
516
|
+
*,
|
|
517
|
+
store: AttemptStore | None = None,
|
|
518
|
+
store_path: str | Path | None = None,
|
|
519
|
+
key: str | None = None,
|
|
520
|
+
key_fn: Callable[[Callable[..., Any], tuple[Any, ...], dict[str, Any]], str]
|
|
521
|
+
| None = None,
|
|
522
|
+
clear_on_success: bool = True,
|
|
523
|
+
max_tries: int | None = None,
|
|
524
|
+
**tenacity_kwargs: Any,
|
|
525
|
+
) -> None:
|
|
526
|
+
tenacity_kwargs.setdefault("sleep", _portable_async_sleep)
|
|
527
|
+
tenacity_kwargs.setdefault(
|
|
528
|
+
"wait", wait_exponential(multiplier=1, min=2, max=10)
|
|
529
|
+
)
|
|
530
|
+
tenacity_kwargs.setdefault("retry", retry_if_exception_type())
|
|
531
|
+
tenacity_kwargs.setdefault("before", before_nothing)
|
|
532
|
+
tenacity_kwargs.setdefault("after", after_nothing)
|
|
533
|
+
tenacity_kwargs.setdefault("reraise", True)
|
|
534
|
+
super().__init__(
|
|
535
|
+
base_cls=AsyncRetrying,
|
|
536
|
+
store=store,
|
|
537
|
+
store_path=store_path,
|
|
538
|
+
key=key,
|
|
539
|
+
key_fn=key_fn,
|
|
540
|
+
clear_on_success=clear_on_success,
|
|
541
|
+
max_tries=max_tries,
|
|
542
|
+
**tenacity_kwargs,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def _wrap_retry(self) -> Callable[[RetryCallState], Any]:
|
|
546
|
+
if _utils.is_coroutine_callable(self._user_retry):
|
|
547
|
+
|
|
548
|
+
async def async_wrapped_retry(retry_state: RetryCallState) -> bool:
|
|
549
|
+
return bool(await self._user_retry(self._translate_state(retry_state)))
|
|
550
|
+
|
|
551
|
+
return async_wrapped_retry
|
|
552
|
+
|
|
553
|
+
def wrapped_retry(retry_state: RetryCallState) -> bool:
|
|
554
|
+
return bool(self._user_retry(self._translate_state(retry_state)))
|
|
555
|
+
|
|
556
|
+
return wrapped_retry
|
|
557
|
+
|
|
558
|
+
def _wrap_wait(self) -> Callable[[RetryCallState], Any]:
|
|
559
|
+
if self._user_wait is None:
|
|
560
|
+
|
|
561
|
+
async def empty_wait(retry_state: RetryCallState) -> float:
|
|
562
|
+
return 0.0
|
|
563
|
+
|
|
564
|
+
return empty_wait
|
|
565
|
+
|
|
566
|
+
if _utils.is_coroutine_callable(self._user_wait):
|
|
567
|
+
|
|
568
|
+
async def async_wrapped_wait(retry_state: RetryCallState) -> float:
|
|
569
|
+
return float(await self._user_wait(self._translate_state(retry_state)))
|
|
570
|
+
|
|
571
|
+
return async_wrapped_wait
|
|
572
|
+
|
|
573
|
+
def wrapped_wait(retry_state: RetryCallState) -> float:
|
|
574
|
+
return float(self._user_wait(self._translate_state(retry_state)))
|
|
575
|
+
|
|
576
|
+
return wrapped_wait
|
|
577
|
+
|
|
578
|
+
def _wrap_stop(self) -> Callable[[RetryCallState], Any]:
|
|
579
|
+
if _utils.is_coroutine_callable(self._user_stop):
|
|
580
|
+
|
|
581
|
+
async def async_wrapped_stop(retry_state: RetryCallState) -> bool:
|
|
582
|
+
should_stop = bool(
|
|
583
|
+
await self._user_stop(self._translate_state(retry_state))
|
|
584
|
+
)
|
|
585
|
+
self._persist_attempt(retry_state, exhausted=should_stop)
|
|
586
|
+
return should_stop
|
|
587
|
+
|
|
588
|
+
return async_wrapped_stop
|
|
589
|
+
|
|
590
|
+
def wrapped_stop(retry_state: RetryCallState) -> bool:
|
|
591
|
+
should_stop = bool(self._user_stop(self._translate_state(retry_state)))
|
|
592
|
+
self._persist_attempt(retry_state, exhausted=should_stop)
|
|
593
|
+
return should_stop
|
|
594
|
+
|
|
595
|
+
return wrapped_stop
|
|
596
|
+
|
|
597
|
+
def _wrap_callback(
|
|
598
|
+
self, callback: Callable[[RetryCallState], Any] | None
|
|
599
|
+
) -> Callable[[RetryCallState], Any] | None:
|
|
600
|
+
if callback is None:
|
|
601
|
+
return None
|
|
602
|
+
if _utils.is_coroutine_callable(callback):
|
|
603
|
+
|
|
604
|
+
async def async_wrapped_callback(retry_state: RetryCallState) -> Any:
|
|
605
|
+
return await callback(self._translate_state(retry_state))
|
|
606
|
+
|
|
607
|
+
return async_wrapped_callback
|
|
608
|
+
|
|
609
|
+
def wrapped_callback(retry_state: RetryCallState) -> Any:
|
|
610
|
+
return callback(self._translate_state(retry_state))
|
|
611
|
+
|
|
612
|
+
return wrapped_callback
|
|
613
|
+
|
|
614
|
+
def _wrap_retry_error_callback(self) -> Callable[[RetryCallState], Any] | None:
|
|
615
|
+
if self._user_retry_error_callback is None:
|
|
616
|
+
return None
|
|
617
|
+
if _utils.is_coroutine_callable(self._user_retry_error_callback):
|
|
618
|
+
|
|
619
|
+
async def async_wrapped_retry_error_callback(
|
|
620
|
+
retry_state: RetryCallState,
|
|
621
|
+
) -> Any:
|
|
622
|
+
self._persist_attempt(retry_state, exhausted=True)
|
|
623
|
+
return await self._user_retry_error_callback(
|
|
624
|
+
self._translate_state(retry_state)
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
return async_wrapped_retry_error_callback
|
|
628
|
+
|
|
629
|
+
def wrapped_retry_error_callback(retry_state: RetryCallState) -> Any:
|
|
630
|
+
self._persist_attempt(retry_state, exhausted=True)
|
|
631
|
+
return self._user_retry_error_callback(self._translate_state(retry_state))
|
|
632
|
+
|
|
633
|
+
return wrapped_retry_error_callback
|
|
634
|
+
|
|
635
|
+
async def __call__(self, fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
|
636
|
+
self.begin()
|
|
637
|
+
context = self._load_context(fn, args, kwargs)
|
|
638
|
+
if context.record.exhausted:
|
|
639
|
+
raise PersistentRetryExhausted(context.key, context.record.attempts)
|
|
640
|
+
|
|
641
|
+
self._local.persistent_context = context
|
|
642
|
+
try:
|
|
643
|
+
retry_state = RetryCallState(
|
|
644
|
+
retry_object=cast(Any, self._retrying), fn=fn, args=args, kwargs=kwargs
|
|
645
|
+
)
|
|
646
|
+
while True:
|
|
647
|
+
do = await cast(AsyncRetrying, self._retrying).iter(
|
|
648
|
+
retry_state=retry_state
|
|
649
|
+
)
|
|
650
|
+
if isinstance(do, DoAttempt):
|
|
651
|
+
try:
|
|
652
|
+
result = fn(*args, **kwargs)
|
|
653
|
+
if inspect.isawaitable(result):
|
|
654
|
+
result = await result
|
|
655
|
+
except BaseException: # noqa: B902
|
|
656
|
+
exc_info = sys.exc_info()
|
|
657
|
+
if exc_info[0] is None:
|
|
658
|
+
raise
|
|
659
|
+
retry_state.set_exception(
|
|
660
|
+
cast(
|
|
661
|
+
"tuple[type[BaseException], BaseException, Any]",
|
|
662
|
+
exc_info,
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
else:
|
|
666
|
+
retry_state.set_result(result)
|
|
667
|
+
elif isinstance(do, DoSleep):
|
|
668
|
+
retry_state.prepare_for_next_attempt()
|
|
669
|
+
await cast(Any, self._retrying).sleep(do)
|
|
670
|
+
else:
|
|
671
|
+
self._clear_if_success(retry_state)
|
|
672
|
+
return do
|
|
673
|
+
finally:
|
|
674
|
+
if hasattr(self._local, "persistent_context"):
|
|
675
|
+
del self._local.persistent_context
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def persistent_retry(**kwargs: Any) -> Callable[[WrappedFn], WrappedFn]:
|
|
679
|
+
return cast(Callable[[WrappedFn], WrappedFn], PersistentRetrying(**kwargs).wraps)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def persistent_async_retry(**kwargs: Any) -> Callable[[WrappedFn], WrappedFn]:
|
|
683
|
+
return cast(
|
|
684
|
+
Callable[[WrappedFn], WrappedFn], PersistentAsyncRetrying(**kwargs).wraps
|
|
685
|
+
)
|