dycw-utilities 0.166.30__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 (45) hide show
  1. dycw_utilities-0.175.17.dist-info/METADATA +34 -0
  2. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.175.17.dist-info}/RECORD +43 -38
  3. dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.175.17.dist-info}/entry_points.txt +1 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +9 -4
  7. utilities/asyncio.py +10 -16
  8. utilities/cachetools.py +9 -6
  9. utilities/click.py +76 -20
  10. utilities/docker.py +293 -0
  11. utilities/functions.py +1 -1
  12. utilities/grp.py +28 -0
  13. utilities/hypothesis.py +38 -6
  14. utilities/importlib.py +17 -1
  15. utilities/jinja2.py +148 -0
  16. utilities/logging.py +7 -9
  17. utilities/orjson.py +18 -18
  18. utilities/os.py +38 -0
  19. utilities/parse.py +2 -2
  20. utilities/pathlib.py +18 -1
  21. utilities/permissions.py +298 -0
  22. utilities/platform.py +1 -1
  23. utilities/polars.py +4 -1
  24. utilities/postgres.py +28 -29
  25. utilities/pwd.py +28 -0
  26. utilities/pydantic.py +11 -0
  27. utilities/pydantic_settings.py +81 -8
  28. utilities/pydantic_settings_sops.py +13 -0
  29. utilities/pytest.py +60 -30
  30. utilities/pytest_regressions.py +26 -7
  31. utilities/shutil.py +25 -0
  32. utilities/sqlalchemy.py +15 -0
  33. utilities/subprocess.py +1572 -0
  34. utilities/tempfile.py +60 -1
  35. utilities/text.py +48 -32
  36. utilities/timer.py +2 -2
  37. utilities/traceback.py +1 -1
  38. utilities/types.py +5 -0
  39. utilities/typing.py +8 -2
  40. utilities/whenever.py +36 -5
  41. dycw_utilities-0.166.30.dist-info/METADATA +0 -41
  42. dycw_utilities-0.166.30.dist-info/WHEEL +0 -4
  43. dycw_utilities-0.166.30.dist-info/licenses/LICENSE +0 -21
  44. utilities/aeventkit.py +0 -388
  45. utilities/typed_settings.py +0 -152
