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/pathlib.py CHANGED
@@ -12,7 +12,9 @@ from typing import TYPE_CHECKING, Literal, assert_never, overload, override
12
12
 
13
13
  from utilities.contextlib import enhanced_context_manager
14
14
  from utilities.errors import ImpossibleCaseError
15
- from utilities.sentinel import Sentinel, sentinel
15
+ from utilities.grp import get_gid_name
16
+ from utilities.pwd import get_uid_name
17
+ from utilities.sentinel import Sentinel
16
18
 
17
19
  if TYPE_CHECKING:
18
20
  from collections.abc import Iterator, Sequence
@@ -52,33 +54,22 @@ def expand_path(path: PathLike, /) -> Path:
52
54
  ##
53
55
 
54
56
 
55
- @overload
56
- def get_path(*, path: MaybeCallablePathLike | None) -> Path: ...
57
- @overload
58
- def get_path(*, path: Sentinel) -> Sentinel: ...
59
- def get_path(
60
- *, path: MaybeCallablePathLike | None | Sentinel = sentinel
61
- ) -> Path | None | Sentinel:
62
- """Get the path."""
63
- match path:
64
- case Path() | Sentinel():
65
- return path
66
- case str():
67
- return Path(path)
68
- case None:
69
- return Path.cwd()
70
- case Callable() as func:
71
- return get_path(path=func())
72
- case _ as never:
73
- assert_never(never)
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)
74
65
 
75
66
 
76
67
  ##
77
68
 
78
69
 
79
- def get_package_root(*, path: MaybeCallablePathLike | None = None) -> Path:
70
+ def get_package_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
80
71
  """Get the package root."""
81
- path = get_path(path=path)
72
+ path = to_path(path)
82
73
  path_dir = path.parent if path.is_file() else path
83
74
  all_paths = list(chain([path_dir], path_dir.parents))
84
75
  try:
@@ -103,9 +94,9 @@ class GetPackageRootError(Exception):
103
94
  ##
104
95
 
105
96
 
106
- def get_repo_root(*, path: MaybeCallablePathLike | None = None) -> Path:
97
+ def get_repo_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
107
98
  """Get the repo root."""
108
- path = get_path(path=path)
99
+ path = to_path(path)
109
100
  path_dir = path.parent if path.is_file() else path
110
101
  try:
