dycw-utilities 0.126.2__py3-none-any.whl → 0.126.4__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,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.126.2
3
+ Version: 0.126.4
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
7
7
  Requires-Dist: typing-extensions<4.14,>=4.13.1
8
8
  Provides-Extra: test
9
- Requires-Dist: hypothesis<6.132,>=6.131.32; extra == 'test'
9
+ Requires-Dist: hypothesis<6.133,>=6.132.0; extra == 'test'
10
10
  Requires-Dist: pytest-asyncio<1.1,>=1.0.0; extra == 'test'
11
11
  Requires-Dist: pytest-cov<6.2,>=6.1.1; extra == 'test'
12
12
  Requires-Dist: pytest-instafail<0.6,>=0.5.0; extra == 'test'
@@ -78,7 +78,7 @@ Provides-Extra: zzz-test-hypothesis
78
78
  Requires-Dist: aiosqlite<0.22,>=0.21.0; extra == 'zzz-test-hypothesis'
79
79
  Requires-Dist: asyncpg<0.31,>=0.30.0; extra == 'zzz-test-hypothesis'
80
80
  Requires-Dist: greenlet<3.3,>=3.2.0; extra == 'zzz-test-hypothesis'
81
- Requires-Dist: hypothesis<6.132,>=6.131.32; extra == 'zzz-test-hypothesis'
81
+ Requires-Dist: hypothesis<6.133,>=6.132.0; extra == 'zzz-test-hypothesis'
82
82
  Requires-Dist: luigi<3.7,>=3.6.0; extra == 'zzz-test-hypothesis'
83
83
  Requires-Dist: numpy<2.3,>=2.2.6; extra == 'zzz-test-hypothesis'
84
84
  Requires-Dist: pathvalidate<3.3,>=3.2.3; extra == 'zzz-test-hypothesis'
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=oG_3_PTYR4RRwJNt7YUNCU570YlGyZIR_E1X-yfdGqY,60
1
+ utilities/__init__.py,sha256=q-AzOWuCGiffIcmyXrrhqNK_TnB_0a0oNDlPGkcdXQE,60
2
2
  utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
3
  utilities/asyncio.py,sha256=K5Kj7rsM0nA17-b7d7mrNgPR1U_NbkfQmTruq5LBLRA,51778
4
4
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
@@ -23,12 +23,12 @@ utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
23
23
  utilities/git.py,sha256=wpt5dZ5Oi5931pN24_VLZYaQOvmR0OcQuVtgHzFUN1k,2359
24
24
  utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
25
25
  utilities/http.py,sha256=WcahTcKYRtZ04WXQoWt5EGCgFPcyHD3EJdlMfxvDt-0,946
26
- utilities/hypothesis.py,sha256=a75izXg9aCBhhDkj_ZgK3TDzlzk38evP8TO7JbYYQvg,46264
26
+ utilities/hypothesis.py,sha256=DwhPpnz9w5H9YxLpoXTRjhTpadDjrX5-yUVT-Kq6Tgc,45216
27
27
  utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
28
28
  utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
29
29
  utilities/iterables.py,sha256=mDqw2_0MUVp-P8FklgcaVTi2TXduH0MxbhTDzzhSBho,44915
30
30
  utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
31
- utilities/libcst.py,sha256=L0IkwAWXPuNpYyceeJGOC8CAwHdVPRpbXC8MQphCXh4,4954
31
+ utilities/libcst.py,sha256=Jto5ppzRzsxn4AD32IS8n0lbgLYXwsVJB6EY8giNZyY,4974
32
32
  utilities/lightweight_charts.py,sha256=0xNfcsrgFI0R9xL25LtSm-W5yhfBI93qQNT6HyaXAhg,2769
33
33
  utilities/logging.py,sha256=gwo3pusPjnWO1ollrtn1VKYyRAQJTue4SkCbMeNvec4,25715
34
34
  utilities/loguru.py,sha256=MEMQVWrdECxk1e3FxGzmOf21vWT9j8CAir98SEXFKPA,3809
@@ -49,8 +49,9 @@ utilities/pickle.py,sha256=Bhvd7cZl-zQKQDFjUerqGuSKlHvnW1K2QXeU5UZibtg,657
49
49
  utilities/platform.py,sha256=48IOKx1IC6ZJXWG-b56ZQptITcNFhWRjELW72o2dGTA,2398
50
50
  utilities/polars.py,sha256=QlmUpYTqHNkcLnWOQh1TW22W2QyLzvifCvBcbsqhpdE,63272
