dycw-utilities 0.129.10__py3-none-any.whl → 0.175.17__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.
Files changed (103) hide show
  1. dycw_utilities-0.175.17.dist-info/METADATA +34 -0
  2. dycw_utilities-0.175.17.dist-info/RECORD +103 -0
  3. dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
  4. dycw_utilities-0.175.17.dist-info/entry_points.txt +4 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +14 -14
  7. utilities/asyncio.py +350 -819
  8. utilities/atomicwrites.py +18 -6
  9. utilities/atools.py +77 -22
  10. utilities/cachetools.py +24 -29
  11. utilities/click.py +393 -237
  12. utilities/concurrent.py +8 -11
  13. utilities/contextlib.py +216 -17
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +83 -118
  17. utilities/docker.py +293 -0
  18. utilities/enum.py +26 -23
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +29 -65
  21. utilities/fpdf2.py +3 -3
  22. utilities/functions.py +169 -416
  23. utilities/functools.py +18 -19
  24. utilities/git.py +9 -30
  25. utilities/grp.py +28 -0
  26. utilities/gzip.py +31 -0
  27. utilities/http.py +3 -2
  28. utilities/hypothesis.py +738 -589
  29. utilities/importlib.py +17 -1
  30. utilities/inflect.py +25 -0
  31. utilities/iterables.py +194 -262
  32. utilities/jinja2.py +148 -0
  33. utilities/json.py +70 -0
  34. utilities/libcst.py +38 -17
  35. utilities/lightweight_charts.py +5 -9
  36. utilities/logging.py +345 -543
  37. utilities/math.py +18 -13
  38. utilities/memory_profiler.py +11 -15
  39. utilities/more_itertools.py +200 -131
  40. utilities/operator.py +33 -29
  41. utilities/optuna.py +6 -6
  42. utilities/orjson.py +272 -137
  43. utilities/os.py +61 -4
  44. utilities/parse.py +59 -61
  45. utilities/pathlib.py +281 -40
  46. utilities/permissions.py +298 -0
  47. utilities/pickle.py +2 -2
  48. utilities/platform.py +24 -5
  49. utilities/polars.py +1214 -430
  50. utilities/polars_ols.py +1 -1
  51. utilities/postgres.py +408 -0
  52. utilities/pottery.py +113 -26
  53. utilities/pqdm.py +10 -11
  54. utilities/psutil.py +6 -57
  55. utilities/pwd.py +28 -0
  56. utilities/pydantic.py +4 -54
  57. utilities/pydantic_settings.py +240 -0
  58. utilities/pydantic_settings_sops.py +76 -0
  59. utilities/pyinstrument.py +8 -10
  60. utilities/pytest.py +227 -121
  61. utilities/pytest_plugins/__init__.py +1 -0
  62. utilities/pytest_plugins/pytest_randomly.py +23 -0
  63. utilities/pytest_plugins/pytest_regressions.py +56 -0
  64. utilities/pytest_regressions.py +26 -46
  65. utilities/random.py +13 -9
  66. utilities/re.py +58 -28
  67. utilities/redis.py +401 -550
  68. utilities/scipy.py +1 -1
  69. utilities/sentinel.py +10 -0
  70. utilities/shelve.py +4 -1
  71. utilities/shutil.py +25 -0
  72. utilities/slack_sdk.py +36 -106
  73. utilities/sqlalchemy.py +502 -473
  74. utilities/sqlalchemy_polars.py +38 -94
  75. utilities/string.py +2 -3
  76. utilities/subprocess.py +1572 -0
  77. utilities/tempfile.py +86 -4
  78. utilities/testbook.py +50 -0
  79. utilities/text.py +165 -42
  80. utilities/timer.py +37 -65
  81. utilities/traceback.py +158 -929
  82. utilities/types.py +146 -116
  83. utilities/typing.py +531 -71
  84. utilities/tzdata.py +1 -53
  85. utilities/tzlocal.py +6 -23
  86. utilities/uuid.py +43 -5
  87. utilities/version.py +27 -26
  88. utilities/whenever.py +1776 -386
  89. utilities/zoneinfo.py +84 -22
  90. dycw_utilities-0.129.10.dist-info/METADATA +0 -241
  91. dycw_utilities-0.129.10.dist-info/RECORD +0 -96
  92. dycw_utilities-0.129.10.dist-info/WHEEL +0 -4
  93. dycw_utilities-0.129.10.dist-info/licenses/LICENSE +0 -21
  94. utilities/datetime.py +0 -1409
  95. utilities/eventkit.py +0 -402
  96. utilities/loguru.py +0 -144
  97. utilities/luigi.py +0 -228
  98. utilities/period.py +0 -324
  99. utilities/pyrsistent.py +0 -89
  100. utilities/python_dotenv.py +0 -105
  101. utilities/streamlit.py +0 -105
  102. utilities/sys.py +0 -87
  103. utilities/tenacity.py +0 -145
