dycw-utilities 0.147.3__py3-none-any.whl → 0.148.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.147.3
3
+ Version: 0.148.1
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -14,7 +14,7 @@ Provides-Extra: test
14
14
  Requires-Dist: dycw-pytest-only<2.2,>=2.1.1; extra == 'test'
15
15
  Requires-Dist: hypothesis<6.136,>=6.135.24; extra == 'test'
16
16
  Requires-Dist: pudb<2025.2,>=2025.1; extra == 'test'
17
- Requires-Dist: pytest-asyncio<1.1,>=1.0.0; extra == 'test'
17
+ Requires-Dist: pytest-asyncio<1.2,>=1.1.0; extra == 'test'
18
18
  Requires-Dist: pytest-cov<6.3,>=6.2.1; extra == 'test'
19
19
  Requires-Dist: pytest-instafail<0.6,>=0.5.0; extra == 'test'
20
20
  Requires-Dist: pytest-lazy-fixtures<1.2,>=1.1.4; extra == 'test'
@@ -1,6 +1,6 @@
1
- utilities/__init__.py,sha256=CYx_3txVREoGWejJRJBItmHBOZ94QymP1DKxtNU8fIo,60
1
+ utilities/__init__.py,sha256=iMEViWolW9oRg6PDmC46RTFOqP-O9BCaLj3gJai78wg,60
2
2
  utilities/altair.py,sha256=92E2lCdyHY4Zb-vCw6rEJIsWdKipuu-Tu2ab1ufUfAk,9079
3
- utilities/asyncio.py,sha256=aB0EtUbUJ5ZKQ5ET-Xfyx6wfUJG2G4vihEX0blK4TGE,14964
3
+ utilities/asyncio.py,sha256=z0w3fb-U5Ml5YXVaFFPClizXaQmjDO6YgZg-V9QL0VQ,16021
4
4
  utilities/atomicwrites.py,sha256=xcOWenTBRS0oat3kg7Sqe51AohNThMQ2ixPL7QCG8hw,5795
5
5
  utilities/atools.py,sha256=9im2g8OCf-Iynqa8bAv8N0Ycj9QvrJmGO7yLCZEdgII,986
6
6
  utilities/cachetools.py,sha256=v1-9sXHLdOLiwmkq6NB0OUbxeKBuVVN6wmAWefWoaHI,2744
@@ -12,7 +12,7 @@ utilities/cryptography.py,sha256=_CiK_K6c_-uQuUhsUNjNjTL-nqxAh4_1zTfS11Xe120,972
12
12
  utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
13
13
  utilities/dataclasses.py,sha256=1-REGxtzCo2BbGkjLw5idjbTaiItpOiWvMNbVkJrFXM,32663
14
14
  utilities/enum.py,sha256=IEPMGiNJBcofGPuoGZErf4bMNSBE4SLs32nvm2r81h0,5791
15
- utilities/errors.py,sha256=nC7ZYtxxDBMfrTHtT_MByBfup_wfGQFRo3eDt-0ZPe8,1045
15
+ utilities/errors.py,sha256=k_VvqSi6geTYhlzRRQaGN2j6ZGng-AD8SnX7Znrirm4,1484
16
16
  utilities/eventkit.py,sha256=s9p3WjGc7h7TdbSgVfHrRNNiuOXcQoVQ5sFCbJyqg48,12646
17
17
  utilities/fastapi.py,sha256=3wpd63Tw9paSyy7STpAD7GGe8fLkLaRC6TPCwIGm1BU,1361
18
18
  utilities/fpdf2.py,sha256=776PkEX5xEK-whFOzqaVaQVHPy1Xf01kCSyj7TEp80g,1886
@@ -49,7 +49,7 @@ utilities/platform.py,sha256=Ue9LSxYvg9yUXGKuz5aZoy_qkUEXde-v6B09exgSctU,2813
49
49
  utilities/polars.py,sha256=BgiDryAVOapi41ddfJqN0wYh_sDj8BNEYtPB36LaHdo,71824
50
50
  utilities/polars_ols.py,sha256=Uc9V5kvlWZ5cU93lKZ-cfAKdVFFw81tqwLW9PxtUvMs,5618
51
51
  utilities/postgres.py,sha256=dbQrUFOoV6huNeuYTnSpNhSNq-QhXkBfrJPMtoAGJtY,7877
52
- utilities/pottery.py,sha256=JluZG7SUSw-JnN73_7QV-vJyPrTsct3q86p8eqVUasc,7019
52
+ utilities/pottery.py,sha256=w2X80PXWwzdHdqSYJP6ESrPNNDP3xzpyuJn-fp-Vt3M,5969
53
53
  utilities/pqdm.py,sha256=BTsYPtbKQWwX-iXF4qCkfPG7DPxIB54J989n83bXrIo,3092
