dycw-utilities 0.174.5__py3-none-any.whl → 0.174.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dycw-utilities
3
- Version: 0.174.5
3
+ Version: 0.174.7
4
4
  Author: Derek Wan
5
5
  Author-email: Derek Wan <d.wan@icloud.com>
6
6
  Requires-Dist: atomicwrites>=1.4.1,<1.5
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=HSEHPwrjbITUKM0rHSI676yh7VgQMNCB5ksk8o1Hb8M,60
1
+ utilities/__init__.py,sha256=m6IBJ6HmwjCqt8xNdi4aHkF4CjRw4nrxq8SpNIQAq2Q,60
2
2
  utilities/aeventkit.py,sha256=OmDBhYGgbsKrB7cdC5FFpJHUatX9O76eTeKVVTksp2Y,12673
3
3
  utilities/altair.py,sha256=rUK99g9x6CYDDfiZrf-aTx5fSRbL1Q8ctgKORowzXHg,9060
4
4
  utilities/asyncio.py,sha256=aJySVxBY0gqsIYnoNmH7-1r8djKuf4vSsU69VCD08t8,16772
@@ -80,7 +80,7 @@ utilities/sqlalchemy.py,sha256=HQYpd7LFxdTF5WYVWYtCJeEBI71EJm7ytvCGyAH9B-U,37163
80
80
  utilities/sqlalchemy_polars.py,sha256=JCGhB37raSR7fqeWV5dTsciRTMVzIdVT9YSqKT0piT0,13370
81
81
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
82
82
  utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
83
- utilities/subprocess.py,sha256=r91VteJ7I1upuiWxRT2kvm17xffS5eXMZ3CJXdmZT4s,15686
83
+ utilities/subprocess.py,sha256=1mcFSteDnzIfFZT-UOWPg8lBjDKjRWvgSs5hzHXvsts,22231
84
84
  utilities/tempfile.py,sha256=Lx6qa16lL1XVH6WdmD_G9vlN6gLI8nrIurxmsFkPKvg,3022
85
85
  utilities/testbook.py,sha256=j1KmaVbrX9VrbeMgtPh5gk55myAsn3dyRUn7jGbPbRk,1294
86
86
  utilities/text.py,sha256=7SvwcSR2l_5cOrm1samGnR4C-ZI6qyFLHLzSpO1zeHQ,13958
@@ -97,7 +97,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
97
97
  utilities/whenever.py,sha256=F4ek0-OBWxHYrZdmoZt76N2RnNyKY5KrEHt7rqO4AQE,60183
98
98
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
99
99
  utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
100
- dycw_utilities-0.174.5.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
101
- dycw_utilities-0.174.5.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
102
- dycw_utilities-0.174.5.dist-info/METADATA,sha256=qYrj2g20dkkvXEdb1v43eAPpIoIS2vJOu4nM5QEFG54,1709
103
- dycw_utilities-0.174.5.dist-info/RECORD,,
100
+ dycw_utilities-0.174.7.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
101
+ dycw_utilities-0.174.7.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
102
+ dycw_utilities-0.174.7.dist-info/METADATA,sha256=q0-Hb1ta7Jppeysc4MdeoUaTPr3Nqwwb0uiMzysigw8,1709
103
+ dycw_utilities-0.174.7.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.174.5"
3
+ __version__ = "0.174.7"
utilities/subprocess.py CHANGED
@@ -2,15 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import sys
4
4
  from contextlib import contextmanager
5
+ from dataclasses import dataclass
5
6
  from io import StringIO
6
7
  from pathlib import Path
8
+ from shlex import join, quote
7
9
  from string import Template
8
10
  from subprocess import PIPE, CalledProcessError, Popen
9
11
  from threading import Thread
10
12
  from time import sleep
11
- from typing import IO, TYPE_CHECKING, Literal, assert_never, overload
13
+ from typing import IO, TYPE_CHECKING, Literal, assert_never, overload, override
12
14
 
