dycw-utilities 0.173.5__tar.gz → 0.174.0__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.173.5 → dycw_utilities-0.174.0}/PKG-INFO +1 -1
  2. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/pyproject.toml +2 -2
  3. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/__init__.py +1 -1
  4. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/docker.py +12 -15
  5. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/subprocess.py +166 -64
  6. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/README.md +0 -0
  7. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/aeventkit.py +0 -0
  8. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/altair.py +0 -0
  9. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/asyncio.py +0 -0
  10. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/atomicwrites.py +0 -0
  11. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/atools.py +0 -0
  12. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/cachetools.py +0 -0
  13. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/click.py +0 -0
  14. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/concurrent.py +0 -0
  15. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/contextlib.py +0 -0
  16. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/contextvars.py +0 -0
  17. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/cryptography.py +0 -0
  18. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/cvxpy.py +0 -0
  19. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/dataclasses.py +0 -0
  20. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/enum.py +0 -0
  21. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/errors.py +0 -0
  22. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/fastapi.py +0 -0
  23. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/fpdf2.py +0 -0
  24. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/functions.py +0 -0
  25. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/functools.py +0 -0
  26. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/getpass.py +0 -0
  27. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/git.py +0 -0
  28. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/grp.py +0 -0
  29. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/gzip.py +0 -0
  30. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/hashlib.py +0 -0
  31. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/http.py +0 -0
  32. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/hypothesis.py +0 -0
  33. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/importlib.py +0 -0
  34. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/inflect.py +0 -0
  35. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/ipython.py +0 -0
  36. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/iterables.py +0 -0
  37. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/jinja2.py +0 -0
  38. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/json.py +0 -0
  39. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/jupyter.py +0 -0
  40. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/libcst.py +0 -0
  41. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/lightweight_charts.py +0 -0
  42. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/logging.py +0 -0
  43. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/math.py +0 -0
  44. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/memory_profiler.py +0 -0
  45. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/modules.py +0 -0
  46. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/more_itertools.py +0 -0
  47. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/numpy.py +0 -0
  48. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/operator.py +0 -0
  49. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/optuna.py +0 -0
  50. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/orjson.py +0 -0
  51. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/os.py +0 -0
  52. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/parse.py +0 -0
  53. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pathlib.py +0 -0
  54. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pickle.py +0 -0
  55. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/platform.py +0 -0
  56. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/polars.py +0 -0
  57. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/polars_ols.py +0 -0
  58. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/postgres.py +0 -0
  59. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pottery.py +0 -0
  60. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pqdm.py +0 -0
  61. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/psutil.py +0 -0
  62. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pwd.py +0 -0
  63. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/py.typed +0 -0
  64. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pydantic.py +0 -0
  65. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pydantic_settings.py +0 -0
  66. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pydantic_settings_sops.py +0 -0
  67. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pyinstrument.py +0 -0
  68. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pytest.py +0 -0
  69. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pytest_plugins/__init__.py +0 -0
  70. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pytest_plugins/pytest_randomly.py +0 -0
  71. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pytest_plugins/pytest_regressions.py +0 -0
  72. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/pytest_regressions.py +0 -0
  73. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/random.py +0 -0
  74. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/re.py +0 -0
  75. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/redis.py +0 -0
  76. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/reprlib.py +0 -0
  77. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/scipy.py +0 -0
  78. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/sentinel.py +0 -0
  79. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/shelve.py +0 -0
  80. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/shutil.py +0 -0
  81. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/slack_sdk.py +0 -0
  82. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/socket.py +0 -0
  83. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/sqlalchemy.py +0 -0
  84. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/sqlalchemy_polars.py +0 -0
  85. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/statsmodels.py +0 -0
  86. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/string.py +0 -0
  87. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/tempfile.py +0 -0
  88. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/testbook.py +0 -0
  89. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/text.py +0 -0
  90. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/threading.py +0 -0
  91. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/timer.py +0 -0
  92. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/traceback.py +0 -0
  93. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/types.py +0 -0
  94. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/typing.py +0 -0
  95. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/tzdata.py +0 -0
  96. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/tzlocal.py +0 -0
  97. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/uuid.py +0 -0
  98. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/version.py +0 -0
  99. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/warnings.py +0 -0
  100. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/whenever.py +0 -0
  101. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/zipfile.py +0 -0
  102. {dycw_utilities-0.173.5 → dycw_utilities-0.174.0}/src/utilities/zoneinfo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dycw-utilities
