dycw-utilities 0.174.6__tar.gz → 0.174.8__tar.gz

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.
Files changed (102) hide show
  1. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/PKG-INFO +1 -1
  2. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/pyproject.toml +3 -3
  3. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/__init__.py +1 -1
  4. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/subprocess.py +244 -10
  5. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/README.md +0 -0
  6. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/aeventkit.py +0 -0
  7. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/altair.py +0 -0
  8. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/asyncio.py +0 -0
  9. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/atomicwrites.py +0 -0
  10. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/atools.py +0 -0
  11. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/cachetools.py +0 -0
  12. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/click.py +0 -0
  13. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/concurrent.py +0 -0
  14. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/contextlib.py +0 -0
  15. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/contextvars.py +0 -0
  16. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/cryptography.py +0 -0
  17. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/cvxpy.py +0 -0
  18. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/dataclasses.py +0 -0
  19. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/docker.py +0 -0
  20. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/enum.py +0 -0
  21. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/errors.py +0 -0
  22. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/fastapi.py +0 -0
  23. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/fpdf2.py +0 -0
  24. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/functions.py +0 -0
  25. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/functools.py +0 -0
  26. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/getpass.py +0 -0
  27. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/git.py +0 -0
  28. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/grp.py +0 -0
  29. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/gzip.py +0 -0
  30. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/hashlib.py +0 -0
  31. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/http.py +0 -0
  32. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/hypothesis.py +0 -0
  33. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/importlib.py +0 -0
  34. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/inflect.py +0 -0
  35. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/ipython.py +0 -0
  36. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/iterables.py +0 -0
  37. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/jinja2.py +0 -0
  38. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/json.py +0 -0
  39. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/jupyter.py +0 -0
  40. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/libcst.py +0 -0
  41. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/lightweight_charts.py +0 -0
  42. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/logging.py +0 -0
  43. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/math.py +0 -0
  44. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/memory_profiler.py +0 -0
  45. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/modules.py +0 -0
  46. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/more_itertools.py +0 -0
  47. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/numpy.py +0 -0
  48. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/operator.py +0 -0
  49. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/optuna.py +0 -0
  50. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/orjson.py +0 -0
  51. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/os.py +0 -0
  52. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/parse.py +0 -0
  53. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pathlib.py +0 -0
  54. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pickle.py +0 -0
  55. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/platform.py +0 -0
  56. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/polars.py +0 -0
  57. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/polars_ols.py +0 -0
  58. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/postgres.py +0 -0
  59. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pottery.py +0 -0
  60. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pqdm.py +0 -0
  61. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/psutil.py +0 -0
  62. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pwd.py +0 -0
  63. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/py.typed +0 -0
  64. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pydantic.py +0 -0
  65. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pydantic_settings.py +0 -0
  66. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pydantic_settings_sops.py +0 -0
  67. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pyinstrument.py +0 -0
  68. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pytest.py +0 -0
  69. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pytest_plugins/__init__.py +0 -0
  70. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pytest_plugins/pytest_randomly.py +0 -0
  71. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pytest_plugins/pytest_regressions.py +0 -0
  72. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/pytest_regressions.py +0 -0
  73. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/random.py +0 -0
  74. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/re.py +0 -0
  75. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/redis.py +0 -0
  76. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/reprlib.py +0 -0
  77. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/scipy.py +0 -0
  78. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/sentinel.py +0 -0
  79. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/shelve.py +0 -0
  80. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/shutil.py +0 -0
  81. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/slack_sdk.py +0 -0
  82. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/socket.py +0 -0
  83. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/sqlalchemy.py +0 -0
  84. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/sqlalchemy_polars.py +0 -0
  85. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/statsmodels.py +0 -0
  86. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/string.py +0 -0
  87. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/tempfile.py +0 -0
  88. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/testbook.py +0 -0
  89. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/text.py +0 -0
  90. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/threading.py +0 -0
  91. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/timer.py +0 -0
  92. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/traceback.py +0 -0
  93. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/types.py +0 -0
  94. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/typing.py +0 -0
  95. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/tzdata.py +0 -0
  96. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/tzlocal.py +0 -0
  97. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/uuid.py +0 -0
  98. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/version.py +0 -0
  99. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/warnings.py +0 -0
  100. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/whenever.py +0 -0
  101. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/zipfile.py +0 -0
  102. {dycw_utilities-0.174.6 → dycw_utilities-0.174.8}/src/utilities/zoneinfo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dycw-utilities
