backon 3.3.0__tar.gz → 3.4.0__tar.gz

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.
Files changed (30) hide show
  1. {backon-3.3.0 → backon-3.4.0}/PKG-INFO +1 -1
  2. {backon-3.3.0 → backon-3.4.0}/backon/__init__.py +2 -1
  3. {backon-3.3.0 → backon-3.4.0}/backon/_decorator.py +18 -0
  4. {backon-3.3.0 → backon-3.4.0}/backon/_retry.py +163 -52
  5. {backon-3.3.0 → backon-3.4.0}/backon/_state.py +42 -0
  6. {backon-3.3.0 → backon-3.4.0}/pyproject.toml +1 -1
  7. {backon-3.3.0 → backon-3.4.0}/tests/test_backon.py +1 -0
  8. backon-3.4.0/tests/test_retry_call_state.py +292 -0
  9. {backon-3.3.0 → backon-3.4.0}/LICENSE +0 -0
  10. {backon-3.3.0 → backon-3.4.0}/README.md +0 -0
  11. {backon-3.3.0 → backon-3.4.0}/backon/_async.py +0 -0
  12. {backon-3.3.0 → backon-3.4.0}/backon/_common.py +0 -0
  13. {backon-3.3.0 → backon-3.4.0}/backon/_conditions.py +0 -0
  14. {backon-3.3.0 → backon-3.4.0}/backon/_jitter.py +0 -0
  15. {backon-3.3.0 → backon-3.4.0}/backon/_sync.py +0 -0
  16. {backon-3.3.0 → backon-3.4.0}/backon/_typing.py +0 -0
  17. {backon-3.3.0 → backon-3.4.0}/backon/_wait_gen.py +0 -0
  18. {backon-3.3.0 → backon-3.4.0}/backon/py.typed +0 -0
  19. {backon-3.3.0 → backon-3.4.0}/backon/types.py +0 -0
  20. {backon-3.3.0 → backon-3.4.0}/tests/__init__.py +0 -0
  21. {backon-3.3.0 → backon-3.4.0}/tests/test_advanced_features.py +0 -0
  22. {backon-3.3.0 → backon-3.4.0}/tests/test_backon_async.py +0 -0
  23. {backon-3.3.0 → backon-3.4.0}/tests/test_backon_predicate.py +0 -0
  24. {backon-3.3.0 → backon-3.4.0}/tests/test_backon_sync.py +0 -0
  25. {backon-3.3.0 → backon-3.4.0}/tests/test_features.py +0 -0
  26. {backon-3.3.0 → backon-3.4.0}/tests/test_jitter.py +0 -0
  27. {backon-3.3.0 → backon-3.4.0}/tests/test_retry.py +0 -0
  28. {backon-3.3.0 → backon-3.4.0}/tests/test_types.py +0 -0
  29. {backon-3.3.0 → backon-3.4.0}/tests/test_typing.py +0 -0
  30. {backon-3.3.0 → backon-3.4.0}/tests/test_wait_gen.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: backon
3
- Version: 3.3.0
3
+ Version: 3.4.0
4
4
  Summary: Function decoration for backoff and retry
5
5
  Keywords: retry,backoff,decorators
6
6
  Author-Email: Llucs <c307lucas@gmail.com>
