dycw-utilities 0.175.17__py3-none-any.whl → 0.185.8__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 (94) hide show
  1. dycw_utilities-0.185.8.dist-info/METADATA +33 -0
  2. dycw_utilities-0.185.8.dist-info/RECORD +90 -0
  3. {dycw_utilities-0.175.17.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +2 -2
  4. utilities/__init__.py +1 -1
  5. utilities/altair.py +8 -6
  6. utilities/asyncio.py +40 -56
  7. utilities/atools.py +9 -11
  8. utilities/cachetools.py +8 -6
  9. utilities/click.py +4 -3
  10. utilities/concurrent.py +1 -1
  11. utilities/constants.py +492 -0
  12. utilities/contextlib.py +23 -30
  13. utilities/contextvars.py +1 -23
  14. utilities/core.py +2581 -0
  15. utilities/dataclasses.py +16 -119
  16. utilities/docker.py +139 -45
  17. utilities/enum.py +1 -1
  18. utilities/errors.py +2 -16
  19. utilities/fastapi.py +5 -5
  20. utilities/fpdf2.py +2 -1
  21. utilities/functions.py +33 -264
  22. utilities/http.py +2 -3
  23. utilities/hypothesis.py +48 -25
  24. utilities/iterables.py +39 -575
  25. utilities/jinja2.py +3 -6
  26. utilities/jupyter.py +5 -3
  27. utilities/libcst.py +1 -1
  28. utilities/lightweight_charts.py +4 -6
  29. utilities/logging.py +17 -15
  30. utilities/math.py +1 -36
  31. utilities/more_itertools.py +4 -6
  32. utilities/numpy.py +2 -1
  33. utilities/operator.py +2 -2
  34. utilities/orjson.py +24 -25
  35. utilities/os.py +4 -185
  36. utilities/packaging.py +129 -0
  37. utilities/parse.py +33 -13
  38. utilities/pathlib.py +2 -136
  39. utilities/platform.py +8 -90
  40. utilities/polars.py +34 -31
  41. utilities/postgres.py +9 -4
  42. utilities/pottery.py +20 -18
  43. utilities/pqdm.py +3 -4
  44. utilities/psutil.py +2 -3
  45. utilities/pydantic.py +18 -4
  46. utilities/pydantic_settings.py +7 -9
  47. utilities/pydantic_settings_sops.py +3 -3
  48. utilities/pyinstrument.py +4 -4
  49. utilities/pytest.py +49 -108
  50. utilities/pytest_plugins/pytest_regressions.py +2 -2
  51. utilities/pytest_regressions.py +8 -6
  52. utilities/random.py +2 -8
  53. utilities/redis.py +98 -94
  54. utilities/reprlib.py +11 -118
  55. utilities/shellingham.py +66 -0
  56. utilities/slack_sdk.py +13 -12
  57. utilities/sqlalchemy.py +42 -30
  58. utilities/sqlalchemy_polars.py +16 -25
  59. utilities/subprocess.py +1166 -148
  60. utilities/tabulate.py +32 -0
  61. utilities/testbook.py +8 -8
  62. utilities/text.py +24 -115
  63. utilities/throttle.py +159 -0
  64. utilities/time.py +18 -0
  65. utilities/timer.py +29 -12
  66. utilities/traceback.py +15 -22
  67. utilities/types.py +38 -3
  68. utilities/typing.py +18 -12
  69. utilities/uuid.py +1 -1
  70. utilities/version.py +202 -45
  71. utilities/whenever.py +22 -150
  72. dycw_utilities-0.175.17.dist-info/METADATA +0 -34
  73. dycw_utilities-0.175.17.dist-info/RECORD +0 -103
  74. utilities/atomicwrites.py +0 -182
  75. utilities/cryptography.py +0 -41
  76. utilities/getpass.py +0 -8
  77. utilities/git.py +0 -19
  78. utilities/grp.py +0 -28
  79. utilities/gzip.py +0 -31
  80. utilities/json.py +0 -70
  81. utilities/permissions.py +0 -298
  82. utilities/pickle.py +0 -25
  83. utilities/pwd.py +0 -28
  84. utilities/re.py +0 -156
  85. utilities/sentinel.py +0 -73
  86. utilities/socket.py +0 -8
  87. utilities/string.py +0 -20
  88. utilities/tempfile.py +0 -136
  89. utilities/tzdata.py +0 -11
  90. utilities/tzlocal.py +0 -28
  91. utilities/warnings.py +0 -65
  92. utilities/zipfile.py +0 -25
  93. utilities/zoneinfo.py +0 -133
  94. {dycw_utilities-0.175.17.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +0 -0
utilities/pytest.py CHANGED
@@ -8,26 +8,37 @@ from os import environ
8
8
  from pathlib import Path
9
9
  from re import sub
10
10
  from types import FunctionType
11
- from typing import TYPE_CHECKING, Any, assert_never, cast, override
12
-
13
- from whenever import ZonedDateTime
14
-
15
- from utilities.atomicwrites import writer
11
+ from typing import TYPE_CHECKING, Any, NoReturn, assert_never, cast, override
12
+
13
+ from utilities.constants import (
14
+ IS_CI,
15
+ IS_CI_AND_NOT_LINUX,
16
+ IS_LINUX,
17
+ IS_MAC,
18
+ IS_NOT_LINUX,
19
+ IS_NOT_MAC,
20
+ SECOND,
21
+ )
16
22
  from utilities.functools import cache
17
23
  from utilities.hashlib import md5_hash
18
- from utilities.os import get_env_var
19
24
  from utilities.pathlib import (
20
25
  _GetTailEmptyError,
21
26
  ensure_suffix,
22
- get_root,
27
+ get_repo_root,
23
28
  get_tail,
24
29
  module_path,
25
30
  )
26
- from utilities.platform import IS_LINUX, IS_MAC, IS_NOT_LINUX, IS_NOT_MAC
27
31
  from utilities.random import bernoulli
28
32
  from utilities.text import to_bool
29
- from utilities.types import MaybeCallableBoolLike, MaybeCoro, Seed
30
- from utilities.whenever import SECOND, get_now_local
33
+ from utilities.throttle import throttle
34
+ from utilities.types import (
35
+ Coro,
36
+ Duration,
37
+ MaybeCallableBoolLike,
38
+ MaybeCoro,
39
+ PathLike,
40
+ Seed,
41
+ )
31
42
 
32
43
  if TYPE_CHECKING:
33
44
  from collections.abc import Iterable
@@ -36,11 +47,7 @@ if TYPE_CHECKING:
36
47
  from _pytest.config.argparsing import Parser
37
48
  from _pytest.python import Function
38
49
 
39
- from utilities.types import Coro, Delta, PathLike
40
-
41
-
42
- IS_CI = "CI" in environ
43
- IS_CI_AND_NOT_LINUX = IS_CI and IS_NOT_LINUX
50
+ from utilities.types import PathLike
44
51
 
45
52
 
46
53
  try: # WARNING: this package cannot use unguarded `pytest` imports
@@ -138,9 +145,9 @@ def add_pytest_configure(config: Config, options: Iterable[tuple[str, str]], /)
138
145
 
139
146
 
140
147
  def make_ids(obj: Any, /) -> str:
141
- if isinstance(obj, FunctionType):
148
+ if isinstance(obj, FunctionType): # pragma: no cover
142
149
  return sub(r"\s+at +0x[0-9a-fA-F]+", "", repr(obj))
143
- return repr(obj)
150
+ return repr(obj) # pragma: no cover
144
151
 
145
152
 
146
153
  ##
@@ -195,7 +202,7 @@ class _NodeIdToPathNotGetTailError(NodeIdToPathError):
195
202
  ##
196
203
 
197
204
 
198
- def run_frac[F: Callable[..., MaybeCoro[None]]](
205
+ def run_test_frac[F: Callable[..., MaybeCoro[None]]](
199
206
  *,
200
207
  predicate: MaybeCallableBoolLike | None = None,
201
208
  frac: float = 0.5,
@@ -203,11 +210,11 @@ def run_frac[F: Callable[..., MaybeCoro[None]]](
203
210
  ) -> Callable[[F], F]:
204
211
  """Run a test only a fraction of the time.."""
205
212
  return cast(
206
- "Any", partial(_run_frac_inner, predicate=predicate, frac=frac, seed=seed)
213
+ "Any", partial(_run_test_frac_inner, predicate=predicate, frac=frac, seed=seed)
207
214
  )
208
215
 
209
216
 
210
- def _run_frac_inner[F: Callable[..., MaybeCoro[None]]](
217
+ def _run_test_frac_inner[F: Callable[..., MaybeCoro[None]]](
211
218
  func: F,
212
219
  /,
213
220
  *,
@@ -255,109 +262,43 @@ def _skipif_frac(
255
262
  ##
256
263
 
257
264
 
258
- def throttle[F: Callable[..., MaybeCoro[None]]](
259
- *, root: PathLike | None = None, delta: Delta = SECOND, on_try: bool = False
265
+ def throttle_test[F: Callable[..., MaybeCoro[None]]](
266
+ *, on_try: bool = False, root: PathLike | None = None, duration: Duration = SECOND
260
267
  ) -> Callable[[F], F]:
261
268
  """Throttle a test. On success by default, on try otherwise."""
262
- return cast("Any", partial(_throttle_inner, root=root, delta=delta, on_try=on_try))
263
-
264
-
265
- def _throttle_inner[F: Callable[..., MaybeCoro[None]]](
266
- func: F,
267
- /,
268
- *,
269
- root: PathLike | None = None,
270
- delta: Delta = SECOND,
271
- on_try: bool = False,
272
- ) -> F:
273
- if get_env_var("THROTTLE", nullable=True) is not None:
274
- return func
275
- match bool(iscoroutinefunction(func)), on_try:
276
- case False, False:
277
-
278
- @wraps(func)
279
- def throttle_sync_on_pass(*args: Any, **kwargs: Any) -> None:
280
- _skipif_recent(root=root, delta=delta)
281
- cast("Callable[..., None]", func)(*args, **kwargs)
282
- _write(root)
283
-
284
- return cast("Any", throttle_sync_on_pass)
285
-
286
- case False, True:
287
-
288
- @wraps(func)
289
- def throttle_sync_on_try(*args: Any, **kwargs: Any) -> None:
290
- _skipif_recent(root=root, delta=delta)
291
- _write(root)
292
- cast("Callable[..., None]", func)(*args, **kwargs)
293
-
294
- return cast("Any", throttle_sync_on_try)
295
-
296
- case True, False:
269
+ return throttle(
270
+ on_try=on_try,
271
+ duration=duration,
272
+ path=partial(_get_test_path, root=root),
273
+ raiser=_run_skip,
274
+ )
297
275
 
298
- @wraps(func)
299
- async def throttle_async_on_pass(*args: Any, **kwargs: Any) -> None:
300
- _skipif_recent(root=root, delta=delta)
301
- await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
302
- _write(root)
303
276
 
304
- return cast("Any", throttle_async_on_pass)
277
+ def _run_skip() -> NoReturn:
278
+ from pytest import skip
305
279
 
306
- case True, True:
280
+ skip(reason=f"{_get_name()} throttled")
307
281
 
308
- @wraps(func)
309
- async def throttle_async_on_try(*args: Any, **kwargs: Any) -> None:
310
- _skipif_recent(root=root, delta=delta)
311
- _write(root)
312
- await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
313
282
 
314
- return cast("Any", throttle_async_on_try)
283
+ def _get_name() -> str:
284
+ return environ["PYTEST_CURRENT_TEST"]
315
285
 
316
- case never:
317
- assert_never(never)
318
286
 
287
+ @cache
288
+ def _md5_hash_cached(text: str, /) -> str:
289
+ return md5_hash(text)
319
290
 
320
- def _skipif_recent(*, root: PathLike | None = None, delta: Delta = SECOND) -> None:
321
- from pytest import skip
322
291
 
323
- path = _get_path(root)
324
- try:
325
- contents = path.read_text()
326
- except FileNotFoundError:
327
- return
328
- try:
329
- last = ZonedDateTime.parse_iso(contents)
330
- except ValueError:
331
- return
332
- now = get_now_local()
333
- if (now - delta) < last:
334
- age = now - last
335
- _ = skip(reason=f"{_get_name()} throttled (age {age})")
336
-
337
-
338
- def _get_path(root: PathLike | None = None, /) -> Path:
292
+ def _get_test_path(*, root: PathLike | None = None) -> Path:
339
293
  if root is None:
340
- root_use = get_root().joinpath(".pytest_cache", "throttle") # pragma: no cover
294
+ root_use = get_repo_root().joinpath(
295
+ ".pytest_cache", "throttle"
296
+ ) # pragma: no cover
341
297
  else:
342
298
  root_use = root
343
299
  return Path(root_use, _md5_hash_cached(_get_name()))
344
300
 
345
301
 
346
- @cache
347
- def _md5_hash_cached(text: str, /) -> str:
348
- return md5_hash(text)
349
-
350
-
351
- def _get_name() -> str:
352
- return environ["PYTEST_CURRENT_TEST"]
353
-
354
-
355
- def _write(root: PathLike | None = None, /) -> None:
356
- path = _get_path(root)
357
- with writer(path, overwrite=True) as temp:
358
- _ = temp.write_text(get_now_local().format_iso())
359
-
360
-
361
302
  __all__ = [
362
303
  "IS_CI",
363
304
  "IS_CI_AND_NOT_LINUX",
@@ -367,12 +308,12 @@ __all__ = [
367
308
  "add_pytest_configure",
368
309
  "make_ids",
369
310
  "node_id_path",
370
- "run_frac",
311
+ "run_test_frac",
371
312
  "skipif_ci",
372
313
  "skipif_ci_and_not_linux",
373
314
  "skipif_linux",
374
315
  "skipif_mac",
375
316
  "skipif_not_linux",
376
317
  "skipif_not_mac",
377
- "throttle",
318
+ "throttle_test",
378
319
  ]
@@ -40,7 +40,7 @@ else:
40
40
 
41
41
 
42
42
  def _get_path(request: FixtureRequest, /) -> Path:
43
- from utilities.pathlib import get_root
43
+ from utilities.pathlib import get_repo_root
44
44
  from utilities.pytest import _NodeIdToPathNotGetTailError, node_id_path
45
45
 
46
46
  path = Path(cast("Any", request).fspath)
@@ -50,7 +50,7 @@ def _get_path(request: FixtureRequest, /) -> Path:
50
50
  except _NodeIdToPathNotGetTailError:
51
51
  root = Path("tests")
52
52
  tail = node_id_path(request.node.nodeid, root=root)
53
- return get_root(path).joinpath(root, "regressions", tail)
53
+ return get_repo_root(path).joinpath(root, "regressions", tail)
54
54
 
55
55
 
56
56
  __all__ = ["orjson_regression", "polars_regression"]
@@ -4,14 +4,14 @@ from contextlib import suppress
4
4
  from dataclasses import dataclass
5
5
  from json import loads
6
6
  from pathlib import Path
7
- from shutil import copytree
8
7
  from typing import TYPE_CHECKING, Any, assert_never, override
9
8
 
9
+ from pytest_datadir.plugin import LazyDataDir
10
10
  from pytest_regressions.file_regression import FileRegressionFixture
11
11
 
12
+ from utilities.core import _CopyOrMoveSourceNotFoundError, copy, repr_
12
13
  from utilities.functions import ensure_str
13
14
  from utilities.operator import is_equal
14
- from utilities.reprlib import get_repr
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from polars import DataFrame, Series
@@ -33,10 +33,12 @@ class OrjsonRegressionFixture:
33
33
  path = Path(path)
34
34
  original_datadir = path.parent
35
35
  data_dir = tmp_path.joinpath(ensure_str(request.fixturename))
36
- with suppress(FileNotFoundError):
37
- _ = copytree(original_datadir, data_dir)
36
+ with suppress(_CopyOrMoveSourceNotFoundError):
37
+ copy(original_datadir, data_dir, overwrite=True)
38
38
  self._fixture = FileRegressionFixture(
39
- datadir=data_dir, original_datadir=original_datadir, request=request
39
+ datadir=LazyDataDir(original_datadir=original_datadir, tmp_path=data_dir),
40
+ original_datadir=original_datadir,
41
+ request=request,
40
42
  )
41
43
  self._basename = path.name
42
44
 
@@ -93,7 +95,7 @@ class OrjsonRegressionError(Exception):
93
95
 
94
96
  @override
95
97
  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)}"
98
+ return f"Obtained object (at {str(self.path_obtained)!r}) and existing object (at {str(self.path_existing)!r}) differ; got {repr_(self.obtained)} and {repr_(self.existing)}"
97
99
 
98
100
 
99
101
  ##
utilities/random.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from random import Random, SystemRandom
3
+ from random import Random
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  from utilities.functools import cache
@@ -11,12 +11,6 @@ if TYPE_CHECKING:
11
11
  from utilities.types import Seed
12
12
 
13
13
 
14
- SYSTEM_RANDOM = SystemRandom()
15
-
16
-
17
- ##
18
-
19
-
20
14
  def bernoulli(*, true: float = 0.5, seed: Seed | None = None) -> bool:
21
15
  """Return a Bernoulli random variate."""
22
16
  state = get_state(seed)
@@ -66,4 +60,4 @@ def shuffle[T](iterable: Iterable[T], /, *, seed: Seed | None = None) -> list[T]
66
60
  return copy
67
61
 
68
62
 
69
- __all__ = ["SYSTEM_RANDOM", "bernoulli", "get_docker_name", "get_state", "shuffle"]
63
+ __all__ = ["bernoulli", "get_docker_name", "get_state", "shuffle"]