dycw-utilities 0.146.2__py3-none-any.whl → 0.178.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.

Potentially problematic release.


This version of dycw-utilities might be problematic. Click here for more details.

Files changed (89) hide show
  1. dycw_utilities-0.178.1.dist-info/METADATA +34 -0
  2. dycw_utilities-0.178.1.dist-info/RECORD +105 -0
  3. dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.146.2.dist-info → dycw_utilities-0.178.1.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +10 -7
  7. utilities/asyncio.py +129 -50
  8. utilities/atomicwrites.py +1 -1
  9. utilities/atools.py +64 -4
  10. utilities/cachetools.py +9 -6
  11. utilities/click.py +144 -49
  12. utilities/concurrent.py +1 -1
  13. utilities/contextlib.py +4 -2
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +15 -28
  17. utilities/docker.py +387 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +8 -3
  21. utilities/fpdf2.py +2 -2
  22. utilities/functions.py +20 -297
  23. utilities/git.py +19 -0
  24. utilities/grp.py +28 -0
  25. utilities/hypothesis.py +361 -79
  26. utilities/importlib.py +17 -1
  27. utilities/inflect.py +1 -1
  28. utilities/iterables.py +33 -58
  29. utilities/jinja2.py +148 -0
  30. utilities/json.py +1 -1
  31. utilities/libcst.py +7 -7
  32. utilities/logging.py +131 -93
  33. utilities/math.py +8 -4
  34. utilities/more_itertools.py +4 -6
  35. utilities/operator.py +1 -1
  36. utilities/orjson.py +86 -34
  37. utilities/os.py +49 -2
  38. utilities/packaging.py +115 -0
  39. utilities/parse.py +2 -2
  40. utilities/pathlib.py +66 -34
  41. utilities/permissions.py +298 -0
  42. utilities/platform.py +5 -4
  43. utilities/polars.py +934 -420
  44. utilities/polars_ols.py +1 -1
  45. utilities/postgres.py +317 -153
  46. utilities/pottery.py +10 -86
  47. utilities/pqdm.py +3 -3
  48. utilities/pwd.py +28 -0
  49. utilities/pydantic.py +4 -51
  50. utilities/pydantic_settings.py +240 -0
  51. utilities/pydantic_settings_sops.py +76 -0
  52. utilities/pyinstrument.py +5 -5
  53. utilities/pytest.py +100 -126
  54. utilities/pytest_plugins/pytest_randomly.py +1 -1
  55. utilities/pytest_plugins/pytest_regressions.py +7 -3
  56. utilities/pytest_regressions.py +27 -8
  57. utilities/random.py +11 -6
  58. utilities/re.py +1 -1
  59. utilities/redis.py +101 -64
  60. utilities/sentinel.py +10 -0
  61. utilities/shelve.py +4 -1
  62. utilities/shutil.py +25 -0
  63. utilities/slack_sdk.py +9 -4
  64. utilities/sqlalchemy.py +422 -352
  65. utilities/sqlalchemy_polars.py +28 -52
  66. utilities/string.py +1 -1
  67. utilities/subprocess.py +1977 -0
  68. utilities/tempfile.py +112 -4
  69. utilities/testbook.py +50 -0
  70. utilities/text.py +174 -42
  71. utilities/throttle.py +158 -0
  72. utilities/timer.py +2 -2
  73. utilities/traceback.py +59 -38
  74. utilities/types.py +68 -22
  75. utilities/typing.py +479 -19
  76. utilities/uuid.py +42 -5
  77. utilities/version.py +27 -26
  78. utilities/whenever.py +663 -178
  79. utilities/zoneinfo.py +80 -22
  80. dycw_utilities-0.146.2.dist-info/METADATA +0 -41
  81. dycw_utilities-0.146.2.dist-info/RECORD +0 -99
  82. dycw_utilities-0.146.2.dist-info/WHEEL +0 -4
  83. dycw_utilities-0.146.2.dist-info/licenses/LICENSE +0 -21
  84. utilities/aiolimiter.py +0 -25
  85. utilities/eventkit.py +0 -388
  86. utilities/period.py +0 -237
  87. utilities/python_dotenv.py +0 -101
  88. utilities/streamlit.py +0 -105
  89. utilities/typed_settings.py +0 -144
