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/packaging.py ADDED
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Self, overload, override
5
+
6
+ import packaging._parser
7
+ import packaging.requirements
8
+ from packaging.requirements import _parse_requirement
9
+ from packaging.specifiers import Specifier, SpecifierSet
10
+
11
+ from utilities.core import OneEmptyError, one
12
+
13
+ if TYPE_CHECKING:
14
+ from packaging._parser import MarkerList
15
+
16
+
17
+ @dataclass(order=True, unsafe_hash=True, slots=True)
18
+ class Requirement:
19
+ requirement: str
20
+ _parsed_req: packaging._parser.ParsedRequirement = field(init=False, repr=False)
21
+ _custom_req: _CustomRequirement = field(init=False, repr=False)
22
+
23
+ def __getitem__(self, operator: str, /) -> str:
24
+ return self.specifier_set[operator]
25
+
26
+ def __post_init__(self) -> None:
27
+ self._parsed_req = _parse_requirement(self.requirement)
28
+ self._custom_req = _CustomRequirement(self.requirement)
29
+
30
+ @override
31
+ def __str__(self) -> str:
32
+ return str(self._custom_req)
33
+
34
+ def drop(self, operator: str, /) -> Self:
35
+ return type(self)(str(self._custom_req.drop(operator)))
36
+
37
+ @property
38
+ def extras(self) -> list[str]:
39
+ return self._parsed_req.extras
40
+
41
+ @overload
42
+ def get(self, operator: str, default: str, /) -> str: ...
43
+ @overload
44
+ def get(self, operator: str, default: None = None, /) -> str | None: ...
45
+ def get(self, operator: str, default: str | None = None, /) -> str | None:
46
+ return self.specifier_set.get(operator, default)
47
+
48
+ @property
49
+ def marker(self) -> MarkerList | None:
50
+ return self._parsed_req.marker
51
+
52
+ @property
53
+ def name(self) -> str:
54
+ return self._parsed_req.name
55
+
56
+ def replace(self, operator: str, version: str | None, /) -> Self:
57
+ return type(self)(str(self._custom_req.replace(operator, version)))
58
+
59
+ @property
60
+ def specifier(self) -> str:
61
+ return self._parsed_req.specifier
62
+
63
+ @property
64
+ def specifier_set(self) -> _CustomSpecifierSet:
65
+ return _CustomSpecifierSet(_parse_requirement(self.requirement).specifier)
66
+
67
+ @property
68
+ def url(self) -> str:
69
+ return self._parsed_req.url
70
+
71
+
72
+ class _CustomRequirement(packaging.requirements.Requirement):
73
+ specifier: _CustomSpecifierSet
74
+
75
+ @override
76
+ def __init__(self, requirement_string: str) -> None:
77
+ super().__init__(requirement_string)
78
+ parsed = _parse_requirement(requirement_string)
79
+ self.specifier = _CustomSpecifierSet(parsed.specifier) # pyright: ignore[reportIncompatibleVariableOverride]
80
+
81
+ def drop(self, operator: str, /) -> Self:
82
+ new = type(self)(super().__str__())
83
+ new.specifier = self.specifier.drop(operator)
84
+ return new
85
+
86
+ def replace(self, operator: str, version: str | None, /) -> Self:
87
+ new = type(self)(super().__str__())
88
+ new.specifier = self.specifier.replace(operator, version)
89
+ return new
90
+
91
+
92
+ class _CustomSpecifierSet(SpecifierSet):
93
+ def __getitem__(self, operator: str, /) -> str:
94
+ try:
95
+ return one(s.version for s in self if s.operator == operator)
96
+ except OneEmptyError:
97
+ raise KeyError(operator) from None
98
+
99
+ @override
100
+ def __str__(self) -> str:
101
+ specs = sorted(self._specs, key=self._sort_key)
102
+ return ", ".join(map(str, specs))
103
+
104
+ def drop(self, operator: str, /) -> Self:
105
+ if any(s.operator == operator for s in self):
106
+ return type(self)(s for s in self if s.operator != operator)
107
+ raise KeyError(operator)
108
+
109
+ @overload
110
+ def get(self, operator: str, default: str, /) -> str: ...
111
+ @overload
112
+ def get(self, operator: str, default: None = None, /) -> str | None: ...
113
+ def get(self, operator: str, default: str | None = None, /) -> str | None:
114
+ try:
115
+ return self[operator]
116
+ except KeyError:
117
+ return default
118
+
119
+ def replace(self, operator: str, version: str | None, /) -> Self:
120
+ specifiers = [s for s in self if s.operator != operator]
121
+ if version is not None:
122
+ specifiers.append(Specifier(spec=f"{operator}{version}"))
123
+ return type(self)(specifiers)
124
+
125
+ def _sort_key(self, spec: Specifier, /) -> int:
126
+ return ["==", "!=", "~=", ">", ">=", "<", "<="].index(spec.operator)
127
+
128
+
129
+ __all__ = ["Requirement"]
utilities/parse.py CHANGED
@@ -21,15 +21,24 @@ from whenever import (
21
21
  ZonedDateTime,
22
22
  )
