plexus-python-common 1.1.87__py3-none-any.whl → 1.1.89__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.
@@ -21,7 +21,8 @@ __all__ = [
21
21
 
22
22
  def const[T](value: T) -> Callable[..., T]:
23
23
  """
24
- Returns a function that always returns the specified ``value``, regardless of the input arguments.
24
+ Returns a function that always returns the specified ``value``, regardless of the input
25
+ arguments.
25
26
 
26
27
  :param value: The constant value to return.
27
28
  :return: A function that takes any arguments and returns ``value``.
@@ -65,15 +66,17 @@ def second[V]() -> Callable[[tuple[Any, V]], V]:
65
66
 
66
67
  def packed[R](func: Callable[..., R]) -> Callable[[tuple[Any, ...]], R]:
67
68
  """
68
- Wraps a function to accept its arguments as a single tuple, unpacking them when called. This is useful for
69
- scenarios where arguments are naturally grouped in tuples, such as when working with data structures like maps or
70
- lists of tuples, or when interfacing with APIs that provide arguments in tuple form.
69
+ Wraps a function to accept its arguments as a single tuple, unpacking them when called. This is
70
+ useful for scenarios where arguments are naturally grouped in tuples, such as when working with
71
+ data structures like maps or lists of tuples, or when interfacing with APIs that provide
72
+ arguments in tuple form.
71
73
 
72
74
  >>> data = [(1, 2), (3, 4), (5, 6)]
73
75
  >>> sums = map(packed(lambda x, y: x + y), data)
74
76
 
75
77
  :param func: The function to wrap.
76
- :return: A function that takes a tuple of arguments and calls the original function with them unpacked.
78
+ :return: A function that takes a tuple of arguments and calls the original function with them
79
+ unpacked.
77
80
  """
78
81
 
79
82
  @functools.wraps(func)
@@ -85,7 +88,8 @@ def packed[R](func: Callable[..., R]) -> Callable[[tuple[Any, ...]], R]:
85
88
 
86
89
  def identity[T](instance: T) -> T:
87
90
  """
88
- Returns the input ``instance`` unchanged. This is a utility function often used as a default or placeholder.
91
+ Returns the input ``instance`` unchanged. This is a utility function often used as a default or
92
+ placeholder.
89
93
 
90
94
  :param instance: The value to return.
91
95
  :return: The same value as provided in ``instance``.
@@ -130,9 +134,10 @@ def composable[T, R](func: Callable[[T], R]) -> Composable[T, R]:
130
134
  return func
131
135
 
132
136
 
133
- def singleton[R](tar: Callable[..., R] = None):
137
+ def singleton[R](tar: Callable[..., R] | None = None):
134
138
  """
135
- Decorator to ensure a function or class is only instantiated once. Subsequent calls return the same instance.
139
+ Decorator to ensure a function or class is only instantiated once. Subsequent calls return the
140
+ same instance.
136
141
 
137
142
  :param tar: The target callable to decorate.
138
143
  :return: The singleton instance of the callable.
@@ -155,9 +160,10 @@ def singleton[R](tar: Callable[..., R] = None):
155
160
  return decorator if tar is None else decorator(tar)
156
161
 
157
162
 
158
- def memorized[R](tar: Callable[..., R] = None, *, ordered: bool = False, typed: bool = False):
163
+ def memorized[R](tar: Callable[..., R] | None = None, *, ordered: bool = False, typed: bool = False):
159
164
  """
160
- Decorator to cache the results of a function based on its arguments. Supports options for argument order and type.
165
+ Decorator to cache the results of a function based on its arguments. Supports options for
166
+ argument order and type.
161
167
 
162
168
  :param tar: The target callable to decorate.
163
169
  :param ordered: If ``True``, keyword argument order is significant in the cache key.
@@ -198,7 +204,7 @@ def memorized[R](tar: Callable[..., R] = None, *, ordered: bool = False, typed:
198
204
  return decorator if tar is None else decorator(tar)
199
205
 
200
206
 
201
- def lazy[R](tar: Callable[..., R] = None):
207
+ def lazy[R](tar: Callable[..., R] | None = None):
202
208
  """
203
209
  Decorator to defer the execution of a function until its result is explicitly requested.
204
210
 
@@ -219,10 +225,10 @@ def lazy[R](tar: Callable[..., R] = None):
219
225
  return decorator if tar is None else decorator(tar)
220
226
 
221
227
 
222
- def unique_returns[R](tar: Callable[..., R] = None, *, max_trials: int | None = None):
228
+ def unique_returns[R](tar: Callable[..., R] | None = None, *, max_trials: int | None = None):
223
229
  """
224
- Decorator to ensure a function produces unique return values. If no unique value is found within max_trials,
225
- raises an error.
230
+ Decorator to ensure a function produces unique return values. If no unique value is found
231
+ within ``max_trials``, raises an error.
226
232
 
227
233
  :param tar: The target callable to decorate.
228
234
  :param max_trials: The maximum number of attempts to find a unique return value. If ``None``,
@@ -255,9 +261,9 @@ def unique_returns[R](tar: Callable[..., R] = None, *, max_trials: int | None =
255
261
 
256
262
  class ChainableResult[T, R]:
257
263
  """
258
- A proxy object that holds a result of type R and delegates attribute access to a chained object of type C.
259
- This is useful for chaining operations that produce intermediate results while still allowing access to the
260
- original object.
264
+ A proxy object that holds a result of type ``R`` and delegates attribute access to a chained
265
+ object of type ``T``. This is useful for chaining operations that produce intermediate results
266
+ while still allowing access to the original object.
261
267
  """
262
268
 
263
269
  def __init__(self, target: T, result: R):
@@ -105,13 +105,14 @@ def __get_logger_caller_module_name() -> str:
105
105
 
106
106
  :return: The name of the caller's module, or an empty string if not found.
107
107
  """
108
- frames = inspect.stack()
109
- if len(frames) < 3:
110
- return "" # cause degeneration to root logger
111
-
112
- # 0: this frame, 1: caller frame (logger), 2: caller's caller frame (logging function)
113
- caller_frame = frames[2]
114
- module = inspect.getmodule(caller_frame[0])
115
- if module is None:
116
- return ""
117
- return module.__name__
108
+ frame = inspect.currentframe()
109
+ try:
110
+ # This helper -> public logging function -> caller.
111
+ caller_frame = frame.f_back.f_back if frame is not None and frame.f_back is not None else None
112
+ if caller_frame is None:
113
+ return "" # cause degeneration to root logger
114
+ module = inspect.getmodule(caller_frame)
115
+ return "" if module is None else module.__name__
116
+ finally:
117
+ # Frame references can otherwise participate in reference cycles.
118
+ del frame
@@ -1,9 +1,9 @@
1
1
  import abc
2
+ import functools
2
3
  import time
3
4
  from collections.abc import Callable
4
5
  from typing import Any
5
6
 
6
- from plexus.common.utils.dtutils import dt_utc_now
7
7
  from plexus.common.utils.randutils import randomizer
8
8
 
9
9
  __all__ = [
@@ -24,7 +24,7 @@ class Attempt(object):
24
24
  next_wait: int,
25
25
  start_ts: float,
26
26
  check_ts: float,
27
- last_exception: Exception,
27
+ last_exception: Exception | None = None,
28
28
  ):
29
29
  """
30
30
  Represents an attempt in retrying.
@@ -87,10 +87,11 @@ class RetryWrapper(object):
87
87
  def __init__(
88
88
  self,
89
89
  target: Callable[..., Any] | Retry,
90
- wait: int = None,
91
- wait_func: Callable[[int], int] = None,
92
- retrials: int = None,
93
- timeout: int = None,
90
+ wait: int | None = None,
91
+ wait_func: Callable[[int], int] | None = None,
92
+ retrials: int | None = None,
93
+ timeout: int | None = None,
94
+ exceptions: type[Exception] | tuple[type[Exception], ...] = Exception,
94
95
  ):
95
96
  """
96
97
  Retry executor that wraps a callable or ``Retry`` instance, providing flexible retry strategies including fixed,
@@ -101,12 +102,27 @@ class RetryWrapper(object):
101
102
  :param wait_func: Function to determine wait time based on attempt number.
102
103
  :param retrials: Maximum number of retrials (``None`` for unlimited).
103
104
  :param timeout: Maximum total time (in seconds) allowed for all attempts (``None`` for unlimited).
105
+ :param exceptions: Exceptions to catch during retrials.
104
106
  """
107
+ if wait is not None and wait < 0:
108
+ raise ValueError("'wait' must be greater than or equal to zero")
109
+ if retrials is not None and retrials < 0:
110
+ raise ValueError("'retrials' must be greater than or equal to zero")
111
+ if timeout is not None and timeout < 0:
112
+ raise ValueError("'timeout' must be greater than or equal to zero")
113
+ if isinstance(exceptions, type):
114
+ exceptions = (exceptions,)
115
+ if not exceptions or not all(isinstance(exc, type) and issubclass(exc, Exception) for exc in exceptions):
116
+ raise TypeError("'exceptions' must contain 'Exception' subclasses")
117
+
105
118
  self.target = target
106
119
  self.wait = wait
107
120
  self.wait_func = wait_func
108
121
  self.retrials = retrials
109
122
  self.timeout = timeout
123
+ self.exceptions = exceptions
124
+ if not isinstance(target, Retry) and hasattr(target, "__name__"):
125
+ functools.update_wrapper(self, target)
110
126
 
111
127
  def __call__(self, *args, **kwargs):
112
128
  """
@@ -118,7 +134,7 @@ class RetryWrapper(object):
118
134
  """
119
135
  return self.run(*args, **kwargs)
120
136
 
121
- def next_wait(self, attempt_number: int):
137
+ def next_wait(self, attempt_number: int) -> int:
122
138
  """
123
139
  Determines the wait time before the next retry attempt based on the configured strategy.
124
140
 
@@ -126,7 +142,7 @@ class RetryWrapper(object):
126
142
  :return: The wait time in seconds, or ``None`` if not applicable.
127
143
  """
128
144
  if attempt_number <= 0:
129
- return None
145
+ return 0
130
146
  if self.wait is not None:
131
147
  return self.wait
132
148
  if self.wait_func is not None:
@@ -140,7 +156,7 @@ class RetryWrapper(object):
140
156
  :param start_ts: The start timestamp of the first attempt.
141
157
  :return: Tuple (``True`` if within timeout, current timestamp).
142
158
  """
143
- current_ts = dt_utc_now().timestamp()
159
+ current_ts = time.monotonic()
144
160
  if self.timeout is None:
145
161
  return True, current_ts
146
162
  return current_ts < start_ts + self.timeout, current_ts
@@ -156,7 +172,7 @@ class RetryWrapper(object):
156
172
  :raises RuntimeError: If all attempts fail or timeout is reached.
157
173
  """
158
174
  attempt_number = 0
159
- start_ts = dt_utc_now().timestamp()
175
+ start_ts = time.monotonic()
160
176
  last_exception = None
161
177
 
162
178
  while self.retrials is None or attempt_number <= self.retrials:
@@ -182,33 +198,55 @@ class RetryWrapper(object):
182
198
  return result
183
199
  else:
184
200
  return self.target(*args, **kwargs)
185
- except Exception as e:
201
+ except self.exceptions as e:
186
202
  last_exception = e
187
203
  if isinstance(self.target, Retry):
188
204
  self.target.on_failure(attempt, e)
189
- time.sleep(self.next_wait(attempt_number))
205
+ has_retrial = self.retrials is None or attempt_number <= self.retrials
206
+ if not has_retrial:
207
+ break
208
+
209
+ wait = self.next_wait(attempt_number)
210
+ if self.timeout is not None:
211
+ remaining = start_ts + self.timeout - time.monotonic()
212
+ if remaining <= 0:
213
+ break
214
+ wait = min(wait, remaining)
215
+ time.sleep(wait)
190
216
 
191
217
  raise RuntimeError(
192
218
  f"failed to execute target '{self.target}' after {attempt_number} attempts") from last_exception
193
219
 
194
220
 
195
- def retry(wait: int = None, retrials: int = None, timeout: int = None):
221
+ def retry(
222
+ wait: int | None = None,
223
+ retrials: int | None = None,
224
+ timeout: int | None = None,
225
+ exceptions: type[Exception] | tuple[type[Exception], ...] = Exception,
226
+ ):
196
227
  """
197
228
  Decorator to apply fixed wait retry logic to a function or callable.
198
229
 
199
230
  :param wait: Fixed wait time (in seconds) between retrials.
200
231
  :param retrials: Maximum number of retrials (``None`` for unlimited).
201
232
  :param timeout: Maximum total time (in seconds) allowed for all attempts (``None`` for unlimited).
233
+ :param exceptions: Exceptions to apply to retrials.
202
234
  :return: A decorated function with retry logic.
203
235
  """
204
236
 
205
237
  def wrapper(target):
206
- return RetryWrapper(target, wait=wait, retrials=retrials, timeout=timeout)
238
+ return RetryWrapper(target, wait=wait, retrials=retrials, timeout=timeout, exceptions=exceptions)
207
239
 
208
240
  return wrapper
209
241
 
210
242
 
211
- def retry_exponent(wait_init: int, wait_max: int, retrials: int = None, timeout: int = None):
243
+ def retry_exponent(
244
+ wait_init: int,
245
+ wait_max: int,
246
+ retrials: int | None = None,
247
+ timeout: int | None = None,
248
+ exceptions: type[Exception] | tuple[type[Exception], ...] = Exception,
249
+ ):
212
250
  """
213
251
  Decorator to apply exponential backoff retry logic to a function or callable.
214
252
 
@@ -216,21 +254,34 @@ def retry_exponent(wait_init: int, wait_max: int, retrials: int = None, timeout:
216
254
  :param wait_max: Maximum wait time for exponential backoff.
217
255
  :param retrials: Maximum number of retrials (``None`` for unlimited).
218
256
  :param timeout: Maximum total time (in seconds) allowed for all attempts (``None`` for unlimited).
257
+ :param exceptions: Exceptions to apply to retrials.
219
258
  :return: A decorated function with retry logic.
220
259
  """
221
260
 
261
+ if wait_init < 0 or wait_max < 0:
262
+ raise ValueError("'wait_init' and 'wait_max' must be greater than or equal to zero")
263
+ if wait_init > wait_max:
264
+ raise ValueError("'wait_init' must be less than or equal to 'wait_max'")
265
+
222
266
  def wrapper(target):
223
267
  return RetryWrapper(
224
268
  target,
225
269
  wait_func=lambda x: min(wait_init * (2 ** (x - 1)), wait_max),
226
270
  retrials=retrials,
227
271
  timeout=timeout,
272
+ exceptions=exceptions,
228
273
  )
229
274
 
230
275
  return wrapper
231
276
 
232
277
 
233
- def retry_random(wait_min: int, wait_max: int, retrials: int = None, timeout: int = None):
278
+ def retry_random(
279
+ wait_min: int,
280
+ wait_max: int,
281
+ retrials: int | None = None,
282
+ timeout: int | None = None,
283
+ exceptions: type[Exception] | tuple[type[Exception], ...] = Exception,
284
+ ):
234
285
  """
235
286
  Decorator to apply random wait retry logic to a function or callable.
236
287
 
@@ -238,15 +289,22 @@ def retry_random(wait_min: int, wait_max: int, retrials: int = None, timeout: in
238
289
  :param wait_max: Maximum wait time for random backoff.
239
290
  :param retrials: Maximum number of retrials (``None`` for unlimited).
240
291
  :param timeout: Maximum total time (in seconds) allowed for all attempts (``None`` for unlimited).
292
+ :param exceptions: Exceptions to apply to retrials.
241
293
  :return: A decorated function with retry logic.
242
294
  """
243
295
 
296
+ if wait_min < 0 or wait_max < 0:
297
+ raise ValueError("'wait_min' and 'wait_max' must be greater than or equal to zero")
298
+ if wait_min > wait_max:
299
+ raise ValueError("'wait_min' must be less than or equal to 'wait_max'")
300
+
244
301
  def wrapper(target):
245
302
  return RetryWrapper(
246
303
  target,
247
304
  wait_func=lambda x: randomizer().next_int(wait_min, wait_max),
248
305
  retrials=retrials,
249
306
  timeout=timeout,
307
+ exceptions=exceptions,
250
308
  )
251
309
 
252
310
  return wrapper
@@ -66,7 +66,7 @@ def parse_bool(v: Any) -> bool:
66
66
  elif isinstance(v, bool):
67
67
  return v
68
68
  else:
69
- raise ValueError(f"type '{type(v)}' is not convertible to bool")
69
+ raise ValueError(f"type '{type(v)}' is not convertible to 'bool'")
70
70
 
71
71
 
72
72
  def parse_bool_or(v: Any, default: bool | None = False) -> bool | None:
@@ -1,4 +1,5 @@
1
1
  import abc
2
+ import datetime
2
3
  from decimal import Decimal
3
4
  from typing import Any, Protocol
4
5
 
@@ -69,7 +70,9 @@ def nested_approx(expected, rel: float = None, abs: float = None, nan_ok: bool =
69
70
  return ApproxNestedSequenceLike(expected, rel, abs, nan_ok)
70
71
  if isinstance(expected, Decimal):
71
72
  return ApproxDecimal(expected, to_decimal(rel), to_decimal(abs), nan_ok)
72
- return pytest.approx(expected, rel, abs, nan_ok)
73
+ if isinstance(expected, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)):
74
+ return pytest.approx(expected, abs=datetime.timedelta(0))
75
+ return pytest.approx(expected, rel=rel, abs=abs, nan_ok=nan_ok)
73
76
 
74
77
 
75
78
  class MockedCallable(Protocol):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python-common
3
- Version: 1.1.87
3
+ Version: 1.1.89
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -5,19 +5,19 @@ plexus/common/utils/config.py,sha256=Hc35GzKWyKU4jyjsay5JtGhAtQ9GReNWKJEOskJVe3s
5
5
  plexus/common/utils/csvutils.py,sha256=quGN_F-WxtznhCLqst2coXaK-DVgxzwe2FA0qiiskf8,8344
6
6
  plexus/common/utils/dbutils.py,sha256=0GhnW1P1fJsqB4NjS4V6R-oV8d4R-ZZzi-IpiZCRjAs,16575
7
7
  plexus/common/utils/dtutils.py,sha256=A_upNJJJ3q6e3QZ2m5jXkDLwpAtUU9lEBY2MotAV5aY,12229
8
- plexus/common/utils/funcutils.py,sha256=5ciB_kiASUfQelRkKrtNkRru7E7835e5g-8j33Qb8V4,9789
8
+ plexus/common/utils/funcutils.py,sha256=fdWmQ3GG0RjBwe2kgx4BIrsBUIs89saa60VN5lvZb4I,9862
9
9
  plexus/common/utils/iterutils.py,sha256=JL8j8plIoc2hF8D-BY6dOHEBoeoFZ1nLrhw4dFth_lA,30988
10
10
  plexus/common/utils/jsonutils.py,sha256=BDtqkvmWHiVFo4d5nGkmFtC00JQkGLC7ybchrpID5pU,18529
11
- plexus/common/utils/logger.py,sha256=FJaai6Sbchy4wKHcUMUCrrkBcXvIxq4qByERZ_TJBps,3881
11
+ plexus/common/utils/logger.py,sha256=ce8B8CrxeoZnPIWNkNSL4JXFsK_3yXnBPDzxntUwG48,4046
12
12
  plexus/common/utils/numutils.py,sha256=p6Rz1qyCcUru3v1zDy2PM-nds2NWJdL5A_vLmG-kswk,4294
13
13
  plexus/common/utils/pathutils.py,sha256=vUuBZZShkK4oJN_GqWzMYeSGmnUFTcT8NoMgjh1YuUs,10201
14
14
  plexus/common/utils/randutils.py,sha256=Mkba-g2ZnasuhdtBWoVDsP_VuwKMJ8p2UCf6-GF4drg,12852
15
- plexus/common/utils/retry.py,sha256=u5t9Nl8GS8f6bRE3QbJFIfTcSIKXRQo03Psn61hk9AA,8981
15
+ plexus/common/utils/retry.py,sha256=S_IfYrA30ci_sx6_ar0WUkNpKufoWi8xOSiWeYeAemA,11424
16
16
  plexus/common/utils/span.py,sha256=RqgUuupN01xCjm4l5uxrr8BFFrUrytRVGihmENUawpk,6839
17
- plexus/common/utils/strutils.py,sha256=PGOSYRgTnQcP0YXORzeTpiU9RweInxd5f6YcIHm6yiY,5096
18
- plexus/common/utils/testutils.py,sha256=D-EvK-Og8chyiks377PGDjqYid-l4F4Xfq9Q4yzrJtM,5441
17
+ plexus/common/utils/strutils.py,sha256=uB46pPgaO_UMulwfW-hO7QOpCyhB9AdupeWSsJR3wGA,5098
18
+ plexus/common/utils/testutils.py,sha256=AIuGok17mRackiSXO0ZiWXEwlna6Bv2J8ZFN48nypDY,5638
19
19
  plexus/common/utils/typeutils.py,sha256=RVkYkFRgDrx77OHFH7PavMV0AIB0S8ly40rs4g7JWE4,8220
20
- plexus_python_common-1.1.87.dist-info/METADATA,sha256=VBcicurcAqvKiH8HbY_b3ojdAE1kc20d0ChBz3FnvuM,871
21
- plexus_python_common-1.1.87.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
22
- plexus_python_common-1.1.87.dist-info/top_level.txt,sha256=ug_g7CVwaMQuas5UzAXbHUrQvKGCn8ezc6ZNvvRlJOE,7
23
- plexus_python_common-1.1.87.dist-info/RECORD,,
20
+ plexus_python_common-1.1.89.dist-info/METADATA,sha256=ak_tbOiy9nSsTLaWvmqwr6k9kfUxukLOIqbk4rk4nmo,871
21
+ plexus_python_common-1.1.89.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
22
+ plexus_python_common-1.1.89.dist-info/top_level.txt,sha256=ug_g7CVwaMQuas5UzAXbHUrQvKGCn8ezc6ZNvvRlJOE,7
23
+ plexus_python_common-1.1.89.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (83.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5