dycw-utilities 0.148.5__py3-none-any.whl → 0.174.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dycw-utilities might be problematic. Click here for more details.

Files changed (83) hide show
  1. dycw_utilities-0.174.12.dist-info/METADATA +41 -0
  2. dycw_utilities-0.174.12.dist-info/RECORD +104 -0
  3. dycw_utilities-0.174.12.dist-info/WHEEL +4 -0
  4. {dycw_utilities-0.148.5.dist-info → dycw_utilities-0.174.12.dist-info}/entry_points.txt +3 -0
  5. utilities/__init__.py +1 -1
  6. utilities/{eventkit.py → aeventkit.py} +12 -11
  7. utilities/altair.py +7 -6
  8. utilities/asyncio.py +113 -64
  9. utilities/atomicwrites.py +1 -1
  10. utilities/atools.py +64 -4
  11. utilities/cachetools.py +9 -6
  12. utilities/click.py +145 -49
  13. utilities/concurrent.py +1 -1
  14. utilities/contextlib.py +4 -2
  15. utilities/contextvars.py +20 -1
  16. utilities/cryptography.py +3 -3
  17. utilities/dataclasses.py +15 -28
  18. utilities/docker.py +292 -0
  19. utilities/enum.py +2 -2
  20. utilities/errors.py +1 -1
  21. utilities/fastapi.py +8 -3
  22. utilities/fpdf2.py +2 -2
  23. utilities/functions.py +20 -297
  24. utilities/git.py +19 -0
  25. utilities/grp.py +28 -0
  26. utilities/hypothesis.py +360 -78
  27. utilities/inflect.py +1 -1
  28. utilities/iterables.py +12 -58
  29. utilities/jinja2.py +148 -0
  30. utilities/json.py +1 -1
  31. utilities/libcst.py +7 -7
  32. utilities/logging.py +74 -85
  33. utilities/math.py +8 -4
  34. utilities/more_itertools.py +4 -6
  35. utilities/operator.py +1 -1
  36. utilities/orjson.py +86 -34
  37. utilities/os.py +49 -2
  38. utilities/parse.py +2 -2
  39. utilities/pathlib.py +66 -34
  40. utilities/permissions.py +297 -0
  41. utilities/platform.py +5 -5
  42. utilities/polars.py +932 -420
  43. utilities/polars_ols.py +1 -1
  44. utilities/postgres.py +296 -174
  45. utilities/pottery.py +8 -73
  46. utilities/pqdm.py +3 -3
  47. utilities/pwd.py +28 -0
  48. utilities/pydantic.py +11 -0
  49. utilities/pydantic_settings.py +240 -0
  50. utilities/pydantic_settings_sops.py +76 -0
  51. utilities/pyinstrument.py +5 -5
  52. utilities/pytest.py +155 -46
  53. utilities/pytest_plugins/pytest_randomly.py +1 -1
  54. utilities/pytest_plugins/pytest_regressions.py +7 -3
  55. utilities/pytest_regressions.py +2 -3
  56. utilities/random.py +11 -6
  57. utilities/re.py +1 -1
  58. utilities/redis.py +101 -64
  59. utilities/sentinel.py +10 -0
  60. utilities/shelve.py +4 -1
  61. utilities/shutil.py +25 -0
  62. utilities/slack_sdk.py +8 -3
  63. utilities/sqlalchemy.py +422 -352
  64. utilities/sqlalchemy_polars.py +28 -52
  65. utilities/string.py +1 -1
  66. utilities/subprocess.py +864 -0
  67. utilities/tempfile.py +62 -4
  68. utilities/testbook.py +50 -0
  69. utilities/text.py +165 -42
  70. utilities/timer.py +2 -2
  71. utilities/traceback.py +46 -36
  72. utilities/types.py +62 -23
  73. utilities/typing.py +479 -19
  74. utilities/uuid.py +42 -5
  75. utilities/version.py +27 -26
  76. utilities/whenever.py +661 -151
  77. utilities/zoneinfo.py +80 -22
  78. dycw_utilities-0.148.5.dist-info/METADATA +0 -41
  79. dycw_utilities-0.148.5.dist-info/RECORD +0 -95
  80. dycw_utilities-0.148.5.dist-info/WHEEL +0 -4
  81. dycw_utilities-0.148.5.dist-info/licenses/LICENSE +0 -21
  82. utilities/period.py +0 -237
  83. utilities/typed_settings.py +0 -144
