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.
- dycw_utilities-0.175.17.dist-info/METADATA +34 -0
- {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.175.17.dist-info}/RECORD +43 -38
- dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
- {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.175.17.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +9 -4
- utilities/asyncio.py +10 -16
- utilities/cachetools.py +9 -6
- utilities/click.py +76 -20
- utilities/docker.py +293 -0
- utilities/functions.py +1 -1
- utilities/grp.py +28 -0
- utilities/hypothesis.py +38 -6
- utilities/importlib.py +17 -1
- utilities/jinja2.py +148 -0
- utilities/logging.py +7 -9
- utilities/orjson.py +18 -18
- utilities/os.py +38 -0
- utilities/parse.py +2 -2
- utilities/pathlib.py +18 -1
- utilities/permissions.py +298 -0
- utilities/platform.py +1 -1
- utilities/polars.py +4 -1
- utilities/postgres.py +28 -29
- utilities/pwd.py +28 -0
- utilities/pydantic.py +11 -0
- utilities/pydantic_settings.py +81 -8
- utilities/pydantic_settings_sops.py +13 -0
- utilities/pytest.py +60 -30
- utilities/pytest_regressions.py +26 -7
- utilities/shutil.py +25 -0
- utilities/sqlalchemy.py +15 -0
- utilities/subprocess.py +1572 -0
- utilities/tempfile.py +60 -1
- utilities/text.py +48 -32
- utilities/timer.py +2 -2
- utilities/traceback.py +1 -1
- utilities/types.py +5 -0
- utilities/typing.py +8 -2
- utilities/whenever.py +36 -5
- dycw_utilities-0.166.30.dist-info/METADATA +0 -41
- dycw_utilities-0.166.30.dist-info/WHEEL +0 -4
- dycw_utilities-0.166.30.dist-info/licenses/LICENSE +0 -21
- utilities/aeventkit.py +0 -388
- 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.
|
|
484
|
+
return Date.parse_iso(match.group(1))
|
|
485
485
|
if match := _DATE_DELTA_PATTERN.search(text):
|
|
486
|
-
return DateDelta.
|
|
486
|
+
return DateDelta.parse_iso(match.group(1))
|
|
487
487
|
if match := _DATE_PERIOD_PATTERN.search(text):
|
|
488
|
-
start, end = map(Date.
|
|
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.
|
|
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.
|
|
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.
|
|
499
|
+
return PlainDateTime.parse_iso(match.group(1))
|
|
500
500
|
if match := _PY_DATE_PATTERN.search(text):
|
|
501
|
-
return 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.
|
|
503
|
+
return PlainDateTime.parse_iso(match.group(1)).py_datetime()
|
|
504
504
|
if match := _PY_TIME_PATTERN.search(text):
|
|
505
|
-
return 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.
|
|
507
|
+
return ZonedDateTime.parse_iso(match.group(1)).py_datetime()
|
|
508
508
|
if match := _TIME_PATTERN.search(text):
|
|
509
|
-
return Time.
|
|
509
|
+
return Time.parse_iso(match.group(1))
|
|
510
510
|
if match := _TIME_DELTA_PATTERN.search(text):
|
|
511
|
-
return TimeDelta.
|
|
511
|
+
return TimeDelta.parse_iso(match.group(1))
|
|
512
512
|
if match := _TIME_PERIOD_PATTERN.search(text):
|
|
513
|
-
start, end = map(Time.
|
|
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.
|
|
520
|
+
return YearMonth.parse_iso(match.group(1))
|
|
521
521
|
if match := _ZONED_DATE_TIME_PATTERN.search(text):
|
|
522
|
-
return ZonedDateTime.
|
|
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.
|
|
526
|
-
start = PlainDateTime.
|
|
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:
|
|
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.
|
|
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.
|
|
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",
|
utilities/permissions.py
ADDED
|
@@ -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": #
|
|
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
|
|
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
|
##
|