dycw-utilities 0.147.3__py3-none-any.whl → 0.148.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.147.3
3
+ Version: 0.148.0
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,6 +1,6 @@
1
- utilities/__init__.py,sha256=CYx_3txVREoGWejJRJBItmHBOZ94QymP1DKxtNU8fIo,60
1
+ utilities/__init__.py,sha256=2TRLfWl0dK_2ThOu9hSeTmoG0iZ_XFR_8beebjQMWqg,60
2
2
  utilities/altair.py,sha256=92E2lCdyHY4Zb-vCw6rEJIsWdKipuu-Tu2ab1ufUfAk,9079
3
- utilities/asyncio.py,sha256=aB0EtUbUJ5ZKQ5ET-Xfyx6wfUJG2G4vihEX0blK4TGE,14964
3
+ utilities/asyncio.py,sha256=J40KmMxTfpgj-GaTNikN0uQNpJnKqqViTD9IRYUvlvM,15985
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
@@ -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
@@ -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.0.dist-info/METADATA,sha256=xk8TDabhrZWpV7yjJMe8lvY3v4ut2CWbfjcRHyIdRVg,1697
92
+ dycw_utilities-0.148.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
93
+ dycw_utilities-0.148.0.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
94
+ dycw_utilities-0.148.0.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
95
+ dycw_utilities-0.148.0.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.0"
utilities/asyncio.py CHANGED
@@ -37,8 +37,10 @@ from typing import (
37
37
 
38
38
  from utilities.errors import ImpossibleCaseError
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,7 @@ if TYPE_CHECKING:
63
65
  from utilities.types import (
64
66
  Coro,
65
67
  Delta,
68
+ LoggerOrName,
66
69
  MaybeCallableBool,
67
70
  MaybeType,
68
71
  SupportsKeysAndGetItem,
@@ -340,6 +343,20 @@ class EnhancedTaskGroup(TaskGroup):
340
343
  ##
341
344
 
342
345
 
346
+ def get_coroutine_name(func: Callable[[], Coro[Any]], /) -> str:
347
+ """Get the name of a coroutine, and then dispose of it gracefully."""
348
+ coro = func()
349
+ name = coro.__name__
350
+ with suppress_warnings(
351
+ message="coroutine '.*' was never awaited", category=RuntimeWarning
352
+ ):
353
+ del coro
354
+ return name
355
+
356
+
357
+ ##
358
+
359
+
343
360
  async def get_items[T](queue: Queue[T], /, *, max_size: int | None = None) -> list[T]:
344
361
  """Get items from a queue; if empty then wait."""
345
362
  try:
@@ -380,23 +397,34 @@ async def loop_until_succeed(
380
397
  func: Callable[[], Coro[None]],
381
398
  /,
382
399
  *,
383
- error: Callable[[Exception], None] | None = None,
400
+ logger: LoggerOrName | None = None,
401
+ errors: type[Exception] | tuple[type[Exception], ...] | None = None,
384
402
  sleep: Delta | None = None,
385
- ) -> None:
403
+ ) -> bool:
386
404
  """Repeatedly call a coroutine until it succeeds."""
405
+ name = get_coroutine_name(func)
387
406
  while True:
388
407
  try:
389
- return await func()
390
- except Exception as err: # noqa: BLE001
391
- if error is not None:
392
- error(err)
408
+ await func()
409
+ except Exception as error: # noqa: BLE001
410
+ if logger is not None:
411
+ get_logger(logger=logger).error("Error running %r", name, exc_info=True)
393
412
  exc_type, exc_value, traceback = sys.exc_info()
394
413
  if (exc_type is None) or (exc_value is None): # pragma: no cover
395
414
  raise ImpossibleCaseError(
396
415
  case=[f"{exc_type=}", f"{exc_value=}"]
397
416
  ) from None
398
417
  sys.excepthook(exc_type, exc_value, traceback)
399
- await sleep_td(sleep)
418
+ if (errors is not None) and isinstance(error, errors):
419
+ return False
420
+ if sleep is not None:
421
+ if logger is not None:
422
+ get_logger(logger=logger).info("Sleeping for %s...", sleep)
423
+ await sleep_td(sleep)
424
+ if logger is not None:
425
+ get_logger(logger=logger).info("Retrying %r...", name)
426
+ else:
427
+ return True
400
428
 
401
429
 
402
430
  ##
@@ -522,6 +550,7 @@ __all__ = [
522
550
  "AsyncDict",
523
551
  "EnhancedTaskGroup",
524
552
  "StreamCommandOutput",
553
+ "get_coroutine_name",
525
554
  "get_items",
526
555
  "get_items_nowait",
527
556
  "loop_until_succeed",
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",