3
- Version: 0.173.5
3
+ Version: 0.174.0
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
@@ -101,7 +101,7 @@
101
101
  name = "dycw-utilities"
102
102
  readme = "README.md"
103
103
  requires-python = ">= 3.12"
104
- version = "0.173.5"
104
+ version = "0.174.0"
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.173.5"
138
+ current_version = "0.174.0"
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.173.5"
3
+ __version__ = "0.174.0"
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Literal, overload
7
7
  from utilities.errors import ImpossibleCaseError
8
8
  from utilities.subprocess import (
9
9
  MKTEMP_DIR_CMD,
10
- bash_cmd_and_args,
11
10
  maybe_sudo_cmd,
12
11
  mkdir,
13
12
  mkdir_cmd,
@@ -100,7 +99,7 @@ def docker_exec(
100
99
  env: StrStrMapping | None = None,
101
100
  user: str | None = None,
102
101
  workdir: PathLike | None = None,
103
- bash: bool = False,
102
+ input: str | None = None,
104
103
  print: bool = False,
105
104
  print_stdout: bool = False,
106
105
  print_stderr: bool = False,
@@ -119,7 +118,7 @@ def docker_exec(
119
118
  env: StrStrMapping | None = None,
120
119
  user: str | None = None,
121
120
  workdir: PathLike | None = None,
122
- bash: bool = False,
121
+ input: str | None = None,
123
122
  print: bool = False,
124
123
  print_stdout: bool = False,
125
124
  print_stderr: bool = False,
@@ -138,7 +137,7 @@ def docker_exec(
138
137
  env: StrStrMapping | None = None,
139
138
  user: str | None = None,
140
139
  workdir: PathLike | None = None,
141
- bash: bool = False,
140
+ input: str | None = None,
142
141
  print: bool = False,
143
142
  print_stdout: bool = False,
144
143
  print_stderr: bool = False,
@@ -157,7 +156,7 @@ def docker_exec(
157
156
  env: StrStrMapping | None = None,
158
157
  user: str | None = None,
159
158
  workdir: PathLike | None = None,
160
- bash: bool = False,
159
+ input: str | None = None,
161
160
  print: bool = False,
162
161
  print_stdout: bool = False,
163
162
  print_stderr: bool = False,
@@ -176,7 +175,7 @@ def docker_exec(
176
175
  env: StrStrMapping | None = None,
177
176
  user: str | None = None,
178
177
  workdir: PathLike | None = None,
179
- bash: bool = False,
178
+ input: str | None = None,
180
179
  print: bool = False,
181
180
  print_stdout: bool = False,
182
181
  print_stderr: bool = False,
@@ -194,7 +193,7 @@ def docker_exec(
194
193
  env: StrStrMapping | None = None,
195
194
  user: str | None = None,
196
195
  workdir: PathLike | None = None,
197
- bash: bool = False,
196
+ input: str | None = None, # noqa: A002
198
197
  print: bool = False, # noqa: A002
199
198
  print_stdout: bool = False,
200
199
  print_stderr: bool = False,
@@ -209,13 +208,14 @@ def docker_exec(
209
208
  cmd,
210
209
  *cmds_or_args,
211
210
  env=env,
211
+ interactive=input is not None,
212
212
  user=user,
213
213
  workdir=workdir,
214
- bash=bash,
215
214
  **env_kwargs,
216
215
  )
217
216
  return run( # skipif-ci
218
217
  *cmd_and_args,
218
+ input=input,
219
219
  print=print,
220
220
  print_stdout=print_stdout,
221
221
  print_stderr=print_stderr,
@@ -232,9 +232,9 @@ def docker_exec_cmd(
232
232
  /,
233
233
  *cmds_or_args: str,
234
234
  env: StrStrMapping | None = None,
235
+ interactive: bool = False,
235
236
  user: str | None = None,
236
237
  workdir: PathLike | None = None,
237
- bash: bool = False,
238
238
  **env_kwargs: str,
239
239
  ) -> list[str]:
240
240
  """Build a command for `docker exec`."""
@@ -242,16 +242,13 @@ def docker_exec_cmd(
242
242
  mapping: dict[str, str] = ({} if env is None else dict(env)) | env_kwargs
243
243
  for key, value in mapping.items():
244
244
  args.extend(["--env", f"{key}={value}"])
245
+ if interactive:
246
+ args.append("--interactive")
245
247
  if user is not None:
246
248
  args.extend(["--user", user])
247
249
  if workdir is not None:
248
250
  args.extend(["--workdir", str(workdir)])
249
- args.append(container)
250
- if bash:
251
- args.extend(bash_cmd_and_args(cmd, *cmds_or_args))
252
- else:
253
- args.extend([cmd, *cmds_or_args])
254
- return args
251
+ return [*args, container, cmd, *cmds_or_args]
255
252
 
256
253
 
257
254
  @contextmanager
@@ -20,13 +20,11 @@ if TYPE_CHECKING:
20
20
 
21
21
 
22
22
  _HOST_KEY_ALGORITHMS = ["ssh-ed25519"]
23
+ BASH_LC = ["bash", "-lc"]
24
+ BASH_LS = ["bash", "-ls"]
23
25
  MKTEMP_DIR_CMD = ["mktemp", "-d"]
24
26
 
25
27
 
26
- def bash_cmd_and_args(cmd: str, /, *cmds: str) -> list[str]:
27
- return ["bash", "-lc", "\n".join([cmd, *cmds])]
28
-
29
-
30
28
  def echo_cmd(text: str, /) -> list[str]:
31
29
  return ["echo", text]
32
30
 
@@ -69,12 +67,12 @@ def run(
69
67
  cmd: str,
70
68
  /,
71
69
  *cmds_or_args: str,
72
- bash: bool = False,
73
70
  user: str | int | None = None,
74
71
  executable: str | None = None,
75
72
  shell: bool = False,
76
73
  cwd: PathLike | None = None,
77
74
  env: StrStrMapping | None = None,
75
+ input: str | None = None,
78
76
  print: bool = False,
79
77
  print_stdout: bool = False,
80
78
  print_stderr: bool = False,
@@ -88,12 +86,12 @@ def run(
88
86
  cmd: str,
89
87
  /,
90
88
  *cmds_or_args: str,
91
- bash: bool = False,
92
89
  user: str | int | None = None,
93
90
  executable: str | None = None,
94
91
  shell: bool = False,
95
92
  cwd: PathLike | None = None,
96
93
  env: StrStrMapping | None = None,
94
+ input: str | None = None,
97
95
  print: bool = False,
98
96
  print_stdout: bool = False,
99
97
  print_stderr: bool = False,
@@ -107,12 +105,12 @@ def run(
107
105
  cmd: str,
108
106
  /,
109
107
  *cmds_or_args: str,
110
- bash: bool = False,
111
108
  user: str | int | None = None,
112
109
  executable: str | None = None,
113
110
  shell: bool = False,
114
111
  cwd: PathLike | None = None,
115
112
  env: StrStrMapping | None = None,
113
+ input: str | None = None,
116
114
  print: bool = False,
117
115
  print_stdout: bool = False,
118
116
  print_stderr: bool = False,
@@ -126,12 +124,12 @@ def run(
126
124
  cmd: str,
127
125
  /,
128
126
  *cmds_or_args: str,
129
- bash: bool = False,
130
127
  user: str | int | None = None,
131
128
  executable: str | None = None,
132
129
  shell: bool = False,
133
130
  cwd: PathLike | None = None,
134
131
  env: StrStrMapping | None = None,
132
+ input: str | None = None,
135
133
  print: bool = False,
136
134
  print_stdout: bool = False,
137
135
  print_stderr: bool = False,
@@ -145,12 +143,12 @@ def run(
145
143
  cmd: str,
146
144
  /,
147
145
  *cmds_or_args: str,
148
- bash: bool = False,
149
146
  user: str | int | None = None,
150
147
  executable: str | None = None,
151
148
  shell: bool = False,
152
149
  cwd: PathLike | None = None,
153
150
  env: StrStrMapping | None = None,
151
+ input: str | None = None,
154
152
  print: bool = False,
155
153
  print_stdout: bool = False,
156
154
  print_stderr: bool = False,
@@ -163,12 +161,12 @@ def run(
163
161
  cmd: str,
164
162
  /,
165
163
  *cmds_or_args: str,
166
- bash: bool = False,
167
164
  user: str | int | None = None,
168
165
  executable: str | None = None,
169
166
  shell: bool = False,
170
167
  cwd: PathLike | None = None,
171
168
  env: StrStrMapping | None = None,
169
+ input: str | None = None, # noqa: A002
172
170
  print: bool = False, # noqa: A002
173
171
  print_stdout: bool = False,
174
172
  print_stderr: bool = False,
@@ -177,55 +175,46 @@ def run(
177
175
  return_stderr: bool = False,
178
176
  logger: LoggerLike | None = None,
179
177
  ) -> str | None:
180
- match bash, user:
181
- case False, user_use:
182
- args: list[str] = [cmd, *cmds_or_args]
183
- case True, None:
184
- args: list[str] = bash_cmd_and_args(cmd, *cmds_or_args)
185
- user_use = None
186
- case True, str() | int(): # skipif-ci-or-mac
187
- args: list[str] = [
188
- "su",
189
- "-",
190
- str(user),
191
- *bash_cmd_and_args(cmd, *cmds_or_args),
192
- ]
193
- user_use = None
194
- case never:
195
- assert_never(never)
178
+ args: list[str] = []
179
+ if user is not None:
180
+ args.extend(["su", "-", str(user)])
181
+ args.extend([cmd, *cmds_or_args])
196
182
  buffer = StringIO()
197
183
  stdout = StringIO()
198
184
  stderr = StringIO()
185
+ stdout_outputs: list[IO[str]] = [buffer, stdout]
186
+ if print or print_stdout:
187
+ stdout_outputs.append(sys.stdout)
188
+ stderr_outputs: list[IO[str]] = [buffer, stderr]
189
+ if print or print_stderr:
190
+ stderr_outputs.append(sys.stderr)
199
191
  with Popen(
200
192
  args,
201
193
  bufsize=1,
202
194
  executable=executable,
195
+ stdin=PIPE,
203
196
  stdout=PIPE,
204
197
  stderr=PIPE,
205
198
  shell=shell,
206
199
  cwd=cwd,
207
200
  env=env,
208
201
  text=True,
209
- user=user_use,
202
+ user=user,
210
203
  ) as proc:
204
+ if proc.stdin is None: # pragma: no cover
205
+ raise ImpossibleCaseError(case=[f"{proc.stdin=}"])
211
206
  if proc.stdout is None: # pragma: no cover
212
207
  raise ImpossibleCaseError(case=[f"{proc.stdout=}"])
213
208
  if proc.stderr is None: # pragma: no cover
214
209
  raise ImpossibleCaseError(case=[f"{proc.stderr=}"])
215
210
  with (
216
- _yield_write(
217
- proc.stdout,
218
- buffer,
219
- stdout,
220
- *([sys.stdout] if print or print_stdout else []),
221
- ),
222
- _yield_write(
223
- proc.stderr,
224
- buffer,
225
- stderr,
226
- *([sys.stderr] if print or print_stderr else []),
227
- ),
211
+ _yield_write(proc.stdout, "proc.stdout", *stdout_outputs),
212
+ _yield_write(proc.stderr, "proc.stderr", *stderr_outputs),
228
213
  ):
214
+ if input is not None:
215
+ _ = proc.stdin.write(input)
216
+ proc.stdin.flush()
217
+ proc.stdin.close()
229
218
  return_code = proc.wait()
230
219
  match return_code, return_ or return_stdout, return_ or return_stderr:
231
220
  case 0, True, True:
@@ -247,14 +236,14 @@ def run(
247
236
  if logger is not None:
248
237
  msg = strip_and_dedent(f"""
249
238
  'run' failed with:
250
- - cmd = {cmd}
251
- - cmds = {cmds_or_args}
252
- - bash = {bash}
253
- - user = {user}
254
- - executable = {executable}
255
- - shell = {shell}
256
- - cwd = {cwd}
257
- - env = {env}
239
+ - cmd = {cmd}
240
+ - cmds_or_args = {cmds_or_args}
241
+ - user = {user}
242
+ - executable = {executable}
243
+ - shell = {shell}
244
+ - cwd = {cwd}
245
+ - env = {env}
246
+ - input = {input}
258
247
 
259
248
  -- stdout ---------------------------------------------------------------------
260
249
  {stdout_text}-------------------------------------------------------------------------------
@@ -270,8 +259,8 @@ def run(
270
259
 
271
260
 
272
261
  @contextmanager
273
- def _yield_write(input_: IO[str], /, *outputs: IO[str]) -> Iterator[None]:
274
- thread = Thread(target=_run_target, args=(input_, *outputs), daemon=True)
262
+ def _yield_write(input_: IO[str], desc: str, /, *outputs: IO[str]) -> Iterator[None]:
263
+ thread = Thread(target=_run_target, args=(input_, desc, *outputs), daemon=True)
275
264
  thread.start()
276
265
  try:
277
266
  yield
@@ -279,23 +268,139 @@ def _yield_write(input_: IO[str], /, *outputs: IO[str]) -> Iterator[None]:
279
268
  thread.join()
280
269
 
281
270
 
282
- def _run_target(input_: IO[str], /, *outputs: IO[str]) -> None:
283
- with input_:
284
- for line in iter(input_.readline, ""):
285
- for output in outputs:
286
- _ = output.write(line)
271
+ def _run_target(input_: IO[str], desc: str, /, *outputs: IO[str]) -> None:
272
+ try:
273
+ with input_:
274
+ for text in iter(input_.readline, ""):
275
+ _write_to_streams(text, *outputs)
276
+ except ValueError:
277
+ _ = sys.stderr.write(f"Failed to write to {desc!r}...")
278
+ raise
279
+
280
+
281
+ def _write_to_streams(text: str, /, *outputs: IO[str]) -> None:
282
+ for output in outputs:
283
+ _ = output.write(text)
284
+
285
+
286
+ @overload
287
+ def ssh(
288
+ user: str,
289
+ hostname: str,
290
+ /,
291
+ *cmd_and_cmds_or_args: str,
292
+ batch_mode: bool = True,
293
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
294
+ strict_host_key_checking: bool = True,
295
+ input: str | None = None,
296
+ print: bool = False,
297
+ print_stdout: bool = False,
298
+ print_stderr: bool = False,
299
+ return_: Literal[True],
300
+ return_stdout: bool = False,
301
+ return_stderr: bool = False,
302
+ logger: LoggerLike | None = None,
303
+ ) -> str: ...
304
+ @overload
305
+ def ssh(
306
+ user: str,
307
+ hostname: str,
308
+ /,
309
+ *cmd_and_cmds_or_args: str,
310
+ batch_mode: bool = True,
311
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
312
+ strict_host_key_checking: bool = True,
313
+ input: str | None = None,
314
+ print: bool = False,
315
+ print_stdout: bool = False,
316
+ print_stderr: bool = False,
317
+ return_: bool = False,
318
+ return_stdout: Literal[True],
319
+ return_stderr: bool = False,
320
+ logger: LoggerLike | None = None,
321
+ ) -> str: ...
322
+ @overload
323
+ def ssh(
324
+ user: str,
325
+ hostname: str,
326
+ /,
327
+ *cmd_and_cmds_or_args: str,
328
+ batch_mode: bool = True,
329
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
330
+ strict_host_key_checking: bool = True,
331
+ input: str | None = None,
332
+ print: bool = False,
333
+ print_stdout: bool = False,
334
+ print_stderr: bool = False,
335
+ return_: bool = False,
336
+ return_stdout: bool = False,
337
+ return_stderr: Literal[True],
338
+ logger: LoggerLike | None = None,
339
+ ) -> str: ...
340
+ @overload
341
+ def ssh(
342
+ user: str,
343
+ hostname: str,
344
+ /,
345
+ *cmd_and_cmds_or_args: str,
346
+ batch_mode: bool = True,
347
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
348
+ strict_host_key_checking: bool = True,
349
+ input: str | None = None,
350
+ print: bool = False,
351
+ print_stdout: bool = False,
352
+ print_stderr: bool = False,
353
+ return_: Literal[False] = False,
354
+ return_stdout: Literal[False] = False,
355
+ return_stderr: Literal[False] = False,
356
+ logger: LoggerLike | None = None,
357
+ ) -> None: ...
358
+ def ssh(
359
+ user: str,
360
+ hostname: str,
361
+ /,
362
+ *cmd_and_cmds_or_args: str,
363
+ batch_mode: bool = True,
364
+ host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
365
+ strict_host_key_checking: bool = True,
366
+ input: str | None = None, # noqa: A002
367
+ print: bool = False, # noqa: A002
368
+ print_stdout: bool = False,
369
+ print_stderr: bool = False,
370
+ return_: bool = False,
371
+ return_stdout: bool = False,
372
+ return_stderr: bool = False,
373
+ logger: LoggerLike | None = None,
374
+ ) -> str | None:
375
+ cmd_and_args = ssh_cmd( # skipif-ci
376
+ user,
377
+ hostname,
378
+ *cmd_and_cmds_or_args,
379
+ batch_mode=batch_mode,
380
+ host_key_algorithms=host_key_algorithms,
381
+ strict_host_key_checking=strict_host_key_checking,
382
+ )
383
+ return run( # skipif-ci
384
+ *cmd_and_args,
385
+ input=input,
386
+ print=print,
387
+ print_stdout=print_stdout,
388
+ print_stderr=print_stderr,
389
+ return_=return_,
390
+ return_stdout=return_stdout,
391
+ return_stderr=return_stderr,
392
+ logger=logger,
393
+ )
287
394
 
288
395
 
289
396
  def ssh_cmd(
290
397
  user: str,
291
398
  hostname: str,
292
- cmd: str,
293
399
  /,
294
- *cmds_or_args: str,
400
+ *cmd_and_cmds_or_args: str,
295
401
  batch_mode: bool = True,
296
402
  host_key_algorithms: list[str] = _HOST_KEY_ALGORITHMS,
297
403
  strict_host_key_checking: bool = True,
298
- bash: bool = False,
299
404
  ) -> list[str]:
300
405
  args: list[str] = ["ssh"]
301
406
  if batch_mode:
@@ -303,12 +408,7 @@ def ssh_cmd(
303
408
  args.extend(["-o", f"HostKeyAlgorithms={','.join(host_key_algorithms)}"])
304
409
  if strict_host_key_checking:
305
410
  args.extend(["-o", "StrictHostKeyChecking=yes"])
306
- args.append(f"{user}@{hostname}")
307
- if bash:
308
- args.extend(bash_cmd_and_args(cmd, *cmds_or_args))
309
- else:
310
- args.extend([cmd, *cmds_or_args])
311
- return args
411
+ return [*args, "-T", f"{user}@{hostname}", *cmd_and_cmds_or_args]
312
412
 
313
413
 
314
414
  def sudo_cmd(cmd: str, /, *args: str) -> list[str]:
@@ -320,8 +420,9 @@ def touch_cmd(path: PathLike, /) -> list[str]:
320
420
 
321
421
 
322
422
  __all__ = [
423
+ "BASH_LC",
424
+ "BASH_LS",
323
425
  "MKTEMP_DIR_CMD",
324
- "bash_cmd_and_args",
325
426
  "echo_cmd",
326
427
  "expand_path",
327
428
  "maybe_sudo_cmd",
@@ -329,6 +430,7 @@ __all__ = [
329
430
  "mkdir_cmd",
330
431
  "rm_cmd",
331
432
  "run",
433
+ "ssh",
332
434
  "ssh_cmd",
333
435
  "sudo_cmd",
334
436
  "touch_cmd",