@@ -25,7 +25,7 @@ from backon._conditions import (
25
25
  from backon._decorator import on_exception, on_predicate
26
26
  from backon._jitter import full_jitter, random_jitter
27
27
  from backon._retry import Retrying, retry, sleep_using_event
28
- from backon._state import RetryError, RetryState, TryAgain
28
+ from backon._state import RetryCallState, RetryError, RetryState, TryAgain
29
29
  from backon._wait_gen import (
30
30
  constant,
31
31
  decay,
@@ -66,6 +66,7 @@ __all__ = [
66
66
  "TryAgain",
67
67
  "RetryError",
68
68
  "RetryState",
69
+ "RetryCallState",
69
70
  "Stop",
70
71
  "RetryCondition",
71
72
  "stop_after_attempt",
@@ -54,10 +54,13 @@ def on_predicate(
54
54
  retry_error_callback: Callable[[dict], Any] | None = None,
55
55
  raise_on_giveup: bool = True,
56
56
  sleep: Callable[[float], Any] | None = None,
57
+ before: _Handler | Iterable[_Handler] | None = None,
58
+ after: _Handler | Iterable[_Handler] | None = None,
57
59
  **wait_gen_kwargs: Any,
58
60
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
59
61
  def decorate(target: Callable[P, R]) -> Callable[P, R]:
60
62
  nonlocal logger, on_success, on_backoff, on_giveup, on_attempt, before_sleep
63
+ nonlocal before, after
61
64
 
62
65
  logger = _prepare_logger(logger)
63
66
  on_success = _config_handlers(on_success)
@@ -75,6 +78,8 @@ def on_predicate(
75
78
  )
76
79
  on_attempt = _config_handlers(on_attempt)
77
80
  before_sleep = _config_handlers(before_sleep)
81
+ before = _config_handlers(before)
82
+ after = _config_handlers(after)
78
83
 
79
84
  condition: RetryCondition = retry_if_result(predicate)
80
85
 
@@ -107,6 +112,8 @@ def on_predicate(
107
112
  retry_error_callback=retry_error_callback,
108
113
  raise_on_giveup=raise_on_giveup,
109
114
  wait_gen_kwargs=wait_gen_kwargs,
115
+ before=before,
116
+ after=after,
110
117
  ),
111
118
  )
112
119
 
@@ -137,6 +144,8 @@ def on_predicate(
137
144
  retry_error_callback=retry_error_callback,
138
145
  raise_on_giveup=raise_on_giveup,
139
146
  wait_gen_kwargs=wait_gen_kwargs,
147
+ before=before,
148
+ after=after,
140
149
  ),
141
150
  )
142
151
 
@@ -164,10 +173,13 @@ def on_exception(
164
173
  backoff_log_level: int = logging.INFO,
165
174
  giveup_log_level: int = logging.ERROR,
166
175
  sleep: Callable[[float], Any] | None = None,
176
+ before: _Handler | Iterable[_Handler] | None = None,
177
+ after: _Handler | Iterable[_Handler] | None = None,
167
178
  **wait_gen_kwargs: Any,
168
179
  ) -> Callable[[Callable[P, R]], Callable[P, R]]:
169
180
  def decorate(target: Callable[P, R]) -> Callable[P, R]:
170
181
  nonlocal logger, on_success, on_backoff, on_giveup, on_attempt, before_sleep
182
+ nonlocal before, after
171
183
 
172
184
  logger = _prepare_logger(logger)
173
185
  on_success = _config_handlers(on_success)
@@ -185,6 +197,8 @@ def on_exception(
185
197
  )
186
198
  on_attempt = _config_handlers(on_attempt)
187
199
  before_sleep = _config_handlers(before_sleep)
200
+ before = _config_handlers(before)
201
+ after = _config_handlers(after)
188
202
 
189
203
  exc_types: tuple[type[Exception], ...]
190
204
  if isinstance(exception, type):
@@ -233,6 +247,8 @@ def on_exception(
233
247
  retry_error_callback=retry_error_callback,
234
248
  raise_on_giveup=raise_on_giveup,
235
249
  wait_gen_kwargs=wait_gen_kwargs,
250
+ before=before,
251
+ after=after,
236
252
  ),
237
253
  )
238
254
 
@@ -263,6 +279,8 @@ def on_exception(
263
279
  retry_error_callback=retry_error_callback,
264
280
  raise_on_giveup=raise_on_giveup,
265
281
  wait_gen_kwargs=wait_gen_kwargs,
282
+ before=before,
283
+ after=after,
266
284
  ),
267
285
  )
268
286
 
@@ -30,7 +30,7 @@ from backon._conditions import (
30
30
  stop_never,
31
31
  )
32
32
  from backon._jitter import full_jitter
