iker-python-common 1.0.65__tar.gz → 1.0.66__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 (68) hide show
  1. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/PKG-INFO +1 -1
  2. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/config.py +2 -2
  3. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/retry.py +35 -48
  4. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/shutils.py +0 -3
  5. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker_python_common.egg-info/PKG-INFO +1 -1
  6. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/retry_test.py +10 -12
  7. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/.editorconfig +0 -0
  8. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/.github/workflows/pr.yml +0 -0
  9. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/.github/workflows/push.yml +0 -0
  10. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/.gitignore +0 -0
  11. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/MANIFEST.in +0 -0
  12. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/README.md +0 -0
  13. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/VERSION +0 -0
  14. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/pyproject.toml +0 -0
  15. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/config/config.cfg +0 -0
  16. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/csv/data.csv +0 -0
  17. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/csv/data.tsv +0 -0
  18. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/shutils/dir.baz/file.bar.baz +0 -0
  19. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/shutils/dir.baz/file.foo.bar +0 -0
  20. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/shutils/dir.baz/file.foo.baz +0 -0
  21. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/shutils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  22. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  23. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  24. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  25. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/shutils/dir.foo/file.bar +0 -0
  26. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/shutils/dir.foo/file.baz +0 -0
  27. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/resources/unittest/shutils/dir.foo/file.foo +0 -0
  28. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/setup.cfg +0 -0
  29. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/setup.py +0 -0
  30. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/__init__.py +0 -0
  31. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/__init__.py +0 -0
  32. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/argutils.py +0 -0
  33. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/csv.py +0 -0
  34. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/dbutils.py +0 -0
  35. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/dtutils.py +0 -0
  36. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/funcutils.py +0 -0
  37. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/iterutils.py +0 -0
  38. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/jsonutils.py +0 -0
  39. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/logger.py +0 -0
  40. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/numutils.py +0 -0
  41. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/randutils.py +0 -0
  42. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/span.py +0 -0
  43. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/strutils.py +0 -0
  44. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/testutils.py +0 -0
  45. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker/common/utils/typeutils.py +0 -0
  46. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker_python_common.egg-info/SOURCES.txt +0 -0
  47. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker_python_common.egg-info/dependency_links.txt +0 -0
  48. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker_python_common.egg-info/not-zip-safe +0 -0
  49. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker_python_common.egg-info/requires.txt +0 -0
  50. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/src/iker_python_common.egg-info/top_level.txt +0 -0
  51. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_test.py +0 -0
  52. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/__init__.py +0 -0
  53. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/argutils_test.py +0 -0
  54. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/config_test.py +0 -0
  55. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/csv_test.py +0 -0
  56. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/dbutils_test.py +0 -0
  57. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/dtutils_test.py +0 -0
  58. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/funcutils_test.py +0 -0
  59. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/iterutils_test.py +0 -0
  60. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/jsonutils_test.py +0 -0
  61. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/logger_test.py +0 -0
  62. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/numutils_test.py +0 -0
  63. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/randutils_test.py +0 -0
  64. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/shutils_test.py +0 -0
  65. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/span_test.py +0 -0
  66. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/strutils_test.py +0 -0
  67. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/testutils_test.py +0 -0
  68. {iker_python_common-1.0.65 → iker_python_common-1.0.66}/test/iker_tests/common/utils/typeutils_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.65
3
+ Version: 1.0.66
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -31,7 +31,7 @@ class Config(object):
31
31
  self.config_parser.read(self.config_path, encoding="utf-8")
32
32
  return True
33
33
  except IOError as e:
34
- logger.exception("Failed to restore config from file <%s>", self.config_path)
34
+ logger.exception("Failed to restore config from file '%s'", self.config_path)
35
35
  return False
36
36
 
37
37
  def persist(self) -> bool:
@@ -42,7 +42,7 @@ class Config(object):
42
42
  self.config_parser.write(fh)
43
43
  return True
44
44
  except IOError as e:
45
- logger.exception("Failed to persist config to file <%s>", self.config_path)
45
+ logger.exception("Failed to persist config to file '%s'", self.config_path)
46
46
  return False
47
47
 
48
48
  def has_section(self, section: str) -> bool:
@@ -1,9 +1,11 @@
1
1
  import abc
2
- import random
3
2
  import time
3
+ from collections.abc import Callable
4
+ from typing import Any
4
5
 
5
6
  from iker.common.utils import logger
6
7
  from iker.common.utils.dtutils import dt_utc_now
8
+ from iker.common.utils.randutils import randomizer
7
9
 