utilities/pytest.py CHANGED
@@ -1,58 +1,76 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Callable
3
4
  from dataclasses import dataclass
4
- from functools import partial, wraps
5
- from inspect import iscoroutinefunction
5
+ from functools import partial
6
6
  from os import environ
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING, Any, assert_never, cast, override
8
+ from re import sub
9
+ from types import FunctionType
10
+ from typing import TYPE_CHECKING, Any, NoReturn, override
9
11
 
10
- from whenever import ZonedDateTime
11
-
12
- from utilities.atomicwrites import writer
13
12
  from utilities.functools import cache
14
13
  from utilities.hashlib import md5_hash
15
- from utilities.os import get_env_var
16
- from utilities.pathlib import ensure_suffix, get_root, get_tail, module_path
17
- from utilities.platform import (
18
- IS_LINUX,
19
- IS_MAC,
20
- IS_NOT_LINUX,
21
- IS_NOT_MAC,
22
- IS_NOT_WINDOWS,
23
- IS_WINDOWS,
14
+ from utilities.pathlib import (
15
+ _GetTailEmptyError,
16
+ ensure_suffix,
17
+ get_root,
18
+ get_tail,
19
+ module_path,
24
20
  )
21
+ from utilities.platform import IS_LINUX, IS_MAC, IS_NOT_LINUX, IS_NOT_MAC
22
+ from utilities.throttle import throttle
25
23
  from utilities.types import MaybeCoro
26
- from utilities.whenever import SECOND, get_now_local
24
+ from utilities.whenever import SECOND
27
25
 
28
26
  if TYPE_CHECKING:
29
- from collections.abc import Callable, Iterable, Sequence
30
-
31
- from utilities.types import Coro, Delta, PathLike
27
+ from collections.abc import Iterable
32
28
 
33
- try: # WARNING: this package cannot use unguarded `pytest` imports
34
29
  from _pytest.config import Config
35
30
  from _pytest.config.argparsing import Parser
36
31
  from _pytest.python import Function
37
- from pytest import mark, skip
32
+
33
+ from utilities.types import Delta, PathLike
34
+
35
+
36
+ IS_CI = "CI" in environ
37
+ IS_CI_AND_NOT_LINUX = IS_CI and IS_NOT_LINUX
38
+
39
+
40
+ try: # WARNING: this package cannot use unguarded `pytest` imports
41
+ from pytest import mark
38
42
  except ModuleNotFoundError: # pragma: no cover
39
- from typing import Any as Config
40
- from typing import Any as Function
41
- from typing import Any as Parser
42
43
 
43
- mark = skip = skipif_windows = skipif_mac = skipif_linux = skipif_not_windows = (
44
- skipif_not_mac
45
- ) = skipif_not_linux = None
44
+ def skipif_ci[F: Callable](func: F) -> F:
45
+ return func
46
+
47
+ def skipif_mac[F: Callable](func: F) -> F:
48
+ return func
49
+
50
+ def skipif_linux[F: Callable](func: F) -> F:
51
+ return func
52
+
53
+ def skipif_not_mac[F: Callable](func: F) -> F:
54
+ return func
55
+
56
+ def skipif_not_linux[F: Callable](func: F) -> F:
57
+ return func
58
+
59
+ def skipif_ci_and_not_linux[F: Callable](func: F) -> F:
60
+ return func
61
+
46
62
  else:
47
- skipif_windows = mark.skipif(IS_WINDOWS, reason="Skipped for Windows")
63
+ skipif_ci = mark.skipif(IS_CI, reason="Skipped for CI")
48
64
  skipif_mac = mark.skipif(IS_MAC, reason="Skipped for Mac")
49
65
  skipif_linux = mark.skipif(IS_LINUX, reason="Skipped for Linux")
50
- skipif_not_windows = mark.skipif(IS_NOT_WINDOWS, reason="Skipped for non-Windows")
51
66
  skipif_not_mac = mark.skipif(IS_NOT_MAC, reason="Skipped for non-Mac")
52
67
  skipif_not_linux = mark.skipif(IS_NOT_LINUX, reason="Skipped for non-Linux")
68
+ skipif_ci_and_not_linux = mark.skipif(
69
+ IS_CI_AND_NOT_LINUX, reason="Skipped for CI/non-Linux"
70
+ )
53
71
 
54
72
 
55
- def add_pytest_addoption(parser: Parser, options: Sequence[str], /) -> None:
73
+ def add_pytest_addoption(parser: Parser, options: list[str], /) -> None:
56
74
  """Add the `--slow`, etc options to pytest.
57
75
 
58
76
  Usage:
@@ -73,7 +91,7 @@ def add_pytest_addoption(parser: Parser, options: Sequence[str], /) -> None:
73
91
 
74
92
 
75
93
  def add_pytest_collection_modifyitems(
76
- config: Config, items: Iterable[Function], options: Sequence[str], /
94
+ config: Config, items: Iterable[Function], options: list[str], /
77
95
  ) -> None:
78
96
  """Add the @mark.skips as necessary.
79
97
 
@@ -82,6 +100,8 @@ def add_pytest_collection_modifyitems(
82
100
  def pytest_collection_modifyitems(config, items):
83
101
  add_pytest_collection_modifyitems(config, items, ["slow"])
84
102
  """
103
+ from pytest import mark
104
+
85
105
  options = list(options)
86
106
  missing = {opt for opt in options if not config.getoption(f"--{opt}")}
87
107
  for item in items:
@@ -111,9 +131,10 @@ def add_pytest_configure(config: Config, options: Iterable[tuple[str, str]], /)
111
131
  ##
112
132
 
113
133
 
114
- def is_pytest() -> bool:
115
- """Check if `pytest` is running."""
116
- return "PYTEST_VERSION" in environ
134
+ def make_ids(obj: Any, /) -> str:
135
+ if isinstance(obj, FunctionType): # pragma: no cover
136
+ return sub(r"\s+at +0x[0-9a-fA-F]+", "", repr(obj))
137
+ return repr(obj) # pragma: no cover
117
138
 
118
139
 
119
140
  ##
@@ -126,10 +147,15 @@ def node_id_path(
126
147
  path_file, *parts = node_id.split("::")
127
148
  path_file = Path(path_file)
128
149
  if path_file.suffix != ".py":
129
- raise NodeIdToPathError(node_id=node_id)
150
+ raise _NodeIdToPathNotPythonFileError(node_id=node_id)
130
151
  path = path_file.with_suffix("")
131
152
  if root is not None:
132
- path = get_tail(path, root)
153
+ try:
154
+ path = get_tail(path, root)
155
+ except _GetTailEmptyError as error:
156
+ raise _NodeIdToPathNotGetTailError(
157
+ node_id=node_id, path=error.path, root=error.root
158
+ ) from None
133
159
  path = Path(module_path(path), "__".join(parts))
134
160
  if suffix is not None:
135
161
  path = ensure_suffix(path, suffix)
@@ -140,96 +166,57 @@ def node_id_path(
140
166
  class NodeIdToPathError(Exception):
141
167
  node_id: str
142
168
 
169
+
170
+ @dataclass(kw_only=True, slots=True)
171
+ class _NodeIdToPathNotPythonFileError(NodeIdToPathError):
143
172
  @override
144
173
  def __str__(self) -> str:
145
174
  return f"Node ID must be a Python file; got {self.node_id!r}"
146
175
 
147
176
 
148
- ##
149
-
150
-
151
- def throttle[F: Callable[..., MaybeCoro[None]]](
152
- *, root: PathLike | None = None, delta: Delta = SECOND, on_try: bool = False
153
- ) -> Callable[[F], F]:
154
- """Throttle a test. On success by default, on try otherwise."""
155
- return cast("Any", partial(_throttle_inner, root=root, delta=delta, on_try=on_try))
156
-
157
-
158
- def _throttle_inner[F: Callable[..., MaybeCoro[None]]](
159
- func: F,
160
- /,
161
- *,
162
- root: PathLike | None = None,
163
- delta: Delta = SECOND,
164
- on_try: bool = False,
165
- ) -> F:
166
- """Throttle a test function/method."""
167
- if get_env_var("THROTTLE", nullable=True) is not None:
168
- return func
169
- match bool(iscoroutinefunction(func)), on_try:
170
- case False, False:
171
-
172
- @wraps(func)
173
- def throttle_sync_on_pass(*args: Any, **kwargs: Any) -> None:
174
- _skipif_recent(root=root, delta=delta)
175
- cast("Callable[..., None]", func)(*args, **kwargs)
176
- _write(root=root)
177
-
178
- return cast("Any", throttle_sync_on_pass)
177
+ @dataclass(kw_only=True, slots=True)
178
+ class _NodeIdToPathNotGetTailError(NodeIdToPathError):
179
+ path: PathLike
180
+ root: PathLike
179
181
 
180
- case False, True:
182
+ @override
183
+ def __str__(self) -> str:
184
+ return (
185
+ f"Unable to get the tail of {str(self.path)!r} with root {str(self.root)!r}"
186
+ )
181
187
 
182
- @wraps(func)
183
- def throttle_sync_on_try(*args: Any, **kwargs: Any) -> None:
184
- _skipif_recent(root=root, delta=delta)
185
- _write(root=root)
186
- cast("Callable[..., None]", func)(*args, **kwargs)
187
188
 
188
- return cast("Any", throttle_sync_on_try)
189
+ ##
189
190
 
190
- case True, False:
191
191
 
192
- @wraps(func)
193
- async def throttle_async_on_pass(*args: Any, **kwargs: Any) -> None:
194
- _skipif_recent(root=root, delta=delta)
195
- await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
196
- _write(root=root)
192
+ def throttle_test[F: Callable[..., MaybeCoro[None]]](
193
+ *, on_try: bool = False, root: PathLike | None = None, delta: Delta = SECOND
194
+ ) -> Callable[[F], F]:
195
+ """Throttle a test. On success by default, on try otherwise."""
196
+ return throttle(
197
+ on_try=on_try,
198
+ delta=delta,
199
+ path=partial(_get_test_path, root=root),
200
+ raiser=_run_skip,
201
+ )
197
202
 
198
- return cast("Any", throttle_async_on_pass)
199
203
 
200
- case True, True:
204
+ def _run_skip() -> NoReturn:
205
+ from pytest import skip
201
206
 
202
- @wraps(func)
203
- async def throttle_async_on_try(*args: Any, **kwargs: Any) -> None:
204
- _skipif_recent(root=root, delta=delta)
205
- _write(root=root)
206
- await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
207
+ skip(reason=f"{_get_name()} throttled")
207
208
 
208
- return cast("Any", throttle_async_on_try)
209
209
 
210
- case _ as never:
211
- assert_never(never)
210
+ def _get_name() -> str:
211
+ return environ["PYTEST_CURRENT_TEST"]
212
212
 
213
213
 
214
- def _skipif_recent(*, root: PathLike | None = None, delta: Delta = SECOND) -> None:
215
- if skip is None:
216
- return # pragma: no cover
217
- path = _get_path(root=root)
218
- try:
219
- contents = path.read_text()
220
- except FileNotFoundError:
221
- return
222
- try:
223
- last = ZonedDateTime.parse_common_iso(contents)
224
- except ValueError:
225
- return
226
- now = get_now_local()
227
- if (now - delta) < last:
228
- age = now - last
229
- _ = skip(reason=f"{_get_name()} throttled (age {age})")
214
+ @cache
215
+ def _md5_hash_cached(text: str, /) -> str:
216
+ return md5_hash(text)
230
217
 
231
218
 
232
- def _get_path(*, root: PathLike | None = None) -> Path:
219
+ def _get_test_path(*, root: PathLike | None = None) -> Path:
233
220
  if root is None:
234
221
  root_use = get_root().joinpath(".pytest_cache", "throttle") # pragma: no cover
235
222
  else:
@@ -237,33 +224,20 @@ def _get_path(*, root: PathLike | None = None) -> Path:
237
224
  return Path(root_use, _md5_hash_cached(_get_name()))
238
225
 
239
226
 
240
- @cache
241
- def _md5_hash_cached(text: str, /) -> str:
242
- return md5_hash(text)
243
-
244
-
245
- def _get_name() -> str:
246
- return environ["PYTEST_CURRENT_TEST"]
247
-
248
-
249
- def _write(*, root: PathLike | None = None) -> None:
250
- path = _get_path(root=root)
251
- with writer(path, overwrite=True) as temp:
252
- _ = temp.write_text(get_now_local().format_common_iso())
253
-
254
-
255
227
  __all__ = [
228
+ "IS_CI",
229
+ "IS_CI_AND_NOT_LINUX",
256
230
  "NodeIdToPathError",
257
231
  "add_pytest_addoption",
258
232
  "add_pytest_collection_modifyitems",
259
233
  "add_pytest_configure",
260
- "is_pytest",
234
+ "make_ids",
261
235
  "node_id_path",
236
+ "skipif_ci",
237
+ "skipif_ci_and_not_linux",
262
238
  "skipif_linux",
263
239
  "skipif_mac",
264
240
  "skipif_not_linux",
265
241
  "skipif_not_mac",
266
- "skipif_not_windows",
267
- "skipif_windows",
268
- "throttle",
242
+ "throttle_test",
269
243
  ]
@@ -17,7 +17,7 @@ else:
17
17
  """Fixture for a random state."""
18
18
  from utilities.random import get_state
19
19
 
20
- return get_state(seed=seed)
20
+ return get_state(seed)
21
21
 
22
22
 
23
23
  __all__ = ["random_state"]
@@ -41,12 +41,16 @@ else:
41
41
 
42
42
  def _get_path(request: FixtureRequest, /) -> Path:
43
43
  from utilities.pathlib import get_root
44
- from utilities.pytest import node_id_path
44
+ from utilities.pytest import _NodeIdToPathNotGetTailError, node_id_path
45
45
 
46
46
  path = Path(cast("Any", request).fspath)
47
47
  root = Path("src", "tests")
48
- tail = node_id_path(request.node.nodeid, root=root)
49
- return get_root(path=path).joinpath(root, "regressions", tail)
48
+ try:
49
+ tail = node_id_path(request.node.nodeid, root=root)
50
+ except _NodeIdToPathNotGetTailError:
51
+ root = Path("tests")
52
+ tail = node_id_path(request.node.nodeid, root=root)
53
+ return get_root(path).joinpath(root, "regressions", tail)
50
54
 
51
55
 
52
56
  __all__ = ["orjson_regression", "polars_regression"]
@@ -1,15 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from contextlib import suppress
4
+ from dataclasses import dataclass
4
5
  from json import loads
5
6
  from pathlib import Path
6
7
  from shutil import copytree
7
- from typing import TYPE_CHECKING, Any, assert_never
8
+ from typing import TYPE_CHECKING, Any, assert_never, override
8
9
 
9
10
  from pytest_regressions.file_regression import FileRegressionFixture
10
11
 
11
12
  from utilities.functions import ensure_str
12
13
  from utilities.operator import is_equal
14
+ from utilities.reprlib import get_repr
13
15
 
14
16
  if TYPE_CHECKING:
15
17
  from polars import DataFrame, Series
@@ -70,10 +72,28 @@ class OrjsonRegressionFixture:
70
72
  check_fn=self._check_fn,
71
73
  )
72
74
 
73
- def _check_fn(self, path1: Path, path2: Path, /) -> None:
74
- left = loads(path1.read_text())
75
- right = loads(path2.read_text())
76
- assert is_equal(left, right), f"{left=}, {right=}"
75
+ def _check_fn(self, path_obtained: Path, path_existing: Path, /) -> None:
76
+ obtained = loads(path_obtained.read_text())
77
+ existing = loads(path_existing.read_text())
78
+ if not is_equal(obtained, existing):
79
+ raise OrjsonRegressionError(
80
+ path_obtained=path_obtained,
81
+ path_existing=path_existing,
82
+ obtained=obtained,
83
+ existing=existing,
84
+ )
85
+
86
+
87
+ @dataclass(kw_only=True, slots=True)
88
+ class OrjsonRegressionError(Exception):
89
+ path_obtained: Path
90
+ path_existing: Path
91
+ obtained: Any
92
+ existing: Any
93
+
94
+ @override
95
+ def __str__(self) -> str:
96
+ return f"Obtained object (at {str(self.path_obtained)!r}) and existing object (at {str(self.path_existing)!r}) differ; got {get_repr(self.obtained)} and {get_repr(self.existing)}"
77
97
 
78
98
 
79
99
  ##
@@ -97,7 +117,6 @@ class PolarsRegressionFixture:
97
117
  "describe": obj.describe(percentiles=[i / 10 for i in range(1, 10)]).rows(
98
118
  named=True
99
119
  ),
100
- "estimated_size": obj.estimated_size(),
101
120
  "is_empty": obj.is_empty(),
102
121
  "n_unique": obj.n_unique(),
103
122
  }
@@ -115,9 +134,9 @@ class PolarsRegressionFixture:
115
134
  col(column).approx_n_unique()
116
135
  ).item()
117
136
  data["approx_n_unique"] = approx_n_unique
118
- data["glimpse"] = df.glimpse(return_as_string=True)
137
+ data["glimpse"] = df.glimpse(return_type="string")
119
138
  data["null_count"] = df.null_count().row(0, named=True)
120
- case _ as never:
139
+ case never:
121
140
  assert_never(never)
122
141
  self._fixture.check(data, suffix=suffix)
123
142
 
utilities/random.py CHANGED
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  from random import Random, SystemRandom
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from utilities.functools import cache
7
+
6
8
  if TYPE_CHECKING:
7
9
  from collections.abc import Iterable
8
10
 
@@ -17,16 +19,16 @@ SYSTEM_RANDOM = SystemRandom()
17
19
 
18
20
  def bernoulli(*, true: float = 0.5, seed: Seed | None = None) -> bool:
19
21
  """Return a Bernoulli random variate."""
20
- seed = get_state(seed=seed)
21
- return bool(seed.binomialvariate(p=true))
22
+ state = get_state(seed)
23
+ return bool(state.binomialvariate(p=true))
22
24
 
23
25
 
24
26
  ##
25
27
 
26
28
 
27
- def get_docker_name(*, seed: Seed | None = None) -> str:
29
+ def get_docker_name(seed: Seed | None = None, /) -> str:
28
30
  """Get a docker name."""
29
- state = get_state(seed=seed)
31
+ state = get_state(seed)
30
32
  prefix = state.choice(_DOCKER_PREFIXES)
31
33
  suffix = state.choice(_DOCKER_SUFFIXES)
32
34
  digit = state.randint(0, 9)
@@ -47,16 +49,19 @@ _DOCKER_SUFFIXES = [
47
49
  ##
48
50
 
49
51
 
50
- def get_state(*, seed: Seed | None = None) -> Random:
52
+ @cache
53
+ def get_state(seed: Seed | None = None, /) -> Random:
51
54
  """Get a random state."""
52
55
  return seed if isinstance(seed, Random) else Random(x=seed)
53
56
 
54
57
 
55
58
  ##
59
+
60
+
56
61
  def shuffle[T](iterable: Iterable[T], /, *, seed: Seed | None = None) -> list[T]:
57
62
  """Shuffle an iterable."""
58
63
  copy = list(iterable).copy()
59
- state = get_state(seed=seed)
64
+ state = get_state(seed)
60
65
  state.shuffle(copy)
61
66
  return copy
62
67
 
utilities/re.py CHANGED
@@ -16,7 +16,7 @@ def ensure_pattern(pattern: PatternLike, /, *, flags: int = 0) -> Pattern[str]:
16
16
  return pattern
17
17
  case str():
18
18
  return re.compile(pattern, flags=flags)
19
- case _ as never:
19
+ case never:
20
20
  assert_never(never)
21
21
 
22
22