dycw-utilities 0.174.7__py3-none-any.whl → 0.174.9__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.174.7.dist-info → dycw_utilities-0.174.9.dist-info}/METADATA +1 -1
- {dycw_utilities-0.174.7.dist-info → dycw_utilities-0.174.9.dist-info}/RECORD +8 -7
- utilities/__init__.py +1 -1
- utilities/hypothesis.py +34 -0
- utilities/permissions.py +297 -0
- utilities/subprocess.py +109 -14
- {dycw_utilities-0.174.7.dist-info → dycw_utilities-0.174.9.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.174.7.dist-info → dycw_utilities-0.174.9.dist-info}/entry_points.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
utilities/__init__.py,sha256=
|
|
1
|
+
utilities/__init__.py,sha256=s0yfL-qs6UI6ltNUkBKltAzsUuIudzaPhHhfNI2ILBI,60
|
|
2
2
|
utilities/aeventkit.py,sha256=OmDBhYGgbsKrB7cdC5FFpJHUatX9O76eTeKVVTksp2Y,12673
|
|
3
3
|
utilities/altair.py,sha256=rUK99g9x6CYDDfiZrf-aTx5fSRbL1Q8ctgKORowzXHg,9060
|
|
4
4
|
utilities/asyncio.py,sha256=aJySVxBY0gqsIYnoNmH7-1r8djKuf4vSsU69VCD08t8,16772
|
|
@@ -25,7 +25,7 @@ utilities/grp.py,sha256=1vV3gNR9dQsl1vtUtvC_2qgVdQzm7O8lLMSh56cTbeg,694
|
|
|
25
25
|
utilities/gzip.py,sha256=fkGP3KdsBfXlstodT4wtlp-PwNyUsogpbDCVVVGdsm4,781
|
|
26
26
|
utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
|
|
27
27
|
utilities/http.py,sha256=TsavEfHlRtlLaeV21Z6KZh0qbPw-kvD1zsQdZ7Kep5Q,977
|
|
28
|
-
utilities/hypothesis.py,sha256=
|
|
28
|
+
utilities/hypothesis.py,sha256=wk1HiNdBg7tGPEKLZ5uiNVbtlSZl58QJjlediYoSHkA,46753
|
|
29
29
|
utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
|
|
30
30
|
utilities/inflect.py,sha256=v7YkOWSu8NAmVghPcf4F3YBZQoJCS47_DLf9jbfWIs0,581
|
|
31
31
|
utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
|
|
@@ -47,6 +47,7 @@ utilities/orjson.py,sha256=T_0SlK811ysg46d3orvIPY3JpBa4FRMpP2wlPQo7-gU,41854
|
|
|
47
47
|
utilities/os.py,sha256=kjKKSQfnRqFTTZ315iavaaGd3gGuYNoSWlxVLCJjyQs,4852
|
|
48
48
|
utilities/parse.py,sha256=g7Qm9eBOIeDId2tGA021CIaeF6jp1TI8rx4srdvlyoo,17937
|
|
49
49
|
utilities/pathlib.py,sha256=EKZn-wWxH7MEWFrQGqHIoB-GJzyXeiEj8iDIgvkr8Wk,9325
|
|
50
|
+
utilities/permissions.py,sha256=PlY95KIon6onivvZt8H1dKlzPXatLdZ5cUPdfwZDfs4,9408
|
|
50
51
|
utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
|
|
51
52
|
utilities/platform.py,sha256=0pYO5v7L2sU5UN87zHhEEhTKsZ9NIEM8N6UCr0F7bLY,2778
|
|
52
53
|
utilities/polars.py,sha256=cNFBLWgOMUAp_Sz4xtlto17uZswZRrcfQYC95QKyaY4,87483
|
|
@@ -80,7 +81,7 @@ utilities/sqlalchemy.py,sha256=HQYpd7LFxdTF5WYVWYtCJeEBI71EJm7ytvCGyAH9B-U,37163
|
|
|
80
81
|
utilities/sqlalchemy_polars.py,sha256=JCGhB37raSR7fqeWV5dTsciRTMVzIdVT9YSqKT0piT0,13370
|
|
81
82
|
utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
|
|
82
83
|
utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
|
|
83
|
-
utilities/subprocess.py,sha256=
|
|
84
|
+
utilities/subprocess.py,sha256=dIBguvLB0WepJKqgBxlzF6r8z9HW5JiGKLg-eslpuec,22549
|
|
84
85
|
utilities/tempfile.py,sha256=Lx6qa16lL1XVH6WdmD_G9vlN6gLI8nrIurxmsFkPKvg,3022
|
|
85
86
|
utilities/testbook.py,sha256=j1KmaVbrX9VrbeMgtPh5gk55myAsn3dyRUn7jGbPbRk,1294
|
|
86
87
|
utilities/text.py,sha256=7SvwcSR2l_5cOrm1samGnR4C-ZI6qyFLHLzSpO1zeHQ,13958
|
|
@@ -97,7 +98,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
|
|
|
97
98
|
utilities/whenever.py,sha256=F4ek0-OBWxHYrZdmoZt76N2RnNyKY5KrEHt7rqO4AQE,60183
|
|
98
99
|
utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
|
|
99
100
|
utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
|
|
100
|
-
dycw_utilities-0.174.
|
|
101
|
-
dycw_utilities-0.174.
|
|
102
|
-
dycw_utilities-0.174.
|
|
103
|
-
dycw_utilities-0.174.
|
|
101
|
+
dycw_utilities-0.174.9.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
|
|
102
|
+
dycw_utilities-0.174.9.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
|
|
103
|
+
dycw_utilities-0.174.9.dist-info/METADATA,sha256=LpxiNDUuPoCbqx1Jb4H4UsVRdjGHNrGyeslCZqKy2AE,1709
|
|
104
|
+
dycw_utilities-0.174.9.dist-info/RECORD,,
|
utilities/__init__.py
CHANGED
utilities/hypothesis.py
CHANGED
|
@@ -77,6 +77,7 @@ from utilities.math import (
|
|
|
77
77
|
)
|
|
78
78
|
from utilities.os import get_env_var
|
|
79
79
|
from utilities.pathlib import module_path, temp_cwd
|
|
80
|
+
from utilities.permissions import Permissions
|
|
80
81
|
from utilities.platform import IS_LINUX
|
|
81
82
|
from utilities.sentinel import Sentinel, is_sentinel, sentinel
|
|
82
83
|
from utilities.tempfile import TEMP_DIR, TemporaryDirectory
|
|
@@ -864,6 +865,38 @@ def _path_parts(draw: DrawFn, /) -> str:
|
|
|
864
865
|
##
|
|
865
866
|
|
|
866
867
|
|
|
868
|
+
@composite
|
|
869
|
+
def permissions(
|
|
870
|
+
draw: DrawFn,
|
|
871
|
+
/,
|
|
872
|
+
*,
|
|
873
|
+
user_read: MaybeSearchStrategy[bool | None] = None,
|
|
874
|
+
user_write: MaybeSearchStrategy[bool | None] = None,
|
|
875
|
+
user_execute: MaybeSearchStrategy[bool | None] = None,
|
|
876
|
+
group_read: MaybeSearchStrategy[bool | None] = None,
|
|
877
|
+
group_write: MaybeSearchStrategy[bool | None] = None,
|
|
878
|
+
group_execute: MaybeSearchStrategy[bool | None] = None,
|
|
879
|
+
others_read: MaybeSearchStrategy[bool | None] = None,
|
|
880
|
+
others_write: MaybeSearchStrategy[bool | None] = None,
|
|
881
|
+
others_execute: MaybeSearchStrategy[bool | None] = None,
|
|
882
|
+
) -> Permissions:
|
|
883
|
+
"""Strategy for generating `Permissions`."""
|
|
884
|
+
return Permissions(
|
|
885
|
+
user_read=draw2(draw, user_read, booleans()),
|
|
886
|
+
user_write=draw2(draw, user_write, booleans()),
|
|
887
|
+
user_execute=draw2(draw, user_execute, booleans()),
|
|
888
|
+
group_read=draw2(draw, group_read, booleans()),
|
|
889
|
+
group_write=draw2(draw, group_write, booleans()),
|
|
890
|
+
group_execute=draw2(draw, group_execute, booleans()),
|
|
891
|
+
others_read=draw2(draw, others_read, booleans()),
|
|
892
|
+
others_write=draw2(draw, others_write, booleans()),
|
|
893
|
+
others_execute=draw2(draw, others_execute, booleans()),
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
##
|
|
898
|
+
|
|
899
|
+
|
|
867
900
|
@composite
|
|
868
901
|
def plain_date_times(
|
|
869
902
|
draw: DrawFn,
|
|
@@ -1611,6 +1644,7 @@ __all__ = [
|
|
|
1611
1644
|
"numbers",
|
|
1612
1645
|
"pairs",
|
|
1613
1646
|
"paths",
|
|
1647
|
+
"permissions",
|
|
1614
1648
|
"plain_date_times",
|
|
1615
1649
|
"py_datetimes",
|
|
1616
1650
|
"quadruples",
|
utilities/permissions.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from functools import reduce
|
|
6
|
+
from operator import or_
|
|
7
|
+
from stat import (
|
|
8
|
+
S_IRGRP,
|
|
9
|
+
S_IROTH,
|
|
10
|
+
S_IRUSR,
|
|
11
|
+
S_IWGRP,
|
|
12
|
+
S_IWOTH,
|
|
13
|
+
S_IWUSR,
|
|
14
|
+
S_IXGRP,
|
|
15
|
+
S_IXOTH,
|
|
16
|
+
S_IXUSR,
|
|
17
|
+
)
|
|
18
|
+
from typing import Literal, Self, override
|
|
19
|
+
|
|
20
|
+
from utilities.dataclasses import replace_non_sentinel
|
|
21
|
+
from utilities.functions import ensure_member
|
|
22
|
+
from utilities.re import (
|
|
23
|
+
ExtractGroupError,
|
|
24
|
+
ExtractGroupsError,
|
|
25
|
+
extract_group,
|
|
26
|
+
extract_groups,
|
|
27
|
+
)
|
|
28
|
+
from utilities.sentinel import Sentinel, sentinel
|
|
29
|
+
from utilities.typing import get_args
|
|
30
|
+
|
|
31
|
+
_MIN_INT = 0o0
|
|
32
|
+
_MAX_INT = 0o777
|
|
33
|
+
type _ZeroToSeven = Literal[0, 1, 2, 3, 4, 5, 6, 7]
|
|
34
|
+
_ZERO_TO_SEVEN: list[_ZeroToSeven] = list(get_args(_ZeroToSeven.__value__))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
|
38
|
+
class Permissions:
|
|
39
|
+
user_read: bool = False
|
|
40
|
+
user_write: bool = False
|
|
41
|
+
user_execute: bool = False
|
|
42
|
+
group_read: bool = False
|
|
43
|
+
group_write: bool = False
|
|
44
|
+
group_execute: bool = False
|
|
45
|
+
others_read: bool = False
|
|
46
|
+
others_write: bool = False
|
|
47
|
+
others_execute: bool = False
|
|
48
|
+
|
|
49
|
+
def __int__(self) -> int:
|
|
50
|
+
return (
|
|
51
|
+
100
|
|
52
|
+
* self._int(
|
|
53
|
+
read=self.user_read, write=self.user_write, execute=self.user_execute
|
|
54
|
+
)
|
|
55
|
+
+ 10
|
|
56
|
+
* self._int(
|
|
57
|
+
read=self.group_read, write=self.group_write, execute=self.group_execute
|
|
58
|
+
)
|
|
59
|
+
+ self._int(
|
|
60
|
+
read=self.others_read,
|
|
61
|
+
write=self.others_write,
|
|
62
|
+
execute=self.others_execute,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _int(
|
|
67
|
+
self, *, read: bool = False, write: bool = False, execute: bool = False
|
|
68
|
+
) -> _ZeroToSeven:
|
|
69
|
+
return (4 if read else 0) + (2 if write else 0) + (1 if execute else 0)
|
|
70
|
+
|
|
71
|
+
@override
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
return ",".join([
|
|
74
|
+
self._repr_parts(
|
|
75
|
+
"u",
|
|
76
|
+
read=self.user_read,
|
|
77
|
+
write=self.user_write,
|
|
78
|
+
execute=self.user_execute,
|
|
79
|
+
),
|
|
80
|
+
self._repr_parts(
|
|
81
|
+
"g",
|
|
82
|
+
read=self.group_read,
|
|
83
|
+
write=self.group_write,
|
|
84
|
+
execute=self.group_execute,
|
|
85
|
+
),
|
|
86
|
+
self._repr_parts(
|
|
87
|
+
"o",
|
|
88
|
+
read=self.others_read,
|
|
89
|
+
write=self.others_write,
|
|
90
|
+
execute=self.others_execute,
|
|
91
|
+
),
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
def _repr_parts(
|
|
95
|
+
self,
|
|
96
|
+
prefix: Literal["u", "g", "o"],
|
|
97
|
+
/,
|
|
98
|
+
*,
|
|
99
|
+
read: bool = False,
|
|
100
|
+
write: bool = False,
|
|
101
|
+
execute: bool = False,
|
|
102
|
+
) -> str:
|
|
103
|
+
parts: list[str] = []
|
|
104
|
+
if read:
|
|
105
|
+
parts.append("r")
|
|
106
|
+
if write:
|
|
107
|
+
parts.append("w")
|
|
108
|
+
if execute:
|
|
109
|
+
parts.append("x")
|
|
110
|
+
return f"{prefix}={''.join(parts)}"
|
|
111
|
+
|
|
112
|
+
@override
|
|
113
|
+
def __str__(self) -> str:
|
|
114
|
+
return repr(self)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def from_int(cls, n: int, /) -> Self:
|
|
118
|
+
with suppress(ExtractGroupsError):
|
|
119
|
+
user, group, others = extract_groups(r"^([0-7])([0-7])([0-7])$", str(n))
|
|
120
|
+
user_read, user_write, user_execute = cls._from_int(
|
|
121
|
+
ensure_member(int(user), _ZERO_TO_SEVEN)
|
|
122
|
+
)
|
|
123
|
+
group_read, group_write, group_execute = cls._from_int(
|
|
124
|
+
ensure_member(int(group), _ZERO_TO_SEVEN)
|
|
125
|
+
)
|
|
126
|
+
others_read, others_write, others_execute = cls._from_int(
|
|
127
|
+
ensure_member(int(others), _ZERO_TO_SEVEN)
|
|
128
|
+
)
|
|
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
|
+
with suppress(ExtractGroupsError):
|
|
141
|
+
group, others = extract_groups(r"^([0-7])([0-7])$", str(n))
|
|
142
|
+
group_read, group_write, group_execute = cls._from_int(
|
|
143
|
+
ensure_member(int(group), _ZERO_TO_SEVEN)
|
|
144
|
+
)
|
|
145
|
+
others_read, others_write, others_execute = cls._from_int(
|
|
146
|
+
ensure_member(int(others), _ZERO_TO_SEVEN)
|
|
147
|
+
)
|
|
148
|
+
return cls(
|
|
149
|
+
group_read=group_read,
|
|
150
|
+
group_write=group_write,
|
|
151
|
+
group_execute=group_execute,
|
|
152
|
+
others_read=others_read,
|
|
153
|
+
others_write=others_write,
|
|
154
|
+
others_execute=others_execute,
|
|
155
|
+
)
|
|
156
|
+
with suppress(ExtractGroupError):
|
|
157
|
+
others = extract_group(r"^([0-7])$", str(n))
|
|
158
|
+
others_read, others_write, others_execute = cls._from_int(
|
|
159
|
+
ensure_member(int(others), _ZERO_TO_SEVEN)
|
|
160
|
+
)
|
|
161
|
+
return cls(
|
|
162
|
+
others_read=others_read,
|
|
163
|
+
others_write=others_write,
|
|
164
|
+
others_execute=others_execute,
|
|
165
|
+
)
|
|
166
|
+
if n == 0:
|
|
167
|
+
return cls()
|
|
168
|
+
raise PermissionsFromIntError(n=n)
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def _from_int(cls, n: _ZeroToSeven, /) -> tuple[bool, bool, bool]:
|
|
172
|
+
return bool(4 & n), bool(2 & n), bool(1 & n)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def from_octal(cls, n: int, /) -> Self:
|
|
176
|
+
if _MIN_INT <= n <= _MAX_INT:
|
|
177
|
+
return cls(
|
|
178
|
+
user_read=bool(n & S_IRUSR),
|
|
179
|
+
user_write=bool(n & S_IWUSR),
|
|
180
|
+
user_execute=bool(n & S_IXUSR),
|
|
181
|
+
group_read=bool(n & S_IRGRP),
|
|
182
|
+
group_write=bool(n & S_IWGRP),
|
|
183
|
+
group_execute=bool(n & S_IXGRP),
|
|
184
|
+
others_read=bool(n & S_IROTH),
|
|
185
|
+
others_write=bool(n & S_IWOTH),
|
|
186
|
+
others_execute=bool(n & S_IXOTH),
|
|
187
|
+
)
|
|
188
|
+
raise PermissionsFromOctalError(n=n)
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def from_text(cls, text: str, /) -> Self:
|
|
192
|
+
try:
|
|
193
|
+
user, group, others = extract_groups(
|
|
194
|
+
r"^u=(r?w?x?),g=(r?w?x?),o=(r?w?x?)$", text
|
|
195
|
+
)
|
|
196
|
+
except ExtractGroupsError:
|
|
197
|
+
raise PermissionsFromTextError(text=text) from None
|
|
198
|
+
user_read, user_write, user_execute = cls._from_text_part(user)
|
|
199
|
+
group_read, group_write, group_execute = cls._from_text_part(group)
|
|
200
|
+
others_read, others_write, others_execute = cls._from_text_part(others)
|
|
201
|
+
return cls(
|
|
202
|
+
user_read=user_read,
|
|
203
|
+
user_write=user_write,
|
|
204
|
+
user_execute=user_execute,
|
|
205
|
+
group_read=group_read,
|
|
206
|
+
group_write=group_write,
|
|
207
|
+
group_execute=group_execute,
|
|
208
|
+
others_read=others_read,
|
|
209
|
+
others_write=others_write,
|
|
210
|
+
others_execute=others_execute,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def _from_text_part(cls, text: str, /) -> tuple[bool, bool, bool]:
|
|
215
|
+
read, write, execute = extract_groups("^(r?)(w?)(x?)$", text)
|
|
216
|
+
return read != "", write != "", execute != ""
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def octal(self) -> int:
|
|
220
|
+
flags: list[int] = [
|
|
221
|
+
S_IRUSR if self.user_read else 0,
|
|
222
|
+
S_IWUSR if self.user_write else 0,
|
|
223
|
+
S_IXUSR if self.user_execute else 0,
|
|
224
|
+
S_IRGRP if self.group_read else 0,
|
|
225
|
+
S_IWGRP if self.group_write else 0,
|
|
226
|
+
S_IXGRP if self.group_execute else 0,
|
|
227
|
+
S_IROTH if self.others_read else 0,
|
|
228
|
+
S_IWOTH if self.others_write else 0,
|
|
229
|
+
S_IXOTH if self.others_execute else 0,
|
|
230
|
+
]
|
|
231
|
+
return reduce(or_, flags)
|
|
232
|
+
|
|
233
|
+
def replace(
|
|
234
|
+
self,
|
|
235
|
+
*,
|
|
236
|
+
user_read: bool | Sentinel = sentinel,
|
|
237
|
+
user_write: bool | Sentinel = sentinel,
|
|
238
|
+
user_execute: bool | Sentinel = sentinel,
|
|
239
|
+
group_read: bool | Sentinel = sentinel,
|
|
240
|
+
group_write: bool | Sentinel = sentinel,
|
|
241
|
+
group_execute: bool | Sentinel = sentinel,
|
|
242
|
+
others_read: bool | Sentinel = sentinel,
|
|
243
|
+
others_write: bool | Sentinel = sentinel,
|
|
244
|
+
others_execute: bool | Sentinel = sentinel,
|
|
245
|
+
) -> Self:
|
|
246
|
+
return replace_non_sentinel(
|
|
247
|
+
self,
|
|
248
|
+
user_read=user_read,
|
|
249
|
+
user_write=user_write,
|
|
250
|
+
user_execute=user_execute,
|
|
251
|
+
group_read=group_read,
|
|
252
|
+
group_write=group_write,
|
|
253
|
+
group_execute=group_execute,
|
|
254
|
+
others_read=others_read,
|
|
255
|
+
others_write=others_write,
|
|
256
|
+
others_execute=others_execute,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass(kw_only=True, slots=True)
|
|
261
|
+
class PermissionsError(Exception): ...
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclass(kw_only=True, slots=True)
|
|
265
|
+
class PermissionsFromIntError(PermissionsError):
|
|
266
|
+
n: int
|
|
267
|
+
|
|
268
|
+
@override
|
|
269
|
+
def __str__(self) -> str:
|
|
270
|
+
return f"Invalid integer for permissions; got {self.n}"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclass(kw_only=True, slots=True)
|
|
274
|
+
class PermissionsFromOctalError(PermissionsError):
|
|
275
|
+
n: int
|
|
276
|
+
|
|
277
|
+
@override
|
|
278
|
+
def __str__(self) -> str:
|
|
279
|
+
return f"Invalid octal for permissions; got {oct(self.n)}"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass(kw_only=True, slots=True)
|
|
283
|
+
class PermissionsFromTextError(PermissionsError):
|
|
284
|
+
text: str
|
|
285
|
+
|
|
286
|
+
@override
|
|
287
|
+
def __str__(self) -> str:
|
|
288
|
+
return f"Invalid string for permissions; got {self.text!r}"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
__all__ = [
|
|
292
|
+
"Permissions",
|
|
293
|
+
"PermissionsError",
|
|
294
|
+
"PermissionsFromIntError",
|
|
295
|
+
"PermissionsFromOctalError",
|
|
296
|
+
"PermissionsFromTextError",
|
|
297
|
+
]
|
utilities/subprocess.py
CHANGED
|
@@ -5,7 +5,7 @@ from contextlib import contextmanager
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from io import StringIO
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from shlex import join
|
|
8
|
+
from shlex import join
|
|
9
9
|
from string import Template
|
|
10
10
|
from subprocess import PIPE, CalledProcessError, Popen
|
|
11
11
|
from threading import Thread
|
|
@@ -40,22 +40,37 @@ RESTART_SSHD = ["systemctl", "restart", "sshd"]
|
|
|
40
40
|
UPDATE_CA_CERTIFICATES: str = "update-ca-certificates"
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
##
|
|
44
|
+
|
|
45
|
+
|
|
43
46
|
def apt_install_cmd(package: str, /) -> list[str]:
|
|
44
47
|
return ["apt", "install", "-y", package]
|
|
45
48
|
|
|
46
49
|
|
|
50
|
+
##
|
|
51
|
+
|
|
52
|
+
|
|
47
53
|
def cat_cmd(path: PathLike, /) -> list[str]:
|
|
48
54
|
return ["cat", str(path)]
|
|
49
55
|
|
|
50
56
|
|
|
57
|
+
##
|
|
58
|
+
|
|
59
|
+
|
|
51
60
|
def cd_cmd(path: PathLike, /) -> list[str]:
|
|
52
61
|
return ["cd", str(path)]
|
|
53
62
|
|
|
54
63
|
|
|
64
|
+
##
|
|
65
|
+
|
|
66
|
+
|
|
55
67
|
def chmod_cmd(path: PathLike, mode: str, /) -> list[str]:
|
|
56
68
|
return ["chmod", mode, str(path)]
|
|
57
69
|
|
|
58
70
|
|
|
71
|
+
##
|
|
72
|
+
|
|
73
|
+
|
|
59
74
|
def chown_cmd(
|
|
60
75
|
path: PathLike, /, *, user: str | None = None, group: str | None = None
|
|
61
76
|
) -> list[str]:
|
|
@@ -80,14 +95,23 @@ class ChownCmdError(Exception):
|
|
|
80
95
|
return "At least one of 'user' and/or 'group' must be given; got None"
|
|
81
96
|
|
|
82
97
|
|
|
98
|
+
##
|
|
99
|
+
|
|
100
|
+
|
|
83
101
|
def cp_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
|
|
84
102
|
return ["cp", "-r", str(src), str(dest)]
|
|
85
103
|
|
|
86
104
|
|
|
105
|
+
##
|
|
106
|
+
|
|
107
|
+
|
|
87
108
|
def echo_cmd(text: str, /) -> list[str]:
|
|
88
109
|
return ["echo", text]
|
|
89
110
|
|
|
90
111
|
|
|
112
|
+
##
|
|
113
|
+
|
|
114
|
+
|
|
91
115
|
def expand_path(
|
|
92
116
|
path: PathLike, /, *, subs: StrMapping | None = None, sudo: bool = False
|
|
93
117
|
) -> Path:
|
|
@@ -98,47 +122,71 @@ def expand_path(
|
|
|
98
122
|
return Path(path).expanduser()
|
|
99
123
|
|
|
100
124
|
|
|
125
|
+
##
|
|
126
|
+
|
|
127
|
+
|
|
101
128
|
def git_clone_cmd(url: str, path: PathLike, /) -> list[str]:
|
|
102
129
|
return ["git", "clone", "--recurse-submodules", url, str(path)]
|
|
103
130
|
|
|
104
131
|
|
|
132
|
+
##
|
|
133
|
+
|
|
134
|
+
|
|
105
135
|
def git_hard_reset_cmd(*, branch: str | None = None) -> list[str]:
|
|
106
136
|
branch_use = "master" if branch is None else branch
|
|
107
137
|
return ["git", "hard-reset", branch_use]
|
|
108
138
|
|
|
109
139
|
|
|
140
|
+
##
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def maybe_parent(path: PathLike, /, *, parent: bool = False) -> Path:
|
|
144
|
+
path = Path(path)
|
|
145
|
+
return path.parent if parent else path
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
##
|
|
149
|
+
|
|
150
|
+
|
|
110
151
|
def maybe_sudo_cmd(cmd: str, /, *args: str, sudo: bool = False) -> list[str]:
|
|
111
152
|
parts: list[str] = [cmd, *args]
|
|
112
153
|
return sudo_cmd(*parts) if sudo else parts
|
|
113
154
|
|
|
114
155
|
|
|
156
|
+
##
|
|
157
|
+
|
|
158
|
+
|
|
115
159
|
def mkdir(path: PathLike, /, *, sudo: bool = False, parent: bool = False) -> None:
|
|
116
160
|
if sudo: # pragma: no cover
|
|
117
161
|
run(*sudo_cmd(*mkdir_cmd(path, parent=parent)))
|
|
118
162
|
else:
|
|
119
|
-
path =
|
|
120
|
-
|
|
121
|
-
|
|
163
|
+
maybe_parent(path, parent=parent).mkdir(parents=True, exist_ok=True)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
##
|
|
122
167
|
|
|
123
168
|
|
|
124
169
|
def mkdir_cmd(path: PathLike, /, *, parent: bool = False) -> list[str]:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
else:
|
|
130
|
-
args.append(quoted)
|
|
131
|
-
return args
|
|
170
|
+
return ["mkdir", "-p", str(maybe_parent(path, parent=parent))]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
##
|
|
132
174
|
|
|
133
175
|
|
|
134
176
|
def mv_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
|
|
135
177
|
return ["mv", str(src), str(dest)]
|
|
136
178
|
|
|
137
179
|
|
|
180
|
+
##
|
|
181
|
+
|
|
182
|
+
|
|
138
183
|
def rm_cmd(path: PathLike, /) -> list[str]:
|
|
139
184
|
return ["rm", "-rf", str(path)]
|
|
140
185
|
|
|
141
186
|
|
|
187
|
+
##
|
|
188
|
+
|
|
189
|
+
|
|
142
190
|
def rsync(
|
|
143
191
|
src_or_srcs: MaybeIterable[PathLike],
|
|
144
192
|
user: str,
|
|
@@ -153,7 +201,6 @@ def rsync(
|
|
|
153
201
|
print: bool = False, # noqa: A002
|
|
154
202
|
retry: Retry | None = None,
|
|
155
203
|
logger: LoggerLike | None = None,
|
|
156
|
-
archive: bool = False,
|
|
157
204
|
chown_user: str | None = None,
|
|
158
205
|
chown_group: str | None = None,
|
|
159
206
|
exclude: MaybeIterable[str] | None = None,
|
|
@@ -171,12 +218,13 @@ def rsync(
|
|
|
171
218
|
retry=retry,
|
|
172
219
|
logger=logger,
|
|
173
220
|
)
|
|
221
|
+
is_dir = any(Path(s).is_dir() for s in always_iterable(src_or_srcs)) # skipif-ci
|
|
174
222
|
rsync_args = rsync_cmd( # skipif-ci
|
|
175
223
|
src_or_srcs,
|
|
176
224
|
user,
|
|
177
225
|
hostname,
|
|
178
226
|
dest,
|
|
179
|
-
archive=
|
|
227
|
+
archive=is_dir,
|
|
180
228
|
chown_user=chown_user,
|
|
181
229
|
chown_group=chown_group,
|
|
182
230
|
exclude=exclude,
|
|
@@ -184,6 +232,7 @@ def rsync(
|
|
|
184
232
|
host_key_algorithms=host_key_algorithms,
|
|
185
233
|
strict_host_key_checking=strict_host_key_checking,
|
|
186
234
|
sudo=sudo,
|
|
235
|
+
parent=is_dir,
|
|
187
236
|
)
|
|
188
237
|
run(*rsync_args, print=print, retry=retry, logger=logger) # skipif-ci
|
|
189
238
|
if chmod is not None: # skipif-ci
|
|
@@ -201,6 +250,9 @@ def rsync(
|
|
|
201
250
|
)
|
|
202
251
|
|
|
203
252
|
|
|
253
|
+
##
|
|
254
|
+
|
|
255
|
+
|
|
204
256
|
def rsync_cmd(
|
|
205
257
|
src_or_srcs: MaybeIterable[PathLike],
|
|
206
258
|
user: str,
|
|
@@ -216,6 +268,7 @@ def rsync_cmd(
|
|
|
216
268
|
host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
|
|
217
269
|
strict_host_key_checking: bool = True,
|
|
218
270
|
sudo: bool = False,
|
|
271
|
+
parent: bool = False,
|
|
219
272
|
) -> list[str]:
|
|
220
273
|
args: list[str] = ["rsync"]
|
|
221
274
|
if archive:
|
|
@@ -244,7 +297,15 @@ def rsync_cmd(
|
|
|
244
297
|
args.extend(["--rsh", join(rsh_args)])
|
|
245
298
|
if sudo:
|
|
246
299
|
args.extend(["--rsync-path", join(sudo_cmd("rsync"))])
|
|
247
|
-
|
|
300
|
+
dest_use = maybe_parent(dest, parent=parent)
|
|
301
|
+
return [
|
|
302
|
+
*args,
|
|
303
|
+
*map(str, always_iterable(src_or_srcs)),
|
|
304
|
+
f"{user}@{hostname}:{dest_use}",
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
##
|
|
248
309
|
|
|
249
310
|
|
|
250
311
|
@overload
|
|
@@ -502,10 +563,16 @@ def _run_write_to_streams(text: str, /, *outputs: IO[str]) -> None:
|
|
|
502
563
|
_ = output.write(text)
|
|
503
564
|
|
|
504
565
|
|
|
566
|
+
##
|
|
567
|
+
|
|
568
|
+
|
|
505
569
|
def set_hostname_cmd(hostname: str, /) -> list[str]:
|
|
506
570
|
return ["hostnamectl", "set-hostname", hostname]
|
|
507
571
|
|
|
508
572
|
|
|
573
|
+
##
|
|
574
|
+
|
|
575
|
+
|
|
509
576
|
@overload
|
|
510
577
|
def ssh(
|
|
511
578
|
user: str,
|
|
@@ -641,6 +708,9 @@ def ssh(
|
|
|
641
708
|
)
|
|
642
709
|
|
|
643
710
|
|
|
711
|
+
##
|
|
712
|
+
|
|
713
|
+
|
|
644
714
|
def ssh_cmd(
|
|
645
715
|
user: str,
|
|
646
716
|
hostname: str,
|
|
@@ -658,6 +728,9 @@ def ssh_cmd(
|
|
|
658
728
|
return [*args, f"{user}@{hostname}", *cmd_and_cmds_or_args]
|
|
659
729
|
|
|
660
730
|
|
|
731
|
+
##
|
|
732
|
+
|
|
733
|
+
|
|
661
734
|
def ssh_opts_cmd(
|
|
662
735
|
*,
|
|
663
736
|
batch_mode: bool = True,
|
|
@@ -673,26 +746,44 @@ def ssh_opts_cmd(
|
|
|
673
746
|
return [*args, "-T"]
|
|
674
747
|
|
|
675
748
|
|
|
749
|
+
##
|
|
750
|
+
|
|
751
|
+
|
|
676
752
|
def ssh_keygen_cmd(hostname: str, /) -> list[str]:
|
|
677
753
|
return ["ssh-keygen", "-f", "~/.ssh/known_hosts", "-R", hostname]
|
|
678
754
|
|
|
679
755
|
|
|
756
|
+
##
|
|
757
|
+
|
|
758
|
+
|
|
680
759
|
def sudo_cmd(cmd: str, /, *args: str) -> list[str]:
|
|
681
760
|
return ["sudo", cmd, *args]
|
|
682
761
|
|
|
683
762
|
|
|
763
|
+
##
|
|
764
|
+
|
|
765
|
+
|
|
684
766
|
def sudo_nopasswd_cmd(user: str, /) -> str:
|
|
685
767
|
return f"{user} ALL=(ALL) NOPASSWD: ALL"
|
|
686
768
|
|
|
687
769
|
|
|
770
|
+
##
|
|
771
|
+
|
|
772
|
+
|
|
688
773
|
def symlink_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
|
|
689
774
|
return ["ln", "-s", str(src), str(dest)]
|
|
690
775
|
|
|
691
776
|
|
|
777
|
+
##
|
|
778
|
+
|
|
779
|
+
|
|
692
780
|
def touch_cmd(path: PathLike, /) -> list[str]:
|
|
693
781
|
return ["touch", str(path)]
|
|
694
782
|
|
|
695
783
|
|
|
784
|
+
##
|
|
785
|
+
|
|
786
|
+
|
|
696
787
|
def uv_run_cmd(module: str, /, *args: str) -> list[str]:
|
|
697
788
|
return [
|
|
698
789
|
"uv",
|
|
@@ -708,6 +799,9 @@ def uv_run_cmd(module: str, /, *args: str) -> list[str]:
|
|
|
708
799
|
]
|
|
709
800
|
|
|
710
801
|
|
|
802
|
+
##
|
|
803
|
+
|
|
804
|
+
|
|
711
805
|
@contextmanager
|
|
712
806
|
def yield_ssh_temp_dir(
|
|
713
807
|
user: str,
|
|
@@ -748,6 +842,7 @@ __all__ = [
|
|
|
748
842
|
"expand_path",
|
|
749
843
|
"git_clone_cmd",
|
|
750
844
|
"git_hard_reset_cmd",
|
|
845
|
+
"maybe_parent",
|
|
751
846
|
"maybe_sudo_cmd",
|
|
752
847
|
"mkdir",
|
|
753
848
|
"mkdir_cmd",
|
|
File without changes
|
|
File without changes
|