8
10
  __all__ = [
9
11
  "Attempt",
@@ -68,12 +70,9 @@ class Retry(abc.ABC):
68
70
  class RetryWrapper(object):
69
71
  def __init__(
70
72
  self,
71
- cls,
73
+ target: Callable[..., Any] | Retry,
72
74
  wait: int = None,
73
- wait_exponent_init: int = None,
74
- wait_exponent_max: int = None,
75
- wait_random_min: int = None,
76
- wait_random_max: int = None,
75
+ wait_func: Callable[[int], int] = None,
77
76
  retrials: int = None,
78
77
  timeout: int = None,
79
78
  ):
@@ -81,21 +80,15 @@ class RetryWrapper(object):
81
80
  Retry executor that wraps a callable or ``Retry`` instance, providing flexible retry strategies including fixed,
82
81
  exponential, and random waits.
83
82
 
84
- :param cls: The target callable or ``Retry`` instance to execute.
83
+ :param target: The target callable or ``Retry`` instance to execute.
85
84
  :param wait: Fixed wait time (in seconds) between retrials.
86
- :param wait_exponent_init: Initial wait time for exponential backoff.
87
- :param wait_exponent_max: Maximum wait time for exponential backoff.
88
- :param wait_random_min: Minimum wait time for random backoff.
89
- :param wait_random_max: Maximum wait time for random backoff.
85
+ :param wait_func: Function to determine wait time based on attempt number.
90
86
  :param retrials: Maximum number of retrials (``None`` for unlimited).
91
87
  :param timeout: Maximum total time (in seconds) allowed for all attempts (``None`` for unlimited).
92
88
  """
93
- self.__wrapped = cls
89
+ self.target = target
94
90
  self.wait = wait
95
- self.wait_exponent_init = wait_exponent_init
96
- self.wait_exponent_max = wait_exponent_max
97
- self.wait_random_min = wait_random_min
98
- self.wait_random_max = wait_random_max
91
+ self.wait_func = wait_func
99
92
  self.retrials = retrials
100
93
  self.timeout = timeout
101
94
 
@@ -107,9 +100,9 @@ class RetryWrapper(object):
107
100
  :param kwargs: Keyword arguments for the operation.
108
101
  :return: The result of the operation.
109
102
  """
110
- return self.__run(*args, **kwargs)
103
+ return self.run(*args, **kwargs)
111
104
 
112
- def __next_wait(self, attempt_number: int):
105
+ def next_wait(self, attempt_number: int):
113
106
  """
114
107
  Determines the wait time before the next retry attempt based on the configured strategy.
115
108
 
@@ -118,16 +111,13 @@ class RetryWrapper(object):
118
111
  """
119
112
  if attempt_number <= 0:
120
113
  return None
121
- elif self.wait is not None:
114
+ if self.wait is not None:
122
115
  return self.wait
123
- elif self.wait_exponent_init is not None and self.wait_exponent_max is not None:
124
- return min(self.wait_exponent_init * (2 ** (attempt_number - 1)), self.wait_exponent_max)
125
- elif self.wait_random_min is not None and self.wait_random_max is not None:
126
- return random.randint(self.wait_random_min, self.wait_random_max)
127
- else:
128
- return 0
116
+ if self.wait_func is not None:
117
+ return self.wait_func(attempt_number)
118
+ return 0
129
119
 
130
- def __check_timeout(self, start_ts: float) -> tuple[bool, float]:
120
+ def check_timeout(self, start_ts: float) -> tuple[bool, float]:
131
121
  """
132
122
  Checks if the retry operation has exceeded the configured timeout.
133
123
 
@@ -139,7 +129,7 @@ class RetryWrapper(object):
139
129
  return True, current_ts
140
130
  return current_ts < start_ts + self.timeout, current_ts
141
131
 
142
- def __run(self, *args, **kwargs):
132
+ def run(self, *args, **kwargs):
143
133
  """
144
134
  Runs the retry loop, invoking the wrapped callable or ``Retry`` instance until success, retrials exhausted, or
145
135
  timeout reached.
@@ -156,31 +146,30 @@ class RetryWrapper(object):
156
146
  while self.retrials is None or attempt_number <= self.retrials:
157
147
  attempt_number += 1
158
148
 
159
- check_result, check_ts = self.__check_timeout(start_ts)
149
+ check_result, check_ts = self.check_timeout(start_ts)
160
150
  if not check_result:
161
151
  break
162
152
 
163
153
  attempt = Attempt(
164
154
  attempt_number,
165
- self.__next_wait(attempt_number - 1),
166
- self.__next_wait(attempt_number),
155
+ self.next_wait(attempt_number - 1),
156
+ self.next_wait(attempt_number),
167
157
  start_ts,
168
158
  check_ts,
169
159
  last_exception,
170
160
  )
171
161
  try:
172
- if isinstance(self.__wrapped, Retry):
173
- self.__wrapped.on_attempt(attempt)
174
- return self.__wrapped.execute(*args, **kwargs)
162
+ if isinstance(self.target, Retry):
163
+ self.target.on_attempt(attempt)
164
+ return self.target.execute(*args, **kwargs)
175
165
  else:
176
- return self.__wrapped(*args, **kwargs)
166
+ return self.target(*args, **kwargs)
177
167
  except Exception as e:
178
- logger.exception("Function target <%s> failed on attempt <%d>", self.__wrapped, attempt_number)
168
+ logger.exception("Function target '%s' failed on attempt '%d'", self.target, attempt_number)
179
169
  last_exception = e
180
- time.sleep(self.__next_wait(attempt_number))
170
+ time.sleep(self.next_wait(attempt_number))
181
171
 
182
- raise RuntimeError(
183
- "failed to execute function target <%s> after <%d> attempts" % (self.__wrapped, attempt_number))
172
+ raise RuntimeError(f"failed to execute function target '{self.target}' after '{attempt_number}' attempts")
184
173
 
185
174
 
186
175
  def retry(wait: int = None, retrials: int = None, timeout: int = None):
@@ -199,12 +188,12 @@ def retry(wait: int = None, retrials: int = None, timeout: int = None):
199
188
  return wrapper
200
189
 
201
190
 
202
- def retry_exponent(wait_exponent_init: int, wait_exponent_max: int, retrials: int = None, timeout: int = None):
191
+ def retry_exponent(wait_init: int, wait_max: int, retrials: int = None, timeout: int = None):
203
192
  """
204
193
  Decorator to apply exponential backoff retry logic to a function or callable.
205
194
 
206
- :param wait_exponent_init: Initial wait time for exponential backoff.
207
- :param wait_exponent_max: Maximum wait time for exponential backoff.
195
+ :param wait_init: Initial wait time for exponential backoff.
196
+ :param wait_max: Maximum wait time for exponential backoff.
208
197
  :param retrials: Maximum number of retrials (``None`` for unlimited).
209
198
  :param timeout: Maximum total time (in seconds) allowed for all attempts (``None`` for unlimited).
210
199
  :return: A decorated function with retry logic.
@@ -213,8 +202,7 @@ def retry_exponent(wait_exponent_init: int, wait_exponent_max: int, retrials: in
213
202
  def wrapper(target):
214
203
  return RetryWrapper(
215
204
  target,
216
- wait_exponent_init=wait_exponent_init,
217
- wait_exponent_max=wait_exponent_max,
205
+ wait_func=lambda x: min(wait_init * (2 ** (x - 1)), wait_max),
218
206
  retrials=retrials,
219
207
  timeout=timeout,
220
208
  )
@@ -222,12 +210,12 @@ def retry_exponent(wait_exponent_init: int, wait_exponent_max: int, retrials: in
222
210
  return wrapper
223
211
 
224
212
 
225
- def retry_random(wait_random_min: int, wait_random_max: int, retrials: int = None, timeout: int = None):
213
+ def retry_random(wait_min: int, wait_max: int, retrials: int = None, timeout: int = None):
226
214
  """
227
215
  Decorator to apply random wait retry logic to a function or callable.
228
216
 
229
- :param wait_random_min: Minimum wait time for random backoff.
230
- :param wait_random_max: Maximum wait time for random backoff.
217
+ :param wait_min: Minimum wait time for random backoff.
218
+ :param wait_max: Maximum wait time for random backoff.
231
219
  :param retrials: Maximum number of retrials (``None`` for unlimited).
232
220
  :param timeout: Maximum total time (in seconds) allowed for all attempts (``None`` for unlimited).
233
221
  :return: A decorated function with retry logic.
@@ -236,8 +224,7 @@ def retry_random(wait_random_min: int, wait_random_max: int, retrials: int = Non
236
224
  def wrapper(target):
237
225
  return RetryWrapper(
238
226
  target,
239
- wait_random_min=wait_random_min,
240
- wait_random_max=wait_random_max,
227
+ wait_func=lambda x: randomizer().next_int(wait_min, wait_max),
241
228
  retrials=retrials,
242
229
  timeout=timeout,
243
230
  )
@@ -3,7 +3,6 @@ import os
3
3
  import shutil
4
4
  from typing import Protocol
5
5
 
6
- from iker.common.utils import logger
7
6
  from iker.common.utils.iterutils import last, last_or_none, tail_iter
8
7
  from iker.common.utils.strutils import is_empty
9
8
 
@@ -212,7 +211,6 @@ def run(cmd: str) -> bool:
212
211
  :param cmd: Command to run.
213
212
  :return: ``True`` if the command has been successfully run, ``False`` otherwise.
214
213
  """
215
- logger.debug("Running command: %s", cmd)
216
214
  return os.system(cmd) == 0
217
215
 
218
216
 
@@ -224,7 +222,6 @@ def execute(cmd: str, strip: bool = True) -> str:
224
222
  :param strip: If ``True``, the contents will be stripped.
225
223
  :return: The content from standard output.
226
224
  """
227
- logger.debug("Executing command: %s", cmd)
228
225
  if strip:
229
226
  return os.popen(cmd).read().strip()
230
227
  return os.popen(cmd).read()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.65
3
+ Version: 1.0.66
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -125,11 +125,10 @@ class RetryTest(unittest.TestCase):
125
125
 
126
126
  @ddt.idata(data_retry_exponent)
127
127
  @ddt.unpack
128
- def test_retry_exponent(self, content, wait_exponent_init, wait_exponent_max, retrials):
128
+ def test_retry_exponent(self, content, wait_init, wait_max, retrials):
129
129
  result = []
130
130
 
131
- @retry.retry_exponent(wait_exponent_init=wait_exponent_init, wait_exponent_max=wait_exponent_max,
132
- retrials=retrials)
131
+ @retry.retry_exponent(wait_init=wait_init, wait_max=wait_max, retrials=retrials)
133
132
  def callee(text):
134
133
  result.append(text)
135
134
  raise ValueError("dummy value error")
@@ -147,7 +146,7 @@ class RetryTest(unittest.TestCase):
147
146
 
148
147
  @ddt.idata(data_retry_exponent__on_retry_instance)
149
148
  @ddt.unpack
150
- def test_retry_exponent__on_retry_instance(self, content, wait_exponent_init, wait_exponent_max, retrials):
149
+ def test_retry_exponent__on_retry_instance(self, content, wait_init, wait_max, retrials):
151
150
  result = []
152
151
 
153
152
  class Callee(retry.Retry):
@@ -164,11 +163,10 @@ class RetryTest(unittest.TestCase):
164
163
  callee = Callee()
165
164
 
166
165
  with self.assertRaises(RuntimeError):
167
- retry.retry_exponent(wait_exponent_init, wait_exponent_max, retrials)(callee)(content)
166
+ retry.retry_exponent(wait_init, wait_max, retrials)(callee)(content)
168
167
 
169
168
  self.assertEqual([content for _ in range(retrials + 1)], result)
170
- self.assertTrue(all(
171
- a.next_wait == min(wait_exponent_init * (2 ** (a.number - 1)), wait_exponent_max) for a in callee.attempts))
169
+ self.assertTrue(all(a.next_wait == min(wait_init * (2 ** (a.number - 1)), wait_max) for a in callee.attempts))
172
170
 
173
171
  data_retry_random = [
174
172
  ("dummy-content", 1, 4, 3),
@@ -178,10 +176,10 @@ class RetryTest(unittest.TestCase):
178
176
 
179
177
  @ddt.idata(data_retry_random)
180
178
  @ddt.unpack
181
- def test_retry_random(self, content, wait_random_min, wait_random_max, retrials):
179
+ def test_retry_random(self, content, wait_min, wait_max, retrials):
182
180
  result = []
183
181
 
184
- @retry.retry_random(wait_random_min=wait_random_min, wait_random_max=wait_random_max, retrials=retrials)
182
+ @retry.retry_random(wait_min=wait_min, wait_max=wait_max, retrials=retrials)
185
183
  def callee(text):
186
184
  result.append(text)
187
185
  raise ValueError("dummy value error")
@@ -199,7 +197,7 @@ class RetryTest(unittest.TestCase):
199
197
 
200
198
  @ddt.idata(data_retry_random__on_retry_instance)
201
199
  @ddt.unpack
202
- def test_retry_random__on_retry_instance(self, content, wait_random_min, wait_random_max, retrials):
200
+ def test_retry_random__on_retry_instance(self, content, wait_min, wait_max, retrials):
203
201
  result = []
204
202
 
205
203
  class Callee(retry.Retry):
@@ -216,7 +214,7 @@ class RetryTest(unittest.TestCase):
216
214
  callee = Callee()
217
215
 
218
216
  with self.assertRaises(RuntimeError):
219
- retry.retry_random(wait_random_min, wait_random_max, retrials)(callee)(content)
217
+ retry.retry_random(wait_min, wait_max, retrials)(callee)(content)
220
218
 
221
219
  self.assertEqual([content for _ in range(retrials + 1)], result)
222
- self.assertTrue(all(wait_random_min <= a.next_wait <= wait_random_max for a in callee.attempts))
220
+ self.assertTrue(all(wait_min <= a.next_wait <= wait_max for a in callee.attempts))