51
51
  utilities/polars_ols.py,sha256=Uc9V5kvlWZ5cU93lKZ-cfAKdVFFw81tqwLW9PxtUvMs,5618
52
+ utilities/pottery.py,sha256=_PA24g2B23etpiF2awZ4oR7RciUgEHu_j7Ls3QKMkjY,1527
52
53
  utilities/pqdm.py,sha256=foRytQybmOQ05pjt5LF7ANyzrIa--4ScDE3T2wd31a4,3118
53
- utilities/psutil.py,sha256=0dXHKW7t9J3uzY3YtHGiRiZVRWr0uozccioZRCVFQfk,3761
54
+ utilities/psutil.py,sha256=RtbLKOoIJhqrJmEoHDBVeSD-KPzshtS0FtRXBP9_w2s,3751
54
55
  utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
56
  utilities/pydantic.py,sha256=f6qtR5mO2YMuyvNmbaEj5YeD9eGA4YYfb7Bjzh9jUs0,1845
56
57
  utilities/pyinstrument.py,sha256=OJFDh4o1CWIa4aYPYURdQjgap_nvP45KUsCEe94rQHY,829
@@ -60,7 +61,7 @@ utilities/pytest_regressions.py,sha256=-SVT9647Dg6-JcdsiaDKXe3NdOmmrvGevLKWwGjxq
60
61
  utilities/python_dotenv.py,sha256=iWcnpXbH7S6RoXHiLlGgyuH6udCupAcPd_gQ0eAenQ0,3190
61
62
  utilities/random.py,sha256=lYdjgxB7GCfU_fwFVl5U-BIM_HV3q6_urL9byjrwDM8,4157
62
63
  utilities/re.py,sha256=5J4d8VwIPFVrX2Eb8zfoxImDv7IwiN_U7mJ07wR2Wvs,3958
63
- utilities/redis.py,sha256=DTkNR_caxlx06rH-ugcClroErKsu6NDGMYGIgEng05c,32570
64
+ utilities/redis.py,sha256=U-LN6Li3vPTQ2sGGh85_1eD53payujg4qYX0TfmbLSs,32205
64
65
  utilities/reprlib.py,sha256=Re9bk3n-kC__9DxQmRlevqFA86pE6TtVfWjUgpbVOv0,1849
65
66
  utilities/rich.py,sha256=t50MwwVBsoOLxzmeVFSVpjno4OW6Ufum32skXbV8-Bs,1911
66
67
  utilities/scipy.py,sha256=X6ROnHwiUhAmPhM0jkfEh0-Fd9iRvwiqtCQMOLmOQF8,945
@@ -90,7 +91,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
90
91
  utilities/whenever.py,sha256=jS31ZAY5OMxFxLja_Yo5Fidi87Pd-GoVZ7Vi_teqVDA,16743
91
92
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
92
93
  utilities/zoneinfo.py,sha256=-5j7IQ9nb7gR43rdgA7ms05im-XuqhAk9EJnQBXxCoQ,1874
93
- dycw_utilities-0.126.2.dist-info/METADATA,sha256=0QZHN43mRwTBwhoYhygzaYieXnLM6qsG4ANxaqYs4Ls,12851
94
- dycw_utilities-0.126.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
95
- dycw_utilities-0.126.2.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
96
- dycw_utilities-0.126.2.dist-info/RECORD,,
94
+ dycw_utilities-0.126.4.dist-info/METADATA,sha256=GiVs9mIgDnNbbpw9H2PkBdxD3LIObcakDua9XR5Xh6g,12849
95
+ dycw_utilities-0.126.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
96
+ dycw_utilities-0.126.4.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
97
+ dycw_utilities-0.126.4.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.126.2"
3
+ __version__ = "0.126.4"
utilities/hypothesis.py CHANGED
@@ -2,22 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import builtins
4
4
  import datetime as dt
5
- from contextlib import (
6
- AbstractAsyncContextManager,
7
- asynccontextmanager,
8
- contextmanager,
9
- suppress,
10
- )
5
+ from contextlib import contextmanager
11
6
  from dataclasses import dataclass
12
7
  from datetime import timezone
13
8
  from enum import Enum, auto
14
9
  from functools import partial
15
10
  from math import ceil, floor, inf, isclose, isfinite, nan
16
- from os import environ
11
+ from os import environ, getpid
17
12
  from pathlib import Path
18
13
  from re import search
19
14
  from string import ascii_letters, ascii_lowercase, ascii_uppercase, digits, printable
20
15
  from subprocess import check_call