23
23
 
24
- from utilities.enum import ParseEnumError, parse_enum
25
- from utilities.iterables import OneEmptyError, OneNonUniqueError, one, one_str
26
- from utilities.math import ParseNumberError, parse_number
27
- from utilities.re import ExtractGroupError, extract_group
28
- from utilities.sentinel import ParseSentinelError, Sentinel, parse_sentinel
29
- from utilities.text import (
24
+ from utilities.constants import (
30
25
  BRACKETS,
31
26
  LIST_SEPARATOR,
32
27
  PAIR_SEPARATOR,
28
+ Sentinel,
29
+ SentinelParseError,
30
+ )
31
+ from utilities.core import (
32
+ ExtractGroupError,
33
+ OneEmptyError,
34
+ OneNonUniqueError,
35
+ extract_group,
36
+ one,
37
+ one_str,
38
+ )
39
+ from utilities.enum import ParseEnumError, parse_enum
40
+ from utilities.math import ParseNumberError, parse_number
41
+ from utilities.text import (
33
42
  ParseBoolError,
34
43
  ParseNoneError,
35
44
  join_strs,
@@ -52,7 +61,12 @@ from utilities.typing import (
52
61
  is_tuple_type,
53
62
  is_union_type,
54
63
  )
55
- from utilities.version import ParseVersionError, Version, parse_version
64
+ from utilities.version import (
65
+ Version2,
66
+ Version3,
67
+ _Version2ParseError,
68
+ _Version3ParseError,
69
+ )
56
70
 
57
71
  if TYPE_CHECKING:
58
72
  from collections.abc import Iterable, Mapping, Sequence
@@ -211,13 +225,18 @@ def _parse_object_type(
211
225
  return Path(text).expanduser()
212
226
  if issubclass(cls, Sentinel):
213
227
  try:
214
- return parse_sentinel(text)
215
- except ParseSentinelError:
228
+ return Sentinel.parse(text)
229
+ except SentinelParseError:
230
+ raise _ParseObjectParseError(type_=cls, text=text) from None
231
+ if issubclass(cls, Version2):
232
+ try:
233
+ return Version2.parse(text)
234
+ except _Version2ParseError:
216
235
  raise _ParseObjectParseError(type_=cls, text=text) from None
217
- if issubclass(cls, Version):
236
+ if issubclass(cls, Version3):
218
237
  try:
219
- return parse_version(text)
220
- except ParseVersionError:
238
+ return Version3.parse(text)
239
+ except _Version3ParseError:
221
240
  raise _ParseObjectParseError(type_=cls, text=text) from None
222
241
  raise _ParseObjectParseError(type_=cls, text=text)
223
242
 
@@ -460,7 +479,8 @@ def serialize_object(
460
479
  | IPv6Address
461
480
  | Path
462
481
  | Sentinel
463
- | Version,
482
+ | Version2
483
+ | Version3,
464
484
  ):
465
485
  return str(obj)
466
486
  if isinstance(
utilities/pathlib.py CHANGED
@@ -3,28 +3,19 @@ from __future__ import annotations
3
3
  from collections.abc import Callable
4
4
  from dataclasses import dataclass
5
5
  from itertools import chain
6
- from os import chdir
7
- from os.path import expandvars
8
6
  from pathlib import Path
9
7
  from re import IGNORECASE, search
10
8
  from subprocess import PIPE, CalledProcessError, check_output
11
9
  from typing import TYPE_CHECKING, Literal, assert_never, overload, override
12
10
 
13
- from utilities.contextlib import enhanced_context_manager
14
- from utilities.errors import ImpossibleCaseError
15
- from utilities.grp import get_gid_name
16
- from utilities.pwd import get_uid_name
17
- from utilities.sentinel import Sentinel
11
+ from utilities.constants import Sentinel
18
12
 
19
13
  if TYPE_CHECKING:
20
- from collections.abc import Iterator, Sequence
14
+ from collections.abc import Sequence
21
15
 
22
16
  from utilities.types import MaybeCallablePathLike, PathLike
23
17
 
24
18
 
25
- PWD = Path.cwd()
26
-
27
-
28
19
  def ensure_suffix(path: PathLike, suffix: str, /) -> Path:
29
20
  """Ensure a path has a given suffix."""
30
21
  path = Path(path)
@@ -44,56 +35,6 @@ def ensure_suffix(path: PathLike, suffix: str, /) -> Path:
44
35
  ##
45
36
 
46
37
 
47
- def expand_path(path: PathLike, /) -> Path:
48
- """Expand a path."""
49
- path = str(path)
50
- path = expandvars(path)
51
- return Path(path).expanduser()
52
-
53
-
54
- ##
55
-
56
-
57
- def get_file_group(path: PathLike, /) -> str | None:
58
- """Get the group of a file."""
59
- return get_gid_name(to_path(path).stat().st_gid)
60
-
61
-
62
- def get_file_owner(path: PathLike, /) -> str | None:
63
- """Get the owner of a file."""
64
- return get_uid_name(to_path(path).stat().st_uid)
65
-
66
-
67
- ##
68
-
69
-
70
- def get_package_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
71
- """Get the package root."""
72
- path = to_path(path)
73
- path_dir = path.parent if path.is_file() else path
74
- all_paths = list(chain([path_dir], path_dir.parents))
75
- try:
76
- return next(
77
- p.resolve()
78
- for p in all_paths
79
- if any(p_i.name == "pyproject.toml" for p_i in p.iterdir())
80
- )
81
- except StopIteration:
82
- raise GetPackageRootError(path=path) from None
83
-
84
-
85
- @dataclass(kw_only=True, slots=True)
86
- class GetPackageRootError(Exception):
87
- path: PathLike
88
-
89
- @override
90
- def __str__(self) -> str:
91
- return f"Path is not part of a package: {self.path}"
92
-
93
-
94
- ##
95
-
96
-
97
38
  def get_repo_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
98
39
  """Get the repo root."""
99
40
  path = to_path(path)
@@ -142,50 +83,6 @@ class _GetRepoRootNotARepoError(GetRepoRootError):
142
83
  ##
143
84
 
144
85
 
145
- def get_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
146
- """Get the root of a path."""
147
- path = to_path(path)
148
- try:
149
- repo = get_repo_root(path)
150
- except GetRepoRootError:
151
- repo = None
152
- try:
153
- package = get_package_root(path)
154
- except GetPackageRootError:
155
- package = None
156
- match repo, package:
157
- case None, None:
158
- raise GetRootError(path=path)
159
- case Path(), None:
160
- return repo
161
- case None, Path():
162
- return package
163
- case Path(), Path():
164
- if repo == package:
165
- return repo
166
- if is_sub_path(repo, package, strict=True):
167
- return repo
168
- if is_sub_path(package, repo, strict=True):
169
- return package
170
- raise ImpossibleCaseError( # pragma: no cover
171
- case=[f"{repo=}", f"{package=}"]
172
- )
173
- case never:
174
- assert_never(never)
175
-
176
-
177
- @dataclass(kw_only=True, slots=True)
178
- class GetRootError(Exception):
179
- path: PathLike
180
-
181
- @override
182
- def __str__(self) -> str:
183
- return f"Unable to determine root from {str(self.path)!r}"
184
-
185
-
186
- ##
187
-
188
-
189
86
  type _GetTailDisambiguate = Literal["raise", "earlier", "later"]
190
87
 
191
88
 
@@ -283,15 +180,6 @@ def module_path(
283
180
  ##
284
181
 
285
182
 
286
- def is_sub_path(x: PathLike, y: PathLike, /, *, strict: bool = False) -> bool:
287
- """Check if a path is a sub path of another."""
288
- x, y = [Path(i).resolve() for i in [x, y]]
289
- return x.is_relative_to(y) and not (strict and y.is_relative_to(x))
290
-
291
-
292
- ##
293
-
294
-
295
183
  def list_dir(path: PathLike, /) -> Sequence[Path]:
296
184
  """List the contents of a directory."""
297
185
  return sorted(Path(path).iterdir())
@@ -300,20 +188,6 @@ def list_dir(path: PathLike, /) -> Sequence[Path]:
300
188
  ##
301
189
 
302
190
 
303
- @enhanced_context_manager
304
- def temp_cwd(path: PathLike, /) -> Iterator[None]:
305
- """Context manager with temporary current working directory set."""
306
- prev = Path.cwd()
307
- chdir(path)
308
- try:
309
- yield
310
- finally:
311
- chdir(prev)
312
-
313
-
314
- ##
315
-
316
-
317
191
  @overload
318
192
  def to_path(path: Sentinel, /) -> Sentinel: ...
319
193
  @overload
@@ -336,20 +210,12 @@ def to_path(
336
210
 
337
211
 
338
212
  __all__ = [
339
- "PWD",
340
- "GetPackageRootError",
341
213
  "GetRepoRootError",
342
214
  "GetTailError",
343
215
  "ensure_suffix",
344
- "expand_path",
345
- "get_file_group",
346
- "get_file_owner",
347
- "get_package_root",
348
216
  "get_repo_root",
349
217
  "get_tail",
350
- "is_sub_path",
351
218
  "list_dir",
352
219
  "module_path",
353
- "temp_cwd",
354
220
  "to_path",
355
221
  ]
utilities/platform.py CHANGED
@@ -1,74 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
4
- from pathlib import Path
5
- from platform import system
6
3
  from re import sub
7
- from typing import TYPE_CHECKING, Literal, assert_never, override
4
+ from typing import assert_never
8
5
 
9
- if TYPE_CHECKING:
10
- from collections.abc import Iterable, Iterator
11
-
12
-
13
- System = Literal["windows", "mac", "linux"]
14
-
15
-
16
- def get_system() -> System:
17
- """Get the system/OS name."""
18
- sys = system()
19
- if sys == "Windows": # skipif-not-windows
20
- return "windows"
21
- if sys == "Darwin": # skipif-not-macos
22
- return "mac"
23
- if sys == "Linux": # skipif-not-linux
24
- return "linux"
25
- raise GetSystemError(sys=sys) # pragma: no cover
26
-
27
-
28
- @dataclass(kw_only=True, slots=True)
29
- class GetSystemError(Exception):
30
- sys: str
31
-
32
- @override
33
- def __str__(self) -> str:
34
- return ( # pragma: no cover
35
- f"System must be one of Windows, Darwin, Linux; got {self.sys!r} instead"
36
- )
37
-
38
-
39
- SYSTEM = get_system()
40
- IS_WINDOWS = SYSTEM == "windows"
41
- IS_MAC = SYSTEM == "mac"
42
- IS_LINUX = SYSTEM == "linux"
43
- IS_NOT_WINDOWS = not IS_WINDOWS
44
- IS_NOT_MAC = not IS_MAC
45
- IS_NOT_LINUX = not IS_LINUX
46
-
47
-
48
- ##
49
-
50
-
51
- def get_max_pid() -> int | None:
52
- """Get the maximum process ID."""
53
- match SYSTEM:
54
- case "windows": # skipif-not-windows
55
- return None
56
- case "mac": # skipif-not-macos
57
- return 99999
58
- case "linux": # skipif-not-linux
59
- path = Path("/proc/sys/kernel/pid_max")
60
- try:
61
- return int(path.read_text())
62
- except FileNotFoundError: # pragma: no cover
63
- return None
64
- case never:
65
- assert_never(never)
66
-
67
-
68
- MAX_PID = get_max_pid()
69
-
70
-
71
- ##
6
+ from utilities.constants import SYSTEM
72
7
 
73
8
 
74
9
  def get_strftime(text: str, /) -> str:
@@ -87,32 +22,15 @@ def get_strftime(text: str, /) -> str:
87
22
  ##
88
23
 
89
24
 
90
- def maybe_yield_lower_case(text: Iterable[str], /) -> Iterator[str]:
91
- """Yield lower-cased text if the platform is case-insentive."""
25
+ def maybe_lower_case(text: str, /) -> str:
26
+ """Lower-case text if the platform is case-insensitive w.r.t. filenames."""
92
27
  match SYSTEM:
93
- case "windows": # skipif-not-windows
94
- yield from (t.lower() for t in text)
95
- case "mac": # skipif-not-macos
96
- yield from (t.lower() for t in text)
28
+ case "windows" | "mac": # skipif-linux
29
+ return text.lower()
97
30
  case "linux": # skipif-not-linux
98
- yield from text
31
+ return text
99
32
  case never:
100
33
  assert_never(never)
101
34
 
102
35
 
103
- __all__ = [
104
- "IS_LINUX",
105
- "IS_MAC",
106
- "IS_NOT_LINUX",
107
- "IS_NOT_MAC",
108
- "IS_NOT_WINDOWS",
109
- "IS_WINDOWS",
110
- "MAX_PID",
111
- "SYSTEM",
112
- "GetSystemError",
113
- "System",
114
- "get_max_pid",
115
- "get_strftime",
116
- "get_system",
117
- "maybe_yield_lower_case",
118
- ]
36
+ __all__ = ["get_strftime", "maybe_lower_case"]