13
15
  from utilities.errors import ImpossibleCaseError
16
+ from utilities.iterables import always_iterable
14
17
  from utilities.logging import to_logger
15
18
  from utilities.text import strip_and_dedent
16
19
  from utilities.whenever import to_seconds
@@ -18,13 +21,63 @@ from utilities.whenever import to_seconds
18
21
  if TYPE_CHECKING:
19
22
  from collections.abc import Iterator
20
23
 
21
- from utilities.types import LoggerLike, PathLike, Retry, StrMapping, StrStrMapping
24
+ from utilities.types import (
25
+ LoggerLike,
26
+ MaybeIterable,
27
+ PathLike,
28
+ Retry,
29
+ StrMapping,
30
+ StrStrMapping,
31
+ )
22
32
 
23
33
 
24
34
  _HOST_KEY_ALGORITHMS = ["ssh-ed25519"]
35
+ APT_UPDATE = ["apt", "update", "-y"]
25
36
  BASH_LC = ["bash", "-lc"]
26
37
  BASH_LS = ["bash", "-ls"]
27
38
  MKTEMP_DIR_CMD = ["mktemp", "-d"]
39
+ RESTART_SSHD = ["systemctl", "restart", "sshd"]
40
+ UPDATE_CA_CERTIFICATES: str = "update-ca-certificates"
41
+
42
+
43
+ def apt_install_cmd(package: str, /) -> list[str]:
44
+ return ["apt", "install", "-y", package]
45
+
46
+
47
+ def cat_cmd(path: PathLike, /) -> list[str]:
48
+ return ["cat", str(path)]
49
+
50
+
51
+ def cd_cmd(path: PathLike, /) -> list[str]:
52
+ return ["cd", str(path)]
53
+
54
+
55
+ def chmod_cmd(path: PathLike, mode: str, /) -> list[str]:
56
+ return ["chmod", mode, str(path)]
57
+
58
+
59
+ def chown_cmd(
60
+ path: PathLike, /, *, user: str | None = None, group: str | None = None
61
+ ) -> list[str]:
62
+ match user, group:
63
+ case None, None:
64
+ raise ChownCmdError
65
+ case str(), None:
66
+ ownership = "user"
67
+ case None, str():
68
+ ownership = f":{group}"
69
+ case str(), str():
70
+ ownership = f"{user}:{group}"
71
+ case never:
72
+ assert_never(never)
73
+ return ["chown", ownership, str(path)]
74
+
75
+
76
+ @dataclass(kw_only=True, slots=True)
77
+ class ChownCmdError(Exception):
78
+ @override
79
+ def __str__(self) -> str:
80
+ return "At least one of 'user' and/or 'group' must be given; got None"
28
81
 
29
82
 
30
83
  def cp_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
