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.
- {dycw_utilities-0.147.3.dist-info → dycw_utilities-0.148.1.dist-info}/METADATA +2 -2
- {dycw_utilities-0.147.3.dist-info → dycw_utilities-0.148.1.dist-info}/RECORD +10 -10
- utilities/__init__.py +1 -1
- utilities/asyncio.py +38 -8
- utilities/errors.py +16 -2
- utilities/pottery.py +6 -35
- utilities/types.py +5 -0
- {dycw_utilities-0.147.3.dist-info → dycw_utilities-0.148.1.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.147.3.dist-info → dycw_utilities-0.148.1.dist-info}/entry_points.txt +0 -0
- {dycw_utilities-0.147.3.dist-info → dycw_utilities-0.148.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: dycw-utilities
|
3
|
-
Version: 0.
|
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.
|
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=
|
1
|
+
utilities/__init__.py,sha256=iMEViWolW9oRg6PDmC46RTFOqP-O9BCaLj3gJai78wg,60
|
2
2
|
utilities/altair.py,sha256=92E2lCdyHY4Zb-vCw6rEJIsWdKipuu-Tu2ab1ufUfAk,9079
|
3
|
-
utilities/asyncio.py,sha256=
|
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=
|
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=
|
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=
|
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.
|
92
|
-
dycw_utilities-0.
|
93
|
-
dycw_utilities-0.
|
94
|
-
dycw_utilities-0.
|
95
|
-
dycw_utilities-0.
|
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
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
|
-
|
401
|
+
logger: LoggerOrName | None = None,
|
402
|
+
errors: ExceptionTypeLike[Exception] | None = None,
|
384
403
|
sleep: Delta | None = None,
|
385
|
-
) ->
|
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
|
-
|
390
|
-
except Exception as
|
391
|
-
if
|
392
|
-
error(
|
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
|
-
|
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
|
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
|
-
) ->
|
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,
|
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",
|
File without changes
|
File without changes
|
File without changes
|