@@ -0,0 +1,864 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from contextlib import contextmanager
5
+ from dataclasses import dataclass
6
+ from io import StringIO
7
+ from pathlib import Path
8
+ from shlex import join
9
+ from string import Template
10
+ from subprocess import PIPE, CalledProcessError, Popen
11
+ from threading import Thread
12
+ from time import sleep
13
+ from typing import IO, TYPE_CHECKING, Literal, assert_never, overload, override
14
+
15
+ from utilities.errors import ImpossibleCaseError
16
+ from utilities.iterables import always_iterable
17
+ from utilities.logging import to_logger
18
+ from utilities.text import strip_and_dedent
19
+ from utilities.whenever import to_seconds
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Iterator
23
+
24
+ from utilities.types import (
25
+ LoggerLike,
26
+ MaybeIterable,
27
+ PathLike,
28
+ Retry,
29
+ StrMapping,
30
+ StrStrMapping,
31
+ )
32
+
33
+
34
+ _HOST_KEY_ALGORITHMS = ["ssh-ed25519"]
35
+ APT_UPDATE = ["apt", "update", "-y"]
36
+ BASH_LC = ["bash", "-lc"]
37
+ BASH_LS = ["bash", "-ls"]
38
+ MKTEMP_DIR_CMD = ["mktemp", "-d"]
39
+ RESTART_SSHD = ["systemctl", "restart", "sshd"]
40
+ UPDATE_CA_CERTIFICATES: str = "update-ca-certificates"
41
+
42
+
43
+ ##
44
+
45
+
46
+ def apt_install_cmd(package: str, /) -> list[str]:
47
+ return ["apt", "install", "-y", package]
48
+
49
+
50
+ ##
51
+
52
+
53
+ def cat_cmd(path: PathLike, /) -> list[str]:
54
+ return ["cat", str(path)]
55
+
56
+
57
+ ##
58
+
59
+
60
+ def cd_cmd(path: PathLike, /) -> list[str]:
61
+ return ["cd", str(path)]
62
+
63
+
64
+ ##
65
+
66
+
67
+ def chmod_cmd(path: PathLike, mode: str, /) -> list[str]:
68
+ return ["chmod", mode, str(path)]
69
+
70
+
71
+ ##
72
+
73
+
74
+ def chown_cmd(
75
+ path: PathLike, /, *, user: str | None = None, group: str | None = None
76
+ ) -> list[str]:
77
+ match user, group:
78
+ case None, None:
79
+ raise ChownCmdError
80
+ case str(), None:
81
+ ownership = "user"
82
+ case None, str():
83
+ ownership = f":{group}"
84
+ case str(), str():
85
+ ownership = f"{user}:{group}"
86
+ case never:
87
+ assert_never(never)
88
+ return ["chown", ownership, str(path)]
89
+
90
+
91
+ @dataclass(kw_only=True, slots=True)
92
+ class ChownCmdError(Exception):
93
+ @override
94
+ def __str__(self) -> str:
95
+ return "At least one of 'user' and/or 'group' must be given; got None"
96
+
97
+
98
+ ##
99
+
100
+
101
+ def cp_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
102
+ return ["cp", "-r", str(src), str(dest)]
103
+
104
+
105
+ ##
106
+
107
+
108
+ def echo_cmd(text: str, /) -> list[str]:
109
+ return ["echo", text]
110
+
111
+
112
+ ##
113
+
114
+
115
+ def expand_path(
116
+ path: PathLike, /, *, subs: StrMapping | None = None, sudo: bool = False
117
+ ) -> Path:
118
+ if subs is not None:
119
+ path = Template(str(path)).substitute(**subs)
120
+ if sudo: # pragma: no cover
121
+ return Path(run(*sudo_cmd(*echo_cmd(str(path))), return_=True))
122
+ return Path(path).expanduser()
123
+
124
+
125
+ ##
126
+
127
+
128
+ def git_clone_cmd(url: str, path: PathLike, /) -> list[str]:
129
+ return ["git", "clone", "--recurse-submodules", url, str(path)]
130
+
131
+
132
+ ##
133
+
134
+
135
+ def git_hard_reset_cmd(*, branch: str | None = None) -> list[str]:
136
+ branch_use = "master" if branch is None else branch
137
+ return ["git", "hard-reset", branch_use]
138
+
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
+
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
+ def mkdir(path: PathLike, /, *, sudo: bool = False, parent: bool = False) -> None:
160
+ if sudo: # pragma: no cover
161
+ run(*sudo_cmd(*mkdir_cmd(path, parent=parent)))
162
+ else:
163
+ maybe_parent(path, parent=parent).mkdir(parents=True, exist_ok=True)
164
+
165
+
166
+ ##
167
+
168
+
169
+ def mkdir_cmd(path: PathLike, /, *, parent: bool = False) -> list[str]:
170
+ return ["mkdir", "-p", str(maybe_parent(path, parent=parent))]
171
+
172
+
173
+ ##
174
+
175
+
176
+ def mv_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
177
+ return ["mv", str(src), str(dest)]
178
+
179
+
180
+ ##
181
+
182
+
183
+ def rm_cmd(path: PathLike, /) -> list[str]:
184
+ return ["rm", "-rf", str(path)]
185
+
186
+
187
+ ##
188
+
189
+
190
+ def rsync(
191
+ src_or_srcs: MaybeIterable[PathLike],
192
+ user: str,
193
+ hostname: str,
194
+ dest: PathLike,
195
+ /,
196
+ *,
197
+ sudo: bool = False,
198
+ batch_mode: bool = True,
199
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
200
+ strict_host_key_checking: bool = True,
201
+ print: bool = False, # noqa: A002
202
+ retry: Retry | None = None,
203
+ logger: LoggerLike | None = None,
204
+ chown_user: str | None = None,
205
+ chown_group: str | None = None,
206
+ exclude: MaybeIterable[str] | None = None,
207
+ chmod: str | None = None,
208
+ ) -> None:
209
+ mkdir_args = maybe_sudo_cmd(*mkdir_cmd(dest, parent=True), sudo=sudo) # skipif-ci
210
+ ssh( # skipif-ci
211
+ user,
212
+ hostname,
213
+ *mkdir_args,
214
+ batch_mode=batch_mode,
215
+ host_key_algorithms=host_key_algorithms,
216
+ strict_host_key_checking=strict_host_key_checking,
217
+ print=print,
218
+ retry=retry,
219
+ logger=logger,
220
+ )
221
+ is_dir = any(Path(s).is_dir() for s in always_iterable(src_or_srcs)) # skipif-ci
222
+ rsync_args = rsync_cmd( # skipif-ci
223
+ src_or_srcs,
224
+ user,
225
+ hostname,
226
+ dest,
227
+ archive=is_dir,
228
+ chown_user=chown_user,
229
+ chown_group=chown_group,
230
+ exclude=exclude,
231
+ batch_mode=batch_mode,
232
+ host_key_algorithms=host_key_algorithms,
233
+ strict_host_key_checking=strict_host_key_checking,
234
+ sudo=sudo,
235
+ parent=is_dir,
236
+ )
237
+ run(*rsync_args, print=print, retry=retry, logger=logger) # skipif-ci
238
+ if chmod is not None: # skipif-ci
239
+ chmod_args = maybe_sudo_cmd(*chmod_cmd(dest, chmod), sudo=sudo)
240
+ ssh(
241
+ user,
242
+ hostname,
243
+ *chmod_args,
244
+ batch_mode=batch_mode,
245
+ host_key_algorithms=host_key_algorithms,
246
+ strict_host_key_checking=strict_host_key_checking,
247
+ print=print,
248
+ retry=retry,
249
+ logger=logger,
250
+ )
251
+
252
+
253
+ ##
254
+
255
+
256
+ def rsync_cmd(
257
+ src_or_srcs: MaybeIterable[PathLike],
258
+ user: str,
259
+ hostname: str,
260
+ dest: PathLike,
261
+ /,
262
+ *,
263
+ archive: bool = False,
264
+ chown_user: str | None = None,
265
+ chown_group: str | None = None,
266
+ exclude: MaybeIterable[str] | None = None,
267
+ batch_mode: bool = True,
268
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
269
+ strict_host_key_checking: bool = True,
270
+ sudo: bool = False,
271
+ parent: bool = False,
272
+ ) -> list[str]:
273
+ args: list[str] = ["rsync"]
274
+ if archive:
275
+ args.append("--archive")
276
+ args.append("--checksum")
277
+ match chown_user, chown_group:
278
+ case None, None:
279
+ ...
280
+ case str(), None:
281
+ args.extend(["--chown", chown_user])
282
+ case None, str():
283
+ args.extend(["--chown", f":{chown_group}"])
284
+ case str(), str():
285
+ args.extend(["--chown", f"{chown_user}:{chown_group}"])
286
+ case never:
287
+ assert_never(never)
288
+ args.append("--compress")
289
+ if exclude is not None:
290
+ for exclude_i in always_iterable(exclude):
291
+ args.extend(["--exclude", exclude_i])
292
+ rsh_args: list[str] = ssh_opts_cmd(
293
+ batch_mode=batch_mode,
294
+ host_key_algorithms=host_key_algorithms,
295
+ strict_host_key_checking=strict_host_key_checking,
296
+ )
297
+ args.extend(["--rsh", join(rsh_args)])
298
+ if sudo:
299
+ args.extend(["--rsync-path", join(sudo_cmd("rsync"))])
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
+ ##
309
+
310
+
311
+ @overload
312
+ def run(
313
+ cmd: str,
314
+ /,
315
+ *cmds_or_args: str,
316
+ user: str | int | None = None,
317
+ executable: str | None = None,
318
+ shell: bool = False,
319
+ cwd: PathLike | None = None,
320
+ env: StrStrMapping | None = None,
321
+ input: str | None = None,
322
+ print: bool = False,
323
+ print_stdout: bool = False,
324
+ print_stderr: bool = False,
325
+ return_: Literal[True],
326
+ return_stdout: bool = False,
327
+ return_stderr: bool = False,
328
+ retry: Retry | None = None,
329
+ logger: LoggerLike | None = None,
330
+ ) -> str: ...
331
+ @overload
332
+ def run(
333
+ cmd: str,
334
+ /,
335
+ *cmds_or_args: str,
336
+ user: str | int | None = None,
337
+ executable: str | None = None,
338
+ shell: bool = False,
339
+ cwd: PathLike | None = None,
340
+ env: StrStrMapping | None = None,
341
+ input: str | None = None,
342
+ print: bool = False,
343
+ print_stdout: bool = False,
344
+ print_stderr: bool = False,
345
+ return_: bool = False,
346
+ return_stdout: Literal[True],
347
+ return_stderr: bool = False,
348
+ retry: Retry | None = None,
349
+ logger: LoggerLike | None = None,
350
+ ) -> str: ...
351
+ @overload
352
+ def run(
353
+ cmd: str,
354
+ /,
355
+ *cmds_or_args: str,
356
+ user: str | int | None = None,
357
+ executable: str | None = None,
358
+ shell: bool = False,
359
+ cwd: PathLike | None = None,
360
+ env: StrStrMapping | None = None,
361
+ input: str | None = None,
362
+ print: bool = False,
363
+ print_stdout: bool = False,
364
+ print_stderr: bool = False,
365
+ return_: bool = False,
366
+ return_stdout: bool = False,
367
+ return_stderr: Literal[True],
368
+ retry: Retry | None = None,
369
+ logger: LoggerLike | None = None,
370
+ ) -> str: ...
371
+ @overload
372
+ def run(
373
+ cmd: str,
374
+ /,
375
+ *cmds_or_args: str,
376
+ user: str | int | None = None,
377
+ executable: str | None = None,
378
+ shell: bool = False,
379
+ cwd: PathLike | None = None,
380
+ env: StrStrMapping | None = None,
381
+ input: str | None = None,
382
+ print: bool = False,
383
+ print_stdout: bool = False,
384
+ print_stderr: bool = False,
385
+ return_: Literal[False] = False,
386
+ return_stdout: Literal[False] = False,
387
+ return_stderr: Literal[False] = False,
388
+ retry: Retry | None = None,
389
+ logger: LoggerLike | None = None,
390
+ ) -> None: ...
391
+ @overload
392
+ def run(
393
+ cmd: str,
394
+ /,
395
+ *cmds_or_args: str,
396
+ user: str | int | None = None,
397
+ executable: str | None = None,
398
+ shell: bool = False,
399
+ cwd: PathLike | None = None,
400
+ env: StrStrMapping | None = None,
401
+ input: str | None = None,
402
+ print: bool = False,
403
+ print_stdout: bool = False,
404
+ print_stderr: bool = False,
405
+ return_: bool = False,
406
+ return_stdout: bool = False,
407
+ return_stderr: bool = False,
408
+ retry: Retry | None = None,
409
+ logger: LoggerLike | None = None,
410
+ ) -> str | None: ...
411
+ def run(
412
+ cmd: str,
413
+ /,
414
+ *cmds_or_args: str,
415
+ user: str | int | None = None,
416
+ executable: str | None = None,
417
+ shell: bool = False,
418
+ cwd: PathLike | None = None,
419
+ env: StrStrMapping | None = None,
420
+ input: str | None = None, # noqa: A002
421
+ print: bool = False, # noqa: A002
422
+ print_stdout: bool = False,
423
+ print_stderr: bool = False,
424
+ return_: bool = False,
425
+ return_stdout: bool = False,
426
+ return_stderr: bool = False,
427
+ retry: Retry | None = None,
428
+ logger: LoggerLike | None = None,
429
+ ) -> str | None:
430
+ args: list[str] = []
431
+ if user is not None: # pragma: no cover
432
+ args.extend(["su", "-", str(user)])
433
+ args.extend([cmd, *cmds_or_args])
434
+ buffer = StringIO()
435
+ stdout = StringIO()
436
+ stderr = StringIO()
437
+ stdout_outputs: list[IO[str]] = [buffer, stdout]
438
+ if print or print_stdout:
439
+ stdout_outputs.append(sys.stdout)
440
+ stderr_outputs: list[IO[str]] = [buffer, stderr]
441
+ if print or print_stderr:
442
+ stderr_outputs.append(sys.stderr)
443
+ with Popen(
444
+ args,
445
+ bufsize=1,
446
+ executable=executable,
447
+ stdin=PIPE,
448
+ stdout=PIPE,
449
+ stderr=PIPE,
450
+ shell=shell,
451
+ cwd=cwd,
452
+ env=env,
453
+ text=True,
454
+ user=user,
455
+ ) as proc:
456
+ if proc.stdin is None: # pragma: no cover
457
+ raise ImpossibleCaseError(case=[f"{proc.stdin=}"])
458
+ if proc.stdout is None: # pragma: no cover
459
+ raise ImpossibleCaseError(case=[f"{proc.stdout=}"])
460
+ if proc.stderr is None: # pragma: no cover
461
+ raise ImpossibleCaseError(case=[f"{proc.stderr=}"])
462
+ with (
463
+ _run_yield_write(proc.stdout, *stdout_outputs),
464
+ _run_yield_write(proc.stderr, *stderr_outputs),
465
+ ):
466
+ if input is not None:
467
+ _ = proc.stdin.write(input)
468
+ proc.stdin.flush()
469
+ proc.stdin.close()
470
+ return_code = proc.wait()
471
+ match return_code, return_ or return_stdout, return_ or return_stderr:
472
+ case 0, True, True:
473
+ _ = buffer.seek(0)
474
+ return buffer.read().rstrip("\n")
475
+ case 0, True, False:
476
+ _ = stdout.seek(0)
477
+ return stdout.read().rstrip("\n")
478
+ case 0, False, True:
479
+ _ = stderr.seek(0)
480
+ return stderr.read().rstrip("\n")
481
+ case 0, False, False:
482
+ return None
483
+ case _, _, _:
484
+ if retry is None:
485
+ attempts = delta = None
486
+ else:
487
+ attempts, delta = retry
488
+ _ = stdout.seek(0)
489
+ stdout_text = stdout.read()
490
+ _ = stderr.seek(0)
491
+ stderr_text = stderr.read()
492
+ if logger is not None:
493
+ msg = strip_and_dedent(f"""
494
+ 'run' failed with:
495
+ - cmd = {cmd}
496
+ - cmds_or_args = {cmds_or_args}
497
+ - user = {user}
498
+ - executable = {executable}
499
+ - shell = {shell}
500
+ - cwd = {cwd}
501
+ - env = {env}
502
+
503
+ -- stdin ----------------------------------------------------------------------
504
+ {"" if input is None else input}-------------------------------------------------------------------------------
505
+ -- stdout ---------------------------------------------------------------------
506
+ {stdout_text}-------------------------------------------------------------------------------
507
+ -- stderr ---------------------------------------------------------------------
508
+ {stderr_text}-------------------------------------------------------------------------------
509
+ """)
510
+ if (attempts is not None) and (attempts >= 1):
511
+ if delta is None:
512
+ msg = f"{msg}\n\nRetrying {attempts} more time(s)..."
513
+ else:
514
+ msg = f"{msg}\n\nRetrying {attempts} more time(s) after {delta}..."
515
+ to_logger(logger).error(msg)
516
+ error = CalledProcessError(
517
+ return_code, args, output=stdout_text, stderr=stderr_text
518
+ )
519
+ if (attempts is None) or (attempts <= 0):
520
+ raise error
521
+ if delta is not None:
522
+ sleep(to_seconds(delta))
523
+ return run(
524
+ cmd,
525
+ *cmds_or_args,
526
+ user=user,
527
+ executable=executable,
528
+ shell=shell,
529
+ cwd=cwd,
530
+ env=env,
531
+ input=input,
532
+ print=print,
533
+ print_stdout=print_stdout,
534
+ print_stderr=print_stderr,
535
+ return_=return_,
536
+ return_stdout=return_stdout,
537
+ return_stderr=return_stderr,
538
+ retry=(attempts - 1, delta),
539
+ logger=logger,
540
+ )
541
+ case never:
542
+ assert_never(never)
543
+
544
+
545
+ @contextmanager
546
+ def _run_yield_write(input_: IO[str], /, *outputs: IO[str]) -> Iterator[None]:
547
+ thread = Thread(target=_run_daemon_target, args=(input_, *outputs), daemon=True)
548
+ thread.start()
549
+ try:
550
+ yield
551
+ finally:
552
+ thread.join()
553
+
554
+
555
+ def _run_daemon_target(input_: IO[str], /, *outputs: IO[str]) -> None:
556
+ with input_:
557
+ for text in iter(input_.readline, ""):
558
+ _run_write_to_streams(text, *outputs)
559
+
560
+
561
+ def _run_write_to_streams(text: str, /, *outputs: IO[str]) -> None:
562
+ for output in outputs:
563
+ _ = output.write(text)
564
+
565
+
566
+ ##
567
+
568
+
569
+ def set_hostname_cmd(hostname: str, /) -> list[str]:
570
+ return ["hostnamectl", "set-hostname", hostname]
571
+
572
+
573
+ ##
574
+
575
+
576
+ @overload
577
+ def ssh(
578
+ user: str,
579
+ hostname: str,
580
+ /,
581
+ *cmd_and_cmds_or_args: str,
582
+ batch_mode: bool = True,
583
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
584
+ strict_host_key_checking: bool = True,
585
+ input: str | None = None,
586
+ print: bool = False,
587
+ print_stdout: bool = False,
588
+ print_stderr: bool = False,
589
+ return_: Literal[True],
590
+ return_stdout: bool = False,
591
+ return_stderr: bool = False,
592
+ retry: Retry | None = None,
593
+ logger: LoggerLike | None = None,
594
+ ) -> str: ...
595
+ @overload
596
+ def ssh(
597
+ user: str,
598
+ hostname: str,
599
+ /,
600
+ *cmd_and_cmds_or_args: str,
601
+ batch_mode: bool = True,
602
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
603
+ strict_host_key_checking: bool = True,
604
+ input: str | None = None,
605
+ print: bool = False,
606
+ print_stdout: bool = False,
607
+ print_stderr: bool = False,
608
+ return_: bool = False,
609
+ return_stdout: Literal[True],
610
+ return_stderr: bool = False,
611
+ retry: Retry | None = None,
612
+ logger: LoggerLike | None = None,
613
+ ) -> str: ...
614
+ @overload
615
+ def ssh(
616
+ user: str,
617
+ hostname: str,
618
+ /,
619
+ *cmd_and_cmds_or_args: str,
620
+ batch_mode: bool = True,
621
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
622
+ strict_host_key_checking: bool = True,
623
+ input: str | None = None,
624
+ print: bool = False,
625
+ print_stdout: bool = False,
626
+ print_stderr: bool = False,
627
+ return_: bool = False,
628
+ return_stdout: bool = False,
629
+ return_stderr: Literal[True],
630
+ retry: Retry | None = None,
631
+ logger: LoggerLike | None = None,
632
+ ) -> str: ...
633
+ @overload
634
+ def ssh(
635
+ user: str,
636
+ hostname: str,
637
+ /,
638
+ *cmd_and_cmds_or_args: str,
639
+ batch_mode: bool = True,
640
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
641
+ strict_host_key_checking: bool = True,
642
+ input: str | None = None,
643
+ print: bool = False,
644
+ print_stdout: bool = False,
645
+ print_stderr: bool = False,
646
+ return_: Literal[False] = False,
647
+ return_stdout: Literal[False] = False,
648
+ return_stderr: Literal[False] = False,
649
+ retry: Retry | None = None,
650
+ logger: LoggerLike | None = None,
651
+ ) -> None: ...
652
+ @overload
653
+ def ssh(
654
+ user: str,
655
+ hostname: str,
656
+ /,
657
+ *cmd_and_cmds_or_args: str,
658
+ batch_mode: bool = True,
659
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
660
+ strict_host_key_checking: bool = True,
661
+ input: str | None = None,
662
+ print: bool = False,
663
+ print_stdout: bool = False,
664
+ print_stderr: bool = False,
665
+ return_: bool = False,
666
+ return_stdout: bool = False,
667
+ return_stderr: bool = False,
668
+ retry: Retry | None = None,
669
+ logger: LoggerLike | None = None,
670
+ ) -> str | None: ...
671
+ def ssh(
672
+ user: str,
673
+ hostname: str,
674
+ /,
675
+ *cmd_and_cmds_or_args: str,
676
+ batch_mode: bool = True,
677
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
678
+ strict_host_key_checking: bool = True,
679
+ input: str | None = None, # noqa: A002
680
+ print: bool = False, # noqa: A002
681
+ print_stdout: bool = False,
682
+ print_stderr: bool = False,
683
+ return_: bool = False,
684
+ return_stdout: bool = False,
685
+ return_stderr: bool = False,
686
+ retry: Retry | None = None,
687
+ logger: LoggerLike | None = None,
688
+ ) -> str | None:
689
+ cmd_and_args = ssh_cmd( # skipif-ci
690
+ user,
691
+ hostname,
692
+ *cmd_and_cmds_or_args,
693
+ batch_mode=batch_mode,
694
+ host_key_algorithms=host_key_algorithms,
695
+ strict_host_key_checking=strict_host_key_checking,
696
+ )
697
+ return run( # skipif-ci
698
+ *cmd_and_args,
699
+ input=input,
700
+ print=print,
701
+ print_stdout=print_stdout,
702
+ print_stderr=print_stderr,
703
+ return_=return_,
704
+ return_stdout=return_stdout,
705
+ return_stderr=return_stderr,
706
+ retry=retry,
707
+ logger=logger,
708
+ )
709
+
710
+
711
+ ##
712
+
713
+
714
+ def ssh_cmd(
715
+ user: str,
716
+ hostname: str,
717
+ /,
718
+ *cmd_and_cmds_or_args: str,
719
+ batch_mode: bool = True,
720
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
721
+ strict_host_key_checking: bool = True,
722
+ ) -> list[str]:
723
+ args: list[str] = ssh_opts_cmd(
724
+ batch_mode=batch_mode,
725
+ host_key_algorithms=host_key_algorithms,
726
+ strict_host_key_checking=strict_host_key_checking,
727
+ )
728
+ return [*args, f"{user}@{hostname}", *cmd_and_cmds_or_args]
729
+
730
+
731
+ ##
732
+
733
+
734
+ def ssh_opts_cmd(
735
+ *,
736
+ batch_mode: bool = True,
737
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
738
+ strict_host_key_checking: bool = True,
739
+ ) -> list[str]:
740
+ args: list[str] = ["ssh"]
741
+ if batch_mode:
742
+ args.extend(["-o", "BatchMode=yes"])
743
+ args.extend(["-o", f"HostKeyAlgorithms={','.join(host_key_algorithms)}"])
744
+ if strict_host_key_checking:
745
+ args.extend(["-o", "StrictHostKeyChecking=yes"])
746
+ return [*args, "-T"]
747
+
748
+
749
+ ##
750
+
751
+
752
+ def ssh_keygen_cmd(hostname: str, /) -> list[str]:
753
+ return ["ssh-keygen", "-f", "~/.ssh/known_hosts", "-R", hostname]
754
+
755
+
756
+ ##
757
+
758
+
759
+ def sudo_cmd(cmd: str, /, *args: str) -> list[str]:
760
+ return ["sudo", cmd, *args]
761
+
762
+
763
+ ##
764
+
765
+
766
+ def sudo_nopasswd_cmd(user: str, /) -> str:
767
+ return f"{user} ALL=(ALL) NOPASSWD: ALL"
768
+
769
+
770
+ ##
771
+
772
+
773
+ def symlink_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
774
+ return ["ln", "-s", str(src), str(dest)]
775
+
776
+
777
+ ##
778
+
779
+
780
+ def touch_cmd(path: PathLike, /) -> list[str]:
781
+ return ["touch", str(path)]
782
+
783
+
784
+ ##
785
+
786
+
787
+ def uv_run_cmd(module: str, /, *args: str) -> list[str]:
788
+ return [
789
+ "uv",
790
+ "run",
791
+ "--no-dev",
792
+ "--active",
793
+ "--prerelease=disallow",
794
+ "--managed-python",
795
+ "python",
796
+ "-m",
797
+ module,
798
+ *args,
799
+ ]
800
+
801
+
802
+ ##
803
+
804
+
805
+ @contextmanager
806
+ def yield_ssh_temp_dir(
807
+ user: str,
808
+ hostname: str,
809
+ /,
810
+ *,
811
+ retry: Retry | None = None,
812
+ logger: LoggerLike | None = None,
813
+ keep: bool = False,
814
+ ) -> Iterator[Path]:
815
+ path = Path( # skipif-ci
816
+ ssh(user, hostname, *MKTEMP_DIR_CMD, return_=True, retry=retry, logger=logger)
817
+ )
818
+ try: # skipif-ci
819
+ yield path
820
+ finally: # skipif-ci
821
+ if keep:
822
+ if logger is not None:
823
+ to_logger(logger).info("Keeping temporary directory '%s'...", path)
824
+ else:
825
+ ssh(user, hostname, *rm_cmd(path), retry=retry, logger=logger)
826
+
827
+
828
+ __all__ = [
829
+ "APT_UPDATE",
830
+ "BASH_LC",
831
+ "BASH_LS",
832
+ "MKTEMP_DIR_CMD",
833
+ "RESTART_SSHD",
834
+ "UPDATE_CA_CERTIFICATES",
835
+ "ChownCmdError",
836
+ "apt_install_cmd",
837
+ "cd_cmd",
838
+ "chmod_cmd",
839
+ "chown_cmd",
840
+ "cp_cmd",
841
+ "echo_cmd",
842
+ "expand_path",
843
+ "git_clone_cmd",
844
+ "git_hard_reset_cmd",
845
+ "maybe_parent",
846
+ "maybe_sudo_cmd",
847
+ "mkdir",
848
+ "mkdir_cmd",
849
+ "mv_cmd",
850
+ "rm_cmd",
851
+ "rsync",
852
+ "rsync_cmd",
853
+ "run",
854
+ "set_hostname_cmd",
855
+ "ssh",
856
+ "ssh_cmd",
857
+ "ssh_opts_cmd",
858
+ "sudo_cmd",
859
+ "sudo_nopasswd_cmd",
860
+ "symlink_cmd",
861
+ "touch_cmd",
862
+ "uv_run_cmd",
863
+ "yield_ssh_temp_dir",
864
+ ]