@@ -45,6 +98,15 @@ def expand_path(
45
98
  return Path(path).expanduser()
46
99
 
47
100
 
101
+ def git_clone_cmd(url: str, path: PathLike, /) -> list[str]:
102
+ return ["git", "clone", "--recurse-submodules", url, str(path)]
103
+
104
+
105
+ def git_hard_reset_cmd(*, branch: str | None = None) -> list[str]:
106
+ branch_use = "master" if branch is None else branch
107
+ return ["git", "hard-reset", branch_use]
108
+
109
+
48
110
  def maybe_sudo_cmd(cmd: str, /, *args: str, sudo: bool = False) -> list[str]:
49
111
  parts: list[str] = [cmd, *args]
50
112
  return sudo_cmd(*parts) if sudo else parts
@@ -60,8 +122,13 @@ def mkdir(path: PathLike, /, *, sudo: bool = False, parent: bool = False) -> Non
60
122
 
61
123
 
62
124
  def mkdir_cmd(path: PathLike, /, *, parent: bool = False) -> list[str]:
63
- path_use = f"$(dirname {path})" if parent else path
64
- return ["mkdir", "-p", str(path_use)]
125
+ args: list[str] = ["mkdir", "-p"]
126
+ quoted = quote(str(path))
127
+ if parent:
128
+ args.append(f"$(dirname {quoted})")
129
+ else:
130
+ args.append(quoted)
131
+ return args
65
132
 
66
133
 
67
134
  def mv_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
@@ -72,6 +139,114 @@ def rm_cmd(path: PathLike, /) -> list[str]:
72
139
  return ["rm", "-rf", str(path)]
73
140
 
74
141
 
142
+ def rsync(
143
+ src_or_srcs: MaybeIterable[PathLike],
144
+ user: str,
145
+ hostname: str,
146
+ dest: PathLike,
147
+ /,
148
+ *,
149
+ sudo: bool = False,
150
+ batch_mode: bool = True,
151
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
152
+ strict_host_key_checking: bool = True,
153
+ print: bool = False, # noqa: A002
154
+ retry: Retry | None = None,
155
+ logger: LoggerLike | None = None,
156
+ archive: bool = False,
157
+ chown_user: str | None = None,
158
+ chown_group: str | None = None,
159
+ exclude: MaybeIterable[str] | None = None,
160
+ chmod: str | None = None,
161
+ ) -> None:
162
+ mkdir_args = maybe_sudo_cmd(*mkdir_cmd(dest, parent=True), sudo=sudo) # skipif-ci
163
+ ssh( # skipif-ci
164
+ user,
165
+ hostname,
166
+ *mkdir_args,
167
+ batch_mode=batch_mode,
168
+ host_key_algorithms=host_key_algorithms,
169
+ strict_host_key_checking=strict_host_key_checking,
170
+ print=print,
171
+ retry=retry,
172
+ logger=logger,
173
+ )
174
+ rsync_args = rsync_cmd( # skipif-ci
175
+ src_or_srcs,
176
+ user,
177
+ hostname,
178
+ dest,
179
+ archive=archive,
180
+ chown_user=chown_user,
181
+ chown_group=chown_group,
182
+ exclude=exclude,
183
+ batch_mode=batch_mode,
184
+ host_key_algorithms=host_key_algorithms,
185
+ strict_host_key_checking=strict_host_key_checking,
186
+ sudo=sudo,
187
+ )
188
+ run(*rsync_args, print=print, retry=retry, logger=logger) # skipif-ci
189
+ if chmod is not None: # skipif-ci
190
+ chmod_args = maybe_sudo_cmd(*chmod_cmd(dest, chmod), sudo=sudo)
191
+ ssh(
192
+ user,
193
+ hostname,
194
+ *chmod_args,
195
+ batch_mode=batch_mode,
196
+ host_key_algorithms=host_key_algorithms,
197
+ strict_host_key_checking=strict_host_key_checking,
198
+ print=print,
199
+ retry=retry,
200
+ logger=logger,
201
+ )
202
+
203
+
204
+ def rsync_cmd(
205
+ src_or_srcs: MaybeIterable[PathLike],
206
+ user: str,
207
+ hostname: str,
208
+ dest: PathLike,
209
+ /,
210
+ *,
211
+ archive: bool = False,
212
+ chown_user: str | None = None,
213
+ chown_group: str | None = None,
214
+ exclude: MaybeIterable[str] | None = None,
215
+ batch_mode: bool = True,
216
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
217
+ strict_host_key_checking: bool = True,
218
+ sudo: bool = False,
219
+ ) -> list[str]:
220
+ args: list[str] = ["rsync"]
221
+ if archive:
222
+ args.append("--archive")
223
+ args.append("--checksum")
224
+ match chown_user, chown_group:
225
+ case None, None:
226
+ ...
227
+ case str(), None:
228
+ args.extend(["--chown", chown_user])
229
+ case None, str():
230
+ args.extend(["--chown", f":{chown_group}"])
231
+ case str(), str():
232
+ args.extend(["--chown", f"{chown_user}:{chown_group}"])
233
+ case never:
234
+ assert_never(never)
235
+ args.append("--compress")
236
+ if exclude is not None:
237
+ for exclude_i in always_iterable(exclude):
238
+ args.extend(["--exclude", exclude_i])
239
+ rsh_args: list[str] = ssh_opts_cmd(
240
+ batch_mode=batch_mode,
241
+ host_key_algorithms=host_key_algorithms,
242
+ strict_host_key_checking=strict_host_key_checking,
243
+ )
244
+ args.extend(["--rsh", join(rsh_args)])
245
+ if sudo:
246
+ args.extend(["--rsync-path", join(sudo_cmd("rsync"))])
247
+ return [*args, *map(str, always_iterable(src_or_srcs)), f"{user}@{hostname}:{dest}"]
248
+
249
+
75
250
  @overload
76
251
  def run(
77
252
  cmd: str,
@@ -224,8 +399,8 @@ def run(
224
399
  if proc.stderr is None: # pragma: no cover
225
400
  raise ImpossibleCaseError(case=[f"{proc.stderr=}"])
226
401
  with (
227
- _yield_write(proc.stdout, *stdout_outputs),
228
- _yield_write(proc.stderr, *stderr_outputs),
402
+ _run_yield_write(proc.stdout, *stdout_outputs),
403
+ _run_yield_write(proc.stderr, *stderr_outputs),
229
404
  ):
230
405
  if input is not None:
231
406
  _ = proc.stdin.write(input)
@@ -307,8 +482,8 @@ def run(
307
482
 
308
483
 
309
484
  @contextmanager
310
- def _yield_write(input_: IO[str], /, *outputs: IO[str]) -> Iterator[None]:
311
- thread = Thread(target=_run_target, args=(input_, *outputs), daemon=True)
485
+ def _run_yield_write(input_: IO[str], /, *outputs: IO[str]) -> Iterator[None]:
486
+ thread = Thread(target=_run_daemon_target, args=(input_, *outputs), daemon=True)
312
487
  thread.start()
313
488
  try:
314
489
  yield
@@ -316,17 +491,21 @@ def _yield_write(input_: IO[str], /, *outputs: IO[str]) -> Iterator[None]:
316
491
  thread.join()
317
492
 
318
493
 
319
- def _run_target(input_: IO[str], /, *outputs: IO[str]) -> None:
494
+ def _run_daemon_target(input_: IO[str], /, *outputs: IO[str]) -> None:
320
495
  with input_:
321
496
  for text in iter(input_.readline, ""):
322
- _write_to_streams(text, *outputs)
497
+ _run_write_to_streams(text, *outputs)
323
498
 
324
499
 
325
- def _write_to_streams(text: str, /, *outputs: IO[str]) -> None:
500
+ def _run_write_to_streams(text: str, /, *outputs: IO[str]) -> None:
326
501
  for output in outputs:
327
502
  _ = output.write(text)
328
503
 
329
504
 
505
+ def set_hostname_cmd(hostname: str, /) -> list[str]:
506
+ return ["hostnamectl", "set-hostname", hostname]
507
+
508
+
330
509
  @overload
331
510
  def ssh(
332
511
  user: str,
@@ -470,6 +649,20 @@ def ssh_cmd(
470
649
  batch_mode: bool = True,
471
650
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
472
651
  strict_host_key_checking: bool = True,
652
+ ) -> list[str]:
653
+ args: list[str] = ssh_opts_cmd(
654
+ batch_mode=batch_mode,
655
+ host_key_algorithms=host_key_algorithms,
656
+ strict_host_key_checking=strict_host_key_checking,
657
+ )
658
+ return [*args, f"{user}@{hostname}", *cmd_and_cmds_or_args]
659
+
660
+
661
+ def ssh_opts_cmd(
662
+ *,
663
+ batch_mode: bool = True,
664
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
665
+ strict_host_key_checking: bool = True,
473
666
  ) -> list[str]:
474
667
  args: list[str] = ["ssh"]
475
668
  if batch_mode:
@@ -477,17 +670,44 @@ def ssh_cmd(
477
670
  args.extend(["-o", f"HostKeyAlgorithms={','.join(host_key_algorithms)}"])
478
671
  if strict_host_key_checking:
479
672
  args.extend(["-o", "StrictHostKeyChecking=yes"])
480
- return [*args, "-T", f"{user}@{hostname}", *cmd_and_cmds_or_args]
673
+ return [*args, "-T"]
674
+
675
+
676
+ def ssh_keygen_cmd(hostname: str, /) -> list[str]:
677
+ return ["ssh-keygen", "-f", "~/.ssh/known_hosts", "-R", hostname]
481
678
 
482
679
 
483
680
  def sudo_cmd(cmd: str, /, *args: str) -> list[str]:
484
681
  return ["sudo", cmd, *args]
485
682
 
486
683
 
684
+ def sudo_nopasswd_cmd(user: str, /) -> str:
685
+ return f"{user} ALL=(ALL) NOPASSWD: ALL"
686
+
687
+
688
+ def symlink_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
689
+ return ["ln", "-s", str(src), str(dest)]
690
+
691
+
487
692
  def touch_cmd(path: PathLike, /) -> list[str]:
488
693
  return ["touch", str(path)]
489
694
 
490
695
 
696
+ def uv_run_cmd(module: str, /, *args: str) -> list[str]:
697
+ return [
698
+ "uv",
699
+ "run",
700
+ "--no-dev",
701
+ "--active",
702
+ "--prerelease=disallow",
703
+ "--managed-python",
704
+ "python",
705
+ "-m",
706
+ module,
707
+ *args,
708
+ ]
709
+
710
+
491
711
  @contextmanager
492
712
  def yield_ssh_temp_dir(
493
713
  user: str,
@@ -498,12 +718,12 @@ def yield_ssh_temp_dir(
498
718
  logger: LoggerLike | None = None,
499
719
  keep: bool = False,
500
720
  ) -> Iterator[Path]:
501
- path = Path(
721
+ path = Path( # skipif-ci
502
722
  ssh(user, hostname, *MKTEMP_DIR_CMD, return_=True, retry=retry, logger=logger)
503
723
  )
504
- try:
724
+ try: # skipif-ci
505
725
  yield path
506
- finally:
726
+ finally: # skipif-ci
507
727
  if keep:
508
728
  if logger is not None:
509
729
  to_logger(logger).info("Keeping temporary directory '%s'...", path)
@@ -512,21 +732,38 @@ def yield_ssh_temp_dir(
512
732
 
513
733
 
514
734
  __all__ = [
735
+ "APT_UPDATE",
515
736
  "BASH_LC",
516
737
  "BASH_LS",
517
738
  "MKTEMP_DIR_CMD",
739
+ "RESTART_SSHD",
740
+ "UPDATE_CA_CERTIFICATES",
741
+ "ChownCmdError",
742
+ "apt_install_cmd",
743
+ "cd_cmd",
744
+ "chmod_cmd",
745
+ "chown_cmd",
518
746
  "cp_cmd",
519
747
  "echo_cmd",
520
748
  "expand_path",
749
+ "git_clone_cmd",
750
+ "git_hard_reset_cmd",
521
751
  "maybe_sudo_cmd",
522
752
  "mkdir",
523
753
  "mkdir_cmd",
524
754
  "mv_cmd",
525
755
  "rm_cmd",
756
+ "rsync",
757
+ "rsync_cmd",
526
758
  "run",
759
+ "set_hostname_cmd",
527
760
  "ssh",
528
761
  "ssh_cmd",
762
+ "ssh_opts_cmd",
529
763
  "sudo_cmd",
764
+ "sudo_nopasswd_cmd",
765
+ "symlink_cmd",
530
766
  "touch_cmd",
767
+ "uv_run_cmd",
531
768
  "yield_ssh_temp_dir",
532
769
  ]