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.
- dycw_utilities-0.178.1.dist-info/METADATA +34 -0
- dycw_utilities-0.178.1.dist-info/RECORD +105 -0
- dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
- {dycw_utilities-0.146.2.dist-info → dycw_utilities-0.178.1.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +10 -7
- utilities/asyncio.py +129 -50
- utilities/atomicwrites.py +1 -1
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +144 -49
- utilities/concurrent.py +1 -1
- utilities/contextlib.py +4 -2
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +15 -28
- utilities/docker.py +387 -0
- utilities/enum.py +2 -2
- utilities/errors.py +17 -3
- utilities/fastapi.py +8 -3
- utilities/fpdf2.py +2 -2
- utilities/functions.py +20 -297
- utilities/git.py +19 -0
- utilities/grp.py +28 -0
- utilities/hypothesis.py +361 -79
- utilities/importlib.py +17 -1
- utilities/inflect.py +1 -1
- utilities/iterables.py +33 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +1 -1
- utilities/libcst.py +7 -7
- utilities/logging.py +131 -93
- utilities/math.py +8 -4
- utilities/more_itertools.py +4 -6
- utilities/operator.py +1 -1
- utilities/orjson.py +86 -34
- utilities/os.py +49 -2
- utilities/packaging.py +115 -0
- utilities/parse.py +2 -2
- utilities/pathlib.py +66 -34
- utilities/permissions.py +298 -0
- utilities/platform.py +5 -4
- utilities/polars.py +934 -420
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +317 -153
- utilities/pottery.py +10 -86
- utilities/pqdm.py +3 -3
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -51
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +5 -5
- utilities/pytest.py +100 -126
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +7 -3
- utilities/pytest_regressions.py +27 -8
- utilities/random.py +11 -6
- utilities/re.py +1 -1
- utilities/redis.py +101 -64
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +9 -4
- utilities/sqlalchemy.py +422 -352
- utilities/sqlalchemy_polars.py +28 -52
- utilities/string.py +1 -1
- utilities/subprocess.py +1977 -0
- utilities/tempfile.py +112 -4
- utilities/testbook.py +50 -0
- utilities/text.py +174 -42
- utilities/throttle.py +158 -0
- utilities/timer.py +2 -2
- utilities/traceback.py +59 -38
- utilities/types.py +68 -22
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +663 -178
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.146.2.dist-info/METADATA +0 -41
- dycw_utilities-0.146.2.dist-info/RECORD +0 -99
- dycw_utilities-0.146.2.dist-info/WHEEL +0 -4
- dycw_utilities-0.146.2.dist-info/licenses/LICENSE +0 -21
- utilities/aiolimiter.py +0 -25
- utilities/eventkit.py +0 -388
- utilities/period.py +0 -237
- utilities/python_dotenv.py +0 -101
- utilities/streamlit.py +0 -105
- 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.
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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(
|
|
70
|
+
def get_package_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
|
|
80
71
|
"""Get the package root."""
|
|
81
|
-
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(
|
|
97
|
+
def get_repo_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
|
|
107
98
|
"""Get the repo root."""
|
|
108
|
-
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
|
|
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
|
-
|
|
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(
|
|
145
|
+
def get_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
|
|
140
146
|
"""Get the root of a path."""
|
|
141
|
-
path =
|
|
147
|
+
path = to_path(path)
|
|
142
148
|
try:
|
|
143
|
-
repo = get_repo_root(path
|
|
149
|
+
repo = get_repo_root(path)
|
|
144
150
|
except GetRepoRootError:
|
|
145
151
|
repo = None
|
|
146
152
|
try:
|
|
147
|
-
package = get_package_root(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
|
|
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
|
|
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
|
]
|
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
|
@@ -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": #
|
|
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
|
|
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
|
|
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
|
|
99
|
+
case never:
|
|
99
100
|
assert_never(never)
|
|
100
101
|
|
|
101
102
|
|