3
- Version: 0.174.6
3
+ Version: 0.174.8
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
@@ -28,7 +28,7 @@
28
28
  "pytest-cov >=7.0.0, <7.1",
29
29
  "pytest-timeout >=2.4.0, <2.5",
30
30
  ]
31
- fastapi = ["fastapi >=0.127.0, <0.128"]
31
+ fastapi = ["fastapi >=0.127.1, <0.128"]
32
32
  fastapi-test = ["httpx", "uvicorn"]
33
33
  fpdf2 = ["fpdf2 >=2.8.5, <2.9"]
34
34
  gitpython = ["gitpython >=3.1.45, <3.2"]
@@ -101,7 +101,7 @@
101
101
  name = "dycw-utilities"
102
102
  readme = "README.md"
103
103
  requires-python = ">= 3.12"
104
- version = "0.174.6"
104
+ version = "0.174.8"
105
105
 
106
106
  [project.entry-points.pytest11]
107
107
  pytest-randomly = "utilities.pytest_plugins.pytest_randomly"
@@ -135,7 +135,7 @@
135
135
  # bump-my-version
136
136
  [tool.bumpversion]
137
137
  allow_dirty = true
138
- current_version = "0.174.6"
138
+ current_version = "0.174.8"
139
139
 
140
140
  [[tool.bumpversion.files]]
141
141
  filename = "src/utilities/__init__.py"
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.174.6"
3
+ __version__ = "0.174.8"
@@ -5,6 +5,7 @@ from contextlib import contextmanager
5
5
  from dataclasses import dataclass
6
6
  from io import StringIO
7
7
  from pathlib import Path
8
+ from shlex import join
8
9
  from string import Template
9
10
  from subprocess import PIPE, CalledProcessError, Popen
10
11
  from threading import Thread
@@ -12,6 +13,7 @@ from time import sleep
12
13
  from typing import IO, TYPE_CHECKING, Literal, assert_never, overload, override
13
14
 
14
15
  from utilities.errors import ImpossibleCaseError
16
+ from utilities.iterables import always_iterable
15
17
  from utilities.logging import to_logger
16
18
  from utilities.text import strip_and_dedent
17
19
  from utilities.whenever import to_seconds
@@ -19,7 +21,14 @@ from utilities.whenever import to_seconds
19
21
  if TYPE_CHECKING:
20
22
  from collections.abc import Iterator
21
23
 
22
- 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
+ )
23
32
 
24
33
 
25
34
  _HOST_KEY_ALGORITHMS = ["ssh-ed25519"]
@@ -31,22 +40,37 @@ RESTART_SSHD = ["systemctl", "restart", "sshd"]
31
40
  UPDATE_CA_CERTIFICATES: str = "update-ca-certificates"
32
41
 
33
42
 
43
+ ##
44
+
45
+
34
46
  def apt_install_cmd(package: str, /) -> list[str]:
35
47
  return ["apt", "install", "-y", package]
36
48
 
37
49
 
50
+ ##
51
+
52
+
38
53
  def cat_cmd(path: PathLike, /) -> list[str]:
39
54
  return ["cat", str(path)]
40
55
 
41
56
 
57
+ ##
58
+
59
+
42
60
  def cd_cmd(path: PathLike, /) -> list[str]:
43
61
  return ["cd", str(path)]
44
62
 
45
63
 
64
+ ##
65
+
66
+
46
67
  def chmod_cmd(path: PathLike, mode: str, /) -> list[str]:
47
68
  return ["chmod", mode, str(path)]
48
69
 
49
70
 
71
+ ##
72
+
73
+
50
74
  def chown_cmd(
51
75
  path: PathLike, /, *, user: str | None = None, group: str | None = None
52
76
  ) -> list[str]:
@@ -71,14 +95,23 @@ class ChownCmdError(Exception):
71
95
  return "At least one of 'user' and/or 'group' must be given; got None"
72
96
 
73
97
 
98
+ ##
99
+
100
+
74
101
  def cp_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
75
102
  return ["cp", "-r", str(src), str(dest)]
76
103
 
77
104
 
105
+ ##
106
+
107
+
78
108
  def echo_cmd(text: str, /) -> list[str]:
79
109
  return ["echo", text]
80
110
 
81
111
 
112
+ ##
113
+
114
+
82
115
  def expand_path(
83
116
  path: PathLike, /, *, subs: StrMapping | None = None, sudo: bool = False
84
117
  ) -> Path:
@@ -89,42 +122,192 @@ def expand_path(
89
122
  return Path(path).expanduser()
90
123
 
91
124
 
125
+ ##
126
+
127
+
92
128
  def git_clone_cmd(url: str, path: PathLike, /) -> list[str]:
93
129
  return ["git", "clone", "--recurse-submodules", url, str(path)]
94
130
 
95
131
 
132
+ ##
133
+
134
+
96
135
  def git_hard_reset_cmd(*, branch: str | None = None) -> list[str]:
97
136
  branch_use = "master" if branch is None else branch
98
137
  return ["git", "hard-reset", branch_use]
99
138
 
100
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
+
101
151
  def maybe_sudo_cmd(cmd: str, /, *args: str, sudo: bool = False) -> list[str]:
102
152
  parts: list[str] = [cmd, *args]
103
153
  return sudo_cmd(*parts) if sudo else parts
104
154
 
105
155
 
156
+ ##
157
+
158
+
106
159
  def mkdir(path: PathLike, /, *, sudo: bool = False, parent: bool = False) -> None:
107
160
  if sudo: # pragma: no cover
108
161
  run(*sudo_cmd(*mkdir_cmd(path, parent=parent)))
109
162
  else:
110
- path = expand_path(path)
111
- path_use = path.parent if parent else path
112
- path_use.mkdir(parents=True, exist_ok=True)
163
+ maybe_parent(path, parent=parent).mkdir(parents=True, exist_ok=True)
164
+
165
+
166
+ ##
113
167
 
114
168
 
115
169
  def mkdir_cmd(path: PathLike, /, *, parent: bool = False) -> list[str]:
116
- path_use = f"$(dirname {path})" if parent else path
117
- return ["mkdir", "-p", str(path_use)]
170
+ return ["mkdir", "-p", str(maybe_parent(path, parent=parent))]
171
+
172
+
173
+ ##
118
174
 
119
175
 
120
176
  def mv_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
121
177
  return ["mv", str(src), str(dest)]
122
178
 
123
179
 
180
+ ##
181
+
182
+
124
183
  def rm_cmd(path: PathLike, /) -> list[str]:
125
184
  return ["rm", "-rf", str(path)]
126
185
 
127
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
+
128
311
  @overload
129
312
  def run(
130
313
  cmd: str,
@@ -380,10 +563,16 @@ def _run_write_to_streams(text: str, /, *outputs: IO[str]) -> None:
380
563
  _ = output.write(text)
381
564
 
382
565
 
566
+ ##
567
+
568
+
383
569
  def set_hostname_cmd(hostname: str, /) -> list[str]:
384
570
  return ["hostnamectl", "set-hostname", hostname]
385
571
 
386
572
 
573
+ ##
574
+
575
+
387
576
  @overload
388
577
  def ssh(
389
578
  user: str,
@@ -519,6 +708,9 @@ def ssh(
519
708
  )
520
709
 
521
710
 
711
+ ##
712
+
713
+
522
714
  def ssh_cmd(
523
715
  user: str,
524
716
  hostname: str,
@@ -527,6 +719,23 @@ def ssh_cmd(
527
719
  batch_mode: bool = True,
528
720
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
529
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,
530
739
  ) -> list[str]:
531
740
  args: list[str] = ["ssh"]
532
741
  if batch_mode:
@@ -534,29 +743,47 @@ def ssh_cmd(
534
743
  args.extend(["-o", f"HostKeyAlgorithms={','.join(host_key_algorithms)}"])
535
744
  if strict_host_key_checking:
536
745
  args.extend(["-o", "StrictHostKeyChecking=yes"])
537
- return [*args, "-T", f"{user}@{hostname}", *cmd_and_cmds_or_args]
746
+ return [*args, "-T"]
747
+
748
+
749
+ ##
538
750
 
539
751
 
540
752
  def ssh_keygen_cmd(hostname: str, /) -> list[str]:
541
753
  return ["ssh-keygen", "-f", "~/.ssh/known_hosts", "-R", hostname]
542
754
 
543
755
 
756
+ ##
757
+
758
+
544
759
  def sudo_cmd(cmd: str, /, *args: str) -> list[str]:
545
760
  return ["sudo", cmd, *args]
546
761
 
547
762
 
763
+ ##
764
+
765
+
548
766
  def sudo_nopasswd_cmd(user: str, /) -> str:
549
767
  return f"{user} ALL=(ALL) NOPASSWD: ALL"
550
768
 
551
769
 
770
+ ##
771
+
772
+
552
773
  def symlink_cmd(src: PathLike, dest: PathLike, /) -> list[str]:
553
774
  return ["ln", "-s", str(src), str(dest)]
554
775
 
555
776
 
777
+ ##
778
+
779
+
556
780
  def touch_cmd(path: PathLike, /) -> list[str]:
557
781
  return ["touch", str(path)]
558
782
 
559
783
 
784
+ ##
785
+
786
+
560
787
  def uv_run_cmd(module: str, /, *args: str) -> list[str]:
561
788
  return [
562
789
  "uv",
@@ -572,6 +799,9 @@ def uv_run_cmd(module: str, /, *args: str) -> list[str]:
572
799
  ]
573
800
 
574
801
 
802
+ ##
803
+
804
+
575
805
  @contextmanager
576
806
  def yield_ssh_temp_dir(
577
807
  user: str,
@@ -582,12 +812,12 @@ def yield_ssh_temp_dir(
582
812
  logger: LoggerLike | None = None,
583
813
  keep: bool = False,
584
814
  ) -> Iterator[Path]:
585
- path = Path(
815
+ path = Path( # skipif-ci
586
816
  ssh(user, hostname, *MKTEMP_DIR_CMD, return_=True, retry=retry, logger=logger)
587
817
  )
588
- try:
818
+ try: # skipif-ci
589
819
  yield path
590
- finally:
820
+ finally: # skipif-ci
591
821
  if keep:
592
822
  if logger is not None:
593
823
  to_logger(logger).info("Keeping temporary directory '%s'...", path)
@@ -612,15 +842,19 @@ __all__ = [
612
842
  "expand_path",
613
843
  "git_clone_cmd",
614
844
  "git_hard_reset_cmd",
845
+ "maybe_parent",
615
846
  "maybe_sudo_cmd",
616
847
  "mkdir",
617
848
  "mkdir_cmd",
618
849
  "mv_cmd",
619
850
  "rm_cmd",
851
+ "rsync",
852
+ "rsync_cmd",
620
853
  "run",
621
854
  "set_hostname_cmd",
622
855
  "ssh",
623
856
  "ssh_cmd",
857
+ "ssh_opts_cmd",
624
858
  "sudo_cmd",
625
859
  "sudo_nopasswd_cmd",
626
860
  "symlink_cmd",