54
54
  utilities/psutil.py,sha256=KUlu4lrUw9Zg1V7ZGetpWpGb9DB8l_SSDWGbANFNCPU,2104
55
55
  utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -75,7 +75,7 @@ utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
75
75
  utilities/timer.py,sha256=oXfTii6ymu57niP0BDGZjFD55LEHi2a19kqZKiTgaFQ,2588
76
76
  utilities/traceback.py,sha256=-e1D3cMHJCMbggZVFeKVzyAzHCteEcoPc3-3eY0Dtj8,9187
77
77
  utilities/typed_settings.py,sha256=-mzQP5ZCIGWOhm7nPxlajWQhgtX657HVnRCfUYGKQKs,4433
78
- utilities/types.py,sha256=AssdaYdASdtE0HUsdYcagR9lXdt6Bv0QOqP_USm50CQ,18010
78
+ utilities/types.py,sha256=iDfk_Z96v7cIxPlgGYMap0fYmjgRUJ7uQzPPCQe1odY,18115
79
79
  utilities/typing.py,sha256=Z-_XDaWyT_6wIo3qfNK-hvRlzxP2Jxa9PgXzm5rDYRA,13790
80
80
  utilities/tzdata.py,sha256=fgNVj66yUbCSI_-vrRVzSD3gtf-L_8IEJEPjP_Jel5Y,266
81
81
  utilities/tzlocal.py,sha256=KyCXEgCTjqGFx-389JdTuhMRUaT06U1RCMdWoED-qro,728
@@ -88,8 +88,8 @@ utilities/zoneinfo.py,sha256=oEH-nL3t4h9uawyZqWDtNtDAl6M-CLpLYGI_nI6DulM,1971
88
88
  utilities/pytest_plugins/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
89
89
  utilities/pytest_plugins/pytest_randomly.py,sha256=NXzCcGKbpgYouz5yehKb4jmxmi2SexKKpgF4M65bi10,414
90
90
  utilities/pytest_plugins/pytest_regressions.py,sha256=Iwhfv_OJH7UCPZCfoh7ugZ2Xjqjil-BBBsOb8sDwiGI,1471
91
- dycw_utilities-0.147.3.dist-info/METADATA,sha256=0s33DB1w4KCYCicIAaLfq_aP_xxUUqvwFV9rpelm0R0,1697
92
- dycw_utilities-0.147.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
93
- dycw_utilities-0.147.3.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
94
- dycw_utilities-0.147.3.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
- dycw_utilities-0.147.3.dist-info/RECORD,,
91
+ dycw_utilities-0.148.1.dist-info/METADATA,sha256=loLMrx_55h6ZZhUdA2rWHGRDwCsc94yJto0LWyv8DE8,1697
92
+ dycw_utilities-0.148.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
93
+ dycw_utilities-0.148.1.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
94
+ dycw_utilities-0.148.1.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
+ dycw_utilities-0.148.1.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.147.3"
3
+ __version__ = "0.148.1"
utilities/asyncio.py CHANGED
@@ -35,10 +35,12 @@ from typing import (
35
35
  override,
36
36
  )
37
37
 
38
- from utilities.errors import ImpossibleCaseError
38
+ from utilities.errors import ImpossibleCaseError, is_instance_error
39
39
  from utilities.functions import ensure_int, ensure_not_none, to_bool
40
+ from utilities.logging import get_logger
40
41
  from utilities.random import SYSTEM_RANDOM
41
42
  from utilities.sentinel import Sentinel, sentinel
43
+ from utilities.warnings import suppress_warnings
42
44
  from utilities.whenever import get_now, round_date_or_date_time, to_nanoseconds
43
45
 
44
46
  if TYPE_CHECKING:
@@ -63,6 +65,8 @@ if TYPE_CHECKING:
63
65
  from utilities.types import (
64
66
  Coro,
65
67
  Delta,
68
+ ExceptionTypeLike,
69
+ LoggerOrName,
66
70
  MaybeCallableBool,
67
71
  MaybeType,
68
72
  SupportsKeysAndGetItem,
@@ -340,6 +344,20 @@ class EnhancedTaskGroup(TaskGroup):
340
344
  ##
341
345
 
342
346
 
347
+ def get_coroutine_name(func: Callable[[], Coro[Any]], /) -> str:
348
+ """Get the name of a coroutine, and then dispose of it gracefully."""
349
+ coro = func()
350
+ name = coro.__name__
351
+ with suppress_warnings(
352
+ message="coroutine '.*' was never awaited", category=RuntimeWarning
353
+ ):
354
+ del coro
355
+ return name
356
+
357
+
358
+ ##
359
+
360
+
343
361
  async def get_items[T](queue: Queue[T], /, *, max_size: int | None = None) -> list[T]:
344
362
  """Get items from a queue; if empty then wait."""
345
363
  try:
@@ -380,23 +398,34 @@ async def loop_until_succeed(
380
398
  func: Callable[[], Coro[None]],
381
399
  /,
382
400
  *,
383
- error: Callable[[Exception], None] | None = None,
401
+ logger: LoggerOrName | None = None,
402
+ errors: ExceptionTypeLike[Exception] | None = None,
384
403
  sleep: Delta | None = None,
385
- ) -> None:
404
+ ) -> bool:
386
405
  """Repeatedly call a coroutine until it succeeds."""
406
+ name = get_coroutine_name(func)
387
407
  while True:
388
408
  try:
389
- return await func()
390
- except Exception as err: # noqa: BLE001
391
- if error is not None:
392
- error(err)
409
+ await func()
410
+ except Exception as error: # noqa: BLE001
411
+ if logger is not None:
412
+ get_logger(logger=logger).error("Error running %r", name, exc_info=True)
393
413
  exc_type, exc_value, traceback = sys.exc_info()
394
414
  if (exc_type is None) or (exc_value is None): # pragma: no cover
395
415
  raise ImpossibleCaseError(
396
416
  case=[f"{exc_type=}", f"{exc_value=}"]
397
417
  ) from None
398
418
  sys.excepthook(exc_type, exc_value, traceback)
399
- await sleep_td(sleep)
419
+ if (errors is not None) and is_instance_error(error, errors):
420
+ return False
421
+ if sleep is not None:
422
+ if logger is not None:
423
+ get_logger(logger=logger).info("Sleeping for %s...", sleep)
424
+ await sleep_td(sleep)
425
+ if logger is not None:
426
+ get_logger(logger=logger).info("Retrying %r...", name)
427
+ else:
428
+ return True
400
429
 
401
430
 
402
431
  ##
@@ -522,6 +551,7 @@ __all__ = [
522
551
  "AsyncDict",
523
552
  "EnhancedTaskGroup",
524
553
  "StreamCommandOutput",
554
+ "get_coroutine_name",
525
555
  "get_items",
526
556
  "get_items_nowait",
527
557
  "loop_until_succeed",
utilities/errors.py CHANGED
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING, assert_never, override
5
5
 
6
6
  if TYPE_CHECKING:
7
- from utilities.types import MaybeType
7
+ from utilities.types import ExceptionTypeLike, MaybeType
8
8
 
9
9
 
10
10
  @dataclass(kw_only=True, slots=True)
@@ -21,6 +21,20 @@ class ImpossibleCaseError(Exception):
21
21
  ##
22
22
 
23
23
 
24
+ def is_instance_error(
25
+ error: BaseException, class_or_tuple: ExceptionTypeLike[Exception], /
26
+ ) -> bool:
27
+ """Check if an instance relationship holds, allowing for groups."""
28
+ if isinstance(error, class_or_tuple):
29
+ return True
30
+ if not isinstance(error, BaseExceptionGroup):
31
+ return False
32
+ return any(is_instance_error(e, class_or_tuple) for e in error.exceptions)
33
+
34
+
35
+ ##
36
+
37
+
24
38
  def repr_error(error: MaybeType[BaseException], /) -> str:
25
39
  """Get a string representation of an error."""
26
40
  match error:
@@ -36,4 +50,4 @@ def repr_error(error: MaybeType[BaseException], /) -> str:
36
50
  assert_never(never)
37
51
 
38
52
 
39
- __all__ = ["ImpossibleCaseError", "repr_error"]
53
+ __all__ = ["ImpossibleCaseError", "is_instance_error", "repr_error"]
utilities/pottery.py CHANGED
@@ -5,16 +5,14 @@ from dataclasses import dataclass
5
5
  from sys import maxsize
6
6
  from typing import TYPE_CHECKING, override
7
7
 
8
- from pottery import AIORedlock
8
+ from pottery import AIORedlock, ExtendUnlockedLock
9
9
  from pottery.exceptions import ReleaseUnlockedLock
10
10
  from redis.asyncio import Redis
11
11
 
12
12
  from utilities.asyncio import loop_until_succeed, sleep_td, timeout_td
13
13
  from utilities.contextlib import enhanced_async_context_manager
14
- from utilities.functools import partial
15
14
  from utilities.iterables import always_iterable
16
15
  from utilities.logging import get_logger
17
- from utilities.warnings import suppress_warnings
18
16
  from utilities.whenever import MILLISECOND, SECOND, to_seconds
19
17
 
20
18
  if TYPE_CHECKING:
@@ -72,10 +70,7 @@ async def try_yield_coroutine_looper(
72
70
  throttle=throttle,
73
71
  ) as lock:
74
72
  yield CoroutineLooper(lock=lock, logger=logger, sleep=sleep_error)
75
- except ( # skipif-ci-and-not-linux
76
- _YieldAccessUnableToAcquireLockError,
77
- _YieldAccessAcquiredUnlockedLockError,
78
- ) as error:
73
+ except _YieldAccessUnableToAcquireLockError as error: # skipif-ci-and-not-linux
79
74
  if logger is not None:
80
75
  get_logger(logger=logger).info("%s", error)
81
76
  async with nullcontext():
@@ -92,27 +87,14 @@ class CoroutineLooper:
92
87
 
93
88
  async def __call__[**P](
94
89
  self, func: Callable[P, Coro[None]], *args: P.args, **kwargs: P.kwargs
95
- ) -> None:
90
+ ) -> bool:
96
91
  def make_coro() -> Coro[None]:
97
92
  return func(*args, **kwargs)
98
93
 
99
- await loop_until_succeed(
100
- make_coro, error=partial(self._error, func=make_coro), sleep=self.sleep
94
+ return await loop_until_succeed(
95
+ make_coro, logger=self.logger, errors=ExtendUnlockedLock, sleep=self.sleep
101
96
  )
102
97
 
103
- def _error(self, error: Exception, /, *, func: Callable[[], Coro[None]]) -> None:
104
- _ = error
105
- if self.logger is not None:
106
- coro = func()
107
- name = coro.__name__ # skipif-ci-and-not-linux
108
- with suppress_warnings(
109
- message="coroutine '.*' was never awaited", category=RuntimeWarning
110
- ):
111
- del coro
112
- get_logger(logger=self.logger).error(
113
- "Error running %r", name, exc_info=True
114
- )
115
-
116
98
 
117
99
  ##
118
100
 
@@ -150,8 +132,6 @@ async def yield_access(
150
132
  lock = await _get_first_available_lock(
151
133
  key, locks, num=num, timeout=timeout_acquire, sleep=sleep
152
134
  )
153
- if (await lock.locked()) == 0.0: # pragma: no cover
154
- raise _YieldAccessAcquiredUnlockedLockError(key=lock.key)
155
135
  yield lock
156
136
  finally: # skipif-ci-and-not-linux
157
137
  await sleep_td(throttle)
@@ -175,9 +155,7 @@ async def _get_first_available_lock(
175
155
  )
176
156
  async with timeout_td(timeout, error=error): # skipif-ci-and-not-linux
177
157
  while True:
178
- if (
179
- (result := await _get_first_available_lock_if_any(locks)) is not None
180
- ) and (await result.locked() > 0.0):
158
+ if (result := await _get_first_available_lock_if_any(locks)) is not None:
181
159
  return result
182
160
  await sleep_td(sleep)
183
161
 
@@ -215,13 +193,6 @@ class _YieldAccessUnableToAcquireLockError(YieldAccessError):
215
193
  return f"Unable to acquire any 1 of {self.num} locks for {self.key!r} after {self.timeout}" # skipif-ci-and-not-linux
216
194
 
217
195
 
218
- @dataclass(kw_only=True, slots=True)
219
- class _YieldAccessAcquiredUnlockedLockError(YieldAccessError):
220
- @override
221
- def __str__(self) -> str:
222
- return f"Acquired an unlocked lock {self.key!r}" # pragma: no cover
223
-
224
-
225
196
  __all__ = [
226
197
  "CoroutineLooper",
227
198
  "YieldAccessError",
utilities/types.py CHANGED
@@ -105,6 +105,10 @@ type MonthInt = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
105
105
  type EnumLike[E: Enum] = MaybeStr[E]
106
106
 
107
107
 
108
+ # errors
109
+ type ExceptionTypeLike[T: Exception] = type[T] | tuple[type[T], ...]
110
+
111
+
108
112
  # ipaddress
109
113
  IPv4AddressLike = MaybeStr[IPv4Address]
110
114
  IPv6AddressLike = MaybeStr[IPv6Address]
@@ -265,6 +269,7 @@ __all__ = [
265
269
  "Delta",
266
270
  "EnumLike",
267
271
  "ExcInfo",
272
+ "ExceptionTypeLike",
268
273
  "IPv4AddressLike",
269
274
  "IPv6AddressLike",
270
275
  "IterableHashable",