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.
@@ -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
+ )