16
+ from threading import get_ident
21
17
  from typing import (
22
18
  TYPE_CHECKING,
23
19
  Any,
@@ -92,27 +88,19 @@ from utilities.pathlib import temp_cwd
92
88
  from utilities.platform import IS_WINDOWS
93
89
  from utilities.sentinel import Sentinel, sentinel
94
90
  from utilities.tempfile import TEMP_DIR, TemporaryDirectory
91
+ from utilities.tzlocal import get_now_local
95
92
  from utilities.version import Version
96
93
  from utilities.zoneinfo import UTC
97
94
 
98
95
  if TYPE_CHECKING:
99
- from collections.abc import (
100
- AsyncIterator,
101
- Collection,
102
- Hashable,
103
- Iterable,
104
- Iterator,
105
- Sequence,
106
- )
96
+ from collections.abc import Collection, Hashable, Iterable, Iterator, Sequence
107
97
  from zoneinfo import ZoneInfo
108
98
 
109
99
  from hypothesis.database import ExampleDatabase
110
100
  from numpy.random import RandomState
111
- from redis.typing import KeyT
112
101
  from sqlalchemy.ext.asyncio import AsyncEngine
113
102
 
114
103
  from utilities.numpy import NDArrayB, NDArrayF, NDArrayI, NDArrayO
115
- from utilities.redis import _TestRedis
116
104
  from utilities.sqlalchemy import Dialect, TableOrORMInstOrClass
117
105
  from utilities.types import Duration, Number, RoundMode
118
106
 
@@ -1422,6 +1410,19 @@ def uint64s(
1422
1410
  ##
1423
1411
 
1424
1412
 
1413
+ @composite
1414
+ def unique_strs(draw: DrawFn, /) -> str:
1415
+ """Strategy for generating unique strings."""
1416
+ now = get_now_local()
1417
+ pid = getpid()
1418
+ ident = get_ident()
1419
+ key = str(draw(uuids())).replace("-", "")
1420
+ return f"{now:%Y%m%d%H%M%S%f}_{pid}_{ident}_{key}"
1421
+
1422
+
1423
+ ##
1424
+
1425
+
1425
1426
  @composite
1426
1427
  def versions(draw: DrawFn, /, *, suffix: MaybeSearchStrategy[bool] = False) -> Version:
1427
1428
  """Strategy for generating versions."""
@@ -1434,34 +1435,6 @@ def versions(draw: DrawFn, /, *, suffix: MaybeSearchStrategy[bool] = False) -> V
1434
1435
  ##
1435
1436
 
1436
1437
 
1437
- def yield_test_redis(data: DataObject, /) -> AbstractAsyncContextManager[_TestRedis]:
1438
- """Strategy for generating test redis clients."""
1439
- from redis.exceptions import ResponseError # skipif-ci-and-not-linux
1440
-
1441
- from utilities.redis import _TestRedis, yield_redis # skipif-ci-and-not-linux
1442
- from utilities.tzlocal import get_now_local # skipif-ci-and-not-linux
1443
-
1444
- now = get_now_local() # skipif-ci-and-not-linux
1445
- uuid = data.draw(uuids()) # skipif-ci-and-not-linux
1446
- key = f"{now}_{uuid}" # skipif-ci-and-not-linux
1447
-
1448
- @asynccontextmanager
1449
- async def func() -> AsyncIterator[_TestRedis]: # skipif-ci-and-not-linux
1450
- async with yield_redis(db=15) as redis: # skipif-ci-and-not-linux
1451
- keys = cast("list[KeyT]", await redis.keys(pattern=f"{key}_*"))
1452
- with suppress(ResponseError):
1453
- _ = await redis.delete(*keys)
1454
- yield _TestRedis(redis=redis, timestamp=now, uuid=uuid, key=key)
1455
- keys = cast("list[KeyT]", await redis.keys(pattern=f"{key}_*"))
1456
- with suppress(ResponseError):
1457
- _ = await redis.delete(*keys)
1458
-
1459
- return func() # skipif-ci-and-not-linux
1460
-
1461
-
1462
- ##
1463
-
1464
-
1465
1438
  @composite
1466
1439
  def zoned_datetimes(
1467
1440
  draw: DrawFn,
@@ -1578,7 +1551,7 @@ __all__ = [
1578
1551
  "triples",
1579
1552
  "uint32s",
1580
1553
  "uint64s",
1554
+ "unique_strs",
1581
1555
  "versions",
1582
- "yield_test_redis",
1583
1556
  "zoned_datetimes",
1584
1557
  ]
utilities/libcst.py CHANGED
@@ -156,7 +156,7 @@ def render_module(source: str | Module, /) -> str:
156
156
  case str() as text:
157
157
  try:
158
158
  return check_output(["ruff", "format", "-"], input=text, text=True)
159
- except CalledProcessError:
159
+ except CalledProcessError: # pragma: no cover
160
160
  return text
161
161
  case Module() as module:
162
162
  return render_module(module.code)
utilities/pottery.py ADDED
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from contextlib import asynccontextmanager, suppress
5
+ from typing import TYPE_CHECKING
6
+
7
+ from pottery import AIORedlock
8
+ from pottery.exceptions import ReleaseUnlockedLock
9
+ from redis.asyncio import Redis
10
+
11
+ from utilities.datetime import MILLISECOND, SECOND, datetime_duration_to_float
12
+ from utilities.iterables import always_iterable
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import AsyncIterator
16
+
17
+ from utilities.types import Duration, MaybeIterable
18
+
19
+
20
+ @asynccontextmanager
21
+ async def yield_locked_resource(
22
+ redis: MaybeIterable[Redis],
23
+ key: str,
24
+ /,
25
+ *,
26
+ duration: Duration = 10 * SECOND,
27
+ sleep: Duration = MILLISECOND,
28
+ ) -> AsyncIterator[None]:
29
+ """Yield a locked resource."""
30
+ masters = ( # skipif-ci-and-not-linux
31
+ {redis} if isinstance(redis, Redis) else set(always_iterable(redis))
32
+ )
33
+ duration_use = datetime_duration_to_float(duration) # skipif-ci-and-not-linux
34
+ lock = AIORedlock( # skipif-ci-and-not-linux
35
+ key=key,
36
+ masters=masters,
37
+ auto_release_time=duration_use,
38
+ context_manager_timeout=duration_use,
39
+ )
40
+ sleep_use = datetime_duration_to_float(sleep) # skipif-ci-and-not-linux
41
+ while not await lock.acquire(): # pragma: no cover
42
+ _ = await asyncio.sleep(sleep_use)
43
+ try: # skipif-ci-and-not-linux
44
+ yield
45
+ finally: # skipif-ci-and-not-linux
46
+ with suppress(ReleaseUnlockedLock):
47
+ await lock.release()
48
+
49
+
50
+ __all__ = ["yield_locked_resource"]
utilities/psutil.py CHANGED
@@ -31,7 +31,6 @@ class MemoryMonitorService(Looper[None]):
31
31
  console: str | None = field(default=None, repr=False)
32
32
  path: PathLike = "memory.txt"
33
33
  _console: Logger | None = field(init=False, repr=False)
34
- _max_age: int | None = field(default=None, init=False, repr=False)
35
34
  _path: Path = field(init=False, repr=False)
36
35
 
37
36
  @override
@@ -40,6 +39,7 @@ class MemoryMonitorService(Looper[None]):
40
39
  if self.console is not None:
41
40
  self._console = getLogger(self.console)
42
41
  self._path = Path(self.path)
42
+ self._path.parent.mkdir(parents=True, exist_ok=True)
43
43
 
44
44
  @override
45
45
  async def core(self) -> None:
utilities/redis.py CHANGED
@@ -21,7 +21,6 @@ from typing import (
21
21
  overload,
22
22
  override,
23
23
  )
24
- from uuid import UUID, uuid4
25
24
 
26
25
  from redis.asyncio import Redis
27
26
  from redis.typing import EncodableT
@@ -32,14 +31,12 @@ from utilities.datetime import (
32
31
  SECOND,
33
32
  datetime_duration_to_float,
34
33
  datetime_duration_to_timedelta,
35
- get_now,
36
34
  )
37
35
  from utilities.errors import ImpossibleCaseError
38
36
  from utilities.functions import ensure_int, get_class_name, identity
39
37
  from utilities.iterables import always_iterable, one
40
38
 
41
39
  if TYPE_CHECKING:
42
- import datetime as dt
43
40
  from collections.abc import (
44
41
  AsyncIterator,
45
42
  Awaitable,
@@ -984,22 +981,6 @@ def _deserialize(
984
981
  return deserializer_use(data) # skipif-ci-and-not-linux
985
982
 
986
983
 
987
- ##
988
-
989
-
990
- @dataclass(repr=False, kw_only=True, slots=True)
991
- class _TestRedis:
992
- """A container for a redis client; for testing purposes only."""
993
-
994
- redis: Redis
995
- timestamp: dt.datetime = field(default_factory=get_now)
996
- uuid: UUID = field(default_factory=uuid4)
997
- key: str
998
-
999
-
1000
- _ = _TestRedis
1001
-
1002
-
1003
984
  __all__ = [
1004
985
  "PublishService",
1005
986
  "Publisher",