dycw-utilities 0.174.12__py3-none-any.whl → 0.174.14__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.12.dist-info → dycw_utilities-0.174.14.dist-info}/METADATA +1 -1
- {dycw_utilities-0.174.12.dist-info → dycw_utilities-0.174.14.dist-info}/RECORD +7 -7
- utilities/__init__.py +1 -1
- utilities/permissions.py +1 -0
- utilities/subprocess.py +206 -25
- {dycw_utilities-0.174.12.dist-info → dycw_utilities-0.174.14.dist-info}/WHEEL +0 -0
- {dycw_utilities-0.174.12.dist-info → dycw_utilities-0.174.14.dist-info}/entry_points.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
utilities/__init__.py,sha256=
|
|
1
|
+
utilities/__init__.py,sha256=wElh7rM2Gp4Qe3xG2KlBo0z_G6fFiYwVzZ10q9vTQac,61
|
|
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
|
|
@@ -47,7 +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=
|
|
50
|
+
utilities/permissions.py,sha256=vLXlWztSVYffbrxptne7ksj6dU1HLekm4fEvS4ny_4Q,8944
|
|
51
51
|
utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
|
|
52
52
|
utilities/platform.py,sha256=0pYO5v7L2sU5UN87zHhEEhTKsZ9NIEM8N6UCr0F7bLY,2778
|
|
53
53
|
utilities/polars.py,sha256=cNFBLWgOMUAp_Sz4xtlto17uZswZRrcfQYC95QKyaY4,87483
|
|
@@ -81,7 +81,7 @@ utilities/sqlalchemy.py,sha256=HQYpd7LFxdTF5WYVWYtCJeEBI71EJm7ytvCGyAH9B-U,37163
|
|
|
81
81
|
utilities/sqlalchemy_polars.py,sha256=JCGhB37raSR7fqeWV5dTsciRTMVzIdVT9YSqKT0piT0,13370
|
|
82
82
|
utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
|
|
83
83
|
utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
|
|
84
|
-
utilities/subprocess.py,sha256=
|
|
84
|
+
utilities/subprocess.py,sha256=tYfdY7CdVc91rs9AZ-KSwLvKyNNoPunDOWLgCtwuTvs,28490
|
|
85
85
|
utilities/tempfile.py,sha256=Lx6qa16lL1XVH6WdmD_G9vlN6gLI8nrIurxmsFkPKvg,3022
|
|
86
86
|
utilities/testbook.py,sha256=j1KmaVbrX9VrbeMgtPh5gk55myAsn3dyRUn7jGbPbRk,1294
|
|
87
87
|
utilities/text.py,sha256=7SvwcSR2l_5cOrm1samGnR4C-ZI6qyFLHLzSpO1zeHQ,13958
|
|
@@ -98,7 +98,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
|
|
|
98
98
|
utilities/whenever.py,sha256=F4ek0-OBWxHYrZdmoZt76N2RnNyKY5KrEHt7rqO4AQE,60183
|
|
99
99
|
utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
|
|
100
100
|
utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
|
|
101
|
-
dycw_utilities-0.174.
|
|
102
|
-
dycw_utilities-0.174.
|
|
103
|
-
dycw_utilities-0.174.
|
|
104
|
-
dycw_utilities-0.174.
|
|
101
|
+
dycw_utilities-0.174.14.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
|
|
102
|
+
dycw_utilities-0.174.14.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
|
|
103
|
+
dycw_utilities-0.174.14.dist-info/METADATA,sha256=nHCDEWfo7tFV9RLiNWEE9IqfJAvwPWvuv6rn07LnXpw,1710
|
|
104
|
+
dycw_utilities-0.174.14.dist-info/RECORD,,
|
utilities/__init__.py
CHANGED
utilities/permissions.py
CHANGED
utilities/subprocess.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import shutil
|
|
3
4
|
import sys
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from io import StringIO
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from shlex import join
|
|
10
|
+
from shutil import copyfile, copytree, move, rmtree
|
|
9
11
|
from string import Template
|
|
10
12
|
from subprocess import PIPE, CalledProcessError, Popen
|
|
11
13
|
from threading import Thread
|
|
@@ -15,12 +17,14 @@ from typing import IO, TYPE_CHECKING, Literal, assert_never, overload, override
|
|
|
15
17
|
from utilities.errors import ImpossibleCaseError
|
|
16
18
|
from utilities.iterables import always_iterable
|
|
17
19
|
from utilities.logging import to_logger
|
|
20
|
+
from utilities.permissions import ensure_perms
|
|
18
21
|
from utilities.text import strip_and_dedent
|
|
19
22
|
from utilities.whenever import to_seconds
|
|
20
23
|
|
|
21
24
|
if TYPE_CHECKING:
|
|
22
25
|
from collections.abc import Iterator
|
|
23
26
|
|
|
27
|
+
from utilities.permissions import PermissionsLike
|
|
24
28
|
from utilities.types import (
|
|
25
29
|
LoggerLike,
|
|
26
30
|
MaybeIterable,
|
|
@@ -44,6 +48,7 @@ UPDATE_CA_CERTIFICATES: str = "update-ca-certificates"
|
|
|
44
48
|
|
|
45
49
|
|
|
46
50
|
def apt_install_cmd(package: str, /) -> list[str]:
|
|
51
|
+
"""Command to use 'apt' to install a package."""
|
|
47
52
|
return ["apt", "install", "-y", package]
|
|
48
53
|
|
|
49
54
|
|
|
@@ -51,6 +56,7 @@ def apt_install_cmd(package: str, /) -> list[str]:
|
|
|
51
56
|
|
|
52
57
|
|
|
53
58
|
def cat_cmd(path: PathLike, /) -> list[str]:
|
|
59
|
+
"""Command to use 'cat' to concatenate and print files."""
|
|
54
60
|
return ["cat", str(path)]
|
|
55
61
|
|
|
56
62
|
|
|
@@ -58,30 +64,78 @@ def cat_cmd(path: PathLike, /) -> list[str]:
|
|
|
58
64
|
|
|
59
65
|
|
|
60
66
|
def cd_cmd(path: PathLike, /) -> list[str]:
|
|
67
|
+
"""Command to use 'cd' to change working directory."""
|
|
61
68
|
return ["cd", str(path)]
|
|
62
69
|
|
|
63
70
|
|
|
64
71
|
##
|
|
65
72
|
|
|
66
73
|
|
|
67
|
-
def
|
|
68
|
-
|
|
74
|
+
def chmod(path: PathLike, perms: PermissionsLike, /, *, sudo: bool = False) -> None:
|
|
75
|
+
"""Change file mode."""
|
|
76
|
+
if sudo: # pragma: no cover
|
|
77
|
+
run(*sudo_cmd(*chmod_cmd(path, perms)))
|
|
78
|
+
else:
|
|
79
|
+
Path(path).chmod(int(ensure_perms(perms)))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
##
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def chmod_cmd(path: PathLike, perms: PermissionsLike, /) -> list[str]:
|
|
86
|
+
"""Command to use 'chmod' to change file mode."""
|
|
87
|
+
return ["chmod", str(ensure_perms(perms)), str(path)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def chown(
|
|
94
|
+
path: PathLike,
|
|
95
|
+
/,
|
|
96
|
+
*,
|
|
97
|
+
sudo: bool = False,
|
|
98
|
+
user: str | int | None = None,
|
|
99
|
+
group: str | int | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Change file owner and/or group."""
|
|
102
|
+
if sudo: # pragma: no cover
|
|
103
|
+
match user, group:
|
|
104
|
+
case None, None:
|
|
105
|
+
...
|
|
106
|
+
case str() | int() | None, str() | int() | None:
|
|
107
|
+
run(*sudo_cmd(*chown_cmd(path, user=user, group=group)))
|
|
108
|
+
case never:
|
|
109
|
+
assert_never(never)
|
|
110
|
+
else:
|
|
111
|
+
match user, group:
|
|
112
|
+
case None, None:
|
|
113
|
+
...
|
|
114
|
+
case str() | int(), None:
|
|
115
|
+
shutil.chown(path, user, group)
|
|
116
|
+
case None, str() | int():
|
|
117
|
+
shutil.chown(path, user, group)
|
|
118
|
+
case str() | int(), str() | int():
|
|
119
|
+
shutil.chown(path, user, group)
|
|
120
|
+
case never:
|
|
121
|
+
assert_never(never)
|
|
69
122
|
|
|
70
123
|
|
|
71
124
|
##
|
|
72
125
|
|
|
73
126
|
|
|
74
127
|
def chown_cmd(
|
|
75
|
-
path: PathLike, /, *, user: str | None = None, group: str | None = None
|
|
128
|
+
path: PathLike, /, *, user: str | int | None = None, group: str | int | None = None
|
|
76
129
|
) -> list[str]:
|
|
130
|
+
"""Command to use 'chown' to change file owner and/or group."""
|
|
77
131
|
match user, group:
|
|
78
132
|
case None, None:
|
|
79
133
|
raise ChownCmdError
|
|
80
|
-
case str(), None:
|
|
134
|
+
case str() | int(), None:
|
|
81
135
|
ownership = "user"
|
|
82
|
-
case None, str():
|
|
136
|
+
case None, str() | int():
|
|
83
137
|
ownership = f":{group}"
|
|
84
|
-
case str(), str():
|
|
138
|
+
case str() | int(), str() | int():
|
|
85
139
|
ownership = f"{user}:{group}"
|
|
86
140
|
case never:
|
|
87
141
|
assert_never(never)
|
|
@@ -98,7 +152,46 @@ class ChownCmdError(Exception):
|
|
|
98
152
|
##
|
|
99
153
|
|
|
100
154
|
|
|
155
|
+
def cp(
|
|
156
|
+
src: PathLike,
|
|
157
|
+
dest: PathLike,
|
|
158
|
+
/,
|
|
159
|
+
*,
|
|
160
|
+
sudo: bool = False,
|
|
161
|
+
perms: PermissionsLike | None = None,
|
|
162
|
+
owner: str | int | None = None,
|
|
163
|
+
group: str | int | None = None,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Copy a file/directory."""
|
|
166
|
+
mkdir(dest, sudo=sudo, parent=True)
|
|
167
|
+
if sudo:
|
|
168
|
+
run(*sudo_cmd(*cp_cmd(src, dest)))
|
|
169
|
+
else:
|
|
170
|
+
src, dest = map(Path, [src, dest])
|
|
171
|
+
if src.is_file():
|
|
172
|
+
_ = copyfile(src, dest)
|
|
173
|
+
elif src.is_dir():
|
|
174
|
+
_ = copytree(src, dest, dirs_exist_ok=True)
|
|
175
|
+
else:
|
|
176
|
+
raise CpError(src=src, dest=dest)
|
|
177
|
+
if perms is not None:
|
|
178
|
+
chmod(dest, perms, sudo=sudo)
|
|
179
|
+
if (owner is not None) or (group is not None):
|
|
180
|
+
chown(dest, sudo=sudo, user=owner, group=group)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(kw_only=True, slots=True)
|
|
184
|
+
class CpError(Exception):
|
|
185
|
+
src: Path
|
|
186
|
+
dest: Path
|
|
187
|
+
|
|
188
|
+
@override
|
|
189
|
+
def __str__(self) -> str:
|
|
190
|
+
return f"Unable to copy {str(self.src)!r} to {str(self.dest)!r}; source does not exist"
|
|
191
|
+
|
|
192
|
+
|
|
101
193
|
def cp_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
|
|
194
|
+
"""Command to use 'cp' to copy a file/directory."""
|
|
102
195
|
return ["cp", "-r", str(src), str(dest)]
|
|
103
196
|
|
|
104
197
|
|
|
@@ -106,6 +199,7 @@ def cp_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
|
|
|
106
199
|
|
|
107
200
|
|
|
108
201
|
def echo_cmd(text: str, /) -> list[str]:
|
|
202
|
+
"""Command to use 'echo' to write arguments to the standard output."""
|
|
109
203
|
return ["echo", text]
|
|
110
204
|
|
|
111
205
|
|
|
@@ -115,6 +209,7 @@ def echo_cmd(text: str, /) -> list[str]:
|
|
|
115
209
|
def expand_path(
|
|
116
210
|
path: PathLike, /, *, subs: StrMapping | None = None, sudo: bool = False
|
|
117
211
|
) -> Path:
|
|
212
|
+
"""Expand a path using `subprocess`."""
|
|
118
213
|
if subs is not None:
|
|
119
214
|
path = Template(str(path)).substitute(**subs)
|
|
120
215
|
if sudo: # pragma: no cover
|
|
@@ -126,6 +221,7 @@ def expand_path(
|
|
|
126
221
|
|
|
127
222
|
|
|
128
223
|
def git_clone_cmd(url: str, path: PathLike, /) -> list[str]:
|
|
224
|
+
"""Command to use 'git clone' to clone a repository."""
|
|
129
225
|
return ["git", "clone", "--recurse-submodules", url, str(path)]
|
|
130
226
|
|
|
131
227
|
|
|
@@ -133,6 +229,7 @@ def git_clone_cmd(url: str, path: PathLike, /) -> list[str]:
|
|
|
133
229
|
|
|
134
230
|
|
|
135
231
|
def git_hard_reset_cmd(*, branch: str | None = None) -> list[str]:
|
|
232
|
+
"""Command to use 'git hard-reset' to hard reset a repository."""
|
|
136
233
|
branch_use = "master" if branch is None else branch
|
|
137
234
|
return ["git", "hard-reset", branch_use]
|
|
138
235
|
|
|
@@ -141,6 +238,7 @@ def git_hard_reset_cmd(*, branch: str | None = None) -> list[str]:
|
|
|
141
238
|
|
|
142
239
|
|
|
143
240
|
def maybe_parent(path: PathLike, /, *, parent: bool = False) -> Path:
|
|
241
|
+
"""Get the parent of a path, if required."""
|
|
144
242
|
path = Path(path)
|
|
145
243
|
return path.parent if parent else path
|
|
146
244
|
|
|
@@ -148,15 +246,8 @@ def maybe_parent(path: PathLike, /, *, parent: bool = False) -> Path:
|
|
|
148
246
|
##
|
|
149
247
|
|
|
150
248
|
|
|
151
|
-
def maybe_sudo_cmd(cmd: str, /, *args: str, sudo: bool = False) -> list[str]:
|
|
152
|
-
parts: list[str] = [cmd, *args]
|
|
153
|
-
return sudo_cmd(*parts) if sudo else parts
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
##
|
|
157
|
-
|
|
158
|
-
|
|
159
249
|
def mkdir(path: PathLike, /, *, sudo: bool = False, parent: bool = False) -> None:
|
|
250
|
+
"""Make a directory."""
|
|
160
251
|
if sudo: # pragma: no cover
|
|
161
252
|
run(*sudo_cmd(*mkdir_cmd(path, parent=parent)))
|
|
162
253
|
else:
|
|
@@ -167,20 +258,71 @@ def mkdir(path: PathLike, /, *, sudo: bool = False, parent: bool = False) -> Non
|
|
|
167
258
|
|
|
168
259
|
|
|
169
260
|
def mkdir_cmd(path: PathLike, /, *, parent: bool = False) -> list[str]:
|
|
261
|
+
"""Command to use 'mv' to make a directory."""
|
|
170
262
|
return ["mkdir", "-p", str(maybe_parent(path, parent=parent))]
|
|
171
263
|
|
|
172
264
|
|
|
173
265
|
##
|
|
174
266
|
|
|
175
267
|
|
|
268
|
+
def mv(
|
|
269
|
+
src: PathLike,
|
|
270
|
+
dest: PathLike,
|
|
271
|
+
/,
|
|
272
|
+
*,
|
|
273
|
+
sudo: bool = False,
|
|
274
|
+
perms: PermissionsLike | None = None,
|
|
275
|
+
owner: str | int | None = None,
|
|
276
|
+
group: str | int | None = None,
|
|
277
|
+
) -> None:
|
|
278
|
+
"""Move a file/directory."""
|
|
279
|
+
mkdir(dest, sudo=sudo, parent=True)
|
|
280
|
+
if sudo:
|
|
281
|
+
run(*sudo_cmd(*cp_cmd(src, dest)))
|
|
282
|
+
else:
|
|
283
|
+
src, dest = map(Path, [src, dest])
|
|
284
|
+
if src.exists():
|
|
285
|
+
_ = move(src, dest)
|
|
286
|
+
else:
|
|
287
|
+
raise MvFileError(src=src, dest=dest)
|
|
288
|
+
if perms is not None:
|
|
289
|
+
chmod(dest, perms, sudo=sudo)
|
|
290
|
+
if (owner is not None) or (group is not None):
|
|
291
|
+
chown(dest, sudo=sudo, user=owner, group=group)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass(kw_only=True, slots=True)
|
|
295
|
+
class MvFileError(Exception):
|
|
296
|
+
src: Path
|
|
297
|
+
dest: Path
|
|
298
|
+
|
|
299
|
+
@override
|
|
300
|
+
def __str__(self) -> str:
|
|
301
|
+
return f"Unable to move {str(self.src)!r} to {str(self.dest)!r}; source does not exist"
|
|
302
|
+
|
|
303
|
+
|
|
176
304
|
def mv_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
|
|
305
|
+
"""Command to use 'mv' to move a file/directory."""
|
|
177
306
|
return ["mv", str(src), str(dest)]
|
|
178
307
|
|
|
179
308
|
|
|
180
309
|
##
|
|
181
310
|
|
|
182
311
|
|
|
312
|
+
def rm(path: PathLike, /, *, sudo: bool = False) -> None:
|
|
313
|
+
"""Remove a file/directory."""
|
|
314
|
+
if sudo: # pragma: no cover
|
|
315
|
+
run(*sudo_cmd(*rm_cmd(path)))
|
|
316
|
+
else:
|
|
317
|
+
path = Path(path)
|
|
318
|
+
if path.is_file():
|
|
319
|
+
path.unlink(missing_ok=True)
|
|
320
|
+
elif path.is_dir():
|
|
321
|
+
rmtree(path, ignore_errors=True)
|
|
322
|
+
|
|
323
|
+
|
|
183
324
|
def rm_cmd(path: PathLike, /) -> list[str]:
|
|
325
|
+
"""Command to use 'rm' to remove a file/directory."""
|
|
184
326
|
return ["rm", "-rf", str(path)]
|
|
185
327
|
|
|
186
328
|
|
|
@@ -206,6 +348,7 @@ def rsync(
|
|
|
206
348
|
exclude: MaybeIterable[str] | None = None,
|
|
207
349
|
chmod: str | None = None,
|
|
208
350
|
) -> None:
|
|
351
|
+
"""Remote & local file copying."""
|
|
209
352
|
mkdir_args = maybe_sudo_cmd(*mkdir_cmd(dest, parent=True), sudo=sudo) # skipif-ci
|
|
210
353
|
ssh( # skipif-ci
|
|
211
354
|
user,
|
|
@@ -250,9 +393,6 @@ def rsync(
|
|
|
250
393
|
)
|
|
251
394
|
|
|
252
395
|
|
|
253
|
-
##
|
|
254
|
-
|
|
255
|
-
|
|
256
396
|
def rsync_cmd(
|
|
257
397
|
src_or_srcs: MaybeIterable[PathLike],
|
|
258
398
|
user: str,
|
|
@@ -270,6 +410,7 @@ def rsync_cmd(
|
|
|
270
410
|
sudo: bool = False,
|
|
271
411
|
parent: bool = False,
|
|
272
412
|
) -> list[str]:
|
|
413
|
+
"""Command to use 'rsync' to do remote & local file copying."""
|
|
273
414
|
args: list[str] = ["rsync"]
|
|
274
415
|
if archive:
|
|
275
416
|
args.append("--archive")
|
|
@@ -427,6 +568,7 @@ def run(
|
|
|
427
568
|
retry: Retry | None = None,
|
|
428
569
|
logger: LoggerLike | None = None,
|
|
429
570
|
) -> str | None:
|
|
571
|
+
"""Run a command in a subprocess."""
|
|
430
572
|
args: list[str] = []
|
|
431
573
|
if user is not None: # pragma: no cover
|
|
432
574
|
args.extend(["su", "-", str(user)])
|
|
@@ -567,6 +709,7 @@ def _run_write_to_streams(text: str, /, *outputs: IO[str]) -> None:
|
|
|
567
709
|
|
|
568
710
|
|
|
569
711
|
def set_hostname_cmd(hostname: str, /) -> list[str]:
|
|
712
|
+
"""Command to set the system hostname."""
|
|
570
713
|
return ["hostnamectl", "set-hostname", hostname]
|
|
571
714
|
|
|
572
715
|
|
|
@@ -686,6 +829,7 @@ def ssh(
|
|
|
686
829
|
retry: Retry | None = None,
|
|
687
830
|
logger: LoggerLike | None = None,
|
|
688
831
|
) -> str | None:
|
|
832
|
+
"""Execute a command on a remote machine."""
|
|
689
833
|
cmd_and_args = ssh_cmd( # skipif-ci
|
|
690
834
|
user,
|
|
691
835
|
hostname,
|
|
@@ -708,9 +852,6 @@ def ssh(
|
|
|
708
852
|
)
|
|
709
853
|
|
|
710
854
|
|
|
711
|
-
##
|
|
712
|
-
|
|
713
|
-
|
|
714
855
|
def ssh_cmd(
|
|
715
856
|
user: str,
|
|
716
857
|
hostname: str,
|
|
@@ -720,6 +861,7 @@ def ssh_cmd(
|
|
|
720
861
|
host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
|
|
721
862
|
strict_host_key_checking: bool = True,
|
|
722
863
|
) -> list[str]:
|
|
864
|
+
"""Command to use 'ssh' to execute a command on a remote machine."""
|
|
723
865
|
args: list[str] = ssh_opts_cmd(
|
|
724
866
|
batch_mode=batch_mode,
|
|
725
867
|
host_key_algorithms=host_key_algorithms,
|
|
@@ -728,15 +870,13 @@ def ssh_cmd(
|
|
|
728
870
|
return [*args, f"{user}@{hostname}", *cmd_and_cmds_or_args]
|
|
729
871
|
|
|
730
872
|
|
|
731
|
-
##
|
|
732
|
-
|
|
733
|
-
|
|
734
873
|
def ssh_opts_cmd(
|
|
735
874
|
*,
|
|
736
875
|
batch_mode: bool = True,
|
|
737
876
|
host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
|
|
738
877
|
strict_host_key_checking: bool = True,
|
|
739
878
|
) -> list[str]:
|
|
879
|
+
"""Command to use prepare 'ssh' to execute a command on a remote machine."""
|
|
740
880
|
args: list[str] = ["ssh"]
|
|
741
881
|
if batch_mode:
|
|
742
882
|
args.extend(["-o", "BatchMode=yes"])
|
|
@@ -750,6 +890,7 @@ def ssh_opts_cmd(
|
|
|
750
890
|
|
|
751
891
|
|
|
752
892
|
def ssh_keygen_cmd(hostname: str, /) -> list[str]:
|
|
893
|
+
"""Command to use 'ssh-keygen' to add a known host."""
|
|
753
894
|
return ["ssh-keygen", "-f", "~/.ssh/known_hosts", "-R", hostname]
|
|
754
895
|
|
|
755
896
|
|
|
@@ -757,21 +898,52 @@ def ssh_keygen_cmd(hostname: str, /) -> list[str]:
|
|
|
757
898
|
|
|
758
899
|
|
|
759
900
|
def sudo_cmd(cmd: str, /, *args: str) -> list[str]:
|
|
901
|
+
"""Command to use 'sudo' to execute a command as another user."""
|
|
760
902
|
return ["sudo", cmd, *args]
|
|
761
903
|
|
|
762
904
|
|
|
905
|
+
def maybe_sudo_cmd(cmd: str, /, *args: str, sudo: bool = False) -> list[str]:
|
|
906
|
+
"""Command to use 'sudo' to execute a command as another user, if required."""
|
|
907
|
+
parts: list[str] = [cmd, *args]
|
|
908
|
+
return sudo_cmd(*parts) if sudo else parts
|
|
909
|
+
|
|
910
|
+
|
|
763
911
|
##
|
|
764
912
|
|
|
765
913
|
|
|
766
914
|
def sudo_nopasswd_cmd(user: str, /) -> str:
|
|
915
|
+
"""Command to allow a user to use password-free `sudo`."""
|
|
767
916
|
return f"{user} ALL=(ALL) NOPASSWD: ALL"
|
|
768
917
|
|
|
769
918
|
|
|
770
919
|
##
|
|
771
920
|
|
|
772
921
|
|
|
773
|
-
def
|
|
774
|
-
|
|
922
|
+
def symlink(targret: PathLike, link: PathLike, /, *, sudo: bool = False) -> None:
|
|
923
|
+
"""Make a symbolic link."""
|
|
924
|
+
rm(link, sudo=sudo)
|
|
925
|
+
mkdir(link, sudo=sudo, parent=True)
|
|
926
|
+
if sudo: # pragma: no cover
|
|
927
|
+
run(*sudo_cmd(*symlink_cmd(targret, link)))
|
|
928
|
+
else:
|
|
929
|
+
targret, link = map(Path, [targret, link])
|
|
930
|
+
link.symlink_to(targret)
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def symlink_cmd(target: PathLike, link: PathLike, /) -> list[str]:
|
|
934
|
+
"""Command to use 'symlink' to make a symbolic link."""
|
|
935
|
+
return ["ln", "-s", str(target), str(link)]
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
##
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def tee_cmd(*, append: bool = False) -> list[str]:
|
|
942
|
+
"""Command to use 'tee' to duplicate standard input."""
|
|
943
|
+
args: list[str] = ["tee"]
|
|
944
|
+
if append:
|
|
945
|
+
args.append("-a")
|
|
946
|
+
return args
|
|
775
947
|
|
|
776
948
|
|
|
777
949
|
##
|
|
@@ -833,10 +1005,15 @@ __all__ = [
|
|
|
833
1005
|
"RESTART_SSHD",
|
|
834
1006
|
"UPDATE_CA_CERTIFICATES",
|
|
835
1007
|
"ChownCmdError",
|
|
1008
|
+
"CpError",
|
|
1009
|
+
"MvFileError",
|
|
836
1010
|
"apt_install_cmd",
|
|
837
1011
|
"cd_cmd",
|
|
1012
|
+
"chmod",
|
|
838
1013
|
"chmod_cmd",
|
|
1014
|
+
"chown",
|
|
839
1015
|
"chown_cmd",
|
|
1016
|
+
"cp",
|
|
840
1017
|
"cp_cmd",
|
|
841
1018
|
"echo_cmd",
|
|
842
1019
|
"expand_path",
|
|
@@ -846,7 +1023,9 @@ __all__ = [
|
|
|
846
1023
|
"maybe_sudo_cmd",
|
|
847
1024
|
"mkdir",
|
|
848
1025
|
"mkdir_cmd",
|
|
1026
|
+
"mv",
|
|
849
1027
|
"mv_cmd",
|
|
1028
|
+
"rm",
|
|
850
1029
|
"rm_cmd",
|
|
851
1030
|
"rsync",
|
|
852
1031
|
"rsync_cmd",
|
|
@@ -857,7 +1036,9 @@ __all__ = [
|
|
|
857
1036
|
"ssh_opts_cmd",
|
|
858
1037
|
"sudo_cmd",
|
|
859
1038
|
"sudo_nopasswd_cmd",
|
|
1039
|
+
"symlink",
|
|
860
1040
|
"symlink_cmd",
|
|
1041
|
+
"tee_cmd",
|
|
861
1042
|
"touch_cmd",
|
|
862
1043
|
"uv_run_cmd",
|
|
863
1044
|
"yield_ssh_temp_dir",
|
|
File without changes
|
|
File without changes
|