dycw-utilities 0.175.17__py3-none-any.whl → 0.185.8__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.185.8.dist-info/METADATA +33 -0
- dycw_utilities-0.185.8.dist-info/RECORD +90 -0
- {dycw_utilities-0.175.17.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +2 -2
- utilities/__init__.py +1 -1
- utilities/altair.py +8 -6
- utilities/asyncio.py +40 -56
- utilities/atools.py +9 -11
- utilities/cachetools.py +8 -6
- utilities/click.py +4 -3
- utilities/concurrent.py +1 -1
- utilities/constants.py +492 -0
- utilities/contextlib.py +23 -30
- utilities/contextvars.py +1 -23
- utilities/core.py +2581 -0
- utilities/dataclasses.py +16 -119
- utilities/docker.py +139 -45
- utilities/enum.py +1 -1
- utilities/errors.py +2 -16
- utilities/fastapi.py +5 -5
- utilities/fpdf2.py +2 -1
- utilities/functions.py +33 -264
- utilities/http.py +2 -3
- utilities/hypothesis.py +48 -25
- utilities/iterables.py +39 -575
- utilities/jinja2.py +3 -6
- utilities/jupyter.py +5 -3
- utilities/libcst.py +1 -1
- utilities/lightweight_charts.py +4 -6
- utilities/logging.py +17 -15
- utilities/math.py +1 -36
- utilities/more_itertools.py +4 -6
- utilities/numpy.py +2 -1
- utilities/operator.py +2 -2
- utilities/orjson.py +24 -25
- utilities/os.py +4 -185
- utilities/packaging.py +129 -0
- utilities/parse.py +33 -13
- utilities/pathlib.py +2 -136
- utilities/platform.py +8 -90
- utilities/polars.py +34 -31
- utilities/postgres.py +9 -4
- utilities/pottery.py +20 -18
- utilities/pqdm.py +3 -4
- utilities/psutil.py +2 -3
- utilities/pydantic.py +18 -4
- utilities/pydantic_settings.py +7 -9
- utilities/pydantic_settings_sops.py +3 -3
- utilities/pyinstrument.py +4 -4
- utilities/pytest.py +49 -108
- utilities/pytest_plugins/pytest_regressions.py +2 -2
- utilities/pytest_regressions.py +8 -6
- utilities/random.py +2 -8
- utilities/redis.py +98 -94
- utilities/reprlib.py +11 -118
- utilities/shellingham.py +66 -0
- utilities/slack_sdk.py +13 -12
- utilities/sqlalchemy.py +42 -30
- utilities/sqlalchemy_polars.py +16 -25
- utilities/subprocess.py +1166 -148
- utilities/tabulate.py +32 -0
- utilities/testbook.py +8 -8
- utilities/text.py +24 -115
- utilities/throttle.py +159 -0
- utilities/time.py +18 -0
- utilities/timer.py +29 -12
- utilities/traceback.py +15 -22
- utilities/types.py +38 -3
- utilities/typing.py +18 -12
- utilities/uuid.py +1 -1
- utilities/version.py +202 -45
- utilities/whenever.py +22 -150
- dycw_utilities-0.175.17.dist-info/METADATA +0 -34
- dycw_utilities-0.175.17.dist-info/RECORD +0 -103
- utilities/atomicwrites.py +0 -182
- utilities/cryptography.py +0 -41
- utilities/getpass.py +0 -8
- utilities/git.py +0 -19
- utilities/grp.py +0 -28
- utilities/gzip.py +0 -31
- utilities/json.py +0 -70
- utilities/permissions.py +0 -298
- utilities/pickle.py +0 -25
- utilities/pwd.py +0 -28
- utilities/re.py +0 -156
- utilities/sentinel.py +0 -73
- utilities/socket.py +0 -8
- utilities/string.py +0 -20
- utilities/tempfile.py +0 -136
- utilities/tzdata.py +0 -11
- utilities/tzlocal.py +0 -28
- utilities/warnings.py +0 -65
- utilities/zipfile.py +0 -25
- utilities/zoneinfo.py +0 -133
- {dycw_utilities-0.175.17.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +0 -0
utilities/subprocess.py
CHANGED
|
@@ -1,37 +1,56 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import json
|
|
4
4
|
import sys
|
|
5
|
-
from contextlib import contextmanager
|
|
6
5
|
from dataclasses import dataclass
|
|
7
6
|
from io import StringIO
|
|
7
|
+
from itertools import chain, repeat
|
|
8
|
+
from json import JSONDecodeError
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from re import search
|
|
10
|
+
from re import MULTILINE, escape, search
|
|
10
11
|
from shlex import join
|
|
11
|
-
from shutil import
|
|
12
|
+
from shutil import rmtree
|
|
12
13
|
from string import Template
|
|
13
14
|
from subprocess import PIPE, CalledProcessError, Popen
|
|
14
15
|
from threading import Thread
|
|
15
|
-
from time import sleep
|
|
16
16
|
from typing import IO, TYPE_CHECKING, Literal, assert_never, overload, override
|
|
17
17
|
|
|
18
|
+
import utilities.core
|
|
19
|
+
from utilities.constants import HOME, PWD, SECOND
|
|
20
|
+
from utilities.contextlib import enhanced_context_manager
|
|
21
|
+
from utilities.core import (
|
|
22
|
+
OneEmptyError,
|
|
23
|
+
Permissions,
|
|
24
|
+
TemporaryDirectory,
|
|
25
|
+
_CopyOrMoveSourceNotFoundError,
|
|
26
|
+
always_iterable,
|
|
27
|
+
copy,
|
|
28
|
+
file_or_dir,
|
|
29
|
+
move,
|
|
30
|
+
normalize_multi_line_str,
|
|
31
|
+
one,
|
|
32
|
+
repr_str,
|
|
33
|
+
)
|
|
18
34
|
from utilities.errors import ImpossibleCaseError
|
|
19
|
-
from utilities.
|
|
35
|
+
from utilities.functions import in_timedelta
|
|
20
36
|
from utilities.logging import to_logger
|
|
21
|
-
from utilities.
|
|
22
|
-
from utilities.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
from utilities.time import sleep
|
|
38
|
+
from utilities.version import (
|
|
39
|
+
ParseVersion2Or3Error,
|
|
40
|
+
Version2,
|
|
41
|
+
Version3,
|
|
42
|
+
parse_version_2_or_3,
|
|
43
|
+
)
|
|
26
44
|
|
|
27
45
|
if TYPE_CHECKING:
|
|
28
|
-
from collections.abc import Callable, Iterator
|
|
46
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
29
47
|
|
|
30
|
-
from utilities.
|
|
48
|
+
from utilities.core import PermissionsLike
|
|
31
49
|
from utilities.types import (
|
|
32
|
-
|
|
50
|
+
Duration,
|
|
33
51
|
LoggerLike,
|
|
34
52
|
MaybeIterable,
|
|
53
|
+
MaybeSequenceStr,
|
|
35
54
|
PathLike,
|
|
36
55
|
Retry,
|
|
37
56
|
StrMapping,
|
|
@@ -45,8 +64,12 @@ BASH_LC = ["bash", "-lc"]
|
|
|
45
64
|
BASH_LS = ["bash", "-ls"]
|
|
46
65
|
CHPASSWD = "chpasswd"
|
|
47
66
|
GIT_BRANCH_SHOW_CURRENT = ["git", "branch", "--show-current"]
|
|
48
|
-
|
|
67
|
+
ISOLATED = "--isolated"
|
|
68
|
+
KNOWN_HOSTS = HOME / ".ssh/known_hosts"
|
|
69
|
+
MANAGED_PYTHON = "--managed-python"
|
|
49
70
|
MKTEMP_DIR_CMD = ["mktemp", "-d"]
|
|
71
|
+
PRERELEASE_DISALLOW = ["--prerelease", "disallow"]
|
|
72
|
+
RESOLUTION_HIGHEST = ["--resolution", "highest"]
|
|
50
73
|
RESTART_SSHD = ["systemctl", "restart", "sshd"]
|
|
51
74
|
UPDATE_CA_CERTIFICATES: str = "update-ca-certificates"
|
|
52
75
|
|
|
@@ -54,24 +77,89 @@ UPDATE_CA_CERTIFICATES: str = "update-ca-certificates"
|
|
|
54
77
|
##
|
|
55
78
|
|
|
56
79
|
|
|
57
|
-
def
|
|
58
|
-
|
|
80
|
+
def append_text(
|
|
81
|
+
path: PathLike,
|
|
82
|
+
text: str,
|
|
83
|
+
/,
|
|
84
|
+
*,
|
|
85
|
+
sudo: bool = False,
|
|
86
|
+
skip_if_present: bool = False,
|
|
87
|
+
flags: int = 0,
|
|
88
|
+
blank_lines: int = 1,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Append text to a file."""
|
|
91
|
+
try:
|
|
92
|
+
existing = cat(path, sudo=sudo)
|
|
93
|
+
except (CalledProcessError, FileNotFoundError):
|
|
94
|
+
tee(path, text, sudo=sudo)
|
|
95
|
+
return
|
|
96
|
+
if existing == "":
|
|
97
|
+
tee(path, text, sudo=sudo)
|
|
98
|
+
return
|
|
99
|
+
if skip_if_present and (search(escape(text), existing, flags=flags) is not None):
|
|
100
|
+
return
|
|
101
|
+
full = "".join([*repeat("\n", times=blank_lines), text])
|
|
102
|
+
tee(path, full, sudo=sudo, append=True)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def apt_install(
|
|
109
|
+
package: str, /, *packages: str, update: bool = False, sudo: bool = False
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Install packages."""
|
|
59
112
|
if update: # pragma: no cover
|
|
60
|
-
|
|
61
|
-
|
|
113
|
+
apt_update(sudo=sudo)
|
|
114
|
+
args = maybe_sudo_cmd( # pragma: no cover
|
|
115
|
+
*apt_install_cmd(package, *packages), sudo=sudo
|
|
116
|
+
)
|
|
117
|
+
run(*args) # pragma: no cover
|
|
118
|
+
|
|
62
119
|
|
|
120
|
+
def apt_install_cmd(package: str, /, *packages: str) -> list[str]:
|
|
121
|
+
"""Command to use 'apt' to install packages."""
|
|
122
|
+
return ["apt", "install", "-y", package, *packages]
|
|
63
123
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
124
|
+
|
|
125
|
+
##
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def apt_remove(package: str, /, *packages: str, sudo: bool = False) -> None:
|
|
129
|
+
"""Remove a package."""
|
|
130
|
+
args = maybe_sudo_cmd( # pragma: no cover
|
|
131
|
+
*apt_remove_cmd(package, *packages), sudo=sudo
|
|
132
|
+
)
|
|
133
|
+
run(*args) # pragma: no cover
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def apt_remove_cmd(package: str, /, *packages: str) -> list[str]:
|
|
137
|
+
"""Command to use 'apt' to remove packages."""
|
|
138
|
+
return ["apt", "remove", "-y", package, *packages]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
##
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def apt_update(*, sudo: bool = False) -> None:
|
|
145
|
+
"""Update 'apt'."""
|
|
146
|
+
run(*maybe_sudo_cmd(*APT_UPDATE, sudo=sudo)) # pragma: no cover
|
|
67
147
|
|
|
68
148
|
|
|
69
149
|
##
|
|
70
150
|
|
|
71
151
|
|
|
72
|
-
def
|
|
152
|
+
def cat(path: PathLike, /, *paths: PathLike, sudo: bool = False) -> str:
|
|
153
|
+
"""Concatenate and print files."""
|
|
154
|
+
if sudo: # pragma: no cover
|
|
155
|
+
return run(*sudo_cmd(*cat_cmd(path, *paths)), return_=True)
|
|
156
|
+
all_paths = list(map(Path, [path, *paths]))
|
|
157
|
+
return "\n".join(p.read_text() for p in all_paths)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def cat_cmd(path: PathLike, /, *paths: PathLike) -> list[str]:
|
|
73
161
|
"""Command to use 'cat' to concatenate and print files."""
|
|
74
|
-
return ["cat", str(path)]
|
|
162
|
+
return ["cat", str(path), *map(str, paths)]
|
|
75
163
|
|
|
76
164
|
|
|
77
165
|
##
|
|
@@ -85,17 +173,40 @@ def cd_cmd(path: PathLike, /) -> list[str]:
|
|
|
85
173
|
##
|
|
86
174
|
|
|
87
175
|
|
|
176
|
+
def chattr(
|
|
177
|
+
path: PathLike, /, *, immutable: bool | None = None, sudo: bool = False
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Change file attributes."""
|
|
180
|
+
args = maybe_sudo_cmd( # pragma: no cover
|
|
181
|
+
*chattr_cmd(path, immutable=immutable), sudo=sudo
|
|
182
|
+
)
|
|
183
|
+
run(*args) # pragma: no cover
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def chattr_cmd(path: PathLike, /, *, immutable: bool | None = None) -> list[str]:
|
|
187
|
+
"""Command to use 'chattr' to change file attributes."""
|
|
188
|
+
args: list[str] = ["chattr"]
|
|
189
|
+
if immutable is True:
|
|
190
|
+
args.append("+i")
|
|
191
|
+
elif immutable is False:
|
|
192
|
+
args.append("-i")
|
|
193
|
+
return [*args, str(path)]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
##
|
|
197
|
+
|
|
198
|
+
|
|
88
199
|
def chmod(path: PathLike, perms: PermissionsLike, /, *, sudo: bool = False) -> None:
|
|
89
200
|
"""Change file mode."""
|
|
90
201
|
if sudo: # pragma: no cover
|
|
91
202
|
run(*sudo_cmd(*chmod_cmd(path, perms)))
|
|
92
|
-
else:
|
|
93
|
-
|
|
203
|
+
else: # pragma: no cover
|
|
204
|
+
utilities.core.chmod(path, perms)
|
|
94
205
|
|
|
95
206
|
|
|
96
207
|
def chmod_cmd(path: PathLike, perms: PermissionsLike, /) -> list[str]:
|
|
97
208
|
"""Command to use 'chmod' to change file mode."""
|
|
98
|
-
return ["chmod", str(
|
|
209
|
+
return ["chmod", str(Permissions.new(perms)), str(path)]
|
|
99
210
|
|
|
100
211
|
|
|
101
212
|
##
|
|
@@ -106,36 +217,33 @@ def chown(
|
|
|
106
217
|
/,
|
|
107
218
|
*,
|
|
108
219
|
sudo: bool = False,
|
|
220
|
+
recursive: bool = False,
|
|
109
221
|
user: str | int | None = None,
|
|
110
222
|
group: str | int | None = None,
|
|
111
223
|
) -> None:
|
|
112
224
|
"""Change file owner and/or group."""
|
|
113
225
|
if sudo: # pragma: no cover
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else:
|
|
122
|
-
match user, group:
|
|
123
|
-
case None, None:
|
|
124
|
-
...
|
|
125
|
-
case str() | int(), None:
|
|
126
|
-
shutil.chown(path, user, group)
|
|
127
|
-
case None, str() | int():
|
|
128
|
-
shutil.chown(path, user, group)
|
|
129
|
-
case str() | int(), str() | int():
|
|
130
|
-
shutil.chown(path, user, group)
|
|
131
|
-
case never:
|
|
132
|
-
assert_never(never)
|
|
226
|
+
if (user is not None) or (group is not None):
|
|
227
|
+
args = sudo_cmd(
|
|
228
|
+
*chown_cmd(path, recursive=recursive, user=user, group=group)
|
|
229
|
+
)
|
|
230
|
+
run(*args)
|
|
231
|
+
else: # pragma: no cover
|
|
232
|
+
utilities.core.chown(path, recursive=recursive, user=user, group=group)
|
|
133
233
|
|
|
134
234
|
|
|
135
235
|
def chown_cmd(
|
|
136
|
-
path: PathLike,
|
|
236
|
+
path: PathLike,
|
|
237
|
+
/,
|
|
238
|
+
*,
|
|
239
|
+
recursive: bool = False,
|
|
240
|
+
user: str | int | None = None,
|
|
241
|
+
group: str | int | None = None,
|
|
137
242
|
) -> list[str]:
|
|
138
243
|
"""Command to use 'chown' to change file owner and/or group."""
|
|
244
|
+
args: list[str] = ["chown"]
|
|
245
|
+
if recursive:
|
|
246
|
+
args.append("-R")
|
|
139
247
|
match user, group:
|
|
140
248
|
case None, None:
|
|
141
249
|
raise ChownCmdError
|
|
@@ -147,7 +255,7 @@ def chown_cmd(
|
|
|
147
255
|
ownership = f"{user}:{group}"
|
|
148
256
|
case never:
|
|
149
257
|
assert_never(never)
|
|
150
|
-
return [
|
|
258
|
+
return [*args, ownership, str(path)]
|
|
151
259
|
|
|
152
260
|
|
|
153
261
|
@dataclass(kw_only=True, slots=True)
|
|
@@ -162,9 +270,26 @@ class ChownCmdError(Exception):
|
|
|
162
270
|
|
|
163
271
|
def chpasswd(user_name: str, password: str, /, *, sudo: bool = False) -> None:
|
|
164
272
|
"""Update passwords."""
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
273
|
+
args = maybe_sudo_cmd(CHPASSWD, sudo=sudo) # pragma: no cover
|
|
274
|
+
run(*args, input=f"{user_name}:{password}") # pragma: no cover
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
##
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def copy_text(
|
|
281
|
+
src: PathLike,
|
|
282
|
+
dest: PathLike,
|
|
283
|
+
/,
|
|
284
|
+
*,
|
|
285
|
+
sudo: bool = False,
|
|
286
|
+
substitutions: StrMapping | None = None,
|
|
287
|
+
) -> None:
|
|
288
|
+
"""Copy the text contents of a file."""
|
|
289
|
+
text = cat(src, sudo=sudo)
|
|
290
|
+
if substitutions is not None:
|
|
291
|
+
text = Template(text).substitute(**substitutions)
|
|
292
|
+
tee(dest, text, sudo=sudo)
|
|
168
293
|
|
|
169
294
|
|
|
170
295
|
##
|
|
@@ -184,28 +309,24 @@ def cp(
|
|
|
184
309
|
mkdir(dest, sudo=sudo, parent=True)
|
|
185
310
|
if sudo: # pragma: no cover
|
|
186
311
|
run(*sudo_cmd(*cp_cmd(src, dest)))
|
|
312
|
+
if perms is not None:
|
|
313
|
+
chmod(dest, perms, sudo=True)
|
|
314
|
+
if (owner is not None) or (group is not None):
|
|
315
|
+
chown(dest, sudo=True, user=owner, group=group)
|
|
187
316
|
else:
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
_ = copytree(src, dest, dirs_exist_ok=True)
|
|
193
|
-
else:
|
|
194
|
-
raise CpError(src=src, dest=dest)
|
|
195
|
-
if perms is not None:
|
|
196
|
-
chmod(dest, perms, sudo=sudo)
|
|
197
|
-
if (owner is not None) or (group is not None):
|
|
198
|
-
chown(dest, sudo=sudo, user=owner, group=group)
|
|
317
|
+
try:
|
|
318
|
+
copy(src, dest, overwrite=True, perms=perms, owner=owner, group=group)
|
|
319
|
+
except _CopyOrMoveSourceNotFoundError as error:
|
|
320
|
+
raise CpError(src=error.src) from None
|
|
199
321
|
|
|
200
322
|
|
|
201
323
|
@dataclass(kw_only=True, slots=True)
|
|
202
324
|
class CpError(Exception):
|
|
203
325
|
src: Path
|
|
204
|
-
dest: Path
|
|
205
326
|
|
|
206
327
|
@override
|
|
207
328
|
def __str__(self) -> str:
|
|
208
|
-
return f"
|
|
329
|
+
return f"Source {repr_str(self.src)} does not exist"
|
|
209
330
|
|
|
210
331
|
|
|
211
332
|
def cp_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
|
|
@@ -216,6 +337,185 @@ def cp_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
|
|
|
216
337
|
##
|
|
217
338
|
|
|
218
339
|
|
|
340
|
+
@overload
|
|
341
|
+
def curl(
|
|
342
|
+
url: str,
|
|
343
|
+
/,
|
|
344
|
+
*,
|
|
345
|
+
fail: bool = True,
|
|
346
|
+
location: bool = True,
|
|
347
|
+
output: PathLike | None = None,
|
|
348
|
+
show_error: bool = True,
|
|
349
|
+
silent: bool = True,
|
|
350
|
+
sudo: bool = False,
|
|
351
|
+
print: bool = False,
|
|
352
|
+
print_stdout: bool = False,
|
|
353
|
+
print_stderr: bool = False,
|
|
354
|
+
return_: Literal[True],
|
|
355
|
+
return_stdout: Literal[False] = False,
|
|
356
|
+
return_stderr: Literal[False] = False,
|
|
357
|
+
retry: Retry | None = None,
|
|
358
|
+
retry_skip: Callable[[int, str, str], bool] | None = None,
|
|
359
|
+
logger: LoggerLike | None = None,
|
|
360
|
+
) -> str: ...
|
|
361
|
+
@overload
|
|
362
|
+
def curl(
|
|
363
|
+
url: str,
|
|
364
|
+
/,
|
|
365
|
+
*,
|
|
366
|
+
fail: bool = True,
|
|
367
|
+
location: bool = True,
|
|
368
|
+
output: PathLike | None = None,
|
|
369
|
+
show_error: bool = True,
|
|
370
|
+
silent: bool = True,
|
|
371
|
+
sudo: bool = False,
|
|
372
|
+
print: bool = False,
|
|
373
|
+
print_stdout: bool = False,
|
|
374
|
+
print_stderr: bool = False,
|
|
375
|
+
return_: Literal[False] = False,
|
|
376
|
+
return_stdout: Literal[True],
|
|
377
|
+
return_stderr: Literal[False] = False,
|
|
378
|
+
retry: Retry | None = None,
|
|
379
|
+
retry_skip: Callable[[int, str, str], bool] | None = None,
|
|
380
|
+
logger: LoggerLike | None = None,
|
|
381
|
+
) -> str: ...
|
|
382
|
+
@overload
|
|
383
|
+
def curl(
|
|
384
|
+
url: str,
|
|
385
|
+
/,
|
|
386
|
+
*,
|
|
387
|
+
fail: bool = True,
|
|
388
|
+
location: bool = True,
|
|
389
|
+
output: PathLike | None = None,
|
|
390
|
+
show_error: bool = True,
|
|
391
|
+
silent: bool = True,
|
|
392
|
+
sudo: bool = False,
|
|
393
|
+
print: bool = False,
|
|
394
|
+
print_stdout: bool = False,
|
|
395
|
+
print_stderr: bool = False,
|
|
396
|
+
return_: Literal[False] = False,
|
|
397
|
+
return_stdout: Literal[False] = False,
|
|
398
|
+
return_stderr: Literal[True],
|
|
399
|
+
retry: Retry | None = None,
|
|
400
|
+
retry_skip: Callable[[int, str, str], bool] | None = None,
|
|
401
|
+
logger: LoggerLike | None = None,
|
|
402
|
+
) -> str: ...
|
|
403
|
+
@overload
|
|
404
|
+
def curl(
|
|
405
|
+
url: str,
|
|
406
|
+
/,
|
|
407
|
+
*,
|
|
408
|
+
fail: bool = True,
|
|
409
|
+
location: bool = True,
|
|
410
|
+
output: PathLike | None = None,
|
|
411
|
+
show_error: bool = True,
|
|
412
|
+
silent: bool = True,
|
|
413
|
+
sudo: bool = False,
|
|
414
|
+
print: bool = False,
|
|
415
|
+
print_stdout: bool = False,
|
|
416
|
+
print_stderr: bool = False,
|
|
417
|
+
return_: Literal[False] = False,
|
|
418
|
+
return_stdout: Literal[False] = False,
|
|
419
|
+
return_stderr: Literal[False] = False,
|
|
420
|
+
retry: Retry | None = None,
|
|
421
|
+
retry_skip: Callable[[int, str, str], bool] | None = None,
|
|
422
|
+
logger: LoggerLike | None = None,
|
|
423
|
+
) -> None: ...
|
|
424
|
+
@overload
|
|
425
|
+
def curl(
|
|
426
|
+
url: str,
|
|
427
|
+
/,
|
|
428
|
+
*,
|
|
429
|
+
fail: bool = True,
|
|
430
|
+
location: bool = True,
|
|
431
|
+
output: PathLike | None = None,
|
|
432
|
+
show_error: bool = True,
|
|
433
|
+
silent: bool = True,
|
|
434
|
+
sudo: bool = False,
|
|
435
|
+
print: bool = False,
|
|
436
|
+
print_stdout: bool = False,
|
|
437
|
+
print_stderr: bool = False,
|
|
438
|
+
return_: bool = False,
|
|
439
|
+
return_stdout: bool = False,
|
|
440
|
+
return_stderr: bool = False,
|
|
441
|
+
retry: Retry | None = None,
|
|
442
|
+
retry_skip: Callable[[int, str, str], bool] | None = None,
|
|
443
|
+
logger: LoggerLike | None = None,
|
|
444
|
+
) -> str | None: ...
|
|
445
|
+
def curl(
|
|
446
|
+
url: str,
|
|
447
|
+
/,
|
|
448
|
+
*,
|
|
449
|
+
fail: bool = True,
|
|
450
|
+
location: bool = True,
|
|
451
|
+
output: PathLike | None = None,
|
|
452
|
+
show_error: bool = True,
|
|
453
|
+
silent: bool = True,
|
|
454
|
+
sudo: bool = False,
|
|
455
|
+
print: bool = False, # noqa: A002
|
|
456
|
+
print_stdout: bool = False,
|
|
457
|
+
print_stderr: bool = False,
|
|
458
|
+
return_: bool = False,
|
|
459
|
+
return_stdout: bool = False,
|
|
460
|
+
return_stderr: bool = False,
|
|
461
|
+
retry: Retry | None = None,
|
|
462
|
+
retry_skip: Callable[[int, str, str], bool] | None = None,
|
|
463
|
+
logger: LoggerLike | None = None,
|
|
464
|
+
) -> str | None:
|
|
465
|
+
"""Transfer a URL."""
|
|
466
|
+
args = maybe_sudo_cmd( # skipif-ci
|
|
467
|
+
*curl_cmd(
|
|
468
|
+
url,
|
|
469
|
+
fail=fail,
|
|
470
|
+
location=location,
|
|
471
|
+
output=output,
|
|
472
|
+
show_error=show_error,
|
|
473
|
+
silent=silent,
|
|
474
|
+
),
|
|
475
|
+
sudo=sudo,
|
|
476
|
+
)
|
|
477
|
+
return run( # skipif-ci
|
|
478
|
+
*args,
|
|
479
|
+
print=print,
|
|
480
|
+
print_stdout=print_stdout,
|
|
481
|
+
print_stderr=print_stderr,
|
|
482
|
+
return_=return_,
|
|
483
|
+
return_stdout=return_stdout,
|
|
484
|
+
return_stderr=return_stderr,
|
|
485
|
+
retry=retry,
|
|
486
|
+
retry_skip=retry_skip,
|
|
487
|
+
logger=logger,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def curl_cmd(
|
|
492
|
+
url: str,
|
|
493
|
+
/,
|
|
494
|
+
*,
|
|
495
|
+
fail: bool = True,
|
|
496
|
+
location: bool = True,
|
|
497
|
+
output: PathLike | None = None,
|
|
498
|
+
show_error: bool = True,
|
|
499
|
+
silent: bool = True,
|
|
500
|
+
) -> list[str]:
|
|
501
|
+
"""Command to use 'curl' to transfer a URL."""
|
|
502
|
+
args: list[str] = ["curl"]
|
|
503
|
+
if fail:
|
|
504
|
+
args.append("--fail")
|
|
505
|
+
if location:
|
|
506
|
+
args.append("--location")
|
|
507
|
+
if output is not None:
|
|
508
|
+
args.extend(["--create-dirs", "--output", str(output)])
|
|
509
|
+
if show_error:
|
|
510
|
+
args.append("--show-error")
|
|
511
|
+
if silent:
|
|
512
|
+
args.append("--silent")
|
|
513
|
+
return [*args, url]
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
##
|
|
517
|
+
|
|
518
|
+
|
|
219
519
|
def echo_cmd(text: str, /) -> list[str]:
|
|
220
520
|
"""Command to use 'echo' to write arguments to the standard output."""
|
|
221
521
|
return ["echo", text]
|
|
@@ -284,6 +584,53 @@ def git_clone_cmd(url: str, path: PathLike, /) -> list[str]:
|
|
|
284
584
|
##
|
|
285
585
|
|
|
286
586
|
|
|
587
|
+
def install(
|
|
588
|
+
path: PathLike,
|
|
589
|
+
/,
|
|
590
|
+
*,
|
|
591
|
+
directory: bool = False,
|
|
592
|
+
mode: PermissionsLike | None = None,
|
|
593
|
+
owner: str | int | None = None,
|
|
594
|
+
group: str | int | None = None,
|
|
595
|
+
sudo: bool = False,
|
|
596
|
+
) -> None:
|
|
597
|
+
"""Install a binary."""
|
|
598
|
+
args = maybe_sudo_cmd(
|
|
599
|
+
*install_cmd(path, directory=directory, mode=mode, owner=owner, group=group),
|
|
600
|
+
sudo=sudo,
|
|
601
|
+
)
|
|
602
|
+
run(*args)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def install_cmd(
|
|
606
|
+
path: PathLike,
|
|
607
|
+
/,
|
|
608
|
+
*,
|
|
609
|
+
directory: bool = False,
|
|
610
|
+
mode: PermissionsLike | None = None,
|
|
611
|
+
owner: str | int | None = None,
|
|
612
|
+
group: str | int | None = None,
|
|
613
|
+
) -> list[str]:
|
|
614
|
+
"""Command to use 'install' to install a binary."""
|
|
615
|
+
args: list[str] = ["install"]
|
|
616
|
+
if directory:
|
|
617
|
+
args.append("-d")
|
|
618
|
+
if mode is not None:
|
|
619
|
+
args.extend(["-m", str(Permissions.new(mode))])
|
|
620
|
+
if owner is not None:
|
|
621
|
+
args.extend(["-o", str(owner)])
|
|
622
|
+
if group is not None:
|
|
623
|
+
args.extend(["-g", str(group)])
|
|
624
|
+
if directory:
|
|
625
|
+
args.append(str(path))
|
|
626
|
+
else:
|
|
627
|
+
args.extend(["/dev/null", str(path)])
|
|
628
|
+
return args
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
##
|
|
632
|
+
|
|
633
|
+
|
|
287
634
|
def maybe_parent(path: PathLike, /, *, parent: bool = False) -> Path:
|
|
288
635
|
"""Get the parent of a path, if required."""
|
|
289
636
|
path = Path(path)
|
|
@@ -326,26 +673,24 @@ def mv(
|
|
|
326
673
|
mkdir(dest, sudo=sudo, parent=True)
|
|
327
674
|
if sudo: # pragma: no cover
|
|
328
675
|
run(*sudo_cmd(*cp_cmd(src, dest)))
|
|
676
|
+
if perms is not None:
|
|
677
|
+
chmod(dest, perms, sudo=True)
|
|
678
|
+
if (owner is not None) or (group is not None):
|
|
679
|
+
chown(dest, sudo=True, user=owner, group=group)
|
|
329
680
|
else:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
raise MvFileError(src=src, dest=dest)
|
|
335
|
-
if perms is not None:
|
|
336
|
-
chmod(dest, perms, sudo=sudo)
|
|
337
|
-
if (owner is not None) or (group is not None):
|
|
338
|
-
chown(dest, sudo=sudo, user=owner, group=group)
|
|
681
|
+
try:
|
|
682
|
+
move(src, dest, overwrite=True, perms=perms, owner=owner, group=group)
|
|
683
|
+
except _CopyOrMoveSourceNotFoundError as error:
|
|
684
|
+
raise MvFileError(src=error.src) from None
|
|
339
685
|
|
|
340
686
|
|
|
341
687
|
@dataclass(kw_only=True, slots=True)
|
|
342
688
|
class MvFileError(Exception):
|
|
343
689
|
src: Path
|
|
344
|
-
dest: Path
|
|
345
690
|
|
|
346
691
|
@override
|
|
347
692
|
def __str__(self) -> str:
|
|
348
|
-
return f"
|
|
693
|
+
return f"Source {repr_str(self.src)} does not exist"
|
|
349
694
|
|
|
350
695
|
|
|
351
696
|
def mv_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
|
|
@@ -356,6 +701,20 @@ def mv_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
|
|
|
356
701
|
##
|
|
357
702
|
|
|
358
703
|
|
|
704
|
+
def replace_text(
|
|
705
|
+
path: PathLike, /, *replacements: tuple[str, str], sudo: bool = False
|
|
706
|
+
) -> None:
|
|
707
|
+
"""Replace the text in a file."""
|
|
708
|
+
path = Path(path)
|
|
709
|
+
text = cat(path, sudo=sudo)
|
|
710
|
+
for old, new in replacements:
|
|
711
|
+
text = text.replace(old, new)
|
|
712
|
+
tee(path, text, sudo=sudo)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
##
|
|
716
|
+
|
|
717
|
+
|
|
359
718
|
def ripgrep(*args: str, path: PathLike = PWD) -> str | None:
|
|
360
719
|
"""Search for lines."""
|
|
361
720
|
try: # skipif-ci
|
|
@@ -379,11 +738,17 @@ def rm(path: PathLike, /, *paths: PathLike, sudo: bool = False) -> None:
|
|
|
379
738
|
if sudo: # pragma: no cover
|
|
380
739
|
run(*sudo_cmd(*rm_cmd(path, *paths)))
|
|
381
740
|
else:
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
741
|
+
all_paths = list(map(Path, [path, *paths]))
|
|
742
|
+
for p in all_paths:
|
|
743
|
+
match file_or_dir(p):
|
|
744
|
+
case "file":
|
|
745
|
+
p.unlink(missing_ok=True)
|
|
746
|
+
case "dir":
|
|
747
|
+
rmtree(p, ignore_errors=True)
|
|
748
|
+
case None:
|
|
749
|
+
...
|
|
750
|
+
case never:
|
|
751
|
+
assert_never(never)
|
|
387
752
|
|
|
388
753
|
|
|
389
754
|
def rm_cmd(path: PathLike, /, *paths: PathLike) -> list[str]:
|
|
@@ -503,10 +868,10 @@ def rsync_cmd(
|
|
|
503
868
|
args.extend(["--rsync-path", join(sudo_cmd("rsync"))])
|
|
504
869
|
srcs = list(always_iterable(src_or_srcs)) # do not Path()
|
|
505
870
|
if len(srcs) == 0:
|
|
506
|
-
raise
|
|
871
|
+
raise _RsyncCmdNoSourcesError(user=user, hostname=hostname, dest=dest)
|
|
507
872
|
missing = [s for s in srcs if not Path(s).exists()]
|
|
508
873
|
if len(missing) >= 1:
|
|
509
|
-
raise
|
|
874
|
+
raise _RsyncCmdSourcesNotFoundError(
|
|
510
875
|
sources=missing, user=user, hostname=hostname, dest=dest
|
|
511
876
|
)
|
|
512
877
|
return [*args, *map(str, srcs), f"{user}@{hostname}:{dest}"]
|
|
@@ -518,20 +883,16 @@ class RsyncCmdError(Exception):
|
|
|
518
883
|
hostname: str
|
|
519
884
|
dest: PathLike
|
|
520
885
|
|
|
521
|
-
@override
|
|
522
|
-
def __str__(self) -> str:
|
|
523
|
-
return f"No sources selected to send to {self.user}@{self.hostname}:{self.dest}"
|
|
524
|
-
|
|
525
886
|
|
|
526
887
|
@dataclass(kw_only=True, slots=True)
|
|
527
|
-
class
|
|
888
|
+
class _RsyncCmdNoSourcesError(RsyncCmdError):
|
|
528
889
|
@override
|
|
529
890
|
def __str__(self) -> str:
|
|
530
891
|
return f"No sources selected to send to {self.user}@{self.hostname}:{self.dest}"
|
|
531
892
|
|
|
532
893
|
|
|
533
894
|
@dataclass(kw_only=True, slots=True)
|
|
534
|
-
class
|
|
895
|
+
class _RsyncCmdSourcesNotFoundError(RsyncCmdError):
|
|
535
896
|
sources: list[PathLike]
|
|
536
897
|
|
|
537
898
|
@override
|
|
@@ -549,7 +910,8 @@ def rsync_many(
|
|
|
549
910
|
/,
|
|
550
911
|
*items: tuple[PathLike, PathLike]
|
|
551
912
|
| tuple[Literal["sudo"], PathLike, PathLike]
|
|
552
|
-
| tuple[PathLike, PathLike, PermissionsLike]
|
|
913
|
+
| tuple[PathLike, PathLike, PermissionsLike]
|
|
914
|
+
| tuple[Literal["sudo"], PathLike, PathLike, PermissionsLike],
|
|
553
915
|
retry: Retry | None = None,
|
|
554
916
|
logger: LoggerLike | None = None,
|
|
555
917
|
keep: bool = False,
|
|
@@ -570,7 +932,11 @@ def rsync_many(
|
|
|
570
932
|
match item:
|
|
571
933
|
case Path() | str() as src, Path() | str() as dest:
|
|
572
934
|
cmds.extend(_rsync_many_prepare(src, dest, temp_src, temp_dest))
|
|
573
|
-
case
|
|
935
|
+
case ( # pragma: no cover
|
|
936
|
+
"sudo",
|
|
937
|
+
Path() | str() as src,
|
|
938
|
+
Path() | str() as dest,
|
|
939
|
+
):
|
|
574
940
|
cmds.extend(
|
|
575
941
|
_rsync_many_prepare(src, dest, temp_src, temp_dest, sudo=True)
|
|
576
942
|
)
|
|
@@ -582,6 +948,17 @@ def rsync_many(
|
|
|
582
948
|
cmds.extend(
|
|
583
949
|
_rsync_many_prepare(src, dest, temp_src, temp_dest, perms=perms)
|
|
584
950
|
)
|
|
951
|
+
case ( # pragma: no cover
|
|
952
|
+
"sudo",
|
|
953
|
+
Path() | str() as src,
|
|
954
|
+
Path() | str() as dest,
|
|
955
|
+
Permissions() | int() | str() as perms,
|
|
956
|
+
):
|
|
957
|
+
cmds.extend(
|
|
958
|
+
_rsync_many_prepare(
|
|
959
|
+
src, dest, temp_src, temp_dest, sudo=True, perms=perms
|
|
960
|
+
)
|
|
961
|
+
)
|
|
585
962
|
case never:
|
|
586
963
|
assert_never(never)
|
|
587
964
|
rsync(
|
|
@@ -625,13 +1002,19 @@ def _rsync_many_prepare(
|
|
|
625
1002
|
case Path():
|
|
626
1003
|
cp(src, temp_src / name)
|
|
627
1004
|
case str():
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
1005
|
+
try:
|
|
1006
|
+
exists = Path(src).exists()
|
|
1007
|
+
except OSError:
|
|
631
1008
|
tee(temp_src / name, src)
|
|
1009
|
+
else:
|
|
1010
|
+
if exists:
|
|
1011
|
+
cp(src, temp_src / name)
|
|
1012
|
+
else:
|
|
1013
|
+
tee(temp_src / name, src)
|
|
632
1014
|
case never:
|
|
633
1015
|
assert_never(never)
|
|
634
1016
|
cmds: list[list[str]] = [
|
|
1017
|
+
maybe_sudo_cmd(*rm_cmd(dest), sudo=sudo),
|
|
635
1018
|
maybe_sudo_cmd(*mkdir_cmd(dest, parent=True), sudo=sudo),
|
|
636
1019
|
maybe_sudo_cmd(*cp_cmd(temp_dest / name, dest), sudo=sudo),
|
|
637
1020
|
]
|
|
@@ -832,11 +1215,11 @@ def run(
|
|
|
832
1215
|
and (retry_skip is not None)
|
|
833
1216
|
and retry_skip(return_code, stdout_text, stderr_text)
|
|
834
1217
|
):
|
|
835
|
-
attempts =
|
|
1218
|
+
attempts = duration = None
|
|
836
1219
|
else:
|
|
837
|
-
attempts,
|
|
1220
|
+
attempts, duration = retry
|
|
838
1221
|
if logger is not None:
|
|
839
|
-
msg =
|
|
1222
|
+
msg = normalize_multi_line_str(f"""
|
|
840
1223
|
'run' failed with:
|
|
841
1224
|
- cmd = {cmd}
|
|
842
1225
|
- cmds_or_args = {cmds_or_args}
|
|
@@ -854,18 +1237,18 @@ def run(
|
|
|
854
1237
|
{stderr_text}-------------------------------------------------------------------------------
|
|
855
1238
|
""")
|
|
856
1239
|
if (attempts is not None) and (attempts >= 1):
|
|
857
|
-
if
|
|
1240
|
+
if duration is None:
|
|
858
1241
|
msg = f"{msg}\n\nRetrying {attempts} more time(s)..."
|
|
859
1242
|
else:
|
|
860
|
-
msg = f"{msg}\n\nRetrying {attempts} more time(s) after {
|
|
1243
|
+
msg = f"{msg}\n\nRetrying {attempts} more time(s) after {in_timedelta(duration)}..."
|
|
861
1244
|
to_logger(logger).error(msg)
|
|
862
1245
|
error = CalledProcessError(
|
|
863
1246
|
return_code, args, output=stdout_text, stderr=stderr_text
|
|
864
1247
|
)
|
|
865
1248
|
if (attempts is None) or (attempts <= 0):
|
|
866
1249
|
raise error
|
|
867
|
-
if
|
|
868
|
-
sleep(
|
|
1250
|
+
if duration is not None:
|
|
1251
|
+
sleep(duration)
|
|
869
1252
|
return run(
|
|
870
1253
|
cmd,
|
|
871
1254
|
*cmds_or_args,
|
|
@@ -881,14 +1264,14 @@ def run(
|
|
|
881
1264
|
return_=return_,
|
|
882
1265
|
return_stdout=return_stdout,
|
|
883
1266
|
return_stderr=return_stderr,
|
|
884
|
-
retry=(attempts - 1,
|
|
1267
|
+
retry=(attempts - 1, duration),
|
|
885
1268
|
logger=logger,
|
|
886
1269
|
)
|
|
887
1270
|
case never:
|
|
888
1271
|
assert_never(never)
|
|
889
1272
|
|
|
890
1273
|
|
|
891
|
-
@
|
|
1274
|
+
@enhanced_context_manager
|
|
892
1275
|
def _run_yield_write(input_: IO[str], /, *outputs: IO[str]) -> Iterator[None]:
|
|
893
1276
|
thread = Thread(target=_run_daemon_target, args=(input_, *outputs), daemon=True)
|
|
894
1277
|
thread.start()
|
|
@@ -1070,7 +1453,7 @@ def ssh(
|
|
|
1070
1453
|
retry_skip=_ssh_retry_skip,
|
|
1071
1454
|
logger=logger,
|
|
1072
1455
|
)
|
|
1073
|
-
except CalledProcessError as error: #
|
|
1456
|
+
except CalledProcessError as error: # pragma: no cover
|
|
1074
1457
|
if not _ssh_is_strict_checking_error(error.stderr):
|
|
1075
1458
|
raise
|
|
1076
1459
|
ssh_keyscan(hostname, port=port)
|
|
@@ -1101,8 +1484,9 @@ def _ssh_retry_skip(return_code: int, stdout: str, stderr: str, /) -> bool:
|
|
|
1101
1484
|
|
|
1102
1485
|
def _ssh_is_strict_checking_error(text: str, /) -> bool:
|
|
1103
1486
|
match = search(
|
|
1104
|
-
"No ED25519 host key is known for .* and you have requested strict checking",
|
|
1487
|
+
"(Host key for .* has changed|No ED25519 host key is known for .*) and you have requested strict checking",
|
|
1105
1488
|
text,
|
|
1489
|
+
flags=MULTILINE,
|
|
1106
1490
|
)
|
|
1107
1491
|
return match is not None
|
|
1108
1492
|
|
|
@@ -1159,15 +1543,15 @@ def ssh_await(
|
|
|
1159
1543
|
/,
|
|
1160
1544
|
*,
|
|
1161
1545
|
logger: LoggerLike | None = None,
|
|
1162
|
-
|
|
1546
|
+
duration: Duration = SECOND,
|
|
1163
1547
|
) -> None:
|
|
1164
1548
|
while True: # skipif-ci
|
|
1165
1549
|
if logger is not None:
|
|
1166
1550
|
to_logger(logger).info("Waiting for '%s'...", hostname)
|
|
1167
1551
|
try:
|
|
1168
1552
|
ssh(user, hostname, "true")
|
|
1169
|
-
except CalledProcessError:
|
|
1170
|
-
sleep(
|
|
1553
|
+
except CalledProcessError: # pragma: no cover
|
|
1554
|
+
sleep(duration)
|
|
1171
1555
|
else:
|
|
1172
1556
|
if logger is not None:
|
|
1173
1557
|
to_logger(logger).info("'%s' is up", hostname)
|
|
@@ -1178,13 +1562,19 @@ def ssh_await(
|
|
|
1178
1562
|
|
|
1179
1563
|
|
|
1180
1564
|
def ssh_keyscan(
|
|
1181
|
-
hostname: str,
|
|
1565
|
+
hostname: str,
|
|
1566
|
+
/,
|
|
1567
|
+
*,
|
|
1568
|
+
path: PathLike = KNOWN_HOSTS,
|
|
1569
|
+
retry: Retry | None = None,
|
|
1570
|
+
port: int | None = None,
|
|
1182
1571
|
) -> None:
|
|
1183
1572
|
"""Add a known host."""
|
|
1184
|
-
ssh_keygen_remove(hostname, path=path) # skipif-ci
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1573
|
+
ssh_keygen_remove(hostname, path=path, retry=retry) # skipif-ci
|
|
1574
|
+
result = run( # skipif-ci
|
|
1575
|
+
*ssh_keyscan_cmd(hostname, port=port), return_=True, retry=retry
|
|
1576
|
+
)
|
|
1577
|
+
tee(path, result, append=True) # skipif-ci
|
|
1188
1578
|
|
|
1189
1579
|
|
|
1190
1580
|
def ssh_keyscan_cmd(hostname: str, /, *, port: int | None = None) -> list[str]:
|
|
@@ -1198,11 +1588,13 @@ def ssh_keyscan_cmd(hostname: str, /, *, port: int | None = None) -> list[str]:
|
|
|
1198
1588
|
##
|
|
1199
1589
|
|
|
1200
1590
|
|
|
1201
|
-
def ssh_keygen_remove(
|
|
1591
|
+
def ssh_keygen_remove(
|
|
1592
|
+
hostname: str, /, *, path: PathLike = KNOWN_HOSTS, retry: Retry | None = None
|
|
1593
|
+
) -> None:
|
|
1202
1594
|
"""Remove a known host."""
|
|
1203
1595
|
path = Path(path)
|
|
1204
1596
|
if path.exists():
|
|
1205
|
-
run(*ssh_keygen_remove_cmd(hostname, path=path))
|
|
1597
|
+
run(*ssh_keygen_remove_cmd(hostname, path=path), retry=retry)
|
|
1206
1598
|
|
|
1207
1599
|
|
|
1208
1600
|
def ssh_keygen_remove_cmd(
|
|
@@ -1259,7 +1651,8 @@ def symlink_cmd(target: PathLike, link: PathLike, /) -> list[str]:
|
|
|
1259
1651
|
def tee(
|
|
1260
1652
|
path: PathLike, text: str, /, *, sudo: bool = False, append: bool = False
|
|
1261
1653
|
) -> None:
|
|
1262
|
-
"""
|
|
1654
|
+
"""Duplicate standard input."""
|
|
1655
|
+
mkdir(path, sudo=sudo, parent=True)
|
|
1263
1656
|
if sudo: # pragma: no cover
|
|
1264
1657
|
run(*sudo_cmd(*tee_cmd(path, append=append)), input=text)
|
|
1265
1658
|
else:
|
|
@@ -1341,27 +1734,200 @@ def useradd_cmd(
|
|
|
1341
1734
|
##
|
|
1342
1735
|
|
|
1343
1736
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1737
|
+
def uv_index_cmd(*, index: MaybeSequenceStr | None = None) -> list[str]:
|
|
1738
|
+
"""Generate the `--index` command if necessary."""
|
|
1739
|
+
return [] if index is None else ["--index", ",".join(always_iterable(index))]
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
##
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
type _UvPipListFormat = Literal["columns", "freeze", "json"]
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
@dataclass(order=True, unsafe_hash=True, kw_only=True, slots=True)
|
|
1749
|
+
class _UvPipListOutput:
|
|
1750
|
+
name: str
|
|
1751
|
+
version: Version2 | Version3
|
|
1752
|
+
editable_project_location: Path | None = None
|
|
1753
|
+
latest_version: Version2 | Version3 | None = None
|
|
1754
|
+
latest_filetype: str | None = None
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
def uv_pip_list(
|
|
1758
|
+
*,
|
|
1759
|
+
editable: bool = False,
|
|
1760
|
+
exclude_editable: bool = False,
|
|
1761
|
+
index: MaybeSequenceStr | None = None,
|
|
1762
|
+
native_tls: bool = False,
|
|
1763
|
+
) -> list[_UvPipListOutput]:
|
|
1764
|
+
"""List packages installed in an environment."""
|
|
1765
|
+
cmds_base, cmds_outdated = [
|
|
1766
|
+
uv_pip_list_cmd(
|
|
1767
|
+
editable=editable,
|
|
1768
|
+
exclude_editable=exclude_editable,
|
|
1769
|
+
format_="json",
|
|
1770
|
+
outdated=outdated,
|
|
1771
|
+
index=index,
|
|
1772
|
+
native_tls=native_tls,
|
|
1773
|
+
)
|
|
1774
|
+
for outdated in [False, True]
|
|
1775
|
+
]
|
|
1776
|
+
text_base, text_outdated = [
|
|
1777
|
+
run(*cmds, return_stdout=True) for cmds in [cmds_base, cmds_outdated]
|
|
1778
|
+
]
|
|
1779
|
+
dicts_base, dicts_outdated = list(
|
|
1780
|
+
map(_uv_pip_list_loads, [text_base, text_outdated])
|
|
1781
|
+
)
|
|
1782
|
+
return [_uv_pip_list_assemble_output(d, dicts_outdated) for d in dicts_base]
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
def _uv_pip_list_loads(text: str, /) -> list[StrMapping]:
|
|
1786
|
+
try:
|
|
1787
|
+
return json.loads(text)
|
|
1788
|
+
except JSONDecodeError:
|
|
1789
|
+
raise _UvPipListJsonError(text=text) from None
|
|
1790
|
+
|
|
1791
|
+
|
|
1792
|
+
def _uv_pip_list_assemble_output(
|
|
1793
|
+
dict_: StrMapping, outdated: Iterable[StrMapping], /
|
|
1794
|
+
) -> _UvPipListOutput:
|
|
1795
|
+
name = dict_["name"]
|
|
1796
|
+
try:
|
|
1797
|
+
version = parse_version_2_or_3(dict_["version"])
|
|
1798
|
+
except ParseVersion2Or3Error:
|
|
1799
|
+
raise _UvPipListBaseVersionError(data=dict_) from None
|
|
1800
|
+
try:
|
|
1801
|
+
location = Path(dict_["editable_project_location"])
|
|
1802
|
+
except KeyError:
|
|
1803
|
+
location = None
|
|
1804
|
+
try:
|
|
1805
|
+
outdated_i = one(d for d in outdated if d["name"] == name)
|
|
1806
|
+
except OneEmptyError:
|
|
1807
|
+
latest_version = latest_filetype = None
|
|
1808
|
+
else:
|
|
1809
|
+
try:
|
|
1810
|
+
latest_version = parse_version_2_or_3(outdated_i["latest_version"])
|
|
1811
|
+
except ParseVersion2Or3Error:
|
|
1812
|
+
raise _UvPipListOutdatedVersionError(data=outdated_i) from None
|
|
1813
|
+
latest_filetype = outdated_i["latest_filetype"]
|
|
1814
|
+
return _UvPipListOutput(
|
|
1815
|
+
name=dict_["name"],
|
|
1816
|
+
version=version,
|
|
1817
|
+
editable_project_location=location,
|
|
1818
|
+
latest_version=latest_version,
|
|
1819
|
+
latest_filetype=latest_filetype,
|
|
1820
|
+
)
|
|
1821
|
+
|
|
1822
|
+
|
|
1823
|
+
@dataclass(kw_only=True, slots=True)
|
|
1824
|
+
class UvPipListError(Exception): ...
|
|
1825
|
+
|
|
1826
|
+
|
|
1827
|
+
@dataclass(kw_only=True, slots=True)
|
|
1828
|
+
class _UvPipListJsonError(UvPipListError):
|
|
1829
|
+
text: str
|
|
1830
|
+
|
|
1831
|
+
@override
|
|
1832
|
+
def __str__(self) -> str:
|
|
1833
|
+
return f"Unable to parse JSON; got {self.text!r}"
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
@dataclass(kw_only=True, slots=True)
|
|
1837
|
+
class _UvPipListBaseVersionError(UvPipListError):
|
|
1838
|
+
data: StrMapping
|
|
1839
|
+
|
|
1840
|
+
@override
|
|
1841
|
+
def __str__(self) -> str:
|
|
1842
|
+
return f"Unable to parse version; got {self.data}"
|
|
1843
|
+
|
|
1844
|
+
|
|
1845
|
+
@dataclass(kw_only=True, slots=True)
|
|
1846
|
+
class _UvPipListOutdatedVersionError(UvPipListError):
|
|
1847
|
+
data: StrMapping
|
|
1848
|
+
|
|
1849
|
+
@override
|
|
1850
|
+
def __str__(self) -> str:
|
|
1851
|
+
return f"Unable to parse version; got {self.data}"
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
def uv_pip_list_cmd(
|
|
1855
|
+
*,
|
|
1856
|
+
editable: bool = False,
|
|
1857
|
+
exclude_editable: bool = False,
|
|
1858
|
+
format_: _UvPipListFormat = "columns",
|
|
1859
|
+
outdated: bool = False,
|
|
1860
|
+
index: MaybeSequenceStr | None = None,
|
|
1861
|
+
native_tls: bool = False,
|
|
1862
|
+
) -> list[str]:
|
|
1863
|
+
"""Command to use 'uv' to list packages installed in an environment."""
|
|
1864
|
+
args: list[str] = ["uv", "pip", "list"]
|
|
1865
|
+
if editable:
|
|
1866
|
+
args.append("--editable")
|
|
1867
|
+
if exclude_editable:
|
|
1868
|
+
args.append("--exclude-editable")
|
|
1869
|
+
args.extend(["--format", format_])
|
|
1870
|
+
if outdated:
|
|
1871
|
+
args.append("--outdated")
|
|
1872
|
+
return [
|
|
1873
|
+
*args,
|
|
1874
|
+
"--strict",
|
|
1875
|
+
*uv_index_cmd(index=index),
|
|
1876
|
+
MANAGED_PYTHON,
|
|
1877
|
+
*uv_native_tls_cmd(native_tls=native_tls),
|
|
1878
|
+
]
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
##
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
def uv_native_tls_cmd(*, native_tls: bool = False) -> list[str]:
|
|
1885
|
+
"""Generate the `--native-tls` command if necessary."""
|
|
1886
|
+
return ["--native-tls"] if native_tls else []
|
|
1887
|
+
|
|
1888
|
+
|
|
1889
|
+
##
|
|
1890
|
+
|
|
1891
|
+
|
|
1892
|
+
@overload
|
|
1893
|
+
def uv_run(
|
|
1894
|
+
module: str,
|
|
1895
|
+
/,
|
|
1896
|
+
*args: str,
|
|
1897
|
+
extra: MaybeSequenceStr | None = None,
|
|
1898
|
+
all_extras: bool = False,
|
|
1899
|
+
group: MaybeSequenceStr | None = None,
|
|
1900
|
+
all_groups: bool = False,
|
|
1901
|
+
only_dev: bool = False,
|
|
1902
|
+
with_: MaybeSequenceStr | None = None,
|
|
1903
|
+
index: MaybeSequenceStr | None = None,
|
|
1904
|
+
native_tls: bool = False,
|
|
1905
|
+
env: StrStrMapping | None = None,
|
|
1906
|
+
cwd: PathLike | None = None,
|
|
1907
|
+
print: bool = False,
|
|
1908
|
+
print_stdout: bool = False,
|
|
1909
|
+
print_stderr: bool = False,
|
|
1910
|
+
return_: Literal[True],
|
|
1911
|
+
return_stdout: bool = False,
|
|
1912
|
+
return_stderr: bool = False,
|
|
1913
|
+
retry: Retry | None = None,
|
|
1914
|
+
logger: LoggerLike | None = None,
|
|
1915
|
+
) -> str: ...
|
|
1916
|
+
@overload
|
|
1917
|
+
def uv_run(
|
|
1918
|
+
module: str,
|
|
1919
|
+
/,
|
|
1920
|
+
*args: str,
|
|
1921
|
+
extra: MaybeSequenceStr | None = None,
|
|
1922
|
+
all_extras: bool = False,
|
|
1923
|
+
group: MaybeSequenceStr | None = None,
|
|
1924
|
+
all_groups: bool = False,
|
|
1925
|
+
only_dev: bool = False,
|
|
1926
|
+
with_: MaybeSequenceStr | None = None,
|
|
1927
|
+
index: MaybeSequenceStr | None = None,
|
|
1928
|
+
native_tls: bool = False,
|
|
1929
|
+
env: StrStrMapping | None = None,
|
|
1930
|
+
cwd: PathLike | None = None,
|
|
1365
1931
|
print: bool = False,
|
|
1366
1932
|
print_stdout: bool = False,
|
|
1367
1933
|
print_stderr: bool = False,
|
|
@@ -1376,6 +1942,15 @@ def uv_run(
|
|
|
1376
1942
|
module: str,
|
|
1377
1943
|
/,
|
|
1378
1944
|
*args: str,
|
|
1945
|
+
extra: MaybeSequenceStr | None = None,
|
|
1946
|
+
all_extras: bool = False,
|
|
1947
|
+
group: MaybeSequenceStr | None = None,
|
|
1948
|
+
all_groups: bool = False,
|
|
1949
|
+
only_dev: bool = False,
|
|
1950
|
+
with_: MaybeSequenceStr | None = None,
|
|
1951
|
+
index: MaybeSequenceStr | None = None,
|
|
1952
|
+
native_tls: bool = False,
|
|
1953
|
+
env: StrStrMapping | None = None,
|
|
1379
1954
|
cwd: PathLike | None = None,
|
|
1380
1955
|
print: bool = False,
|
|
1381
1956
|
print_stdout: bool = False,
|
|
@@ -1391,6 +1966,15 @@ def uv_run(
|
|
|
1391
1966
|
module: str,
|
|
1392
1967
|
/,
|
|
1393
1968
|
*args: str,
|
|
1969
|
+
extra: MaybeSequenceStr | None = None,
|
|
1970
|
+
all_extras: bool = False,
|
|
1971
|
+
group: MaybeSequenceStr | None = None,
|
|
1972
|
+
all_groups: bool = False,
|
|
1973
|
+
only_dev: bool = False,
|
|
1974
|
+
with_: MaybeSequenceStr | None = None,
|
|
1975
|
+
index: MaybeSequenceStr | None = None,
|
|
1976
|
+
native_tls: bool = False,
|
|
1977
|
+
env: StrStrMapping | None = None,
|
|
1394
1978
|
cwd: PathLike | None = None,
|
|
1395
1979
|
print: bool = False,
|
|
1396
1980
|
print_stdout: bool = False,
|
|
@@ -1406,6 +1990,15 @@ def uv_run(
|
|
|
1406
1990
|
module: str,
|
|
1407
1991
|
/,
|
|
1408
1992
|
*args: str,
|
|
1993
|
+
extra: MaybeSequenceStr | None = None,
|
|
1994
|
+
all_extras: bool = False,
|
|
1995
|
+
group: MaybeSequenceStr | None = None,
|
|
1996
|
+
all_groups: bool = False,
|
|
1997
|
+
only_dev: bool = False,
|
|
1998
|
+
with_: MaybeSequenceStr | None = None,
|
|
1999
|
+
index: MaybeSequenceStr | None = None,
|
|
2000
|
+
native_tls: bool = False,
|
|
2001
|
+
env: StrStrMapping | None = None,
|
|
1409
2002
|
cwd: PathLike | None = None,
|
|
1410
2003
|
print: bool = False,
|
|
1411
2004
|
print_stdout: bool = False,
|
|
@@ -1420,7 +2013,16 @@ def uv_run(
|
|
|
1420
2013
|
module: str,
|
|
1421
2014
|
/,
|
|
1422
2015
|
*args: str,
|
|
2016
|
+
extra: MaybeSequenceStr | None = None,
|
|
2017
|
+
all_extras: bool = False,
|
|
2018
|
+
group: MaybeSequenceStr | None = None,
|
|
2019
|
+
all_groups: bool = False,
|
|
2020
|
+
only_dev: bool = False,
|
|
2021
|
+
with_: MaybeSequenceStr | None = None,
|
|
2022
|
+
index: MaybeSequenceStr | None = None,
|
|
2023
|
+
native_tls: bool = False,
|
|
1423
2024
|
cwd: PathLike | None = None,
|
|
2025
|
+
env: StrStrMapping | None = None,
|
|
1424
2026
|
print: bool = False, # noqa: A002
|
|
1425
2027
|
print_stdout: bool = False,
|
|
1426
2028
|
print_stderr: bool = False,
|
|
@@ -1432,8 +2034,20 @@ def uv_run(
|
|
|
1432
2034
|
) -> str | None:
|
|
1433
2035
|
"""Run a command or script."""
|
|
1434
2036
|
return run( # pragma: no cover
|
|
1435
|
-
*uv_run_cmd(
|
|
2037
|
+
*uv_run_cmd(
|
|
2038
|
+
module,
|
|
2039
|
+
*args,
|
|
2040
|
+
extra=extra,
|
|
2041
|
+
all_extras=all_extras,
|
|
2042
|
+
group=group,
|
|
2043
|
+
all_groups=all_groups,
|
|
2044
|
+
only_dev=only_dev,
|
|
2045
|
+
with_=with_,
|
|
2046
|
+
index=index,
|
|
2047
|
+
native_tls=native_tls,
|
|
2048
|
+
),
|
|
1436
2049
|
cwd=cwd,
|
|
2050
|
+
env=env,
|
|
1437
2051
|
print=print,
|
|
1438
2052
|
print_stdout=print_stdout,
|
|
1439
2053
|
print_stderr=print_stderr,
|
|
@@ -1445,15 +2059,46 @@ def uv_run(
|
|
|
1445
2059
|
)
|
|
1446
2060
|
|
|
1447
2061
|
|
|
1448
|
-
def uv_run_cmd(
|
|
2062
|
+
def uv_run_cmd(
|
|
2063
|
+
module: str,
|
|
2064
|
+
/,
|
|
2065
|
+
*args: str,
|
|
2066
|
+
extra: MaybeSequenceStr | None = None,
|
|
2067
|
+
all_extras: bool = False,
|
|
2068
|
+
group: MaybeSequenceStr | None = None,
|
|
2069
|
+
all_groups: bool = False,
|
|
2070
|
+
only_dev: bool = False,
|
|
2071
|
+
with_: MaybeSequenceStr | None = None,
|
|
2072
|
+
index: MaybeSequenceStr | None = None,
|
|
2073
|
+
native_tls: bool = False,
|
|
2074
|
+
) -> list[str]:
|
|
1449
2075
|
"""Command to use 'uv' to run a command or script."""
|
|
2076
|
+
parts: list[str] = ["uv", "run"]
|
|
2077
|
+
if extra is not None:
|
|
2078
|
+
for extra_i in always_iterable(extra):
|
|
2079
|
+
parts.extend(["--extra", extra_i])
|
|
2080
|
+
if all_extras:
|
|
2081
|
+
parts.append("--all-extras")
|
|
2082
|
+
if not only_dev:
|
|
2083
|
+
parts.append("--no-dev")
|
|
2084
|
+
if group is not None:
|
|
2085
|
+
for group_i in always_iterable(group):
|
|
2086
|
+
parts.extend(["--group", group_i])
|
|
2087
|
+
if all_groups:
|
|
2088
|
+
parts.append("--all-groups")
|
|
2089
|
+
if only_dev:
|
|
2090
|
+
parts.append("--only-dev")
|
|
1450
2091
|
return [
|
|
1451
|
-
|
|
1452
|
-
"
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
2092
|
+
*parts,
|
|
2093
|
+
"--exact",
|
|
2094
|
+
*uv_with_cmd(with_=with_),
|
|
2095
|
+
ISOLATED,
|
|
2096
|
+
*uv_index_cmd(index=index),
|
|
2097
|
+
*RESOLUTION_HIGHEST,
|
|
2098
|
+
*PRERELEASE_DISALLOW,
|
|
2099
|
+
"--reinstall",
|
|
2100
|
+
*uv_native_tls_cmd(native_tls=native_tls),
|
|
2101
|
+
MANAGED_PYTHON,
|
|
1457
2102
|
"python",
|
|
1458
2103
|
"-m",
|
|
1459
2104
|
module,
|
|
@@ -1464,7 +2109,356 @@ def uv_run_cmd(module: str, /, *args: str) -> list[str]:
|
|
|
1464
2109
|
##
|
|
1465
2110
|
|
|
1466
2111
|
|
|
1467
|
-
@
|
|
2112
|
+
@overload
|
|
2113
|
+
def uv_tool_install(
|
|
2114
|
+
package: str,
|
|
2115
|
+
/,
|
|
2116
|
+
*,
|
|
2117
|
+
with_: MaybeSequenceStr | None = None,
|
|
2118
|
+
index: MaybeSequenceStr | None = None,
|
|
2119
|
+
native_tls: bool = False,
|
|
2120
|
+
cwd: PathLike | None = None,
|
|
2121
|
+
env: StrStrMapping | None = None,
|
|
2122
|
+
print: bool = False,
|
|
2123
|
+
print_stdout: bool = False,
|
|
2124
|
+
print_stderr: bool = False,
|
|
2125
|
+
return_: Literal[True],
|
|
2126
|
+
return_stdout: bool = False,
|
|
2127
|
+
return_stderr: bool = False,
|
|
2128
|
+
retry: Retry | None = None,
|
|
2129
|
+
logger: LoggerLike | None = None,
|
|
2130
|
+
) -> str: ...
|
|
2131
|
+
@overload
|
|
2132
|
+
def uv_tool_install(
|
|
2133
|
+
package: str,
|
|
2134
|
+
/,
|
|
2135
|
+
*,
|
|
2136
|
+
with_: MaybeSequenceStr | None = None,
|
|
2137
|
+
index: MaybeSequenceStr | None = None,
|
|
2138
|
+
native_tls: bool = False,
|
|
2139
|
+
cwd: PathLike | None = None,
|
|
2140
|
+
env: StrStrMapping | None = None,
|
|
2141
|
+
print: bool = False,
|
|
2142
|
+
print_stdout: bool = False,
|
|
2143
|
+
print_stderr: bool = False,
|
|
2144
|
+
return_: bool = False,
|
|
2145
|
+
return_stdout: Literal[True],
|
|
2146
|
+
return_stderr: bool = False,
|
|
2147
|
+
retry: Retry | None = None,
|
|
2148
|
+
logger: LoggerLike | None = None,
|
|
2149
|
+
) -> str: ...
|
|
2150
|
+
@overload
|
|
2151
|
+
def uv_tool_install(
|
|
2152
|
+
package: str,
|
|
2153
|
+
/,
|
|
2154
|
+
*,
|
|
2155
|
+
with_: MaybeSequenceStr | None = None,
|
|
2156
|
+
index: MaybeSequenceStr | None = None,
|
|
2157
|
+
native_tls: bool = False,
|
|
2158
|
+
cwd: PathLike | None = None,
|
|
2159
|
+
env: StrStrMapping | None = None,
|
|
2160
|
+
print: bool = False,
|
|
2161
|
+
print_stdout: bool = False,
|
|
2162
|
+
print_stderr: bool = False,
|
|
2163
|
+
return_: bool = False,
|
|
2164
|
+
return_stdout: bool = False,
|
|
2165
|
+
return_stderr: Literal[True],
|
|
2166
|
+
retry: Retry | None = None,
|
|
2167
|
+
logger: LoggerLike | None = None,
|
|
2168
|
+
) -> str: ...
|
|
2169
|
+
@overload
|
|
2170
|
+
def uv_tool_install(
|
|
2171
|
+
package: str,
|
|
2172
|
+
/,
|
|
2173
|
+
*,
|
|
2174
|
+
with_: MaybeSequenceStr | None = None,
|
|
2175
|
+
index: MaybeSequenceStr | None = None,
|
|
2176
|
+
native_tls: bool = False,
|
|
2177
|
+
cwd: PathLike | None = None,
|
|
2178
|
+
env: StrStrMapping | None = None,
|
|
2179
|
+
print: bool = False,
|
|
2180
|
+
print_stdout: bool = False,
|
|
2181
|
+
print_stderr: bool = False,
|
|
2182
|
+
return_: Literal[False] = False,
|
|
2183
|
+
return_stdout: Literal[False] = False,
|
|
2184
|
+
return_stderr: Literal[False] = False,
|
|
2185
|
+
retry: Retry | None = None,
|
|
2186
|
+
logger: LoggerLike | None = None,
|
|
2187
|
+
) -> None: ...
|
|
2188
|
+
@overload
|
|
2189
|
+
def uv_tool_install(
|
|
2190
|
+
package: str,
|
|
2191
|
+
/,
|
|
2192
|
+
*,
|
|
2193
|
+
with_: MaybeSequenceStr | None = None,
|
|
2194
|
+
index: MaybeSequenceStr | None = None,
|
|
2195
|
+
native_tls: bool = False,
|
|
2196
|
+
cwd: PathLike | None = None,
|
|
2197
|
+
env: StrStrMapping | None = None,
|
|
2198
|
+
print: bool = False,
|
|
2199
|
+
print_stdout: bool = False,
|
|
2200
|
+
print_stderr: bool = False,
|
|
2201
|
+
return_: bool = False,
|
|
2202
|
+
return_stdout: bool = False,
|
|
2203
|
+
return_stderr: bool = False,
|
|
2204
|
+
retry: Retry | None = None,
|
|
2205
|
+
logger: LoggerLike | None = None,
|
|
2206
|
+
) -> str | None: ...
|
|
2207
|
+
def uv_tool_install(
|
|
2208
|
+
package: str,
|
|
2209
|
+
/,
|
|
2210
|
+
*,
|
|
2211
|
+
with_: MaybeSequenceStr | None = None,
|
|
2212
|
+
index: MaybeSequenceStr | None = None,
|
|
2213
|
+
native_tls: bool = False,
|
|
2214
|
+
cwd: PathLike | None = None,
|
|
2215
|
+
env: StrStrMapping | None = None,
|
|
2216
|
+
print: bool = False, # noqa: A002
|
|
2217
|
+
print_stdout: bool = False,
|
|
2218
|
+
print_stderr: bool = False,
|
|
2219
|
+
return_: bool = False,
|
|
2220
|
+
return_stdout: bool = False,
|
|
2221
|
+
return_stderr: bool = False,
|
|
2222
|
+
retry: Retry | None = None,
|
|
2223
|
+
logger: LoggerLike | None = None,
|
|
2224
|
+
) -> str | None:
|
|
2225
|
+
"""Install commands provided by a Python package."""
|
|
2226
|
+
return run( # pragma: no cover
|
|
2227
|
+
*uv_tool_install_cmd(package, with_=with_, index=index, native_tls=native_tls),
|
|
2228
|
+
cwd=cwd,
|
|
2229
|
+
env=env,
|
|
2230
|
+
print=print,
|
|
2231
|
+
print_stdout=print_stdout,
|
|
2232
|
+
print_stderr=print_stderr,
|
|
2233
|
+
return_=return_,
|
|
2234
|
+
return_stdout=return_stdout,
|
|
2235
|
+
return_stderr=return_stderr,
|
|
2236
|
+
retry=retry,
|
|
2237
|
+
logger=logger,
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
|
|
2241
|
+
def uv_tool_install_cmd(
|
|
2242
|
+
package: str,
|
|
2243
|
+
/,
|
|
2244
|
+
*,
|
|
2245
|
+
with_: MaybeSequenceStr | None = None,
|
|
2246
|
+
index: MaybeSequenceStr | None = None,
|
|
2247
|
+
native_tls: bool = False,
|
|
2248
|
+
) -> list[str]:
|
|
2249
|
+
"""Command to use 'uv' to install commands provided by a Python package."""
|
|
2250
|
+
return [
|
|
2251
|
+
"uv",
|
|
2252
|
+
"tool",
|
|
2253
|
+
"install",
|
|
2254
|
+
*uv_with_cmd(with_=with_),
|
|
2255
|
+
*uv_index_cmd(index=index),
|
|
2256
|
+
*RESOLUTION_HIGHEST,
|
|
2257
|
+
*PRERELEASE_DISALLOW,
|
|
2258
|
+
"--reinstall",
|
|
2259
|
+
MANAGED_PYTHON,
|
|
2260
|
+
*uv_native_tls_cmd(native_tls=native_tls),
|
|
2261
|
+
package,
|
|
2262
|
+
]
|
|
2263
|
+
|
|
2264
|
+
|
|
2265
|
+
##
|
|
2266
|
+
|
|
2267
|
+
|
|
2268
|
+
@overload
|
|
2269
|
+
def uv_tool_run(
|
|
2270
|
+
command: str,
|
|
2271
|
+
/,
|
|
2272
|
+
*args: str,
|
|
2273
|
+
from_: str | None = None,
|
|
2274
|
+
latest: bool = True,
|
|
2275
|
+
with_: MaybeSequenceStr | None = None,
|
|
2276
|
+
index: MaybeSequenceStr | None = None,
|
|
2277
|
+
native_tls: bool = False,
|
|
2278
|
+
cwd: PathLike | None = None,
|
|
2279
|
+
env: StrStrMapping | None = None,
|
|
2280
|
+
print: bool = False,
|
|
2281
|
+
print_stdout: bool = False,
|
|
2282
|
+
print_stderr: bool = False,
|
|
2283
|
+
return_: Literal[True],
|
|
2284
|
+
return_stdout: bool = False,
|
|
2285
|
+
return_stderr: bool = False,
|
|
2286
|
+
retry: Retry | None = None,
|
|
2287
|
+
logger: LoggerLike | None = None,
|
|
2288
|
+
) -> str: ...
|
|
2289
|
+
@overload
|
|
2290
|
+
def uv_tool_run(
|
|
2291
|
+
command: str,
|
|
2292
|
+
/,
|
|
2293
|
+
*args: str,
|
|
2294
|
+
from_: str | None = None,
|
|
2295
|
+
latest: bool = True,
|
|
2296
|
+
with_: MaybeSequenceStr | None = None,
|
|
2297
|
+
index: MaybeSequenceStr | None = None,
|
|
2298
|
+
native_tls: bool = False,
|
|
2299
|
+
cwd: PathLike | None = None,
|
|
2300
|
+
env: StrStrMapping | None = None,
|
|
2301
|
+
print: bool = False,
|
|
2302
|
+
print_stdout: bool = False,
|
|
2303
|
+
print_stderr: bool = False,
|
|
2304
|
+
return_: bool = False,
|
|
2305
|
+
return_stdout: Literal[True],
|
|
2306
|
+
return_stderr: bool = False,
|
|
2307
|
+
retry: Retry | None = None,
|
|
2308
|
+
logger: LoggerLike | None = None,
|
|
2309
|
+
) -> str: ...
|
|
2310
|
+
@overload
|
|
2311
|
+
def uv_tool_run(
|
|
2312
|
+
command: str,
|
|
2313
|
+
/,
|
|
2314
|
+
*args: str,
|
|
2315
|
+
from_: str | None = None,
|
|
2316
|
+
latest: bool = True,
|
|
2317
|
+
with_: MaybeSequenceStr | None = None,
|
|
2318
|
+
index: MaybeSequenceStr | None = None,
|
|
2319
|
+
native_tls: bool = False,
|
|
2320
|
+
cwd: PathLike | None = None,
|
|
2321
|
+
env: StrStrMapping | None = None,
|
|
2322
|
+
print: bool = False,
|
|
2323
|
+
print_stdout: bool = False,
|
|
2324
|
+
print_stderr: bool = False,
|
|
2325
|
+
return_: bool = False,
|
|
2326
|
+
return_stdout: bool = False,
|
|
2327
|
+
return_stderr: Literal[True],
|
|
2328
|
+
retry: Retry | None = None,
|
|
2329
|
+
logger: LoggerLike | None = None,
|
|
2330
|
+
) -> str: ...
|
|
2331
|
+
@overload
|
|
2332
|
+
def uv_tool_run(
|
|
2333
|
+
command: str,
|
|
2334
|
+
/,
|
|
2335
|
+
*,
|
|
2336
|
+
from_: str | None = None,
|
|
2337
|
+
latest: bool = True,
|
|
2338
|
+
with_: MaybeSequenceStr | None = None,
|
|
2339
|
+
index: MaybeSequenceStr | None = None,
|
|
2340
|
+
native_tls: bool = False,
|
|
2341
|
+
cwd: PathLike | None = None,
|
|
2342
|
+
env: StrStrMapping | None = None,
|
|
2343
|
+
print: bool = False,
|
|
2344
|
+
print_stdout: bool = False,
|
|
2345
|
+
print_stderr: bool = False,
|
|
2346
|
+
return_: Literal[False] = False,
|
|
2347
|
+
return_stdout: Literal[False] = False,
|
|
2348
|
+
return_stderr: Literal[False] = False,
|
|
2349
|
+
retry: Retry | None = None,
|
|
2350
|
+
logger: LoggerLike | None = None,
|
|
2351
|
+
) -> None: ...
|
|
2352
|
+
@overload
|
|
2353
|
+
def uv_tool_run(
|
|
2354
|
+
command: str,
|
|
2355
|
+
/,
|
|
2356
|
+
*args: str,
|
|
2357
|
+
from_: str | None = None,
|
|
2358
|
+
latest: bool = True,
|
|
2359
|
+
with_: MaybeSequenceStr | None = None,
|
|
2360
|
+
index: MaybeSequenceStr | None = None,
|
|
2361
|
+
native_tls: bool = False,
|
|
2362
|
+
cwd: PathLike | None = None,
|
|
2363
|
+
env: StrStrMapping | None = None,
|
|
2364
|
+
print: bool = False,
|
|
2365
|
+
print_stdout: bool = False,
|
|
2366
|
+
print_stderr: bool = False,
|
|
2367
|
+
return_: bool = False,
|
|
2368
|
+
return_stdout: bool = False,
|
|
2369
|
+
return_stderr: bool = False,
|
|
2370
|
+
retry: Retry | None = None,
|
|
2371
|
+
logger: LoggerLike | None = None,
|
|
2372
|
+
) -> str | None: ...
|
|
2373
|
+
def uv_tool_run(
|
|
2374
|
+
command: str,
|
|
2375
|
+
/,
|
|
2376
|
+
*args: str,
|
|
2377
|
+
from_: str | None = None,
|
|
2378
|
+
latest: bool = True,
|
|
2379
|
+
with_: MaybeSequenceStr | None = None,
|
|
2380
|
+
index: MaybeSequenceStr | None = None,
|
|
2381
|
+
native_tls: bool = False,
|
|
2382
|
+
cwd: PathLike | None = None,
|
|
2383
|
+
env: StrStrMapping | None = None,
|
|
2384
|
+
print: bool = False, # noqa: A002
|
|
2385
|
+
print_stdout: bool = False,
|
|
2386
|
+
print_stderr: bool = False,
|
|
2387
|
+
return_: bool = False,
|
|
2388
|
+
return_stdout: bool = False,
|
|
2389
|
+
return_stderr: bool = False,
|
|
2390
|
+
retry: Retry | None = None,
|
|
2391
|
+
logger: LoggerLike | None = None,
|
|
2392
|
+
) -> str | None:
|
|
2393
|
+
"""Run a command provided by a Python package."""
|
|
2394
|
+
return run( # pragma: no cover
|
|
2395
|
+
*uv_tool_run_cmd(
|
|
2396
|
+
command,
|
|
2397
|
+
*args,
|
|
2398
|
+
from_=from_,
|
|
2399
|
+
latest=latest,
|
|
2400
|
+
with_=with_,
|
|
2401
|
+
index=index,
|
|
2402
|
+
native_tls=native_tls,
|
|
2403
|
+
),
|
|
2404
|
+
cwd=cwd,
|
|
2405
|
+
env=env,
|
|
2406
|
+
print=print,
|
|
2407
|
+
print_stdout=print_stdout,
|
|
2408
|
+
print_stderr=print_stderr,
|
|
2409
|
+
return_=return_,
|
|
2410
|
+
return_stdout=return_stdout,
|
|
2411
|
+
return_stderr=return_stderr,
|
|
2412
|
+
retry=retry,
|
|
2413
|
+
logger=logger,
|
|
2414
|
+
)
|
|
2415
|
+
|
|
2416
|
+
|
|
2417
|
+
def uv_tool_run_cmd(
|
|
2418
|
+
command: str,
|
|
2419
|
+
/,
|
|
2420
|
+
*args: str,
|
|
2421
|
+
from_: str | None = None,
|
|
2422
|
+
latest: bool = True,
|
|
2423
|
+
with_: MaybeSequenceStr | None = None,
|
|
2424
|
+
index: MaybeSequenceStr | None = None,
|
|
2425
|
+
native_tls: bool = False,
|
|
2426
|
+
) -> list[str]:
|
|
2427
|
+
"""Command to use 'uv' to run a command provided by a Python package."""
|
|
2428
|
+
parts: list[str] = ["uv", "tool", "run"]
|
|
2429
|
+
if from_ is not None:
|
|
2430
|
+
from_use = f"{from_}@latest" if latest else from_
|
|
2431
|
+
parts.extend(["--from", from_use])
|
|
2432
|
+
return [
|
|
2433
|
+
*parts,
|
|
2434
|
+
*uv_with_cmd(with_=with_),
|
|
2435
|
+
ISOLATED,
|
|
2436
|
+
*uv_index_cmd(index=index),
|
|
2437
|
+
*RESOLUTION_HIGHEST,
|
|
2438
|
+
*PRERELEASE_DISALLOW,
|
|
2439
|
+
MANAGED_PYTHON,
|
|
2440
|
+
*uv_native_tls_cmd(native_tls=native_tls),
|
|
2441
|
+
command,
|
|
2442
|
+
*args,
|
|
2443
|
+
]
|
|
2444
|
+
|
|
2445
|
+
|
|
2446
|
+
##
|
|
2447
|
+
|
|
2448
|
+
|
|
2449
|
+
def uv_with_cmd(*, with_: MaybeSequenceStr | None = None) -> list[str]:
|
|
2450
|
+
"""Generate the `--with` commands if necessary."""
|
|
2451
|
+
return (
|
|
2452
|
+
[]
|
|
2453
|
+
if with_ is None
|
|
2454
|
+
else list(chain.from_iterable(["--with", w] for w in always_iterable(with_)))
|
|
2455
|
+
)
|
|
2456
|
+
|
|
2457
|
+
|
|
2458
|
+
##
|
|
2459
|
+
|
|
2460
|
+
|
|
2461
|
+
@enhanced_context_manager
|
|
1468
2462
|
def yield_git_repo(url: str, /, *, branch: str | None = None) -> Iterator[Path]:
|
|
1469
2463
|
"""Yield a temporary git repository."""
|
|
1470
2464
|
with TemporaryDirectory() as temp_dir:
|
|
@@ -1475,7 +2469,7 @@ def yield_git_repo(url: str, /, *, branch: str | None = None) -> Iterator[Path]:
|
|
|
1475
2469
|
##
|
|
1476
2470
|
|
|
1477
2471
|
|
|
1478
|
-
@
|
|
2472
|
+
@enhanced_context_manager
|
|
1479
2473
|
def yield_ssh_temp_dir(
|
|
1480
2474
|
user: str,
|
|
1481
2475
|
hostname: str,
|
|
@@ -1505,25 +2499,38 @@ __all__ = [
|
|
|
1505
2499
|
"BASH_LS",
|
|
1506
2500
|
"CHPASSWD",
|
|
1507
2501
|
"GIT_BRANCH_SHOW_CURRENT",
|
|
2502
|
+
"ISOLATED",
|
|
2503
|
+
"MANAGED_PYTHON",
|
|
1508
2504
|
"MKTEMP_DIR_CMD",
|
|
2505
|
+
"PRERELEASE_DISALLOW",
|
|
2506
|
+
"RESOLUTION_HIGHEST",
|
|
1509
2507
|
"RESTART_SSHD",
|
|
1510
2508
|
"UPDATE_CA_CERTIFICATES",
|
|
1511
2509
|
"ChownCmdError",
|
|
1512
2510
|
"CpError",
|
|
1513
2511
|
"MvFileError",
|
|
1514
2512
|
"RsyncCmdError",
|
|
1515
|
-
"
|
|
1516
|
-
"
|
|
2513
|
+
"UvPipListError",
|
|
2514
|
+
"append_text",
|
|
1517
2515
|
"apt_install",
|
|
1518
2516
|
"apt_install_cmd",
|
|
2517
|
+
"apt_remove",
|
|
2518
|
+
"apt_remove_cmd",
|
|
2519
|
+
"apt_update",
|
|
2520
|
+
"cat",
|
|
1519
2521
|
"cd_cmd",
|
|
2522
|
+
"chattr",
|
|
2523
|
+
"chattr_cmd",
|
|
1520
2524
|
"chmod",
|
|
1521
2525
|
"chmod_cmd",
|
|
1522
2526
|
"chown",
|
|
1523
2527
|
"chown_cmd",
|
|
1524
2528
|
"chpasswd",
|
|
2529
|
+
"copy_text",
|
|
1525
2530
|
"cp",
|
|
1526
2531
|
"cp_cmd",
|
|
2532
|
+
"curl",
|
|
2533
|
+
"curl_cmd",
|
|
1527
2534
|
"echo_cmd",
|
|
1528
2535
|
"env_cmds",
|
|
1529
2536
|
"expand_path",
|
|
@@ -1532,12 +2539,15 @@ __all__ = [
|
|
|
1532
2539
|
"git_checkout_cmd",
|
|
1533
2540
|
"git_clone",
|
|
1534
2541
|
"git_clone_cmd",
|
|
2542
|
+
"install",
|
|
2543
|
+
"install_cmd",
|
|
1535
2544
|
"maybe_parent",
|
|
1536
2545
|
"maybe_sudo_cmd",
|
|
1537
2546
|
"mkdir",
|
|
1538
2547
|
"mkdir_cmd",
|
|
1539
2548
|
"mv",
|
|
1540
2549
|
"mv_cmd",
|
|
2550
|
+
"replace_text",
|
|
1541
2551
|
"ripgrep",
|
|
1542
2552
|
"ripgrep_cmd",
|
|
1543
2553
|
"rm",
|
|
@@ -1565,8 +2575,16 @@ __all__ = [
|
|
|
1565
2575
|
"update_ca_certificates",
|
|
1566
2576
|
"useradd",
|
|
1567
2577
|
"useradd_cmd",
|
|
2578
|
+
"uv_native_tls_cmd",
|
|
2579
|
+
"uv_pip_list",
|
|
2580
|
+
"uv_pip_list_cmd",
|
|
1568
2581
|
"uv_run",
|
|
1569
2582
|
"uv_run_cmd",
|
|
2583
|
+
"uv_tool_install",
|
|
2584
|
+
"uv_tool_install_cmd",
|
|
2585
|
+
"uv_tool_run",
|
|
2586
|
+
"uv_tool_run_cmd",
|
|
2587
|
+
"uv_with_cmd",
|
|
1570
2588
|
"yield_git_repo",
|
|
1571
2589
|
"yield_ssh_temp_dir",
|
|
1572
2590
|
]
|