33
- from backon._state import Attempt, RetryState, TryAgain
33
+ from backon._state import Attempt, RetryCallState, RetryState, TryAgain
34
34
  from backon._typing import (
35
35
  _Handler,
36
36
  _Jitterer,
@@ -123,25 +123,37 @@ def _retry_loop_sync(
123
123
  raise_on_giveup,
124
124
  max_time,
125
125
  wait_gen_kwargs,
126
+ before=None,
127
+ after=None,
128
+ _holder=None,
126
129
  ):
127
130
  state = RetryState(target=target)
128
131
  start_time = _now()
129
132
  state.start_time = start_time
133
+ call_state = RetryCallState(fn=target, start_time=start_time)
134
+ if _holder is not None:
135
+ _holder["state"] = state
136
+ _holder["call_state"] = call_state
130
137
  wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
131
138
 
132
139
  while True:
133
140
  state.tries += 1
134
141
  state.elapsed = _now() - start_time
142
+ call_state.attempt_number = state.tries
135
143
  outcome = Attempt(tries=state.tries, elapsed=state.elapsed)
136
144
 
137
145
  _call_hdlrs(on_attempt, state.to_details())
138
146
 
147
+ _call_hdlrs(before, state.to_details())
148
+
139
149
  try:
140
150
  ret = target()
141
151
  except TryAgain:
142
152
  outcome.exception = None
143
153
  outcome.value = None
144
154
  state.outcome = outcome
155
+ call_state.outcome = outcome
156
+ call_state.outcome_timestamp = _now()
145
157
  try:
146
158
  value = wait.send(None)
147
159
  if jitter is not None:
@@ -166,6 +178,10 @@ def _retry_loop_sync(
166
178
  outcome.value = None
167
179
  state.outcome = outcome
168
180
  state.idle_for += state.elapsed
181
+ call_state.outcome = outcome
182
+ call_state.outcome_timestamp = _now()
183
+ call_state.idle_for += call_state.elapsed
184
+ _call_hdlrs(after, state.to_details())
169
185
 
170
186
  if not condition(state):
171
187
  details = state.to_details()
@@ -203,6 +219,7 @@ def _retry_loop_sync(
203
219
  raise exc from None
204
220
  return None
205
221
 
222
+ call_state.upcoming_sleep = seconds
206
223
  details = state.to_details()
207
224
  details["wait"] = seconds
208
225
  details["exception"] = exc
@@ -210,10 +227,14 @@ def _retry_loop_sync(
210
227
  _call_hdlrs(on_backoff, details)
211
228
  if seconds > 0:
212
229
  sleep(seconds)
230
+ call_state.idle_for = call_state.idle_for + seconds
213
231
  else:
214
232
  outcome.value = ret
215
233
  outcome.exception = None
216
234
  state.outcome = outcome
235
+ call_state.outcome = outcome
236
+ call_state.outcome_timestamp = _now()
237
+ _call_hdlrs(after, state.to_details())
217
238
 
218
239
  if condition(state):
219
240
  if stop(state):
@@ -236,6 +257,7 @@ def _retry_loop_sync(
236
257
  _call_hdlrs(on_giveup, details)
237
258
  return ret
238
259
 
260
+ call_state.upcoming_sleep = seconds
239
261
  details = state.to_details()
240
262
  details["wait"] = seconds
241
263
  details["value"] = ret
@@ -243,6 +265,7 @@ def _retry_loop_sync(
243
265
  _call_hdlrs(on_backoff, details)
244
266
  if seconds > 0:
245
267
  sleep(seconds)
268
+ call_state.idle_for = call_state.idle_for + seconds
246
269
  else:
247
270
  details = state.to_details()
248
271
  details["value"] = ret
@@ -266,25 +289,37 @@ async def _retry_loop_async(
266
289
  raise_on_giveup,
267
290
  max_time,
268
291
  wait_gen_kwargs,
292
+ before=None,
293
+ after=None,
294
+ _holder=None,
269
295
  ):
270
296
  state = RetryState(target=target)
271
297
  start_time = _now()
272
298
  state.start_time = start_time
299
+ call_state = RetryCallState(fn=target, start_time=start_time)
300
+ if _holder is not None:
301
+ _holder["state"] = state
302
+ _holder["call_state"] = call_state
273
303
  wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
274
304
 
275
305
  while True:
276
306
  state.tries += 1
277
307
  state.elapsed = _now() - start_time
308
+ call_state.attempt_number = state.tries
278
309
  outcome = Attempt(tries=state.tries, elapsed=state.elapsed)
279
310
 
280
311
  await _call_hdlrs_async(on_attempt, state.to_details())
281
312
 
313
+ _call_hdlrs(before, state.to_details())
314
+
282
315
  try:
283
316
  ret = await target()
284
317
  except TryAgain:
285
318
  outcome.exception = None
286
319
  outcome.value = None
287
320
  state.outcome = outcome
321
+ call_state.outcome = outcome
322
+ call_state.outcome_timestamp = _now()
288
323
  try:
289
324
  value = wait.send(None)
290
325
  if jitter is not None:
@@ -309,6 +344,10 @@ async def _retry_loop_async(
309
344
  outcome.value = None
310
345
  state.outcome = outcome
311
346
  state.idle_for += state.elapsed
347
+ call_state.outcome = outcome
348
+ call_state.outcome_timestamp = _now()
349
+ call_state.idle_for += call_state.elapsed
350
+ await _call_hdlrs_async(after, state.to_details())
312
351
 
313
352
  if not condition(state):
314
353
  details = state.to_details()
@@ -346,6 +385,7 @@ async def _retry_loop_async(
346
385
  raise exc from None
347
386
  return None
348
387
 
388
+ call_state.upcoming_sleep = seconds
349
389
  details = state.to_details()
350
390
  details["wait"] = seconds
351
391
  details["exception"] = exc
@@ -353,10 +393,14 @@ async def _retry_loop_async(
353
393
  await _call_hdlrs_async(on_backoff, details)
354
394
  if seconds > 0:
355
395
  await sleep(seconds)
396
+ call_state.idle_for = call_state.idle_for + seconds
356
397
  else:
357
398
  outcome.value = ret
358
399
  outcome.exception = None
359
400
  state.outcome = outcome
401
+ call_state.outcome = outcome
402
+ call_state.outcome_timestamp = _now()
403
+ await _call_hdlrs_async(after, state.to_details())
360
404
 
361
405
  if condition(state):
362
406
  if stop(state):
@@ -379,6 +423,7 @@ async def _retry_loop_async(
379
423
  await _call_hdlrs_async(on_giveup, details)
380
424
  return ret
381
425
 
426
+ call_state.upcoming_sleep = seconds
382
427
  details = state.to_details()
383
428
  details["wait"] = seconds
384
429
  details["value"] = ret
@@ -386,6 +431,7 @@ async def _retry_loop_async(
386
431
  await _call_hdlrs_async(on_backoff, details)
387
432
  if seconds > 0:
388
433
  await sleep(seconds)
434
+ call_state.idle_for = call_state.idle_for + seconds
389
435
  else:
390
436
  details = state.to_details()
391
437
  details["value"] = ret
@@ -411,6 +457,9 @@ def _retry_sync_inner(
411
457
  max_time=None,
412
458
  wait_gen_kwargs=None,
413
459
  max_tries=None,
460
+ before=None,
461
+ after=None,
462
+ _holder=None,
414
463
  ):
415
464
  if not is_enabled():
416
465
  return target()
@@ -437,6 +486,9 @@ def _retry_sync_inner(
437
486
  raise_on_giveup,
438
487
  max_time,
439
488
  wait_gen_kwargs,
489
+ before=before,
490
+ after=after,
491
+ _holder=_holder,
440
492
  )
441
493
 
442
494
 
@@ -458,6 +510,9 @@ async def _retry_async_inner(
458
510
  max_time=None,
459
511
  wait_gen_kwargs=None,
460
512
  max_tries=None,
513
+ before=None,
514
+ after=None,
515
+ _holder=None,
461
516
  ):
462
517
  if not is_enabled():
463
518
  return await target()
@@ -484,6 +539,9 @@ async def _retry_async_inner(
484
539
  raise_on_giveup,
485
540
  max_time,
486
541
  wait_gen_kwargs,
542
+ before=before,
543
+ after=after,
544
+ _holder=_holder,
487
545
  )
488
546
 
489
547
 
@@ -511,6 +569,9 @@ def _retry_sync(
511
569
  giveup_log_level: int = logging.ERROR,
512
570
  sleep: Callable[[float], Any] | None = None,
513
571
  wait_gen_kwargs: dict | None = None,
572
+ before: _Handler | Iterable[_Handler] | None = None,
573
+ after: _Handler | Iterable[_Handler] | None = None,
574
+ _holder: dict | None = None,
514
575
  ) -> Any:
515
576
  if wait_gen_kwargs is None:
516
577
  wait_gen_kwargs = {}
@@ -533,6 +594,8 @@ def _retry_sync(
533
594
  )
534
595
  on_attempt = _config_handlers(on_attempt)
535
596
  before_sleep = _config_handlers(before_sleep)
597
+ before = _config_handlers(before)
598
+ after = _config_handlers(after)
536
599
 
537
600
  if condition is None:
538
601
  condition = _make_default_condition(exception, giveup, predicate)
@@ -557,6 +620,9 @@ def _retry_sync(
557
620
  raise_on_giveup,
558
621
  max_time,
559
622
  wait_gen_kwargs,
623
+ before=before,
624
+ after=after,
625
+ _holder=_holder,
560
626
  )
561
627
 
562
628
 
@@ -584,6 +650,9 @@ async def _retry_async(
584
650
  giveup_log_level: int = logging.ERROR,
585
651
  sleep: Callable[[float], Any] | None = None,
586
652
  wait_gen_kwargs: dict | None = None,
653
+ before: _Handler | Iterable[_Handler] | None = None,
654
+ after: _Handler | Iterable[_Handler] | None = None,
655
+ _holder: dict | None = None,
587
656
  ) -> Any:
588
657
  if wait_gen_kwargs is None:
589
658
  wait_gen_kwargs = {}
@@ -606,6 +675,8 @@ async def _retry_async(
606
675
  )
607
676
  on_attempt = _config_handlers(on_attempt)
608
677
  before_sleep = _config_handlers(before_sleep)
678
+ before = _config_handlers(before)
679
+ after = _config_handlers(after)
609
680
 
610
681
  if condition is None:
611
682
  condition = _make_default_condition(exception, giveup, predicate)
@@ -630,6 +701,9 @@ async def _retry_async(
630
701
  raise_on_giveup,
631
702
  max_time,
632
703
  wait_gen_kwargs,
704
+ before=before,
705
+ after=after,
706
+ _holder=_holder,
633
707
  )
634
708
 
635
709
 
@@ -657,6 +731,8 @@ def retry(
657
731
  giveup_log_level: int = logging.ERROR,
658
732
  sleep: Callable[[float], Any] | None = None,
659
733
  name: str = "",
734
+ before: _Handler | Iterable[_Handler] | None = None,
735
+ after: _Handler | Iterable[_Handler] | None = None,
660
736
  **wait_gen_kwargs: Any,
661
737
  ) -> Any:
662
738
  if inspect.iscoroutinefunction(target):
@@ -683,6 +759,8 @@ def retry(
683
759
  giveup_log_level=giveup_log_level,
684
760
  sleep=sleep,
685
761
  wait_gen_kwargs=wait_gen_kwargs,
762
+ before=before,
763
+ after=after,
686
764
  )
687
765
  return _retry_sync(
688
766
  target,
@@ -707,6 +785,8 @@ def retry(
707
785
  giveup_log_level=giveup_log_level,
708
786
  sleep=sleep,
709
787
  wait_gen_kwargs=wait_gen_kwargs,
788
+ before=before,
789
+ after=after,
710
790
  )
711
791
 
712
792
 
@@ -763,6 +843,8 @@ class Retrying:
763
843
  sleep: Callable[[float], Any] | None = None,
764
844
  enabled: bool = True,
765
845
  name: str = "",
846
+ before: _Handler | Iterable[_Handler] | None = None,
847
+ after: _Handler | Iterable[_Handler] | None = None,
766
848
  **wait_gen_kwargs: Any,
767
849
  ):
768
850
  self._wait_gen = wait_gen
@@ -787,14 +869,23 @@ class Retrying:
787
869
  self._sleep = sleep
788
870
  self._enabled = enabled
789
871
  self._name = name
872
+ self._before = before
873
+ self._after = after
790
874
  self._wait_gen_kwargs = wait_gen_kwargs
791
875
  self._state: RetryState | None = None
876
+ self._call_state: RetryCallState | None = None
792
877
 
793
878
  @property
794
879
  def statistics(self) -> dict:
795
- if self._state is None:
796
- return {}
797
- return self._state.statistics
880
+ if self._call_state is not None:
881
+ return self._call_state.statistics
882
+ if self._state is not None:
883
+ return self._state.statistics
884
+ return {}
885
+
886
+ @property
887
+ def call_state(self) -> RetryCallState | None:
888
+ return self._call_state
798
889
 
799
890
  @property
800
891
  def enabled(self) -> bool:
@@ -903,6 +994,8 @@ class Retrying:
903
994
  sleep=self._sleep,
904
995
  enabled=self._enabled,
905
996
  name=self._name,
997
+ before=self._before,
998
+ after=self._after,
906
999
  **self._wait_gen_kwargs,
907
1000
  )
908
1001
 
@@ -916,30 +1009,39 @@ class Retrying:
916
1009
  def wrapped():
917
1010
  return target(*args, **kwargs)
918
1011
 
919
- return _retry_sync(
920
- wrapped,
921
- self._wait_gen,
922
- predicate=self._predicate,
923
- exception=self._exception,
924
- max_tries=self._max_tries,
925
- max_time=self._max_time,
926
- jitter=self._jitter,
927
- giveup=self._giveup,
928
- condition=self._condition,
929
- stop=self._stop,
930
- on_success=self._on_success,
931
- on_backoff=self._on_backoff,
932
- on_giveup=self._on_giveup,
933
- on_attempt=self._on_attempt,
934
- before_sleep=self._before_sleep,
935
- retry_error_callback=self._retry_error_callback,
936
- raise_on_giveup=self._raise_on_giveup,
937
- logger=self._logger,
938
- backoff_log_level=self._backoff_log_level,
939
- giveup_log_level=self._giveup_log_level,
940
- sleep=self._sleep,
941
- wait_gen_kwargs=self._wait_gen_kwargs,
942
- )
1012
+ _holder: dict = {}
1013
+ try:
1014
+ result = _retry_sync(
1015
+ wrapped,
1016
+ self._wait_gen,
1017
+ predicate=self._predicate,
1018
+ exception=self._exception,
1019
+ max_tries=self._max_tries,
1020
+ max_time=self._max_time,
1021
+ jitter=self._jitter,
1022
+ giveup=self._giveup,
1023
+ condition=self._condition,
1024
+ stop=self._stop,
1025
+ on_success=self._on_success,
1026
+ on_backoff=self._on_backoff,
1027
+ on_giveup=self._on_giveup,
1028
+ on_attempt=self._on_attempt,
1029
+ before_sleep=self._before_sleep,
1030
+ retry_error_callback=self._retry_error_callback,
1031
+ raise_on_giveup=self._raise_on_giveup,
1032
+ logger=self._logger,
1033
+ backoff_log_level=self._backoff_log_level,
1034
+ giveup_log_level=self._giveup_log_level,
1035
+ sleep=self._sleep,
1036
+ wait_gen_kwargs=self._wait_gen_kwargs,
1037
+ before=self._before,
1038
+ after=self._after,
1039
+ _holder=_holder,
1040
+ )
1041
+ return result
1042
+ finally:
1043
+ self._state = _holder.get("state")
1044
+ self._call_state = _holder.get("call_state")
943
1045
 
944
1046
  async def async_call(
945
1047
  self, target: Callable[..., Any], *args: Any, **kwargs: Any
@@ -947,30 +1049,39 @@ class Retrying:
947
1049
  async def wrapped():
948
1050
  return await target(*args, **kwargs)
949
1051
 
950
- return await _retry_async(
951
- wrapped,
952
- self._wait_gen,
953
- predicate=self._predicate,
954
- exception=self._exception,
955
- max_tries=self._max_tries,
956
- max_time=self._max_time,
957
- jitter=self._jitter,
958
- giveup=self._giveup,
959
- condition=self._condition,
960
- stop=self._stop,
961
- on_success=self._on_success,
962
- on_backoff=self._on_backoff,
963
- on_giveup=self._on_giveup,
964
- on_attempt=self._on_attempt,
965
- before_sleep=self._before_sleep,
966
- retry_error_callback=self._retry_error_callback,
967
- raise_on_giveup=self._raise_on_giveup,
968
- logger=self._logger,
969
- backoff_log_level=self._backoff_log_level,
970
- giveup_log_level=self._giveup_log_level,
971
- sleep=self._sleep,
972
- wait_gen_kwargs=self._wait_gen_kwargs,
973
- )
1052
+ _holder: dict = {}
1053
+ try:
1054
+ result = await _retry_async(
1055
+ wrapped,
1056
+ self._wait_gen,
1057
+ predicate=self._predicate,
1058
+ exception=self._exception,
1059
+ max_tries=self._max_tries,
1060
+ max_time=self._max_time,
1061
+ jitter=self._jitter,
1062
+ giveup=self._giveup,
1063
+ condition=self._condition,
1064
+ stop=self._stop,
1065
+ on_success=self._on_success,
1066
+ on_backoff=self._on_backoff,
1067
+ on_giveup=self._on_giveup,
1068
+ on_attempt=self._on_attempt,
1069
+ before_sleep=self._before_sleep,
1070
+ retry_error_callback=self._retry_error_callback,
1071
+ raise_on_giveup=self._raise_on_giveup,
1072
+ logger=self._logger,
1073
+ backoff_log_level=self._backoff_log_level,
1074
+ giveup_log_level=self._giveup_log_level,
1075
+ sleep=self._sleep,
1076
+ wait_gen_kwargs=self._wait_gen_kwargs,
1077
+ before=self._before,
1078
+ after=self._after,
1079
+ _holder=_holder,
1080
+ )
1081
+ return result
1082
+ finally:
1083
+ self._state = _holder.get("state")
1084
+ self._call_state = _holder.get("call_state")
974
1085
 
975
1086
 
976
1087
  def sleep_using_event(event) -> Callable[[float], None]:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import threading
4
+ import time as time_module
4
5
  from collections.abc import Callable
5
6
  from dataclasses import dataclass, field
6
7
  from typing import Any
@@ -73,3 +74,44 @@ class RetryState:
73
74
  "tries": self.tries,
74
75
  "elapsed": self.elapsed,
75
76
  }
77
+
78
+
79
+ @dataclass
80
+ class RetryCallState:
81
+ fn: Callable[..., Any] | None = None
82
+ args: tuple = ()
83
+ kwargs: dict = field(default_factory=dict)
84
+ attempt_number: int = 1
85
+ outcome: Attempt | None = None
86
+ outcome_timestamp: float | None = None
87
+ start_time: float = 0.0
88
+ idle_for: float = 0.0
89
+ upcoming_sleep: float = 0.0
90
+
91
+ @property
92
+ def elapsed(self) -> float:
93
+ if self.start_time == 0:
94
+ return 0.0
95
+ return time_module.monotonic() - self.start_time
96
+
97
+ @property
98
+ def seconds_since_start(self) -> float:
99
+ return self.elapsed
100
+
101
+ @property
102
+ def statistics(self) -> dict[str, Any]:
103
+ return {
104
+ "start_time": self.start_time,
105
+ "attempt_number": self.attempt_number,
106
+ "idle_for": self.idle_for,
107
+ "elapsed": self.elapsed,
108
+ }
109
+
110
+ def to_details(self) -> dict[str, Any]:
111
+ return {
112
+ "target": self.fn,
113
+ "args": self.args,
114
+ "kwargs": self.kwargs,
115
+ "tries": self.attempt_number,
116
+ "elapsed": self.elapsed,
117
+ }
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "backon"
9
- version = "3.3.0"
9
+ version = "3.4.0"
10
10
  description = "Function decoration for backoff and retry"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.10"
@@ -50,6 +50,7 @@ class TestImports:
50
50
  "TryAgain",
51
51
  "RetryError",
52
52
  "RetryState",
53
+ "RetryCallState",
53
54
  "Stop",
54
55
  "RetryCondition",
55
56
  "stop_after_attempt",
@@ -0,0 +1,292 @@
1
+ import time
2
+
3
+ import pytest
4
+
5
+ import backon
6
+ from backon._state import RetryCallState
7
+
8
+
9
+ class TestRetryCallState:
10
+ def test_elapsed_property(self):
11
+ state = RetryCallState(start_time=time.monotonic())
12
+ time.sleep(0.01)
13
+ assert state.elapsed > 0
14
+
15
+ def test_statistics(self):
16
+ state = RetryCallState(
17
+ fn=lambda: None,
18
+ attempt_number=3,
19
+ idle_for=2.5,
20
+ start_time=time.monotonic(),
21
+ )
22
+ stats = state.statistics
23
+ assert stats["attempt_number"] == 3
24
+ assert stats["idle_for"] == 2.5
25
+ assert "start_time" in stats
26
+ assert "elapsed" in stats
27
+
28
+ def test_to_details(self):
29
+ state = RetryCallState(
30
+ fn=lambda: None,
31
+ attempt_number=3,
32
+ start_time=time.monotonic(),
33
+ )
34
+ details = state.to_details()
35
+ assert details["tries"] == 3
36
+ assert details["target"] is state.fn
37
+
38
+ def test_zero_start_time_elapsed(self):
39
+ state = RetryCallState()
40
+ assert state.elapsed == 0.0
41
+
42
+
43
+ class TestBeforeAfterHooks:
44
+ def test_before_and_after_with_success(self):
45
+ order = []
46
+
47
+ def before(details):
48
+ order.append("before")
49
+
50
+ def after(details):
51
+ order.append("after")
52
+
53
+ @backon.on_exception(
54
+ backon.constant,
55
+ ValueError,
56
+ max_tries=3,
57
+ jitter=None,
58
+ interval=0.01,
59
+ before=before,
60
+ after=after,
61
+ )
62
+ def f():
63
+ return "ok"
64
+
65
+ f()
66
+ assert order == ["before", "after"]
67
+
68
+ def test_before_and_after_with_exception(self):
69
+ order = []
70
+
71
+ def before(details):
72
+ order.append("before")
73
+
74
+ def after(details):
75
+ order.append("after")
76
+
77
+ @backon.on_exception(
78
+ backon.constant,
79
+ ValueError,
80
+ max_tries=2,
81
+ jitter=None,
82
+ interval=0.01,
83
+ before=before,
84
+ after=after,
85
+ raise_on_giveup=True,
86
+ )
87
+ def f():
88
+ raise ValueError("fail")
89
+
90
+ with pytest.raises(ValueError):
91
+ f()
92
+ assert order == ["before", "after", "before", "after"]
93
+
94
+ def test_before_and_after_multiple_attempts_success(self):
95
+ calls = []
96
+ order = []
97
+
98
+ def before(details):
99
+ order.append("before")
100
+
101
+ def after(details):
102
+ order.append("after")
103
+
104
+ @backon.on_exception(
105
+ backon.constant,
106
+ ValueError,
107
+ max_tries=3,
108
+ jitter=None,
109
+ interval=0.01,
110
+ before=before,
111
+ after=after,
112
+ )
113
+ def f():
114
+ calls.append(1)
115
+ if len(calls) < 3:
116
+ raise ValueError("fail")
117
+ return "ok"
118
+
119
+ f()
120
+ assert order == ["before", "after", "before", "after", "before", "after"]
121
+ assert len(calls) == 3
122
+
123
+ def test_before_after_via_retry_functional_api(self):
124
+ order = []
125
+ calls = []
126
+
127
+ def before(details):
128
+ order.append("before")
129
+
130
+ def after(details):
131
+ order.append("after")
132
+
133
+ def flaky():
134
+ calls.append(1)
135
+ if len(calls) < 2:
136
+ raise ValueError("fail")
137
+ return "ok"
138
+
139
+ result = backon.retry(
140
+ flaky,
141
+ backon.constant,
142
+ exception=ValueError,
143
+ max_tries=3,
144
+ jitter=None,
145
+ interval=0.01,
146
+ before=before,
147
+ after=after,
148
+ )
149
+ assert result == "ok"
150
+ assert order == ["before", "after", "before", "after"]
151
+
152
+ def test_before_after_multiple_handlers(self):
153
+ order = []
154
+
155
+ def h1(d):
156
+ order.append("h1")
157
+
158
+ def h2(d):
159
+ order.append("h2")
160
+
161
+ @backon.on_exception(
162
+ backon.constant,
163
+ ValueError,
164
+ max_tries=2,
165
+ jitter=None,
166
+ interval=0.01,
167
+ before=[h1, h2],
168
+ after=[h2, h1],
169
+ )
170
+ def f():
171
+ raise ValueError("fail")
172
+
173
+ with pytest.raises(ValueError):
174
+ f()
175
+ assert order == ["h1", "h2", "h2", "h1", "h1", "h2", "h2", "h1"]
176
+
177
+ @pytest.mark.asyncio
178
+ async def test_before_after_async(self):
179
+ order = []
180
+
181
+ def before(details):
182
+ order.append("before")
183
+
184
+ def after(details):
185
+ order.append("after")
186
+
187
+ @backon.on_exception(
188
+ backon.constant,
189
+ ValueError,
190
+ max_tries=3,
191
+ jitter=None,
192
+ interval=0.01,
193
+ before=before,
194
+ after=after,
195
+ )
196
+ async def f():
197
+ return "ok"
198
+
199
+ await f()
200
+ assert order == ["before", "after"]
201
+
202
+ @pytest.mark.asyncio
203
+ async def test_before_after_async_exception(self):
204
+ order = []
205
+
206
+ def before(details):
207
+ order.append("before")
208
+
209
+ def after(details):
210
+ order.append("after")
211
+
212
+ @backon.on_exception(
213
+ backon.constant,
214
+ ValueError,
215
+ max_tries=2,
216
+ jitter=None,
217
+ interval=0.01,
218
+ before=before,
219
+ after=after,
220
+ raise_on_giveup=True,
221
+ )
222
+ async def f():
223
+ raise ValueError("fail")
224
+
225
+ with pytest.raises(ValueError):
226
+ await f()
227
+ assert order == ["before", "after", "before", "after"]
228
+
229
+
230
+ class TestRetryingStatistics:
231
+ def test_statistics_available_after_call(self):
232
+ r = backon.Retrying(
233
+ backon.constant,
234
+ exception=ValueError,
235
+ max_tries=2,
236
+ jitter=None,
237
+ interval=0.01,
238
+ )
239
+
240
+ def flaky():
241
+ raise ValueError("fail")
242
+
243
+ with pytest.raises(ValueError):
244
+ r.call(flaky)
245
+
246
+ stats = r.statistics
247
+ assert "attempt_number" in stats
248
+ assert stats["attempt_number"] == 2
249
+
250
+ def test_statistics_before_call(self):
251
+ r = backon.Retrying(backon.constant)
252
+ assert r.statistics == {}
253
+
254
+ def test_call_state_after_call(self):
255
+ r = backon.Retrying(
256
+ backon.constant,
257
+ exception=ValueError,
258
+ max_tries=2,
259
+ jitter=None,
260
+ interval=0.01,
261
+ )
262
+
263
+ def flaky():
264
+ raise ValueError("fail")
265
+
266
+ with pytest.raises(ValueError):
267
+ r.call(flaky)
268
+
269
+ cs = r.call_state
270
+ assert cs is not None
271
+ assert cs.attempt_number == 2
272
+
273
+ def test_call_state_before_call(self):
274
+ r = backon.Retrying(backon.constant)
275
+ assert r.call_state is None
276
+
277
+ def test_on_predicate_statistics(self):
278
+ r = backon.Retrying(
279
+ backon.constant,
280
+ predicate=lambda x: x is None,
281
+ max_tries=3,
282
+ jitter=None,
283
+ interval=0.01,
284
+ )
285
+
286
+ def flaky():
287
+ return None
288
+
289
+ r.call(flaky)
290
+ stats = r.statistics
291
+ assert "attempt_number" in stats
292
+ assert stats["attempt_number"] == 3
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes