backon 3.0.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.
backon/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ from backon._common import disable, enable
2
+ from backon._decorator import on_exception, on_predicate
3
+ from backon._jitter import full_jitter, random_jitter
4
+ from backon._retry import Retrying, retry
5
+ from backon._wait_gen import constant, decay, expo, fibo, runtime
6
+
7
+ __all__ = [
8
+ "on_predicate",
9
+ "on_exception",
10
+ "retry",
11
+ "Retrying",
12
+ "constant",
13
+ "expo",
14
+ "decay",
15
+ "fibo",
16
+ "runtime",
17
+ "full_jitter",
18
+ "random_jitter",
19
+ "disable",
20
+ "enable",
21
+ ]
22
+
23
+ __version__ = "3.0.0"
backon/_async.py ADDED
@@ -0,0 +1,208 @@
1
+ import asyncio
2
+ import functools
3
+
4
+ from backon._common import (
5
+ _elapsed,
6
+ _init_wait_gen,
7
+ _maybe_call,
8
+ _next_wait,
9
+ _now,
10
+ is_enabled,
11
+ )
12
+
13
+
14
+ def _unwrap(target):
15
+ if isinstance(target, staticmethod):
16
+ return target.__func__
17
+ return target
18
+
19
+
20
+ def _ensure_coroutine(coro_or_func):
21
+ if asyncio.iscoroutinefunction(coro_or_func):
22
+ return coro_or_func
23
+ else:
24
+
25
+ @functools.wraps(coro_or_func)
26
+ async def f(*args, **kwargs):
27
+ return coro_or_func(*args, **kwargs)
28
+
29
+ return f
30
+
31
+
32
+ def _ensure_coroutines(coros_or_funcs):
33
+ return [_ensure_coroutine(f) for f in coros_or_funcs]
34
+
35
+
36
+ async def _call_handlers(handlers, *, target, args, kwargs, tries, elapsed, **extra):
37
+ details = {
38
+ "target": target,
39
+ "args": args,
40
+ "kwargs": kwargs,
41
+ "tries": tries,
42
+ "elapsed": elapsed,
43
+ }
44
+ details.update(extra)
45
+ for handler in handlers:
46
+ await handler(details)
47
+
48
+
49
+ def retry_predicate(
50
+ target,
51
+ wait_gen,
52
+ predicate,
53
+ *,
54
+ max_tries,
55
+ max_time,
56
+ jitter,
57
+ on_success,
58
+ on_backoff,
59
+ on_giveup,
60
+ on_attempt,
61
+ sleep,
62
+ wait_gen_kwargs,
63
+ ):
64
+ target = _unwrap(target)
65
+ on_success = _ensure_coroutines(on_success)
66
+ on_backoff = _ensure_coroutines(on_backoff)
67
+ on_giveup = _ensure_coroutines(on_giveup)
68
+ on_attempt = _ensure_coroutines(on_attempt)
69
+
70
+ assert not asyncio.iscoroutinefunction(max_tries)
71
+ assert not asyncio.iscoroutinefunction(jitter)
72
+
73
+ assert asyncio.iscoroutinefunction(target)
74
+
75
+ @functools.wraps(target)
76
+ async def retry(*args, **kwargs):
77
+ if not is_enabled():
78
+ return await target(*args, **kwargs)
79
+
80
+ max_tries_value = _maybe_call(max_tries)
81
+ max_time_value = _maybe_call(max_time)
82
+
83
+ tries = 0
84
+ start = _now()
85
+ wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
86
+ while True:
87
+ tries += 1
88
+ elapsed = _elapsed(start)
89
+ details = {
90
+ "target": target,
91
+ "args": args,
92
+ "kwargs": kwargs,
93
+ "tries": tries,
94
+ "elapsed": elapsed,
95
+ }
96
+
97
+ await _call_handlers(on_attempt, **details)
98
+
99
+ ret = await target(*args, **kwargs)
100
+ if predicate(ret):
101
+ max_tries_exceeded = tries == max_tries_value
102
+ max_time_exceeded = (
103
+ max_time_value is not None and elapsed >= max_time_value
104
+ )
105
+
106
+ if max_tries_exceeded or max_time_exceeded:
107
+ await _call_handlers(on_giveup, **details, value=ret)
108
+ break
109
+
110
+ try:
111
+ seconds = _next_wait(wait, ret, jitter, elapsed, max_time_value)
112
+ except StopIteration:
113
+ await _call_handlers(on_giveup, **details, value=ret)
114
+ break
115
+
116
+ await _call_handlers(on_backoff, **details, value=ret, wait=seconds)
117
+
118
+ await sleep(seconds)
119
+ continue
120
+ else:
121
+ await _call_handlers(on_success, **details, value=ret)
122
+ break
123
+
124
+ return ret
125
+
126
+ return retry
127
+
128
+
129
+ def retry_exception(
130
+ target,
131
+ wait_gen,
132
+ exception,
133
+ *,
134
+ max_tries,
135
+ max_time,
136
+ jitter,
137
+ giveup,
138
+ on_success,
139
+ on_backoff,
140
+ on_giveup,
141
+ on_attempt,
142
+ raise_on_giveup,
143
+ sleep,
144
+ wait_gen_kwargs,
145
+ ):
146
+ target = _unwrap(target)
147
+ on_success = _ensure_coroutines(on_success)
148
+ on_backoff = _ensure_coroutines(on_backoff)
149
+ on_giveup = _ensure_coroutines(on_giveup)
150
+ on_attempt = _ensure_coroutines(on_attempt)
151
+ giveup = _ensure_coroutine(giveup)
152
+
153
+ assert not asyncio.iscoroutinefunction(max_tries)
154
+ assert not asyncio.iscoroutinefunction(jitter)
155
+
156
+ @functools.wraps(target)
157
+ async def retry(*args, **kwargs):
158
+ if not is_enabled():
159
+ return await target(*args, **kwargs)
160
+
161
+ max_tries_value = _maybe_call(max_tries)
162
+ max_time_value = _maybe_call(max_time)
163
+
164
+ tries = 0
165
+ start = _now()
166
+ wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
167
+ while True:
168
+ tries += 1
169
+ elapsed = _elapsed(start)
170
+ details = {
171
+ "target": target,
172
+ "args": args,
173
+ "kwargs": kwargs,
174
+ "tries": tries,
175
+ "elapsed": elapsed,
176
+ }
177
+
178
+ await _call_handlers(on_attempt, **details)
179
+
180
+ try:
181
+ ret = await target(*args, **kwargs)
182
+ except exception as e:
183
+ giveup_result = await giveup(e)
184
+ max_tries_exceeded = tries == max_tries_value
185
+ max_time_exceeded = (
186
+ max_time_value is not None and elapsed >= max_time_value
187
+ )
188
+
189
+ if giveup_result or max_tries_exceeded or max_time_exceeded:
190
+ await _call_handlers(on_giveup, **details, exception=e)
191
+ if raise_on_giveup:
192
+ raise
193
+ return None
194
+
195
+ try:
196
+ seconds = _next_wait(wait, e, jitter, elapsed, max_time_value)
197
+ except StopIteration:
198
+ await _call_handlers(on_giveup, **details, exception=e)
199
+ raise e
200
+
201
+ await _call_handlers(on_backoff, **details, wait=seconds, exception=e)
202
+
203
+ await sleep(seconds)
204
+ else:
205
+ await _call_handlers(on_success, **details)
206
+ return ret
207
+
208
+ return retry
backon/_common.py ADDED
@@ -0,0 +1,117 @@
1
+ import functools
2
+ import logging
3
+ import sys
4
+ import time as time_module
5
+ import traceback
6
+
7
+ _logger = logging.getLogger("backon")
8
+ _logger.addHandler(logging.NullHandler())
9
+
10
+ _GLOBAL_ENABLED = True
11
+
12
+
13
+ def disable():
14
+ global _GLOBAL_ENABLED
15
+ _GLOBAL_ENABLED = False
16
+
17
+
18
+ def enable():
19
+ global _GLOBAL_ENABLED
20
+ _GLOBAL_ENABLED = True
21
+
22
+
23
+ def is_enabled():
24
+ return _GLOBAL_ENABLED
25
+
26
+
27
+ def _maybe_call(f, *args, **kwargs):
28
+ if callable(f):
29
+ try:
30
+ return f(*args, **kwargs)
31
+ except TypeError:
32
+ return f
33
+ else:
34
+ return f
35
+
36
+
37
+ def _init_wait_gen(wait_gen, wait_gen_kwargs):
38
+ kwargs = {k: _maybe_call(v) for k, v in wait_gen_kwargs.items()}
39
+ initialized = wait_gen(**kwargs)
40
+ initialized.send(None)
41
+ return initialized
42
+
43
+
44
+ def _next_wait(wait, send_value, jitter, elapsed, max_time):
45
+ value = wait.send(send_value)
46
+ if jitter is not None:
47
+ seconds = jitter(value)
48
+ else:
49
+ seconds = value
50
+
51
+ if max_time is not None:
52
+ seconds = min(seconds, max_time - elapsed)
53
+
54
+ return seconds
55
+
56
+
57
+ def _prepare_logger(logger):
58
+ if isinstance(logger, str):
59
+ logger = logging.getLogger(logger)
60
+ return logger
61
+
62
+
63
+ def _config_handlers(
64
+ user_handlers, *, default_handler=None, logger=None, log_level=None
65
+ ):
66
+ handlers = []
67
+ if logger is not None:
68
+ assert log_level is not None
69
+ log_handler = functools.partial(
70
+ default_handler, logger=logger, log_level=log_level
71
+ )
72
+ handlers.append(log_handler)
73
+
74
+ if user_handlers is None:
75
+ return handlers
76
+
77
+ if hasattr(user_handlers, "__iter__"):
78
+ handlers += list(user_handlers)
79
+ else:
80
+ handlers.append(user_handlers)
81
+
82
+ return handlers
83
+
84
+
85
+ def _log_backoff(details, logger, log_level):
86
+ msg = "Backing off %s(...) for %.1fs (%s)"
87
+ log_args = [details["target"].__name__, details["wait"]]
88
+
89
+ exc_typ, exc, _ = sys.exc_info()
90
+ if exc is not None:
91
+ exc_fmt = traceback.format_exception_only(exc_typ, exc)[-1]
92
+ log_args.append(exc_fmt.rstrip("\n"))
93
+ else:
94
+ log_args.append(details["value"])
95
+ logger.log(log_level, msg, *log_args)
96
+
97
+
98
+ def _log_giveup(details, logger, log_level):
99
+ msg = "Giving up %s(...) after %d tries (%s)"
100
+ log_args = [details["target"].__name__, details["tries"]]
101
+
102
+ exc_typ, exc, _ = sys.exc_info()
103
+ if exc is not None:
104
+ exc_fmt = traceback.format_exception_only(exc_typ, exc)[-1]
105
+ log_args.append(exc_fmt.rstrip("\n"))
106
+ else:
107
+ log_args.append(details["value"])
108
+
109
+ logger.log(log_level, msg, *log_args)
110
+
111
+
112
+ def _now():
113
+ return time_module.monotonic()
114
+
115
+
116
+ def _elapsed(start):
117
+ return _now() - start
backon/_decorator.py ADDED
@@ -0,0 +1,150 @@
1
+ import asyncio
2
+ import logging
3
+ import operator
4
+ import time as time_module
5
+ from typing import Any, Callable, Iterable, Optional, Type, Union
6
+
7
+ from backon import _async, _sync
8
+ from backon._common import (
9
+ _config_handlers,
10
+ _log_backoff,
11
+ _log_giveup,
12
+ _prepare_logger,
13
+ )
14
+ from backon._jitter import full_jitter
15
+ from backon._typing import (
16
+ _CallableT,
17
+ _Handler,
18
+ _Jitterer,
19
+ _MaybeCallable,
20
+ _MaybeLogger,
21
+ _MaybeSequence,
22
+ _Predicate,
23
+ _WaitGenerator,
24
+ )
25
+
26
+
27
+ def on_predicate(
28
+ wait_gen: _WaitGenerator,
29
+ predicate: _Predicate[Any] = operator.not_,
30
+ *,
31
+ max_tries: Optional[_MaybeCallable[int]] = None,
32
+ max_time: Optional[_MaybeCallable[float]] = None,
33
+ jitter: Union[_Jitterer, None] = full_jitter,
34
+ on_success: Union[_Handler, Iterable[_Handler], None] = None,
35
+ on_backoff: Union[_Handler, Iterable[_Handler], None] = None,
36
+ on_giveup: Union[_Handler, Iterable[_Handler], None] = None,
37
+ on_attempt: Union[_Handler, Iterable[_Handler], None] = None,
38
+ logger: _MaybeLogger = "backon",
39
+ backoff_log_level: int = logging.INFO,
40
+ giveup_log_level: int = logging.ERROR,
41
+ sleep: Optional[Callable[[float], Any]] = None,
42
+ **wait_gen_kwargs: Any,
43
+ ) -> Callable[[_CallableT], _CallableT]:
44
+ def decorate(target):
45
+ nonlocal logger, on_success, on_backoff, on_giveup, on_attempt
46
+
47
+ logger = _prepare_logger(logger)
48
+ on_success = _config_handlers(on_success)
49
+ on_backoff = _config_handlers(
50
+ on_backoff,
51
+ default_handler=_log_backoff,
52
+ logger=logger,
53
+ log_level=backoff_log_level,
54
+ )
55
+ on_giveup = _config_handlers(
56
+ on_giveup,
57
+ default_handler=_log_giveup,
58
+ logger=logger,
59
+ log_level=giveup_log_level,
60
+ )
61
+ on_attempt = _config_handlers(on_attempt)
62
+
63
+ if asyncio.iscoroutinefunction(target):
64
+ retry = _async.retry_predicate
65
+ _sleep = sleep or asyncio.sleep
66
+ else:
67
+ retry = _sync.retry_predicate
68
+ _sleep = sleep or time_module.sleep
69
+
70
+ return retry(
71
+ target,
72
+ wait_gen,
73
+ predicate,
74
+ max_tries=max_tries,
75
+ max_time=max_time,
76
+ jitter=jitter,
77
+ on_success=on_success,
78
+ on_backoff=on_backoff,
79
+ on_giveup=on_giveup,
80
+ on_attempt=on_attempt,
81
+ sleep=_sleep,
82
+ wait_gen_kwargs=wait_gen_kwargs,
83
+ )
84
+
85
+ return decorate
86
+
87
+
88
+ def on_exception(
89
+ wait_gen: _WaitGenerator,
90
+ exception: _MaybeSequence[Type[Exception]],
91
+ *,
92
+ max_tries: Optional[_MaybeCallable[int]] = None,
93
+ max_time: Optional[_MaybeCallable[float]] = None,
94
+ jitter: Union[_Jitterer, None] = full_jitter,
95
+ giveup: _Predicate[Exception] = lambda e: False,
96
+ on_success: Union[_Handler, Iterable[_Handler], None] = None,
97
+ on_backoff: Union[_Handler, Iterable[_Handler], None] = None,
98
+ on_giveup: Union[_Handler, Iterable[_Handler], None] = None,
99
+ on_attempt: Union[_Handler, Iterable[_Handler], None] = None,
100
+ raise_on_giveup: bool = True,
101
+ logger: _MaybeLogger = "backon",
102
+ backoff_log_level: int = logging.INFO,
103
+ giveup_log_level: int = logging.ERROR,
104
+ sleep: Optional[Callable[[float], Any]] = None,
105
+ **wait_gen_kwargs: Any,
106
+ ) -> Callable[[_CallableT], _CallableT]:
107
+ def decorate(target):
108
+ nonlocal logger, on_success, on_backoff, on_giveup, on_attempt
109
+
110
+ logger = _prepare_logger(logger)
111
+ on_success = _config_handlers(on_success)
112
+ on_backoff = _config_handlers(
113
+ on_backoff,
114
+ default_handler=_log_backoff,
115
+ logger=logger,
116
+ log_level=backoff_log_level,
117
+ )
118
+ on_giveup = _config_handlers(
119
+ on_giveup,
120
+ default_handler=_log_giveup,
121
+ logger=logger,
122
+ log_level=giveup_log_level,
123
+ )
124
+ on_attempt = _config_handlers(on_attempt)
125
+
126
+ if asyncio.iscoroutinefunction(target):
127
+ retry = _async.retry_exception
128
+ _sleep = sleep or asyncio.sleep
129
+ else:
130
+ retry = _sync.retry_exception
131
+ _sleep = sleep or time_module.sleep
132
+
133
+ return retry(
134
+ target,
135
+ wait_gen,
136
+ exception,
137
+ max_tries=max_tries,
138
+ max_time=max_time,
139
+ jitter=jitter,
140
+ giveup=giveup,
141
+ on_success=on_success,
142
+ on_backoff=on_backoff,
143
+ on_giveup=on_giveup,
144
+ on_attempt=on_attempt,
145
+ raise_on_giveup=raise_on_giveup,
146
+ sleep=_sleep,
147
+ wait_gen_kwargs=wait_gen_kwargs,
148
+ )
149
+
150
+ return decorate
backon/_jitter.py ADDED
@@ -0,0 +1,9 @@
1
+ import random
2
+
3
+
4
+ def random_jitter(value: float) -> float:
5
+ return value + random.random()
6
+
7
+
8
+ def full_jitter(value: float) -> float:
9
+ return random.uniform(0, value)