python-backoff 2.2.2rc1__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.
backoff/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ # coding:utf-8
2
+ """
3
+ Function decoration for backoff and retry
4
+
5
+ This module provides function decorators which can be used to wrap a
6
+ function such that it will be retried until some condition is met. It
7
+ is meant to be of use when accessing unreliable resources with the
8
+ potential for intermittent failures i.e. network resources and external
9
+ APIs. Somewhat more generally, it may also be of use for dynamically
10
+ polling resources for externally generated content.
11
+
12
+ For examples and full documentation see the README at
13
+ https://github.com/python-backoff/backoff
14
+ """
15
+ from backoff._decorator import on_exception, on_predicate
16
+ from backoff._jitter import full_jitter, random_jitter
17
+ from backoff._wait_gen import constant, expo, fibo, runtime, decay
18
+
19
+ __all__ = [
20
+ 'on_predicate',
21
+ 'on_exception',
22
+ 'constant',
23
+ 'expo',
24
+ 'decay',
25
+ 'fibo',
26
+ 'runtime',
27
+ 'full_jitter',
28
+ 'random_jitter',
29
+ ]
30
+
31
+ __version__ = "2.2.1"
backoff/_async.py ADDED
@@ -0,0 +1,189 @@
1
+ # coding:utf-8
2
+ import datetime
3
+ import functools
4
+ import asyncio
5
+ import inspect
6
+ from datetime import timedelta
7
+
8
+ from backoff._common import (_init_wait_gen, _maybe_call, _next_wait)
9
+
10
+
11
+ def _ensure_coroutine(coro_or_func):
12
+ if inspect.iscoroutinefunction(coro_or_func):
13
+ return coro_or_func
14
+ else:
15
+ @functools.wraps(coro_or_func)
16
+ async def f(*args, **kwargs):
17
+ return coro_or_func(*args, **kwargs)
18
+ return f
19
+
20
+
21
+ def _ensure_coroutines(coros_or_funcs):
22
+ return [_ensure_coroutine(f) for f in coros_or_funcs]
23
+
24
+
25
+ async def _call_handlers(handlers,
26
+ *,
27
+ target, args, kwargs, tries, elapsed,
28
+ **extra):
29
+ details = {
30
+ 'target': target,
31
+ 'args': args,
32
+ 'kwargs': kwargs,
33
+ 'tries': tries,
34
+ 'elapsed': elapsed,
35
+ }
36
+ details.update(extra)
37
+ for handler in handlers:
38
+ await handler(details)
39
+
40
+
41
+ def retry_predicate(target, wait_gen, predicate,
42
+ *,
43
+ max_tries, max_time, jitter,
44
+ on_success, on_backoff, on_giveup,
45
+ wait_gen_kwargs):
46
+ on_success = _ensure_coroutines(on_success)
47
+ on_backoff = _ensure_coroutines(on_backoff)
48
+ on_giveup = _ensure_coroutines(on_giveup)
49
+
50
+ # Easy to implement, please report if you need this.
51
+ assert not inspect.iscoroutinefunction(max_tries)
52
+ assert not inspect.iscoroutinefunction(jitter)
53
+
54
+ assert inspect.iscoroutinefunction(target)
55
+
56
+ @functools.wraps(target)
57
+ async def retry(*args, **kwargs):
58
+
59
+ # update variables from outer function args
60
+ max_tries_value = _maybe_call(max_tries)
61
+ max_time_value = _maybe_call(max_time)
62
+
63
+ tries = 0
64
+ start = datetime.datetime.now()
65
+ wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
66
+ while True:
67
+ tries += 1
68
+ elapsed = timedelta.total_seconds(datetime.datetime.now() - start)
69
+ details = {
70
+ "target": target,
71
+ "args": args,
72
+ "kwargs": kwargs,
73
+ "tries": tries,
74
+ "elapsed": elapsed,
75
+ }
76
+
77
+ ret = await target(*args, **kwargs)
78
+ if predicate(ret):
79
+ max_tries_exceeded = (tries == max_tries_value)
80
+ max_time_exceeded = (max_time_value is not None and
81
+ elapsed >= max_time_value)
82
+
83
+ if max_tries_exceeded or max_time_exceeded:
84
+ await _call_handlers(on_giveup, **details, value=ret)
85
+ break
86
+
87
+ try:
88
+ seconds = _next_wait(wait, ret, jitter, elapsed,
89
+ max_time_value)
90
+ except StopIteration:
91
+ await _call_handlers(on_giveup, **details, value=ret)
92
+ break
93
+
94
+ await _call_handlers(on_backoff, **details, value=ret,
95
+ wait=seconds)
96
+
97
+ # Note: there is no convenient way to pass explicit event
98
+ # loop to decorator, so here we assume that either default
99
+ # thread event loop is set and correct (it mostly is
100
+ # by default), or Python >= 3.5.3 or Python >= 3.6 is used
101
+ # where loop.get_event_loop() in coroutine guaranteed to
102
+ # return correct value.
103
+ # See for details:
104
+ # <https://groups.google.com/forum/#!topic/python-tulip/yF9C-rFpiKk>
105
+ # <https://bugs.python.org/issue28613>
106
+ await asyncio.sleep(seconds)
107
+ continue
108
+ else:
109
+ await _call_handlers(on_success, **details, value=ret)
110
+ break
111
+
112
+ return ret
113
+
114
+ return retry
115
+
116
+
117
+ def retry_exception(target, wait_gen, exception,
118
+ *,
119
+ max_tries, max_time, jitter, giveup,
120
+ on_success, on_backoff, on_giveup, raise_on_giveup,
121
+ wait_gen_kwargs):
122
+ on_success = _ensure_coroutines(on_success)
123
+ on_backoff = _ensure_coroutines(on_backoff)
124
+ on_giveup = _ensure_coroutines(on_giveup)
125
+ giveup = _ensure_coroutine(giveup)
126
+
127
+ # Easy to implement, please report if you need this.
128
+ assert not inspect.iscoroutinefunction(max_tries)
129
+ assert not inspect.iscoroutinefunction(jitter)
130
+
131
+ @functools.wraps(target)
132
+ async def retry(*args, **kwargs):
133
+
134
+ max_tries_value = _maybe_call(max_tries)
135
+ max_time_value = _maybe_call(max_time)
136
+
137
+ tries = 0
138
+ start = datetime.datetime.now()
139
+ wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
140
+ while True:
141
+ tries += 1
142
+ elapsed = timedelta.total_seconds(datetime.datetime.now() - start)
143
+ details = {
144
+ "target": target,
145
+ "args": args,
146
+ "kwargs": kwargs,
147
+ "tries": tries,
148
+ "elapsed": elapsed,
149
+ }
150
+
151
+ try:
152
+ ret = await target(*args, **kwargs)
153
+ except exception as e:
154
+ giveup_result = await giveup(e)
155
+ max_tries_exceeded = (tries == max_tries_value)
156
+ max_time_exceeded = (max_time_value is not None and
157
+ elapsed >= max_time_value)
158
+
159
+ if giveup_result or max_tries_exceeded or max_time_exceeded:
160
+ await _call_handlers(on_giveup, **details, exception=e)
161
+ if raise_on_giveup:
162
+ raise
163
+ return None
164
+
165
+ try:
166
+ seconds = _next_wait(wait, e, jitter, elapsed,
167
+ max_time_value)
168
+ except StopIteration:
169
+ await _call_handlers(on_giveup, **details, exception=e)
170
+ raise e
171
+
172
+ await _call_handlers(on_backoff, **details, wait=seconds,
173
+ exception=e)
174
+
175
+ # Note: there is no convenient way to pass explicit event
176
+ # loop to decorator, so here we assume that either default
177
+ # thread event loop is set and correct (it mostly is
178
+ # by default), or Python >= 3.5.3 or Python >= 3.6 is used
179
+ # where loop.get_event_loop() in coroutine guaranteed to
180
+ # return correct value.
181
+ # See for details:
182
+ # <https://groups.google.com/forum/#!topic/python-tulip/yF9C-rFpiKk>
183
+ # <https://bugs.python.org/issue28613>
184
+ await asyncio.sleep(seconds)
185
+ else:
186
+ await _call_handlers(on_success, **details)
187
+
188
+ return ret
189
+ return retry
backoff/_common.py ADDED
@@ -0,0 +1,120 @@
1
+ # coding:utf-8
2
+
3
+ import functools
4
+ import logging
5
+ import sys
6
+ import traceback
7
+ import warnings
8
+
9
+
10
+ # Use module-specific logger with a default null handler.
11
+ _logger = logging.getLogger('backoff')
12
+ _logger.addHandler(logging.NullHandler()) # pragma: no cover
13
+ _logger.setLevel(logging.INFO)
14
+
15
+
16
+ # Evaluate arg that can be either a fixed value or a callable.
17
+ def _maybe_call(f, *args, **kwargs):
18
+ if callable(f):
19
+ try:
20
+ return f(*args, **kwargs)
21
+ except TypeError:
22
+ return f
23
+ else:
24
+ return f
25
+
26
+
27
+ def _init_wait_gen(wait_gen, wait_gen_kwargs):
28
+ kwargs = {k: _maybe_call(v) for k, v in wait_gen_kwargs.items()}
29
+ initialized = wait_gen(**kwargs)
30
+ initialized.send(None) # Initialize with an empty send
31
+ return initialized
32
+
33
+
34
+ def _next_wait(wait, send_value, jitter, elapsed, max_time):
35
+ value = wait.send(send_value)
36
+ try:
37
+ if jitter is not None:
38
+ seconds = jitter(value)
39
+ else:
40
+ seconds = value
41
+ except TypeError:
42
+ warnings.warn(
43
+ "Nullary jitter function signature is deprecated. Use "
44
+ "unary signature accepting a wait value in seconds and "
45
+ "returning a jittered version of it.",
46
+ DeprecationWarning,
47
+ stacklevel=2,
48
+ )
49
+
50
+ seconds = value + jitter()
51
+
52
+ # don't sleep longer than remaining allotted max_time
53
+ if max_time is not None:
54
+ seconds = min(seconds, max_time - elapsed)
55
+
56
+ return seconds
57
+
58
+
59
+ def _prepare_logger(logger):
60
+ if isinstance(logger, str):
61
+ logger = logging.getLogger(logger)
62
+ return logger
63
+
64
+
65
+ # Configure handler list with user specified handler and optionally
66
+ # with a default handler bound to the specified logger.
67
+ def _config_handlers(
68
+ user_handlers, *, default_handler=None, logger=None, log_level=None
69
+ ):
70
+ handlers = []
71
+ if logger is not None:
72
+ assert log_level is not None, "Log level is not specified"
73
+ # bind the specified logger to the default log handler
74
+ log_handler = functools.partial(
75
+ default_handler, logger=logger, log_level=log_level
76
+ )
77
+ handlers.append(log_handler)
78
+
79
+ if user_handlers is None:
80
+ return handlers
81
+
82
+ # user specified handlers can either be an iterable of handlers
83
+ # or a single handler. either way append them to the list.
84
+ if hasattr(user_handlers, '__iter__'):
85
+ # add all handlers in the iterable
86
+ handlers += list(user_handlers)
87
+ else:
88
+ # append a single handler
89
+ handlers.append(user_handlers)
90
+
91
+ return handlers
92
+
93
+
94
+ # Default backoff handler
95
+ def _log_backoff(details, logger, log_level):
96
+ msg = "Backing off %s(...) for %.1fs (%s)"
97
+ log_args = [details['target'].__name__, details['wait']]
98
+
99
+ exc_typ, exc, _ = sys.exc_info()
100
+ if exc is not None:
101
+ exc_fmt = traceback.format_exception_only(exc_typ, exc)[-1]
102
+ log_args.append(exc_fmt.rstrip("\n"))
103
+ else:
104
+ log_args.append(details['value'])
105
+ logger.log(log_level, msg, *log_args)
106
+
107
+
108
+ # Default giveup handler
109
+ def _log_giveup(details, logger, log_level):
110
+ msg = "Giving up %s(...) after %d tries (%s)"
111
+ log_args = [details['target'].__name__, details['tries']]
112
+
113
+ exc_typ, exc, _ = sys.exc_info()
114
+ if exc is not None:
115
+ exc_fmt = traceback.format_exception_only(exc_typ, exc)[-1]
116
+ log_args.append(exc_fmt.rstrip("\n"))
117
+ else:
118
+ log_args.append(details['value'])
119
+
120
+ logger.log(log_level, msg, *log_args)
backoff/_decorator.py ADDED
@@ -0,0 +1,222 @@
1
+ # coding:utf-8
2
+ import inspect
3
+ import logging
4
+ import operator
5
+ from typing import Any, Callable, Iterable, Optional, Type, Union
6
+
7
+ from backoff._common import (
8
+ _prepare_logger,
9
+ _config_handlers,
10
+ _log_backoff,
11
+ _log_giveup
12
+ )
13
+ from backoff._jitter import full_jitter
14
+ from backoff import _async, _sync
15
+ from backoff._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(wait_gen: _WaitGenerator,
28
+ predicate: _Predicate[Any] = operator.not_,
29
+ *,
30
+ max_tries: Optional[_MaybeCallable[int]] = None,
31
+ max_time: Optional[_MaybeCallable[float]] = None,
32
+ jitter: Union[_Jitterer, None] = full_jitter,
33
+ on_success: Union[_Handler, Iterable[_Handler], None] = None,
34
+ on_backoff: Union[_Handler, Iterable[_Handler], None] = None,
35
+ on_giveup: Union[_Handler, Iterable[_Handler], None] = None,
36
+ logger: _MaybeLogger = 'backoff',
37
+ backoff_log_level: int = logging.INFO,
38
+ giveup_log_level: int = logging.ERROR,
39
+ **wait_gen_kwargs: Any) -> Callable[[_CallableT], _CallableT]:
40
+ """Returns decorator for backoff and retry triggered by predicate.
41
+
42
+ Args:
43
+ wait_gen: A generator yielding successive wait times in
44
+ seconds.
45
+ predicate: A function which when called on the return value of
46
+ the target function will trigger backoff when considered
47
+ truthily. If not specified, the default behavior is to
48
+ backoff on falsey return values.
49
+ max_tries: The maximum number of attempts to make before giving
50
+ up. In the case of failure, the result of the last attempt
51
+ will be returned. The default value of None means there
52
+ is no limit to the number of tries. If a callable is passed,
53
+ it will be evaluated at runtime and its return value used.
54
+ max_time: The maximum total amount of time in seconds to try for before
55
+ giving up. If this time expires, the result of the last
56
+ attempt will be returned. If a callable is passed, it will
57
+ be evaluated at runtime and its return value used.
58
+ jitter: A function of the value yielded by wait_gen returning
59
+ the actual time to wait. This distributes wait times
60
+ stochastically in order to avoid timing collisions across
61
+ concurrent clients. Wait times are jittered by default
62
+ using the full_jitter function. Jittering may be disabled
63
+ altogether by passing jitter=None.
64
+ on_success: Callable (or iterable of callables) with a unary
65
+ signature to be called in the event of success. The
66
+ parameter is a dict containing details about the invocation.
67
+ on_backoff: Callable (or iterable of callables) with a unary
68
+ signature to be called in the event of a backoff. The
69
+ parameter is a dict containing details about the invocation.
70
+ on_giveup: Callable (or iterable of callables) with a unary
71
+ signature to be called in the event that max_tries
72
+ is exceeded. The parameter is a dict containing details
73
+ about the invocation.
74
+ logger: Name of logger or Logger object to log to. Defaults to
75
+ 'backoff'.
76
+ backoff_log_level: log level for the backoff event. Defaults to "INFO"
77
+ giveup_log_level: log level for the give up event. Defaults to "ERROR"
78
+ **wait_gen_kwargs: Any additional keyword args specified will be
79
+ passed to wait_gen when it is initialized. Any callable
80
+ args will first be evaluated and their return values passed.
81
+ This is useful for runtime configuration.
82
+ """
83
+ def decorate(target):
84
+ nonlocal logger, on_success, on_backoff, on_giveup
85
+
86
+ logger = _prepare_logger(logger)
87
+ on_success = _config_handlers(on_success)
88
+ on_backoff = _config_handlers(
89
+ on_backoff,
90
+ default_handler=_log_backoff,
91
+ logger=logger,
92
+ log_level=backoff_log_level
93
+ )
94
+ on_giveup = _config_handlers(
95
+ on_giveup,
96
+ default_handler=_log_giveup,
97
+ logger=logger,
98
+ log_level=giveup_log_level
99
+ )
100
+
101
+ if inspect.iscoroutinefunction(target):
102
+ retry = _async.retry_predicate
103
+ else:
104
+ retry = _sync.retry_predicate
105
+
106
+ return retry(
107
+ target,
108
+ wait_gen,
109
+ predicate,
110
+ max_tries=max_tries,
111
+ max_time=max_time,
112
+ jitter=jitter,
113
+ on_success=on_success,
114
+ on_backoff=on_backoff,
115
+ on_giveup=on_giveup,
116
+ wait_gen_kwargs=wait_gen_kwargs
117
+ )
118
+
119
+ # Return a function which decorates a target with a retry loop.
120
+ return decorate
121
+
122
+
123
+ def on_exception(wait_gen: _WaitGenerator,
124
+ exception: _MaybeSequence[Type[Exception]],
125
+ *,
126
+ max_tries: Optional[_MaybeCallable[int]] = None,
127
+ max_time: Optional[_MaybeCallable[float]] = None,
128
+ jitter: Union[_Jitterer, None] = full_jitter,
129
+ giveup: _Predicate[Exception] = lambda e: False,
130
+ on_success: Union[_Handler, Iterable[_Handler], None] = None,
131
+ on_backoff: Union[_Handler, Iterable[_Handler], None] = None,
132
+ on_giveup: Union[_Handler, Iterable[_Handler], None] = None,
133
+ raise_on_giveup: bool = True,
134
+ logger: _MaybeLogger = 'backoff',
135
+ backoff_log_level: int = logging.INFO,
136
+ giveup_log_level: int = logging.ERROR,
137
+ **wait_gen_kwargs: Any) -> Callable[[_CallableT], _CallableT]:
138
+ """Returns decorator for backoff and retry triggered by exception.
139
+
140
+ Args:
141
+ wait_gen: A generator yielding successive wait times in
142
+ seconds.
143
+ exception: An exception type (or tuple of types) which triggers
144
+ backoff.
145
+ max_tries: The maximum number of attempts to make before giving
146
+ up. Once exhausted, the exception will be allowed to escape.
147
+ The default value of None means there is no limit to the
148
+ number of tries. If a callable is passed, it will be
149
+ evaluated at runtime and its return value used.
150
+ max_time: The maximum total amount of time to try for before
151
+ giving up. Once expired, the exception will be allowed to
152
+ escape. If a callable is passed, it will be
153
+ evaluated at runtime and its return value used.
154
+ jitter: A function of the value yielded by wait_gen returning
155
+ the actual time to wait. This distributes wait times
156
+ stochastically in order to avoid timing collisions across
157
+ concurrent clients. Wait times are jittered by default
158
+ using the full_jitter function. Jittering may be disabled
159
+ altogether by passing jitter=None.
160
+ giveup: Function accepting an exception instance and
161
+ returning whether or not to give up. Optional. The default
162
+ is to always continue.
163
+ on_success: Callable (or iterable of callables) with a unary
164
+ signature to be called in the event of success. The
165
+ parameter is a dict containing details about the invocation.
166
+ on_backoff: Callable (or iterable of callables) with a unary
167
+ signature to be called in the event of a backoff. The
168
+ parameter is a dict containing details about the invocation.
169
+ on_giveup: Callable (or iterable of callables) with a unary
170
+ signature to be called in the event that max_tries
171
+ is exceeded. The parameter is a dict containing details
172
+ about the invocation.
173
+ raise_on_giveup: Boolean indicating whether the registered exceptions
174
+ should be raised on giveup. Defaults to `True`
175
+ logger: Name or Logger object to log to. Defaults to 'backoff'.
176
+ backoff_log_level: log level for the backoff event. Defaults to "INFO"
177
+ giveup_log_level: log level for the give up event. Defaults to "ERROR"
178
+ **wait_gen_kwargs: Any additional keyword args specified will be
179
+ passed to wait_gen when it is initialized. Any callable
180
+ args will first be evaluated and their return values passed.
181
+ This is useful for runtime configuration.
182
+ """
183
+ def decorate(target):
184
+ nonlocal logger, on_success, on_backoff, on_giveup
185
+
186
+ logger = _prepare_logger(logger)
187
+ on_success = _config_handlers(on_success)
188
+ on_backoff = _config_handlers(
189
+ on_backoff,
190
+ default_handler=_log_backoff,
191
+ logger=logger,
192
+ log_level=backoff_log_level,
193
+ )
194
+ on_giveup = _config_handlers(
195
+ on_giveup,
196
+ default_handler=_log_giveup,
197
+ logger=logger,
198
+ log_level=giveup_log_level,
199
+ )
200
+
201
+ if inspect.iscoroutinefunction(target):
202
+ retry = _async.retry_exception
203
+ else:
204
+ retry = _sync.retry_exception
205
+
206
+ return retry(
207
+ target,
208
+ wait_gen,
209
+ exception,
210
+ max_tries=max_tries,
211
+ max_time=max_time,
212
+ jitter=jitter,
213
+ giveup=giveup,
214
+ on_success=on_success,
215
+ on_backoff=on_backoff,
216
+ on_giveup=on_giveup,
217
+ raise_on_giveup=raise_on_giveup,
218
+ wait_gen_kwargs=wait_gen_kwargs
219
+ )
220
+
221
+ # Return a function which decorates a target with a retry loop.
222
+ return decorate
backoff/_jitter.py ADDED
@@ -0,0 +1,28 @@
1
+ # coding:utf-8
2
+
3
+ import random
4
+
5
+
6
+ def random_jitter(value: float) -> float:
7
+ """Jitter the value a random number of milliseconds.
8
+
9
+ This adds up to 1 second of additional time to the original value.
10
+ Prior to backoff version 1.2 this was the default jitter behavior.
11
+
12
+ Args:
13
+ value: The unadulterated backoff value.
14
+ """
15
+ return value + random.random()
16
+
17
+
18
+ def full_jitter(value: float) -> float:
19
+ """Jitter the value across the full range (0 to value).
20
+
21
+ This corresponds to the "Full Jitter" algorithm specified in the
22
+ AWS blog's post on the performance of various jitter algorithms.
23
+ (http://www.awsarchitectureblog.com/2015/03/backoff.html)
24
+
25
+ Args:
26
+ value: The unadulterated backoff value.
27
+ """
28
+ return random.uniform(0, value)
backoff/_sync.py ADDED
@@ -0,0 +1,132 @@
1
+ # coding:utf-8
2
+ import datetime
3
+ import functools
4
+ import time
5
+ from datetime import timedelta
6
+
7
+ from backoff._common import (_init_wait_gen, _maybe_call, _next_wait)
8
+
9
+
10
+ def _call_handlers(hdlrs, target, args, kwargs, tries, elapsed, **extra):
11
+ details = {
12
+ 'target': target,
13
+ 'args': args,
14
+ 'kwargs': kwargs,
15
+ 'tries': tries,
16
+ 'elapsed': elapsed,
17
+ }
18
+ details.update(extra)
19
+ for hdlr in hdlrs:
20
+ hdlr(details)
21
+
22
+
23
+ def retry_predicate(target, wait_gen, predicate,
24
+ *,
25
+ max_tries, max_time, jitter,
26
+ on_success, on_backoff, on_giveup,
27
+ wait_gen_kwargs):
28
+
29
+ @functools.wraps(target)
30
+ def retry(*args, **kwargs):
31
+ max_tries_value = _maybe_call(max_tries)
32
+ max_time_value = _maybe_call(max_time)
33
+
34
+ tries = 0
35
+ start = datetime.datetime.now()
36
+ wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
37
+ while True:
38
+ tries += 1
39
+ elapsed = timedelta.total_seconds(datetime.datetime.now() - start)
40
+ details = {
41
+ "target": target,
42
+ "args": args,
43
+ "kwargs": kwargs,
44
+ "tries": tries,
45
+ "elapsed": elapsed,
46
+ }
47
+
48
+ ret = target(*args, **kwargs)
49
+ if predicate(ret):
50
+ max_tries_exceeded = (tries == max_tries_value)
51
+ max_time_exceeded = (max_time_value is not None and
52
+ elapsed >= max_time_value)
53
+
54
+ if max_tries_exceeded or max_time_exceeded:
55
+ _call_handlers(on_giveup, **details, value=ret)
56
+ break
57
+
58
+ try:
59
+ seconds = _next_wait(wait, ret, jitter, elapsed,
60
+ max_time_value)
61
+ except StopIteration:
62
+ _call_handlers(on_giveup, **details)
63
+ break
64
+
65
+ _call_handlers(on_backoff, **details,
66
+ value=ret, wait=seconds)
67
+
68
+ time.sleep(seconds)
69
+ continue
70
+ else:
71
+ _call_handlers(on_success, **details, value=ret)
72
+ break
73
+
74
+ return ret
75
+
76
+ return retry
77
+
78
+
79
+ def retry_exception(target, wait_gen, exception,
80
+ *,
81
+ max_tries, max_time, jitter, giveup,
82
+ on_success, on_backoff, on_giveup, raise_on_giveup,
83
+ wait_gen_kwargs):
84
+
85
+ @functools.wraps(target)
86
+ def retry(*args, **kwargs):
87
+ max_tries_value = _maybe_call(max_tries)
88
+ max_time_value = _maybe_call(max_time)
89
+
90
+ tries = 0
91
+ start = datetime.datetime.now()
92
+ wait = _init_wait_gen(wait_gen, wait_gen_kwargs)
93
+ while True:
94
+ tries += 1
95
+ elapsed = timedelta.total_seconds(datetime.datetime.now() - start)
96
+ details = {
97
+ "target": target,
98
+ "args": args,
99
+ "kwargs": kwargs,
100
+ "tries": tries,
101
+ "elapsed": elapsed,
102
+ }
103
+
104
+ try:
105
+ ret = target(*args, **kwargs)
106
+ except exception as e:
107
+ max_tries_exceeded = (tries == max_tries_value)
108
+ max_time_exceeded = (max_time_value is not None and
109
+ elapsed >= max_time_value)
110
+
111
+ if giveup(e) or max_tries_exceeded or max_time_exceeded:
112
+ _call_handlers(on_giveup, **details, exception=e)
113
+ if raise_on_giveup:
114
+ raise
115
+ return None
116
+
117
+ try:
118
+ seconds = _next_wait(wait, e, jitter, elapsed,
119
+ max_time_value)
120
+ except StopIteration:
121
+ _call_handlers(on_giveup, **details, exception=e)
122
+ raise e
123
+
124
+ _call_handlers(on_backoff, **details, wait=seconds,
125
+ exception=e)
126
+
127
+ time.sleep(seconds)
128
+ else:
129
+ _call_handlers(on_success, **details)
130
+
131
+ return ret
132
+ return retry
backoff/_typing.py ADDED
@@ -0,0 +1,45 @@
1
+ # coding:utf-8
2
+ import logging
3
+ import sys
4
+ from typing import (Any, Callable, Coroutine, Dict, Generator, Sequence, Tuple,
5
+ TypeVar, Union)
6
+
7
+ if sys.version_info >= (3, 8): # pragma: no cover
8
+ from typing import TypedDict
9
+ else: # pragma: no cover
10
+ # use typing_extensions if installed but don't require it
11
+ try:
12
+ from typing_extensions import TypedDict
13
+ except ImportError:
14
+ class TypedDict(dict):
15
+ def __init_subclass__(cls, **kwargs: Any) -> None:
16
+ return super().__init_subclass__()
17
+
18
+
19
+ class _Details(TypedDict):
20
+ target: Callable[..., Any]
21
+ args: Tuple[Any, ...]
22
+ kwargs: Dict[str, Any]
23
+ tries: int
24
+ elapsed: float
25
+
26
+
27
+ class Details(_Details, total=False):
28
+ wait: float # present in the on_backoff handler case for either decorator
29
+ value: Any # present in the on_predicate decorator case
30
+ exception: Exception # present in the on_exception decorator case
31
+
32
+
33
+ T = TypeVar("T")
34
+
35
+ _CallableT = TypeVar('_CallableT', bound=Callable[..., Any])
36
+ _Handler = Union[
37
+ Callable[[Details], None],
38
+ Callable[[Details], Coroutine[Any, Any, None]],
39
+ ]
40
+ _Jitterer = Callable[[float], float]
41
+ _MaybeCallable = Union[T, Callable[[], T]]
42
+ _MaybeLogger = Union[str, logging.Logger, logging.LoggerAdapter, None]
43
+ _MaybeSequence = Union[T, Sequence[T]]
44
+ _Predicate = Callable[[T], bool]
45
+ _WaitGenerator = Callable[..., Generator[float, None, None]]
backoff/_wait_gen.py ADDED
@@ -0,0 +1,119 @@
1
+ # coding:utf-8
2
+
3
+ import itertools
4
+ import math
5
+ from typing import Any, Callable, Generator, Iterable, Optional, Union
6
+
7
+
8
+ def expo(
9
+ base: float = 2,
10
+ factor: float = 1,
11
+ max_value: Optional[float] = None
12
+ ) -> Generator[float, Any, None]:
13
+
14
+ """Generator for exponential decay.
15
+
16
+ Args:
17
+ base: The mathematical base of the exponentiation operation
18
+ factor: Factor to multiply the exponentiation by.
19
+ max_value: The maximum value to yield. Once the value in the
20
+ true exponential sequence exceeds this, the value
21
+ of max_value will forever after be yielded.
22
+ """
23
+ # Advance past initial .send() call
24
+ yield # type: ignore[misc]
25
+ n = 0
26
+ while True:
27
+ a = factor * base ** n
28
+ if max_value is None or a < max_value:
29
+ yield a
30
+ n += 1
31
+ else:
32
+ yield max_value
33
+
34
+
35
+ def decay(
36
+ initial_value: float = 1,
37
+ decay_factor: float = 1,
38
+ min_value: Optional[float] = None
39
+ ) -> Generator[float, Any, None]:
40
+
41
+ """Generator for exponential decay[1]:
42
+
43
+ Args:
44
+ initial_value: initial quantity
45
+ decay_factor: exponential decay constant.
46
+ min_value: The minimum value to yield. Once the value in the
47
+ true exponential sequence is lower than this, the value
48
+ of min_value will forever after be yielded.
49
+
50
+ [1] https://en.wikipedia.org/wiki/Exponential_decay
51
+ """
52
+ # Advance past initial .send() call
53
+ yield # type: ignore[misc]
54
+ t = 0
55
+ while True:
56
+ a = initial_value * math.e ** (-t * decay_factor)
57
+ if min_value is None or a > min_value:
58
+ yield a
59
+ t += 1
60
+ else:
61
+ yield min_value
62
+
63
+
64
+ def fibo(max_value: Optional[int] = None) -> Generator[int, None, None]:
65
+ """Generator for fibonaccial decay.
66
+
67
+ Args:
68
+ max_value: The maximum value to yield. Once the value in the
69
+ true fibonacci sequence exceeds this, the value
70
+ of max_value will forever after be yielded.
71
+ """
72
+ # Advance past initial .send() call
73
+ yield # type: ignore[misc]
74
+
75
+ a = 1
76
+ b = 1
77
+ while True:
78
+ if max_value is None or a < max_value:
79
+ yield a
80
+ a, b = b, a + b
81
+ else:
82
+ yield max_value
83
+
84
+
85
+ def constant(
86
+ interval: Union[int, Iterable[float]] = 1
87
+ ) -> Generator[float, None, None]:
88
+ """Generator for constant intervals.
89
+
90
+ Args:
91
+ interval: A constant value to yield or an iterable of such values.
92
+ """
93
+ # Advance past initial .send() call
94
+ yield # type: ignore[misc]
95
+
96
+ try:
97
+ itr = iter(interval) # type: ignore
98
+ except TypeError:
99
+ itr = itertools.repeat(interval) # type: ignore
100
+
101
+ for val in itr:
102
+ yield val
103
+
104
+
105
+ def runtime(
106
+ *,
107
+ value: Callable[[Any], float]
108
+ ) -> Generator[float, None, None]:
109
+ """Generator that is based on parsing the return value or thrown
110
+ exception of the decorated method
111
+
112
+ Args:
113
+ value: a callable which takes as input the decorated
114
+ function's return value or thrown exception and
115
+ determines how long to wait
116
+ """
117
+ ret_or_exc = yield # type: ignore[misc]
118
+ while True:
119
+ ret_or_exc = yield value(ret_or_exc)
backoff/py.typed ADDED
File without changes
backoff/types.py ADDED
@@ -0,0 +1,6 @@
1
+ # coding:utf-8
2
+ from ._typing import Details
3
+
4
+ __all__ = [
5
+ 'Details'
6
+ ]
@@ -0,0 +1,417 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-backoff
3
+ Version: 2.2.2rc1
4
+ Summary: Function decoration for backoff and retry
5
+ Project-URL: Documentation, https://python-backoff.github.io/backoff
6
+ Project-URL: Repository, https://github.com/python-backoff/backoff
7
+ Author-email: Bob Green <rgreen@aquent.com>
8
+ Maintainer-email: Edgar Ramírez-Mondragón <edgarrm358@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: backoff,decorators,retry
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.7
20
+ Classifier: Programming Language :: Python :: 3.8
21
+ Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Programming Language :: Python :: 3.14
27
+ Classifier: Programming Language :: Python :: 3.15
28
+ Classifier: Topic :: Internet :: WWW/HTTP
29
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
+ Classifier: Topic :: Utilities
31
+ Requires-Python: >=3.7
32
+ Description-Content-Type: text/x-rst
33
+
34
+ backoff
35
+ =======
36
+
37
+ .. image:: https://github.com/python-backoff/backoff/actions/workflows/ci.yaml/badge.svg
38
+ :target: https://github.com/python-backoff/backoff/actions/workflows/ci.yaml
39
+ .. image:: https://github.com/python-backoff/backoff/workflows/CodeQL/badge.svg
40
+ :target: https://github.com/python-backoff/backoff/actions/workflows/codeql-analysis.yml
41
+ .. image:: https://img.shields.io/pypi/v/backoff.svg
42
+ :target: https://pypi.python.org/pypi/backoff
43
+ .. image:: https://img.shields.io/github/license/python-backoff/backoff
44
+ :target: https://github.com/python-backoff/backoff/blob/main/LICENSE
45
+
46
+ **Function decoration for backoff and retry**
47
+
48
+ This module provides function decorators which can be used to wrap a
49
+ function such that it will be retried until some condition is met. It
50
+ is meant to be of use when accessing unreliable resources with the
51
+ potential for intermittent failures i.e. network resources and external
52
+ APIs. Somewhat more generally, it may also be of use for dynamically
53
+ polling resources for externally generated content.
54
+
55
+ Decorators support both regular functions for synchronous code and
56
+ `asyncio <https://docs.python.org/3/library/asyncio.html>`__'s coroutines
57
+ for asynchronous code.
58
+
59
+ Examples
60
+ ========
61
+
62
+ Since Kenneth Reitz's `requests <http://python-requests.org>`_ module
63
+ has become a defacto standard for synchronous HTTP clients in Python,
64
+ networking examples below are written using it, but it is in no way required
65
+ by the backoff module.
66
+
67
+ @backoff.on_exception
68
+ ---------------------
69
+
70
+ The ``on_exception`` decorator is used to retry when a specified exception
71
+ is raised. Here's an example using exponential backoff when any
72
+ ``requests`` exception is raised:
73
+
74
+ .. code-block:: python
75
+
76
+ @backoff.on_exception(backoff.expo,
77
+ requests.exceptions.RequestException)
78
+ def get_url(url):
79
+ return requests.get(url)
80
+
81
+ The decorator will also accept a tuple of exceptions for cases where
82
+ the same backoff behavior is desired for more than one exception type:
83
+
84
+ .. code-block:: python
85
+
86
+ @backoff.on_exception(backoff.expo,
87
+ (requests.exceptions.Timeout,
88
+ requests.exceptions.ConnectionError))
89
+ def get_url(url):
90
+ return requests.get(url)
91
+
92
+ **Give Up Conditions**
93
+
94
+ Optional keyword arguments can specify conditions under which to give
95
+ up.
96
+
97
+ The keyword argument ``max_time`` specifies the maximum amount
98
+ of total time in seconds that can elapse before giving up.
99
+
100
+ .. code-block:: python
101
+
102
+ @backoff.on_exception(backoff.expo,
103
+ requests.exceptions.RequestException,
104
+ max_time=60)
105
+ def get_url(url):
106
+ return requests.get(url)
107
+
108
+
109
+ Keyword argument ``max_tries`` specifies the maximum number of calls
110
+ to make to the target function before giving up.
111
+
112
+ .. code-block:: python
113
+
114
+ @backoff.on_exception(backoff.expo,
115
+ requests.exceptions.RequestException,
116
+ max_tries=8,
117
+ jitter=None)
118
+ def get_url(url):
119
+ return requests.get(url)
120
+
121
+
122
+ In some cases the raised exception instance itself may need to be
123
+ inspected in order to determine if it is a retryable condition. The
124
+ ``giveup`` keyword arg can be used to specify a function which accepts
125
+ the exception and returns a truthy value if the exception should not
126
+ be retried:
127
+
128
+ .. code-block:: python
129
+
130
+ def fatal_code(e):
131
+ return 400 <= e.response.status_code < 500
132
+
133
+ @backoff.on_exception(backoff.expo,
134
+ requests.exceptions.RequestException,
135
+ max_time=300,
136
+ giveup=fatal_code)
137
+ def get_url(url):
138
+ return requests.get(url)
139
+
140
+ By default, when a give up event occurs, the exception in question is reraised
141
+ and so code calling an `on_exception`-decorated function may still
142
+ need to do exception handling. This behavior can optionally be disabled
143
+ using the `raise_on_giveup` keyword argument.
144
+
145
+ In the code below, `requests.exceptions.RequestException` will not be raised
146
+ when giveup occurs. Note that the decorated function will return `None` in this
147
+ case, regardless of the logic in the `on_exception` handler.
148
+
149
+ .. code-block:: python
150
+
151
+ def fatal_code(e):
152
+ return 400 <= e.response.status_code < 500
153
+
154
+ @backoff.on_exception(backoff.expo,
155
+ requests.exceptions.RequestException,
156
+ max_time=300,
157
+ raise_on_giveup=False,
158
+ giveup=fatal_code)
159
+ def get_url(url):
160
+ return requests.get(url)
161
+
162
+ This is useful for non-mission critical code where you still wish to retry
163
+ the code inside of `backoff.on_exception` but wish to proceed with execution
164
+ even if all retries fail.
165
+
166
+ @backoff.on_predicate
167
+ ---------------------
168
+
169
+ The ``on_predicate`` decorator is used to retry when a particular
170
+ condition is true of the return value of the target function. This may
171
+ be useful when polling a resource for externally generated content.
172
+
173
+ Here's an example which uses a fibonacci sequence backoff when the
174
+ return value of the target function is the empty list:
175
+
176
+ .. code-block:: python
177
+
178
+ @backoff.on_predicate(backoff.fibo, lambda x: x == [], max_value=13)
179
+ def poll_for_messages(queue):
180
+ return queue.get()
181
+
182
+ Extra keyword arguments are passed when initializing the
183
+ wait generator, so the ``max_value`` param above is passed as a keyword
184
+ arg when initializing the fibo generator.
185
+
186
+ When not specified, the predicate param defaults to the falsey test,
187
+ so the above can more concisely be written:
188
+
189
+ .. code-block:: python
190
+
191
+ @backoff.on_predicate(backoff.fibo, max_value=13)
192
+ def poll_for_message(queue):
193
+ return queue.get()
194
+
195
+ More simply, a function which continues polling every second until it
196
+ gets a non-falsey result could be defined like like this:
197
+
198
+ .. code-block:: python
199
+
200
+ @backoff.on_predicate(backoff.constant, jitter=None, interval=1)
201
+ def poll_for_message(queue):
202
+ return queue.get()
203
+
204
+ The jitter is disabled in order to keep the polling frequency fixed.
205
+
206
+ @backoff.runtime
207
+ ----------------
208
+
209
+ You can also use the ``backoff.runtime`` generator to make use of the
210
+ return value or thrown exception of the decorated method.
211
+
212
+ For example, to use the value in the ``Retry-After`` header of the response:
213
+
214
+ .. code-block:: python
215
+
216
+ @backoff.on_predicate(
217
+ backoff.runtime,
218
+ predicate=lambda r: r.status_code == 429,
219
+ value=lambda r: int(r.headers.get("Retry-After")),
220
+ jitter=None,
221
+ )
222
+ def get_url():
223
+ return requests.get(url)
224
+
225
+ Jitter
226
+ ------
227
+
228
+ A jitter algorithm can be supplied with the ``jitter`` keyword arg to
229
+ either of the backoff decorators. This argument should be a function
230
+ accepting the original unadulterated backoff value and returning it's
231
+ jittered counterpart.
232
+
233
+ As of version 1.2, the default jitter function ``backoff.full_jitter``
234
+ implements the 'Full Jitter' algorithm as defined in the AWS
235
+ Architecture Blog's `Exponential Backoff And Jitter
236
+ <https://www.awsarchitectureblog.com/2015/03/backoff.html>`_ post.
237
+ Note that with this algorithm, the time yielded by the wait generator
238
+ is actually the *maximum* amount of time to wait.
239
+
240
+ Previous versions of backoff defaulted to adding some random number of
241
+ milliseconds (up to 1s) to the raw sleep value. If desired, this
242
+ behavior is now available as ``backoff.random_jitter``.
243
+
244
+ Using multiple decorators
245
+ -------------------------
246
+
247
+ The backoff decorators may also be combined to specify different
248
+ backoff behavior for different cases:
249
+
250
+ .. code-block:: python
251
+
252
+ @backoff.on_predicate(backoff.fibo, max_value=13)
253
+ @backoff.on_exception(backoff.expo,
254
+ requests.exceptions.HTTPError,
255
+ max_time=60)
256
+ @backoff.on_exception(backoff.expo,
257
+ requests.exceptions.Timeout,
258
+ max_time=300)
259
+ def poll_for_message(queue):
260
+ return queue.get()
261
+
262
+
263
+ Runtime Configuration
264
+ ---------------------
265
+
266
+ The decorator functions ``on_exception`` and ``on_predicate`` are
267
+ generally evaluated at import time. This is fine when the keyword args
268
+ are passed as constant values, but suppose we want to consult a
269
+ dictionary with configuration options that only become available at
270
+ runtime. The relevant values are not available at import time. Instead,
271
+ decorator functions can be passed callables which are evaluated at
272
+ runtime to obtain the value:
273
+
274
+ .. code-block:: python
275
+
276
+ def lookup_max_time():
277
+ # pretend we have a global reference to 'app' here
278
+ # and that it has a dictionary-like 'config' property
279
+ return app.config["BACKOFF_MAX_TIME"]
280
+
281
+ @backoff.on_exception(backoff.expo,
282
+ ValueError,
283
+ max_time=lookup_max_time)
284
+
285
+ Event handlers
286
+ --------------
287
+
288
+ Both backoff decorators optionally accept event handler functions
289
+ using the keyword arguments ``on_success``, ``on_backoff``, and ``on_giveup``.
290
+ This may be useful in reporting statistics or performing other custom
291
+ logging.
292
+
293
+ Handlers must be callables with a unary signature accepting a dict
294
+ argument. This dict contains the details of the invocation. Valid keys
295
+ include:
296
+
297
+ * *target*: reference to the function or method being invoked
298
+ * *args*: positional arguments to func
299
+ * *kwargs*: keyword arguments to func
300
+ * *tries*: number of invocation tries so far
301
+ * *elapsed*: elapsed time in seconds so far
302
+ * *wait*: seconds to wait (``on_backoff`` handler only)
303
+ * *value*: value triggering backoff (``on_predicate`` decorator only)
304
+
305
+ A handler which prints the details of the backoff event could be
306
+ implemented like so:
307
+
308
+ .. code-block:: python
309
+
310
+ def backoff_hdlr(details):
311
+ print ("Backing off {wait:0.1f} seconds after {tries} tries "
312
+ "calling function {target} with args {args} and kwargs "
313
+ "{kwargs}".format(**details))
314
+
315
+ @backoff.on_exception(backoff.expo,
316
+ requests.exceptions.RequestException,
317
+ on_backoff=backoff_hdlr)
318
+ def get_url(url):
319
+ return requests.get(url)
320
+
321
+ **Multiple handlers per event type**
322
+
323
+ In all cases, iterables of handler functions are also accepted, which
324
+ are called in turn. For example, you might provide a simple list of
325
+ handler functions as the value of the ``on_backoff`` keyword arg:
326
+
327
+ .. code-block:: python
328
+
329
+ @backoff.on_exception(backoff.expo,
330
+ requests.exceptions.RequestException,
331
+ on_backoff=[backoff_hdlr1, backoff_hdlr2])
332
+ def get_url(url):
333
+ return requests.get(url)
334
+
335
+ **Getting exception info**
336
+
337
+ In the case of the ``on_exception`` decorator, all ``on_backoff`` and
338
+ ``on_giveup`` handlers are called from within the except block for the
339
+ exception being handled. Therefore exception info is available to the
340
+ handler functions via the python standard library, specifically
341
+ ``sys.exc_info()`` or the ``traceback`` module. The exception is also
342
+ available at the *exception* key in the `details` dict passed to the
343
+ handlers.
344
+
345
+ Asynchronous code
346
+ -----------------
347
+
348
+ Backoff supports asynchronous execution in Python 3.5 and above.
349
+
350
+ To use backoff in asynchronous code based on
351
+ `asyncio <https://docs.python.org/3/library/asyncio.html>`__
352
+ you simply need to apply ``backoff.on_exception`` or ``backoff.on_predicate``
353
+ to coroutines.
354
+ You can also use coroutines for the ``on_success``, ``on_backoff``, and
355
+ ``on_giveup`` event handlers, with the interface otherwise being identical.
356
+
357
+ The following examples use `aiohttp <https://aiohttp.readthedocs.io/>`__
358
+ asynchronous HTTP client/server library.
359
+
360
+ .. code-block:: python
361
+
362
+ @backoff.on_exception(backoff.expo, aiohttp.ClientError, max_time=60)
363
+ async def get_url(url):
364
+ async with aiohttp.ClientSession(raise_for_status=True) as session:
365
+ async with session.get(url) as response:
366
+ return await response.text()
367
+
368
+ Logging configuration
369
+ ---------------------
370
+
371
+ By default, backoff and retry attempts are logged to the 'backoff'
372
+ logger. By default, this logger is configured with a NullHandler, so
373
+ there will be nothing output unless you configure a handler.
374
+ Programmatically, this might be accomplished with something as simple
375
+ as:
376
+
377
+ .. code-block:: python
378
+
379
+ logging.getLogger('backoff').addHandler(logging.StreamHandler())
380
+
381
+ The default logging level is INFO, which corresponds to logging
382
+ anytime a retry event occurs. If you would instead like to log
383
+ only when a giveup event occurs, set the logger level to ERROR.
384
+
385
+ .. code-block:: python
386
+
387
+ logging.getLogger('backoff').setLevel(logging.ERROR)
388
+
389
+ It is also possible to specify an alternate logger with the ``logger``
390
+ keyword argument. If a string value is specified the logger will be
391
+ looked up by name.
392
+
393
+ .. code-block:: python
394
+
395
+ @backoff.on_exception(backoff.expo,
396
+ requests.exceptions.RequestException,
397
+ logger='my_logger')
398
+ # ...
399
+
400
+ It is also supported to specify a Logger (or LoggerAdapter) object
401
+ directly.
402
+
403
+ .. code-block:: python
404
+
405
+ my_logger = logging.getLogger('my_logger')
406
+ my_handler = logging.StreamHandler()
407
+ my_logger.addHandler(my_handler)
408
+ my_logger.setLevel(logging.ERROR)
409
+
410
+ @backoff.on_exception(backoff.expo,
411
+ requests.exceptions.RequestException,
412
+ logger=my_logger)
413
+ # ...
414
+
415
+ Default logging can be disabled all together by specifying
416
+ ``logger=None``. In this case, if desired alternative logging behavior
417
+ could be defined by using custom event handlers.
@@ -0,0 +1,14 @@
1
+ backoff/__init__.py,sha256=cxmsrw6PUsF2VsUnaX-gyKsJ5If3iKVIKoVU5Fg2njc,928
2
+ backoff/_async.py,sha256=gX-MFZWs1-6dyUsohHtJxAP_Zg0if5n8MSmHyNFTB5I,6790
3
+ backoff/_common.py,sha256=8s3_5AJH8hiHd9GR2PdKqiaeE2sdEUoyf6cW9OCo1F8,3478
4
+ backoff/_decorator.py,sha256=Vx9IlrmA22tjNgo-YN4FHqcC0W54wL-4Ug4dTdjUHZ8,9815
5
+ backoff/_jitter.py,sha256=LjJShpjryk9sWBCWiz-3UX1DJCx6rebNJ5Bf3nPMlYQ,782
6
+ backoff/_sync.py,sha256=DT_ktufPPb0nut9WCAKe5UE7sTYRl9f93PRPX6jP8ro,4214
7
+ backoff/_typing.py,sha256=K4xjbkoQUuKt5E1cEAUgIPrhrV2lWD3BXmgu8_Mv7rQ,1422
8
+ backoff/_wait_gen.py,sha256=hObFPeXDXdzcvMhkknya3nlSKYoCAMT5LFoto25emuI,3241
9
+ backoff/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ backoff/types.py,sha256=4DGG6Ltcz0wVfXrk0YBOnp_oPpcki4c0BOnodRhgoqg,73
11
+ python_backoff-2.2.2rc1.dist-info/METADATA,sha256=NxbuUUJetGK9vIbuDh1dLf3N-yD08J8fxxvtpwOEkRk,14951
12
+ python_backoff-2.2.2rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
+ python_backoff-2.2.2rc1.dist-info/licenses/LICENSE,sha256=KmtNX4hNTXob8E6n3xlEzxKzLjWnmobQoHWi0_QPuaw,1077
14
+ python_backoff-2.2.2rc1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 litl, LLC.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.