utilities/atomicwrites.py CHANGED
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import gzip
3
4
  import shutil
4
5
  from contextlib import ExitStack, contextmanager
5
6
  from dataclasses import dataclass
6
7
  from pathlib import Path
7
- from shutil import rmtree
8
+ from shutil import copyfileobj, rmtree
8
9
  from typing import TYPE_CHECKING, assert_never, override
9
10
 
10
11
  from atomicwrites import replace_atomic
@@ -56,7 +57,7 @@ def move(
56
57
  raise ImpossibleCaseError(
57
58
  case=[f"{source.is_file()=}", f"{source.is_dir()=}"]
58
59
  )
59
- case _ as never:
60
+ case never:
60
61
  assert_never(never)
61
62
 
62
63
 
@@ -111,21 +112,32 @@ def move_many(*paths: tuple[PathLike, PathLike], overwrite: bool = False) -> Non
111
112
 
112
113
 
113
114
  @contextmanager
114
- def writer(path: PathLike, /, *, overwrite: bool = False) -> Iterator[Path]:
115
+ def writer(
116
+ path: PathLike, /, *, compress: bool = False, overwrite: bool = False
117
+ ) -> Iterator[Path]:
115
118
  """Yield a path for atomically writing files to disk."""
116
119
  path = Path(path)
117
120
  parent = path.parent
118
121
  parent.mkdir(parents=True, exist_ok=True)
119
122
  name = path.name
120
123
  with TemporaryDirectory(suffix=".tmp", prefix=name, dir=parent) as temp_dir:
121
- temp_path = Path(temp_dir, name)
124
+ temp_path1 = Path(temp_dir, name)
122
125
  try:
123
- yield temp_path
126
+ yield temp_path1
124
127
  except KeyboardInterrupt:
125
128
  rmtree(temp_dir)
126
129
  else:
130
+ if compress:
131
+ temp_path2 = Path(temp_dir, f"{name}.gz")
132
+ with (
133
+ temp_path1.open("rb") as source,
134
+ gzip.open(temp_path2, mode="wb") as dest,
135
+ ):
136
+ copyfileobj(source, dest)
137
+ else:
138
+ temp_path2 = temp_path1
127
139
  try:
128
- move(temp_path, path, overwrite=overwrite)
140
+ move(temp_path2, path, overwrite=overwrite)
129
141
  except _MoveSourceNotFoundError as error:
130
142
  raise _WriterTemporaryPathEmptyError(temp_path=error.source) from None
131
143
  except _MoveFileExistsError as error:
utilities/atools.py CHANGED
@@ -1,44 +1,99 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable
4
- from typing import TYPE_CHECKING, ParamSpec, TypeVar
4
+ from functools import partial
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any, cast, overload
5
7
 
6
- from atools import memoize
8
+ import atools
9
+ from whenever import TimeDelta
7
10
 
8
- from utilities.datetime import datetime_duration_to_timedelta
9
- from utilities.types import Coroutine1
11
+ from utilities.types import Coro, PathLike
10
12
 
11
13
  if TYPE_CHECKING:
12
- import datetime as dt
14
+ from atools._memoize_decorator import Keygen, Pickler
13
15
 
14
- from utilities.types import Duration
15
16
 
17
+ type _Key[**P, T] = tuple[Callable[P, Coro[T]], TimeDelta]
18
+ _MEMOIZED_FUNCS: dict[_Key, Callable[..., Coro[Any]]] = {}
16
19
 
17
- _P = ParamSpec("_P")
18
- _R = TypeVar("_R")
19
- _AsyncFunc = Callable[_P, Coroutine1[_R]]
20
- type _Key = tuple[_AsyncFunc, dt.timedelta]
21
- _MEMOIZED_FUNCS: dict[_Key, _AsyncFunc] = {}
22
20
 
23
-
24
- async def call_memoized(
25
- func: _AsyncFunc[_P, _R],
26
- refresh: Duration | None = None,
21
+ async def call_memoized[**P, T](
22
+ func: Callable[P, Coro[T]],
23
+ refresh: TimeDelta | None = None,
27
24
  /,
28
- *args: _P.args,
29
- **kwargs: _P.kwargs,
30
- ) -> _R:
25
+ *args: P.args,
26
+ **kwargs: P.kwargs,
27
+ ) -> T:
31
28
  """Call an asynchronous function, with possible memoization."""
32
29
  if refresh is None:
33
30
  return await func(*args, **kwargs)
34
- timedelta = datetime_duration_to_timedelta(refresh)
35
- key: _Key = (func, timedelta)
36
- memoized_func: _AsyncFunc[_P, _R]
31
+ key: _Key = (func, refresh)
32
+ memoized_func: Callable[P, Coro[T]]
37
33
  try:
38
34
  memoized_func = _MEMOIZED_FUNCS[key]
39
35
  except KeyError:
40
- memoized_func = _MEMOIZED_FUNCS[(key)] = memoize(duration=refresh)(func)
36
+ memoized_func = _MEMOIZED_FUNCS[(key)] = memoize(duration=refresh.in_seconds())(
37
+ func
38
+ )
41
39
  return await memoized_func(*args, **kwargs)
42
40
 
43
41
 
42
+ ##
43
+
44
+
45
+ @overload
46
+ def memoize[F: Callable[..., Coro[Any]]](
47
+ func: F,
48
+ /,
49
+ *,
50
+ db_path: PathLike | None = None,
51
+ duration: float | TimeDelta | None = None,
52
+ keygen: Keygen | None = None,
53
+ pickler: Pickler | None = None,
54
+ size: int | None = None,
55
+ ) -> F: ...
56
+ @overload
57
+ def memoize[F: Callable[..., Coro[Any]]](
58
+ func: None = None,
59
+ /,
60
+ *,
61
+ db_path: PathLike | None = None,
62
+ duration: float | TimeDelta | None = None,
63
+ keygen: Keygen | None = None,
64
+ pickler: Pickler | None = None,
65
+ size: int | None = None,
66
+ ) -> Callable[[F], F]: ...
67
+ def memoize[F: Callable[..., Coro[Any]]](
68
+ func: F | None = None,
69
+ /,
70
+ *,
71
+ db_path: PathLike | None = None,
72
+ duration: float | TimeDelta | None = None,
73
+ keygen: Keygen | None = None,
74
+ pickler: Pickler | None = None,
75
+ size: int | None = None,
76
+ ) -> F | Callable[[F], F]:
77
+ """Memoize a function."""
78
+ if func is None:
79
+ result = partial(
80
+ memoize,
81
+ db_path=db_path,
82
+ duration=duration,
83
+ keygen=keygen,
84
+ pickler=pickler,
85
+ size=size,
86
+ )
87
+ return cast("Callable[[F], F]", result)
88
+ return atools.memoize(
89
+ db_path=None if db_path is None else Path(db_path),
90
+ duration=duration.py_timedelta()
91
+ if isinstance(duration, TimeDelta)
92
+ else duration,
93
+ keygen=keygen,
94
+ pickler=pickler,
95
+ size=size,
96
+ )(func)
97
+
98
+
44
99
  __all__ = ["call_memoized"]
utilities/cachetools.py CHANGED
@@ -1,39 +1,31 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Callable, Iterable, Iterator, MutableSet
3
+ from collections.abc import Callable, Hashable, Iterable, Iterator, MutableSet
4
4
  from math import inf
5
5
  from time import monotonic
6
- from typing import TYPE_CHECKING, Any, TypeVar, override
6
+ from typing import TYPE_CHECKING, Any, cast, override
7
7
 
8
8
  import cachetools
9
9
  from cachetools.func import ttl_cache
10
10
 
11
- from utilities.datetime import datetime_duration_to_float
12
-
13
11
  if TYPE_CHECKING:
14
- from utilities.types import Duration, TCallable
15
-
16
- _K = TypeVar("_K")
17
- _T = TypeVar("_T")
18
- _V = TypeVar("_V")
12
+ from whenever import TimeDelta
19
13
 
20
14
 
21
- class TTLCache(cachetools.TTLCache[_K, _V]):
15
+ class TTLCache[K: Hashable, V](cachetools.TTLCache[K, V]):
22
16
  """A TTL-cache."""
23
17
 
24
18
  def __init__(
25
19
  self,
26
20
  *,
27
21
  max_size: int | None = None,
28
- max_duration: Duration | None = None,
22
+ max_duration: TimeDelta | None = None,
29
23
  timer: Callable[[], float] = monotonic,
30
24
  get_size_of: Callable[[Any], int] | None = None,
31
25
  ) -> None:
32
26
  super().__init__(
33
27
  maxsize=inf if max_size is None else max_size,
34
- ttl=inf
35
- if max_duration is None
36
- else datetime_duration_to_float(max_duration),
28
+ ttl=inf if max_duration is None else max_duration.in_seconds(),
37
29
  timer=timer,
38
30
  getsizeof=get_size_of,
39
31
  )
@@ -42,19 +34,19 @@ class TTLCache(cachetools.TTLCache[_K, _V]):
42
34
  ##
43
35
 
44
36
 
45
- class TTLSet(MutableSet[_T]):
37
+ class TTLSet[T: Hashable](MutableSet[T]):
46
38
  """A TTL-set."""
47
39
 
48
- _cache: TTLCache[_T, None]
40
+ _cache: TTLCache[T, None]
49
41
 
50
42
  @override
51
43
  def __init__(
52
44
  self,
53
- iterable: Iterable[_T] | None = None,
45
+ iterable: Iterable[T] | None = None,
54
46
  /,
55
47
  *,
56
48
  max_size: int | None = None,
57
- max_duration: Duration | None = None,
49
+ max_duration: TimeDelta | None = None,
58
50
  timer: Callable[[], float] = monotonic,
59
51
  get_size_of: Callable[[Any], int] | None = None,
60
52
  ) -> None:
@@ -73,7 +65,7 @@ class TTLSet(MutableSet[_T]):
73
65
  return self._cache.__contains__(x)
74
66
 
75
67
  @override
76
- def __iter__(self) -> Iterator[_T]:
68
+ def __iter__(self) -> Iterator[T]:
77
69
  return self._cache.__iter__()
78
70
 
79
71
  @override
@@ -89,30 +81,33 @@ class TTLSet(MutableSet[_T]):
89
81
  return set(self._cache).__str__()
90
82
 
91
83
  @override
92
- def add(self, value: _T) -> None:
84
+ def add(self, value: T) -> None:
93
85
  self._cache[value] = None
94
86
 
95
87
  @override
96
- def discard(self, value: _T) -> None:
88
+ def discard(self, value: T) -> None:
97
89
  del self._cache[value]
98
90
 
99
91
 
100
92
  ##
101
93
 
102
94
 
103
- def cache(
95
+ def cache[F: Callable](
104
96
  *,
105
97
  max_size: int | None = None,
106
- max_duration: Duration | None = None,
98
+ max_duration: TimeDelta | None = None,
107
99
  timer: Callable[[], float] = monotonic,
108
100
  typed_: bool = False,
109
- ) -> Callable[[TCallable], TCallable]:
101
+ ) -> Callable[[F], F]:
110
102
  """Decorate a function with `max_size` and/or `ttl` settings."""
111
- return ttl_cache(
112
- maxsize=inf if max_size is None else max_size,
113
- ttl=inf if max_duration is None else datetime_duration_to_float(max_duration),
114
- timer=timer,
115
- typed=typed_,
103
+ return cast(
104
+ "F",
105
+ ttl_cache(
106
+ maxsize=max_size,
107
+ ttl=inf if max_duration is None else max_duration.in_seconds(),
108
+ timer=timer,
109
+ typed=typed_,
110
+ ),
116
111
  )
117
112
 
118
113