utilities/orjson.py CHANGED
@@ -481,49 +481,49 @@ def _object_hook(
481
481
  if match := _NONE_PATTERN.search(text):
482
482
  return None
483
483
  if match := _DATE_PATTERN.search(text):
484
- return Date.parse_common_iso(match.group(1))
484
+ return Date.parse_iso(match.group(1))
485
485
  if match := _DATE_DELTA_PATTERN.search(text):
486
- return DateDelta.parse_common_iso(match.group(1))
486
+ return DateDelta.parse_iso(match.group(1))
487
487
  if match := _DATE_PERIOD_PATTERN.search(text):
488
- start, end = map(Date.parse_common_iso, match.group(1).split(","))
488
+ start, end = map(Date.parse_iso, match.group(1).split(","))
489
489
  return DatePeriod(start, end)
490
490
  if match := _DATE_TIME_DELTA_PATTERN.search(text):
491
- return DateTimeDelta.parse_common_iso(match.group(1))
491
+ return DateTimeDelta.parse_iso(match.group(1))
492
492
  if match := _FLOAT_PATTERN.search(text):
493
493
  return float(match.group(1))
494
494
  if match := _MONTH_DAY_PATTERN.search(text):
495
- return MonthDay.parse_common_iso(match.group(1))
495
+ return MonthDay.parse_iso(match.group(1))
496
496
  if match := _PATH_PATTERN.search(text):
497
497
  return Path(match.group(1))
498
498
  if match := _PLAIN_DATE_TIME_PATTERN.search(text):
499
- return PlainDateTime.parse_common_iso(match.group(1))
499
+ return PlainDateTime.parse_iso(match.group(1))
500
500
  if match := _PY_DATE_PATTERN.search(text):
501
- return Date.parse_common_iso(match.group(1)).py_date()
501
+ return Date.parse_iso(match.group(1)).py_date()
502
502
  if match := _PY_PLAIN_DATE_TIME_PATTERN.search(text):
503
- return PlainDateTime.parse_common_iso(match.group(1)).py_datetime()
503
+ return PlainDateTime.parse_iso(match.group(1)).py_datetime()
504
504
  if match := _PY_TIME_PATTERN.search(text):
505
- return Time.parse_common_iso(match.group(1)).py_time()
505
+ return Time.parse_iso(match.group(1)).py_time()
506
506
  if match := _PY_ZONED_DATE_TIME_PATTERN.search(text):
507
- return ZonedDateTime.parse_common_iso(match.group(1)).py_datetime()
507
+ return ZonedDateTime.parse_iso(match.group(1)).py_datetime()
508
508
  if match := _TIME_PATTERN.search(text):
509
- return Time.parse_common_iso(match.group(1))
509
+ return Time.parse_iso(match.group(1))
510
510
  if match := _TIME_DELTA_PATTERN.search(text):
511
- return TimeDelta.parse_common_iso(match.group(1))
511
+ return TimeDelta.parse_iso(match.group(1))
512
512
  if match := _TIME_PERIOD_PATTERN.search(text):
513
- start, end = map(Time.parse_common_iso, match.group(1).split(","))
513
+ start, end = map(Time.parse_iso, match.group(1).split(","))
514
514
  return TimePeriod(start, end)
515
515
  if match := _UUID_PATTERN.search(text):
516
516
  return UUID(match.group(1))
517
517
  if match := _VERSION_PATTERN.search(text):
518
518
  return parse_version(match.group(1))
519
519
  if match := _YEAR_MONTH_PATTERN.search(text):
520
- return YearMonth.parse_common_iso(match.group(1))
520
+ return YearMonth.parse_iso(match.group(1))
521
521
  if match := _ZONED_DATE_TIME_PATTERN.search(text):
522
- return ZonedDateTime.parse_common_iso(match.group(1))
522
+ return ZonedDateTime.parse_iso(match.group(1))
523
523
  if match := _ZONED_DATE_TIME_PERIOD_PATTERN.search(text):
524
524
  start, end = match.group(1).split(",")
525
- end = ZonedDateTime.parse_common_iso(end)
526
- start = PlainDateTime.parse_common_iso(start).assume_tz(end.tz)
525
+ end = ZonedDateTime.parse_iso(end)
526
+ start = PlainDateTime.parse_iso(start).assume_tz(end.tz)
527
527
  return ZonedDateTimePeriod(start, end)
528
528
  if (
529
529
  exc_class := _object_hook_exception_class(
@@ -1178,7 +1178,7 @@ def _get_log_records_one(
1178
1178
  path = Path(path)
1179
1179
  try:
1180
1180
  lines = path.read_text().splitlines()
1181
- except UnicodeDecodeError as error: # skipif-ci-and-windows
1181
+ except UnicodeDecodeError as error:
1182
1182
  return _GetLogRecordsOneOutput(path=path, file_ok=False, other_errors=[error])
1183
1183
  num_lines_blank, num_lines_error = 0, 0
1184
1184
  missing: set[str] = set()
utilities/os.py CHANGED
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Literal, assert_never, overload, override
7
7
 
8
8
  from utilities.contextlib import enhanced_context_manager
9
9
  from utilities.iterables import OneStrEmptyError, one_str
10
+ from utilities.platform import SYSTEM
10
11
 
11
12
  if TYPE_CHECKING:
12
13
  from collections.abc import Iterator, Mapping
@@ -124,6 +125,39 @@ class GetEnvVarError(Exception):
124
125
  ##
125
126
 
126
127
 
128
+ def get_effective_group_id() -> int | None:
129
+ """Get the effective group ID."""
130
+ match SYSTEM:
131
+ case "windows": # skipif-not-windows
132
+ return None
133
+ case "mac" | "linux": # skipif-windows
134
+ from os import getegid
135
+
136
+ return getegid()
137
+ case never:
138
+ assert_never(never)
139
+
140
+
141
+ def get_effective_user_id() -> int | None:
142
+ """Get the effective user ID."""
143
+ match SYSTEM:
144
+ case "windows": # skipif-not-windows
145
+ return None
146
+ case "mac" | "linux": # skipif-windows
147
+ from os import geteuid
148
+
149
+ return geteuid()
150
+ case never:
151
+ assert_never(never)
152
+
153
+
154
+ EFFECTIVE_USER_ID = get_effective_user_id()
155
+ EFFECTIVE_GROUP_ID = get_effective_group_id()
156
+
157
+
158
+ ##
159
+
160
+
127
161
  def is_debug() -> bool:
128
162
  """Check if we are in `DEBUG` mode."""
129
163
  return get_env_var("DEBUG", nullable=True) is not None
@@ -165,11 +199,15 @@ def temp_environ(
165
199
 
166
200
  __all__ = [
167
201
  "CPU_COUNT",
202
+ "EFFECTIVE_GROUP_ID",
203
+ "EFFECTIVE_USER_ID",
168
204
  "GetCPUCountError",
169
205
  "GetCPUUseError",
170
206
  "IntOrAll",
171
207
  "get_cpu_count",
172
208
  "get_cpu_use",
209
+ "get_effective_group_id",
210
+ "get_effective_user_id",
173
211
  "get_env_var",
174
212
  "is_debug",
175
213
  "is_pytest",
utilities/parse.py CHANGED
@@ -204,7 +204,7 @@ def _parse_object_type(
204
204
  ),
205
205
  ):
206
206
  try:
207
- return cls.parse_common_iso(text)
207
+ return cls.parse_iso(text)
208
208
  except ValueError:
209
209
  raise _ParseObjectParseError(type_=cls, text=text) from None
210
210
  if issubclass(cls, Path):
@@ -477,7 +477,7 @@ def serialize_object(
477
477
  ZonedDateTime,
478
478
  ),
479
479
  ):
480
- return obj.format_common_iso()
480
+ return obj.format_iso()
481
481
  if isinstance(obj, Enum):
482
482
  return obj.name
483
483
  if isinstance(obj, dict):
utilities/pathlib.py CHANGED
@@ -12,6 +12,8 @@ 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.grp import get_gid_name
16
+ from utilities.pwd import get_uid_name
15
17
  from utilities.sentinel import Sentinel
16
18
 
17
19
  if TYPE_CHECKING:
@@ -52,6 +54,19 @@ def expand_path(path: PathLike, /) -> Path:
52
54
  ##
53
55
 
54
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
+
55
70
  def get_package_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
56
71
  """Get the package root."""
57
72
  path = to_path(path)
@@ -112,7 +127,7 @@ class GetRepoRootError(Exception): ...
112
127
  class _GetRepoRootGitNotFoundError(GetRepoRootError):
113
128
  @override
114
129
  def __str__(self) -> str:
115
- return "'git' not found"
130
+ return "'git' not found" # pragma: no cover
116
131
 
117
132
 
118
133
  @dataclass(kw_only=True, slots=True)
@@ -327,6 +342,8 @@ __all__ = [
327
342
  "GetTailError",
328
343
  "ensure_suffix",
329
344
  "expand_path",
345
+ "get_file_group",
346
+ "get_file_owner",
330
347
  "get_package_root",
331
348
  "get_repo_root",
332
349
  "get_tail",
@@ -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
@@ -51,7 +51,7 @@ IS_NOT_LINUX = not IS_LINUX
51
51
  def get_max_pid() -> int | None:
52
52
  """Get the maximum process ID."""
53
53
  match SYSTEM:
54
- case "windows": # pragma: no cover
54
+ case "windows": # skipif-not-windows
55
55
  return None
56
56
  case "mac": # skipif-not-macos
57
57
  return 99999
utilities/polars.py CHANGED
@@ -2571,7 +2571,8 @@ def round_to_float(
2571
2571
  return z.round(decimals=utilities.math.number_of_decimals(y) + 1)
2572
2572
  case Series(), Expr() | Series():
2573
2573
  df = (
2574
- x.to_frame()
2574
+ x
2575
+ .to_frame()
2575
2576
  .with_columns(y)
2576
2577
  .with_columns(number_of_decimals(y).alias("_decimals"))
2577
2578
  .with_row_index(name="_index")
@@ -2638,6 +2639,8 @@ def search_period(
2638
2639
  return None
2639
2640
  item: dt.datetime = series[index]["start"]
2640
2641
  return index if py_date_time > item else None
2642
+ case never:
2643
+ assert_never(never)
2641
2644
 
2642
2645
 
2643
2646
  ##