111
102
  output = check_output(
@@ -118,15 +109,30 @@ def get_repo_root(*, path: MaybeCallablePathLike | None = None) -> Path:
118
109
  # newer versions of git report "Not a git repository", whilst older
119
110
  # versions report "not a git repository"
120
111
  if search("fatal: not a git repository", error.stderr, flags=IGNORECASE):
121
- raise GetRepoRootError(path=path) from None
112
+ raise _GetRepoRootNotARepoError(path=path) from None
122
113
  raise # pragma: no cover
114
+ except FileNotFoundError as error: # pragma: no cover
115
+ if search("No such file or directory: 'git'", str(error), flags=IGNORECASE):
116
+ raise _GetRepoRootGitNotFoundError from None
117
+ raise
123
118
  else:
124
119
  return Path(output.strip("\n"))
125
120
 
126
121
 
127
122
  @dataclass(kw_only=True, slots=True)
128
- class GetRepoRootError(Exception):
129
- path: PathLike
123
+ class GetRepoRootError(Exception): ...
124
+
125
+
126
+ @dataclass(kw_only=True, slots=True)
127
+ class _GetRepoRootGitNotFoundError(GetRepoRootError):
128
+ @override
129
+ def __str__(self) -> str:
130
+ return "'git' not found" # pragma: no cover
131
+
132
+
133
+ @dataclass(kw_only=True, slots=True)
134
+ class _GetRepoRootNotARepoError(GetRepoRootError):
135
+ path: Path
130
136
 
131
137
  @override
132
138
  def __str__(self) -> str:
@@ -136,15 +142,15 @@ class GetRepoRootError(Exception):
136
142
  ##
137
143
 
138
144
 
139
- def get_root(*, path: MaybeCallablePathLike | None = None) -> Path:
145
+ def get_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
140
146
  """Get the root of a path."""
141
- path = get_path(path=path)
147
+ path = to_path(path)
142
148
  try:
143
- repo = get_repo_root(path=path)
149
+ repo = get_repo_root(path)
144
150
  except GetRepoRootError:
145
151
  repo = None
146
152
  try:
147
- package = get_package_root(path=path)
153
+ package = get_package_root(path)
148
154
  except GetPackageRootError:
149
155
  package = None
150
156
  match repo, package:
@@ -164,7 +170,7 @@ def get_root(*, path: MaybeCallablePathLike | None = None) -> Path:
164
170
  raise ImpossibleCaseError( # pragma: no cover
165
171
  case=[f"{repo=}", f"{package=}"]
166
172
  )
167
- case _ as never:
173
+ case never:
168
174
  assert_never(never)
169
175
 
170
176
 
@@ -213,7 +219,7 @@ def get_tail(
213
219
  return _get_tail_core(path, next(iter(matches)))
214
220
  case _, "later":
215
221
  return _get_tail_core(path, next(iter(reversed(matches))))
216
- case _ as never:
222
+ case never:
217
223
  assert_never(never)
218
224
 
219
225
 
@@ -305,6 +311,30 @@ def temp_cwd(path: PathLike, /) -> Iterator[None]:
305
311
  chdir(prev)
306
312
 
307
313
 
314
+ ##
315
+
316
+
317
+ @overload
318
+ def to_path(path: Sentinel, /) -> Sentinel: ...
319
+ @overload
320
+ def to_path(path: MaybeCallablePathLike | None = Path.cwd, /) -> Path: ...
321
+ def to_path(
322
+ path: MaybeCallablePathLike | None | Sentinel = Path.cwd, /
323
+ ) -> Path | Sentinel:
324
+ """Get the path."""
325
+ match path:
326
+ case Path() | Sentinel():
327
+ return path
328
+ case None:
329
+ return Path.cwd()
330
+ case str():
331
+ return Path(path)
332
+ case Callable() as func:
333
+ return to_path(func())
334
+ case never:
335
+ assert_never(never)
336
+
337
+
308
338
  __all__ = [
309
339
  "PWD",
310
340
  "GetPackageRootError",
@@ -312,12 +342,14 @@ __all__ = [
312
342
  "GetTailError",
313
343
  "ensure_suffix",
314
344
  "expand_path",
345
+ "get_file_group",
346
+ "get_file_owner",
315
347
  "get_package_root",
316
- "get_path",
317
348
  "get_repo_root",
318
349
  "get_tail",
319
350
  "is_sub_path",
320
351
  "list_dir",
321
352
  "module_path",
322
353
  "temp_cwd",
354
+ "to_path",
323
355
  ]
@@ -0,0 +1,298 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from functools import reduce
5
+ from operator import or_
6
+ from pathlib import Path
7
+ from stat import (
8
+ S_IMODE,
9
+ S_IRGRP,
10
+ S_IROTH,
11
+ S_IRUSR,
12
+ S_IWGRP,
13
+ S_IWOTH,
14
+ S_IWUSR,
15
+ S_IXGRP,
16
+ S_IXOTH,
17
+ S_IXUSR,
18
+ )
19
+ from typing import TYPE_CHECKING, Literal, Self, assert_never, override
20
+
21
+ from utilities.dataclasses import replace_non_sentinel
22
+ from utilities.re import ExtractGroupsError, extract_groups
23
+ from utilities.sentinel import Sentinel, sentinel
24
+
25
+ if TYPE_CHECKING:
26
+ from utilities.types import PathLike
27
+
28
+
29
+ type PermissionsLike = Permissions | int | str
30
+
31
+
32
+ ##
33
+
34
+
35
+ def ensure_perms(perms: PermissionsLike, /) -> Permissions:
36
+ """Ensure a set of file permissions."""
37
+ match perms:
38
+ case Permissions():
39
+ return perms
40
+ case int():
41
+ return Permissions.from_int(perms)
42
+ case str():
43
+ return Permissions.from_text(perms)
44
+ case never:
45
+ assert_never(never)
46
+
47
+
48
+ ##
49
+
50
+
51
+ @dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
52
+ class Permissions:
53
+ """A set of file permissions."""
54
+
55
+ user_read: bool = False
56
+ user_write: bool = False
57
+ user_execute: bool = False
58
+ group_read: bool = False
59
+ group_write: bool = False
60
+ group_execute: bool = False
61
+ others_read: bool = False
62
+ others_write: bool = False
63
+ others_execute: bool = False
64
+
65
+ def __int__(self) -> int:
66
+ flags: list[int] = [
67
+ S_IRUSR if self.user_read else 0,
68
+ S_IWUSR if self.user_write else 0,
69
+ S_IXUSR if self.user_execute else 0,
70
+ S_IRGRP if self.group_read else 0,
71
+ S_IWGRP if self.group_write else 0,
72
+ S_IXGRP if self.group_execute else 0,
73
+ S_IROTH if self.others_read else 0,
74
+ S_IWOTH if self.others_write else 0,
75
+ S_IXOTH if self.others_execute else 0,
76
+ ]
77
+ return reduce(or_, flags)
78
+
79
+ @override
80
+ def __repr__(self) -> str:
81
+ return ",".join([
82
+ self._repr_parts(
83
+ "u",
84
+ read=self.user_read,
85
+ write=self.user_write,
86
+ execute=self.user_execute,
87
+ ),
88
+ self._repr_parts(
89
+ "g",
90
+ read=self.group_read,
91
+ write=self.group_write,
92
+ execute=self.group_execute,
93
+ ),
94
+ self._repr_parts(
95
+ "o",
96
+ read=self.others_read,
97
+ write=self.others_write,
98
+ execute=self.others_execute,
99
+ ),
100
+ ])
101
+
102
+ def _repr_parts(
103
+ self,
104
+ prefix: Literal["u", "g", "o"],
105
+ /,
106
+ *,
107
+ read: bool = False,
108
+ write: bool = False,
109
+ execute: bool = False,
110
+ ) -> str:
111
+ parts: list[str] = [
112
+ "r" if read else "",
113
+ "w" if write else "",
114
+ "x" if execute else "",
115
+ ]
116
+ return f"{prefix}={''.join(parts)}"
117
+
118
+ @override
119
+ def __str__(self) -> str:
120
+ return repr(self)
121
+
122
+ @classmethod
123
+ def from_human_int(cls, n: int, /) -> Self:
124
+ if not (0 <= n <= 777):
125
+ raise PermissionsFromHumanIntRangeError(n=n)
126
+ user_read, user_write, user_execute = cls._from_human_int(n, (n // 100) % 10)
127
+ group_read, group_write, group_execute = cls._from_human_int(n, (n // 10) % 10)
128
+ others_read, others_write, others_execute = cls._from_human_int(n, n % 10)
129
+ return cls(
130
+ user_read=user_read,
131
+ user_write=user_write,
132
+ user_execute=user_execute,
133
+ group_read=group_read,
134
+ group_write=group_write,
135
+ group_execute=group_execute,
136
+ others_read=others_read,
137
+ others_write=others_write,
138
+ others_execute=others_execute,
139
+ )
140
+
141
+ @classmethod
142
+ def _from_human_int(cls, n: int, digit: int, /) -> tuple[bool, bool, bool]:
143
+ if not (0 <= digit <= 7):
144
+ raise PermissionsFromHumanIntDigitError(n=n, digit=digit)
145
+ return bool(4 & digit), bool(2 & digit), bool(1 & digit)
146
+
147
+ @classmethod
148
+ def from_int(cls, n: int, /) -> Self:
149
+ if 0o0 <= n <= 0o777:
150
+ return cls(
151
+ user_read=bool(n & S_IRUSR),
152
+ user_write=bool(n & S_IWUSR),
153
+ user_execute=bool(n & S_IXUSR),
154
+ group_read=bool(n & S_IRGRP),
155
+ group_write=bool(n & S_IWGRP),
156
+ group_execute=bool(n & S_IXGRP),
157
+ others_read=bool(n & S_IROTH),
158
+ others_write=bool(n & S_IWOTH),
159
+ others_execute=bool(n & S_IXOTH),
160
+ )
161
+ raise PermissionsFromIntError(n=n)
162
+
163
+ @classmethod
164
+ def from_path(cls, path: PathLike, /) -> Self:
165
+ return cls.from_int(S_IMODE(Path(path).stat().st_mode))
166
+
167
+ @classmethod
168
+ def from_text(cls, text: str, /) -> Self:
169
+ try:
170
+ user, group, others = extract_groups(
171
+ r"^u=(r?w?x?),g=(r?w?x?),o=(r?w?x?)$", text
172
+ )
173
+ except ExtractGroupsError:
174
+ raise PermissionsFromTextError(text=text) from None
175
+ user_read, user_write, user_execute = cls._from_text_part(user)
176
+ group_read, group_write, group_execute = cls._from_text_part(group)
177
+ others_read, others_write, others_execute = cls._from_text_part(others)
178
+ return cls(
179
+ user_read=user_read,
180
+ user_write=user_write,
181
+ user_execute=user_execute,
182
+ group_read=group_read,
183
+ group_write=group_write,
184
+ group_execute=group_execute,
185
+ others_read=others_read,
186
+ others_write=others_write,
187
+ others_execute=others_execute,
188
+ )
189
+
190
+ @classmethod
191
+ def _from_text_part(cls, text: str, /) -> tuple[bool, bool, bool]:
192
+ read, write, execute = extract_groups("^(r?)(w?)(x?)$", text)
193
+ return read != "", write != "", execute != ""
194
+
195
+ @property
196
+ def human_int(self) -> int:
197
+ return (
198
+ 100
199
+ * self._human_int(
200
+ read=self.user_read, write=self.user_write, execute=self.user_execute
201
+ )
202
+ + 10
203
+ * self._human_int(
204
+ read=self.group_read, write=self.group_write, execute=self.group_execute
205
+ )
206
+ + self._human_int(
207
+ read=self.others_read,
208
+ write=self.others_write,
209
+ execute=self.others_execute,
210
+ )
211
+ )
212
+
213
+ def _human_int(
214
+ self, *, read: bool = False, write: bool = False, execute: bool = False
215
+ ) -> int:
216
+ return (4 if read else 0) + (2 if write else 0) + (1 if execute else 0)
217
+
218
+ def replace(
219
+ self,
220
+ *,
221
+ user_read: bool | Sentinel = sentinel,
222
+ user_write: bool | Sentinel = sentinel,
223
+ user_execute: bool | Sentinel = sentinel,
224
+ group_read: bool | Sentinel = sentinel,
225
+ group_write: bool | Sentinel = sentinel,
226
+ group_execute: bool | Sentinel = sentinel,
227
+ others_read: bool | Sentinel = sentinel,
228
+ others_write: bool | Sentinel = sentinel,
229
+ others_execute: bool | Sentinel = sentinel,
230
+ ) -> Self:
231
+ return replace_non_sentinel(
232
+ self,
233
+ user_read=user_read,
234
+ user_write=user_write,
235
+ user_execute=user_execute,
236
+ group_read=group_read,
237
+ group_write=group_write,
238
+ group_execute=group_execute,
239
+ others_read=others_read,
240
+ others_write=others_write,
241
+ others_execute=others_execute,
242
+ )
243
+
244
+
245
+ @dataclass(kw_only=True, slots=True)
246
+ class PermissionsError(Exception): ...
247
+
248
+
249
+ @dataclass(kw_only=True, slots=True)
250
+ class PermissionsFromHumanIntError(PermissionsError):
251
+ n: int
252
+
253
+
254
+ @dataclass(kw_only=True, slots=True)
255
+ class PermissionsFromHumanIntRangeError(PermissionsFromHumanIntError):
256
+ @override
257
+ def __str__(self) -> str:
258
+ return f"Invalid human integer for permissions; got {self.n}"
259
+
260
+
261
+ @dataclass(kw_only=True, slots=True)
262
+ class PermissionsFromHumanIntDigitError(PermissionsFromHumanIntError):
263
+ digit: int
264
+
265
+ @override
266
+ def __str__(self) -> str:
267
+ return (
268
+ f"Invalid human integer for permissions; got digit {self.digit} in {self.n}"
269
+ )
270
+
271
+
272
+ @dataclass(kw_only=True, slots=True)
273
+ class PermissionsFromIntError(PermissionsError):
274
+ n: int
275
+
276
+ @override
277
+ def __str__(self) -> str:
278
+ return f"Invalid integer for permissions; got {self.n} = {oct(self.n)}"
279
+
280
+
281
+ @dataclass(kw_only=True, slots=True)
282
+ class PermissionsFromTextError(PermissionsError):
283
+ text: str
284
+
285
+ @override
286
+ def __str__(self) -> str:
287
+ return f"Invalid string for permissions; got {self.text!r}"
288
+
289
+
290
+ __all__ = [
291
+ "Permissions",
292
+ "PermissionsError",
293
+ "PermissionsFromHumanIntDigitError",
294
+ "PermissionsFromHumanIntError",
295
+ "PermissionsFromIntError",
296
+ "PermissionsFromTextError",
297
+ "ensure_perms",
298
+ ]
utilities/platform.py CHANGED
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Literal, assert_never, override
9
9
  if TYPE_CHECKING:
10
10
  from collections.abc import Iterable, Iterator
11
11
 
12
+
12
13
  System = Literal["windows", "mac", "linux"]
13
14
 
14
15
 
@@ -50,7 +51,7 @@ IS_NOT_LINUX = not IS_LINUX
50
51
  def get_max_pid() -> int | None:
51
52
  """Get the maximum process ID."""
52
53
  match SYSTEM:
53
- case "windows": # pragma: no cover
54
+ case "windows": # skipif-not-windows
54
55
  return None
55
56
  case "mac": # skipif-not-macos
56
57
  return 99999
@@ -60,7 +61,7 @@ def get_max_pid() -> int | None:
60
61
  return int(path.read_text())
61
62
  except FileNotFoundError: # pragma: no cover
62
63
  return None
63
- case _ as never:
64
+ case never:
64
65
  assert_never(never)
65
66
 
66
67
 
@@ -79,7 +80,7 @@ def get_strftime(text: str, /) -> str:
79
80
  return text
80
81
  case "linux": # skipif-not-linux
81
82
  return sub("%Y", "%4Y", text)
82
- case _ as never:
83
+ case never:
83
84
  assert_never(never)
84
85
 
85
86
 
@@ -95,7 +96,7 @@ def maybe_yield_lower_case(text: Iterable[str], /) -> Iterator[str]:
95
96
  yield from (t.lower() for t in text)
96
97
  case "linux": # skipif-not-linux
97
98
  yield from text
98
- case _ as never:
99
+ case never:
99
100
  assert_never(never)
100
101
 
101
102