shelltastic 0.6.0__tar.gz → 0.8.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 (24) hide show
  1. {shelltastic-0.6.0 → shelltastic-0.8.0}/PKG-INFO +1 -1
  2. {shelltastic-0.6.0 → shelltastic-0.8.0}/pyproject.toml +1 -1
  3. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/backend/default/common.py +130 -171
  4. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/backend/default/local.py +0 -2
  5. shelltastic-0.8.0/src/shelltastic/backend/default/remote.py +147 -0
  6. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/exception.py +3 -6
  7. shelltastic-0.8.0/src/shelltastic/result.py +111 -0
  8. shelltastic-0.6.0/src/shelltastic/backend/default/remote.py +0 -75
  9. shelltastic-0.6.0/src/shelltastic/result.py +0 -47
  10. {shelltastic-0.6.0 → shelltastic-0.8.0}/LICENSE +0 -0
  11. {shelltastic-0.6.0 → shelltastic-0.8.0}/README.md +0 -0
  12. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/__init__.py +0 -0
  13. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/backend/__init__.py +0 -0
  14. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/backend/base.py +0 -0
  15. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/backend/default/__init__.py +0 -0
  16. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/display.py +0 -0
  17. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/enum.py +0 -0
  18. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/frontend/__init__.py +0 -0
  19. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/frontend/common.py +0 -0
  20. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/frontend/git.py +0 -0
  21. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/frontend/scp.py +0 -0
  22. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/frontend/shell.py +0 -0
  23. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/host.py +0 -0
  24. {shelltastic-0.6.0 → shelltastic-0.8.0}/src/shelltastic/io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shelltastic
3
- Version: 0.6.0
3
+ Version: 0.8.0
4
4
  Summary: A fantastic shell command runner for python
5
5
  Author: Bearmine
6
6
  License-Expression: MPL-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shelltastic"
3
- version = "0.6.0"
3
+ version = "0.8.0"
4
4
  description = "A fantastic shell command runner for python"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import logging
4
2
  import os
5
3
  import shlex
@@ -7,8 +5,10 @@ import subprocess
7
5
  import time
8
6
  from io import RawIOBase
9
7
  from multiprocessing import Pipe
8
+ from multiprocessing.connection import Connection
10
9
  from multiprocessing.pool import ThreadPool
11
- from typing import IO, TYPE_CHECKING, Literal
10
+ from pathlib import Path
11
+ from typing import IO, Literal
12
12
 
13
13
  from shelltastic import display
14
14
  from shelltastic.backend.base import ShellBackend
@@ -16,10 +16,6 @@ from shelltastic.enum import CaptureMode, DisplayMode
16
16
  from shelltastic.io import PipeReader
17
17
  from shelltastic.result import ShellResult, ShellResultAsync
18
18
 
19
- if TYPE_CHECKING:
20
- from multiprocessing.connection import Connection
21
- from pathlib import Path
22
-
23
19
  LOGGER = logging.getLogger(__name__)
24
20
 
25
21
 
@@ -91,45 +87,27 @@ class IOTimeoutStream:
91
87
  return return_val
92
88
 
93
89
 
94
- def _output_and_collect(
95
- io: IO[bytes] | None, display: display.IODisplay | None, collect: bool = True
96
- ) -> bytes | None:
97
- if io is None:
98
- return None
99
-
100
- if collect:
101
- if display:
102
- return_bytes: bytes = b""
103
- for line in IOTimeoutStream(io):
104
- return_bytes += line
105
- display.printbytes(line)
106
- return return_bytes
107
- return io.read()
108
-
109
- if display:
110
- for line in IOTimeoutStream(io):
111
- display.printbytes(line)
112
-
113
- return None
114
-
115
-
116
90
  def _output_and_pipe(
117
91
  io: IO[bytes] | None, display: display.IODisplay | None, pipe: Connection | None
118
92
  ) -> None:
119
- if io is None:
120
- return
93
+ try:
94
+ if io is None:
95
+ return
96
+
97
+ if pipe:
98
+ if display:
99
+ for line in IOTimeoutStream(io):
100
+ pipe.send_bytes(line)
101
+ display.printbytes(line)
102
+ for line in IOTimeoutStream(io):
103
+ pipe.send_bytes(line)
121
104
 
122
- if pipe:
123
105
  if display:
124
106
  for line in IOTimeoutStream(io):
125
- pipe.send_bytes(line)
126
107
  display.printbytes(line)
127
- for line in IOTimeoutStream(io):
128
- pipe.send_bytes(line)
129
-
130
- if display:
131
- for line in IOTimeoutStream(io):
132
- display.printbytes(line)
108
+ finally:
109
+ if pipe:
110
+ pipe.close()
133
111
 
134
112
 
135
113
  def _determine_subprocess_output_mode(
@@ -179,55 +157,121 @@ def _determine_subprocess_output_mode(
179
157
  return subprocess.PIPE
180
158
 
181
159
 
182
- class CommonShellAcyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None]):
160
+ class CommonShellAsyncResult(ShellResultAsync[RawIOBase | None, RawIOBase | None]):
183
161
  def __init__(
184
162
  self,
185
163
  cmd: str | list[str],
186
164
  popen: subprocess.Popen,
165
+ stdout_capture: CaptureMode | None,
166
+ stderr_capture: CaptureMode | None,
187
167
  stdout_display: display.IODisplay | None,
188
168
  stderr_display: display.IODisplay | None,
169
+ check: bool,
189
170
  ) -> None:
190
- stdout_rcv, stdout_snd = Pipe(False)
191
- stderr_rcv, stderr_snd = Pipe(False)
192
- super().__init__(cmd, PipeReader(stdout_rcv), PipeReader(stderr_rcv))
193
- self._popen = popen
194
- self._display_pool = ThreadPool(2)
195
-
196
- # Print out and capture stdout
197
- self._stdout_result = self._display_pool.apply_async(
198
- _output_and_pipe,
199
- args=[
200
- popen.stdout,
201
- stdout_display,
202
- stdout_snd,
203
- ],
204
- )
171
+ _display_pool = None
172
+ _stdout_async_result = None
173
+ _stderr_async_result = None
174
+
175
+ if popen.stdout:
176
+ # If stdout is PIPE, then setup reader
177
+ if stdout_capture == CaptureMode.PIPE:
178
+ stdout_rcv, stdout_snd = Pipe(False)
179
+ stdout = PipeReader(stdout_rcv)
180
+ else:
181
+ stdout_snd = None
182
+ stdout = None
183
+
184
+ if stdout_snd or stdout_display:
185
+ if _display_pool is None:
186
+ _display_pool = ThreadPool(2)
187
+
188
+ _stdout_async_result = _display_pool.apply_async(
189
+ _output_and_pipe,
190
+ args=[
191
+ popen.stdout,
192
+ stdout_display,
193
+ stdout_snd,
194
+ ],
195
+ )
196
+ else:
197
+ stdout = None
205
198
 
206
- # Print out and capture stderr
207
- self._stderr_result = self._display_pool.apply_async(
208
- _output_and_pipe,
209
- args=[
210
- popen.stderr,
211
- stderr_display,
212
- stderr_snd,
213
- ],
214
- )
199
+ if popen.stderr:
200
+ if stderr_capture == CaptureMode.PIPE:
201
+ stderr_rcv, stderr_snd = Pipe(False)
202
+ stderr = PipeReader(stderr_rcv)
203
+ else:
204
+ stderr_snd = None
205
+ stderr = None
206
+
207
+ if stderr_snd or stderr_display:
208
+ if _display_pool is None:
209
+ _display_pool = ThreadPool(2)
210
+
211
+ _stderr_async_result = _display_pool.apply_async(
212
+ _output_and_pipe,
213
+ args=[
214
+ popen.stderr,
215
+ stderr_display,
216
+ stderr_snd,
217
+ ],
218
+ )
219
+ else:
220
+ stderr = None
221
+
222
+ super().__init__(cmd, stdout, stderr)
223
+
224
+ self._display_pool = _display_pool
225
+ self._stdout_async_result = _stdout_async_result
226
+ self._stderr_async_result = _stderr_async_result
227
+ self._popen = popen
228
+ self._check = check
215
229
 
216
230
  def __exit__(self, exc_type, exc, tb):
231
+ # Wait for command to finish before closing everything
232
+ self.wait()
233
+
234
+ # Close popen
217
235
  self._popen.__exit__(exc_type, exc, tb)
218
- self._display_pool.__exit__(exc_type, exc, tb)
236
+
237
+ if self._display_pool:
238
+ self._display_pool.__exit__(exc_type, exc, tb)
239
+
240
+ # Close stdout and stderr
241
+ if self.stdout is not None:
242
+ self.stdout.close()
243
+ if self.stderr is not None:
244
+ self.stderr.close()
219
245
 
220
246
  def close(self) -> None:
221
247
  self.__exit__(None, None, None)
222
248
 
249
+ def terminate(self) -> None:
250
+ self._popen.terminate()
251
+ self.close()
252
+
223
253
  def result(self) -> ShellResult[RawIOBase | None, RawIOBase | None]:
224
- return ShellResult(
254
+ # Get result
255
+ result = ShellResult(
225
256
  cmd=self.cmd,
226
257
  returncode=self._popen.wait(),
227
258
  stdout=self.stdout,
228
259
  stderr=self.stderr,
229
260
  )
230
261
 
262
+ # Wait for our async readers to be complete
263
+ if self._stdout_async_result:
264
+ self._stdout_async_result.wait()
265
+
266
+ if self._stderr_async_result:
267
+ self._stderr_async_result.wait()
268
+
269
+ # Check if set
270
+ if self._check:
271
+ result.check_returncode()
272
+
273
+ return result
274
+
231
275
 
232
276
  class CommonDefaultBackend(ShellBackend):
233
277
  __slots__ = ()
@@ -245,106 +289,18 @@ class CommonDefaultBackend(ShellBackend):
245
289
  echo_cmd: DisplayMode | display.IODisplay | bool | None = None,
246
290
  **kwargs,
247
291
  ) -> ShellResult[bytes | None, bytes | None]:
248
- if kwargs:
249
- raise TypeError(f"Uknown argument/s {list(kwargs.keys())}")
250
-
251
- # If cmd is a str, run as shell
252
- shell = isinstance(cmd, str)
253
-
254
- # Echo command based on setting
255
- # Defaults to DEBUG_LOG
256
- # False to disable
257
- if isinstance(echo_cmd, bool):
258
- echo_cmd = DisplayMode.STDOUT if echo_cmd else DisplayMode.DEVNULL
259
-
260
- if echo_cmd is None:
261
- echo_cmd = DisplayMode.DEBUG_LOG
262
-
263
- if isinstance(echo_cmd, display.IODisplay):
264
- echo_display = echo_cmd
265
- else:
266
- echo_display = display.from_mode(echo_cmd)
267
-
268
- if isinstance(cmd, str):
269
- echo_display.printline("%s> %s", cwd or "", cmd)
270
- else:
271
- echo_display.printline("%s> %s", cwd or "", shlex.join(cmd))
272
-
273
- # Determine PIPE mode for stdout
274
- stdout_sub_mode = _determine_subprocess_output_mode(
275
- stdout, stdout_display, "stdout"
276
- )
277
-
278
- # Determine PIPE mode for stderr
279
- stderr_sub_mode = _determine_subprocess_output_mode(
280
- stderr, stderr_display, "stderr"
281
- )
282
-
283
- if stdout_display is None:
284
- stdout_display = (
285
- DisplayMode.STDOUT if stdout is None else DisplayMode.DEVNULL
286
- )
287
-
288
- if stderr_display is None:
289
- stderr_display = (
290
- DisplayMode.STDERR if stderr is None else DisplayMode.DEVNULL
291
- )
292
-
293
- # Run subprocess command
294
- with (
295
- subprocess.Popen(
296
- cmd,
297
- shell=shell,
298
- stdout=stdout_sub_mode,
299
- stderr=stderr_sub_mode,
300
- cwd=cwd,
301
- ) as popen,
302
- ThreadPool(2) as display_pool,
303
- ):
304
- # Print out and capture stdout
305
- stdout_result = display_pool.apply_async(
306
- _output_and_collect,
307
- args=[
308
- popen.stdout,
309
- (
310
- stdout_display
311
- if isinstance(stdout_display, display.IODisplay)
312
- else display.from_mode(stdout_display)
313
- ),
314
- stdout is not None and stdout != CaptureMode.DEVNULL,
315
- ],
316
- )
317
-
318
- # Print out and capture stderr
319
- stderr_result = display_pool.apply_async(
320
- _output_and_collect,
321
- args=[
322
- popen.stderr,
323
- (
324
- stderr_display
325
- if isinstance(stderr_display, display.IODisplay)
326
- else display.from_mode(stderr_display)
327
- ),
328
- stderr is not None and stderr != CaptureMode.DEVNULL,
329
- ],
330
- )
331
-
332
- # Wait on command completion
333
- returncode = popen.wait()
334
-
335
- # Build our result
336
- result = ShellResult(
337
- cmd=cmd,
338
- stdout=stdout_result.get(),
339
- stderr=stderr_result.get(),
340
- returncode=returncode,
341
- )
342
-
343
- # If check is True, raise exception if returncode is not 0
344
- if check:
345
- result.check_returncode()
346
-
347
- return result
292
+ with self.run_async(
293
+ cmd,
294
+ check=check,
295
+ stdout=stdout,
296
+ stderr=stderr,
297
+ stdout_display=stdout_display,
298
+ stderr_display=stderr_display,
299
+ cwd=cwd,
300
+ echo_cmd=echo_cmd,
301
+ **kwargs,
302
+ ) as running_cmd:
303
+ return running_cmd.result_bytes()
348
304
 
349
305
  def run_async(
350
306
  self,
@@ -413,17 +369,20 @@ class CommonDefaultBackend(ShellBackend):
413
369
  cwd=cwd,
414
370
  )
415
371
 
416
- return CommonShellAcyncResult(
417
- cmd,
418
- popen,
419
- (
372
+ return CommonShellAsyncResult(
373
+ cmd=cmd,
374
+ popen=popen,
375
+ stdout_capture=stdout,
376
+ stderr_capture=stderr,
377
+ stdout_display=(
420
378
  stdout_display
421
379
  if isinstance(stdout_display, display.IODisplay)
422
380
  else display.from_mode(stdout_display)
423
381
  ),
424
- (
382
+ stderr_display=(
425
383
  stderr_display
426
384
  if isinstance(stderr_display, display.IODisplay)
427
385
  else display.from_mode(stderr_display)
428
386
  ),
387
+ check=check,
429
388
  )
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  import logging
4
2
 
5
3
  from shelltastic.backend.base import LocalShellBackend
@@ -0,0 +1,147 @@
1
+ import logging
2
+ import shlex
3
+ from io import RawIOBase
4
+ from pathlib import Path
5
+ from subprocess import Popen
6
+
7
+ from shelltastic.backend.base import RemoteShellBackend
8
+ from shelltastic.backend.default.common import (
9
+ CommonDefaultBackend,
10
+ CommonShellAsyncResult,
11
+ )
12
+ from shelltastic.display import IODisplay
13
+ from shelltastic.enum import CaptureMode
14
+ from shelltastic.exception import ShellException, SSHConnectionError
15
+ from shelltastic.host import Host
16
+ from shelltastic.result import ShellResult, ShellResultAsync
17
+
18
+ LOGGER = logging.getLogger(__name__)
19
+
20
+
21
+ def _monkey_patch_async_result(
22
+ async_result: ShellResultAsync[RawIOBase | None, RawIOBase | None], host: Host
23
+ ):
24
+ result_fn = async_result.result
25
+
26
+ def patched_result_fn():
27
+ # Extra check for SSH Errors
28
+ try:
29
+ result = result_fn()
30
+ if (
31
+ result.returncode == 255
32
+ ): # 255 is what ssh returns if it has an error with ssh itself
33
+ result = result.to_result_bytes()
34
+ raise SSHConnectionError(
35
+ host.hostname, result.returncode, result.stdout, result.stderr
36
+ )
37
+ return result
38
+ except ShellException as ex:
39
+ if ex.returncode == 255:
40
+ raise SSHConnectionError(
41
+ host.hostname, ex.returncode, ex.stdout, ex.stderr
42
+ ) from ex
43
+ raise
44
+
45
+ async_result.result = patched_result_fn
46
+
47
+ return async_result
48
+
49
+
50
+ class RemoteShellAcyncResult(CommonShellAsyncResult):
51
+ def __init__(
52
+ self,
53
+ cmd: str | list[str],
54
+ popen: Popen,
55
+ stdout_capture: CaptureMode | None,
56
+ stderr_capture: CaptureMode | None,
57
+ stdout_display: IODisplay | None,
58
+ stderr_display: IODisplay | None,
59
+ check: bool,
60
+ host: Host,
61
+ ) -> None:
62
+ super().__init__(
63
+ cmd,
64
+ popen,
65
+ stdout_capture,
66
+ stderr_capture,
67
+ stdout_display,
68
+ stderr_display,
69
+ check,
70
+ )
71
+ self.host = host
72
+
73
+ def result(self) -> ShellResult[RawIOBase | None, RawIOBase | None]:
74
+ # Extra check for SSH Errors
75
+ try:
76
+ result = super().result()
77
+ if (
78
+ result.returncode == 255
79
+ ): # 255 is what ssh returns if it has an error with ssh itself
80
+ result = result.to_result_bytes()
81
+ raise SSHConnectionError(
82
+ self.host.hostname, result.returncode, result.stdout, result.stderr
83
+ )
84
+ return result
85
+ except ShellException as ex:
86
+ if ex.returncode == 255:
87
+ raise SSHConnectionError(
88
+ self.host.hostname, ex.returncode, ex.stdout, ex.stderr
89
+ ) from ex
90
+ raise
91
+
92
+
93
+ class DefaultRemoteBackend(CommonDefaultBackend, RemoteShellBackend):
94
+ def __init__(self, host: Host) -> None:
95
+ super().__init__()
96
+ self.host: Host = host
97
+
98
+ @staticmethod
99
+ def for_host(host: Host) -> RemoteShellBackend:
100
+ return DefaultRemoteBackend(host)
101
+
102
+ def run(
103
+ self,
104
+ cmd: str | list[str],
105
+ *,
106
+ subshell: bool = True,
107
+ cwd: str | Path | None = None,
108
+ **kwargs,
109
+ ) -> ShellResult:
110
+ """
111
+ Execute a remote command using SSH.
112
+ """
113
+ with self.run_async(cmd, subshell=subshell, cwd=cwd, **kwargs) as running_cmd:
114
+ return running_cmd.result_bytes()
115
+
116
+ def run_async(
117
+ self,
118
+ cmd: str | list[str],
119
+ *,
120
+ subshell: bool = True,
121
+ cwd: str | Path | None = None,
122
+ **kwargs,
123
+ ) -> ShellResultAsync[RawIOBase | None, RawIOBase | None]:
124
+ if isinstance(cmd, list):
125
+ cmd = shlex.join(cmd)
126
+
127
+ # Wrap user command in sh -c call
128
+ if subshell:
129
+ cmd = shlex.join(["bash", "-c", cmd])
130
+
131
+ # If we're redirecting stderr to stdout,
132
+ # add the same redirect inside the ssh command.
133
+ if kwargs.get("stderr") == CaptureMode.STDOUT:
134
+ cmd = cmd + " 2>&1"
135
+
136
+ # cd into cwd first if cwd is set
137
+ if cwd:
138
+ cmd = shlex.join(["cd", str(cwd)]) + " && " + cmd
139
+
140
+ port_flag = ["-p", str(self.host.port)] if self.host.port else []
141
+
142
+ return _monkey_patch_async_result(
143
+ super().run_async(
144
+ ["ssh", *port_flag, self.host.host_specifier(), cmd], **kwargs
145
+ ),
146
+ self.host,
147
+ )
@@ -1,6 +1,3 @@
1
- from io import RawIOBase
2
-
3
-
4
1
  class SSHConnectionError(Exception):
5
2
  def __init__(
6
3
  self, hostname: str, returncode: int, stdout: bytes | None, stderr: bytes | None
@@ -15,13 +12,13 @@ class SSHConnectionError(Exception):
15
12
  self.stderr = stderr
16
13
 
17
14
 
18
- class ShellException[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None](Exception):
15
+ class ShellException(Exception):
19
16
  def __init__(
20
17
  self,
21
18
  cmd: str | list[str],
22
19
  returncode: int,
23
- stdout: SO,
24
- stderr: SE,
20
+ stdout: bytes | None,
21
+ stderr: bytes | None,
25
22
  ):
26
23
  super().__init__(f"Shell execution failed with returncode {returncode}")
27
24
  self.cmd = cmd
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from io import RawIOBase
6
+
7
+ from shelltastic.exception import ShellException
8
+
9
+
10
+ @dataclass
11
+ class ShellResult[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None]:
12
+ cmd: str | list[str]
13
+ returncode: int
14
+ stdout: SO
15
+ stderr: SE
16
+
17
+ def check_returncode(self):
18
+ """If returncode is not 0, raise ShellException"""
19
+ if self.returncode != 0:
20
+ result = self.to_result_bytes()
21
+ raise ShellException(self.cmd, self.returncode, result.stdout, result.stderr)
22
+
23
+ def to_result_bytes(self) -> ShellResult[bytes | None, bytes | None]:
24
+ stdout = self.stdout.readall() if isinstance(self.stdout, RawIOBase) else self.stdout
25
+
26
+ stderr = self.stderr.readall() if isinstance(self.stderr, RawIOBase) else self.stderr
27
+
28
+ return ShellResult(self.cmd, self.returncode, stdout, stderr)
29
+
30
+
31
+ class ShellResultAsync[SO: RawIOBase | None, SE: RawIOBase | None](ABC):
32
+ """
33
+ An asynchronsly running shell command.
34
+
35
+ cmd: String for the running shell command.
36
+
37
+ stdout: Bytes stream for stdout
38
+
39
+ stderr: Bytes stream for stderr
40
+ """
41
+
42
+ def __init__(self, cmd: str | list[str], stdout: SO, stderr: SE) -> None:
43
+ self.cmd = cmd
44
+ """The running command"""
45
+
46
+ self.stdout = stdout
47
+ """Byte based IO stream for stdout"""
48
+
49
+ self.stderr = stderr
50
+ """Byte based IO stream for stderr"""
51
+
52
+ def __enter__(self):
53
+ return self
54
+
55
+ def __exit__(self, exc_type, exc, tb):
56
+ self.close()
57
+
58
+ def close(self) -> None:
59
+ """Close the running command."""
60
+ return
61
+
62
+ def terminate(self) -> None:
63
+ """Terminate the running command"""
64
+ raise NotImplementedError()
65
+
66
+ @abstractmethod
67
+ def result(self) -> ShellResult[SO, SE]:
68
+ """
69
+ Blocking
70
+
71
+ Wait for the command to finish and return a ShellResult with the outcome.
72
+ """
73
+ raise NotImplementedError()
74
+
75
+ def wait(self) -> None:
76
+ """
77
+ Blocking
78
+
79
+ Wait for the command to finish.
80
+ """
81
+ self.result()
82
+
83
+ def returncode(self) -> int:
84
+ """
85
+ Blocking
86
+
87
+ Get the returncode of the command.
88
+ """
89
+ return self.result().returncode
90
+
91
+ def check_returncode(self):
92
+ """
93
+ Blocking
94
+
95
+ Raise a ShellException if the returncode is not zero.
96
+ """
97
+ self.result().check_returncode()
98
+
99
+ def result_bytes(self) -> ShellResult[bytes | None, bytes | None]:
100
+ result = self.result()
101
+ stdout = result.stdout
102
+ stdout_bytes = stdout.readall() if stdout is not None else None
103
+ stderr = result.stderr
104
+ stderr_bytes = stderr.readall() if stderr is not None else None
105
+
106
+ return ShellResult(
107
+ cmd=result.cmd,
108
+ returncode=result.returncode,
109
+ stdout=stdout_bytes,
110
+ stderr=stderr_bytes,
111
+ )
@@ -1,75 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- import shlex
5
- from typing import TYPE_CHECKING
6
-
7
- from shelltastic.backend.base import RemoteShellBackend
8
- from shelltastic.backend.default.common import CommonDefaultBackend
9
- from shelltastic.enum import CaptureMode
10
- from shelltastic.exception import ShellException, SSHConnectionError
11
-
12
- if TYPE_CHECKING:
13
- from pathlib import Path
14
-
15
- from shelltastic.host import Host
16
- from shelltastic.result import ShellResult
17
-
18
- LOGGER = logging.getLogger(__name__)
19
-
20
-
21
- class DefaultRemoteBackend(CommonDefaultBackend, RemoteShellBackend):
22
- def __init__(self, host: Host) -> None:
23
- super().__init__()
24
- self.host: Host = host
25
-
26
- @staticmethod
27
- def for_host(host: Host) -> RemoteShellBackend:
28
- return DefaultRemoteBackend(host)
29
-
30
- def run(
31
- self,
32
- cmd: str | list[str],
33
- *,
34
- subshell: bool = True,
35
- cwd: str | Path | None = None,
36
- **kwargs,
37
- ) -> ShellResult:
38
- """
39
- Execute a remote command using SSH.
40
- """
41
- if isinstance(cmd, list):
42
- cmd = shlex.join(cmd)
43
-
44
- # Wrap user command in sh -c call
45
- if subshell:
46
- cmd = shlex.join(["bash", "-c", cmd])
47
-
48
- # If we're redirecting stderr to stdout,
49
- # add the same redirect inside the ssh command.
50
- if kwargs.get("stderr") == CaptureMode.STDOUT:
51
- cmd = cmd + " 2>&1"
52
-
53
- # cd into cwd first if cwd is set
54
- if cwd:
55
- cmd = shlex.join(["cd", str(cwd)]) + " && " + cmd
56
-
57
- port_flag = ["-p", str(self.host.port)] if self.host.port else []
58
-
59
- try:
60
- result = super().run(
61
- ["ssh", *port_flag, self.host.host_specifier(), cmd], **kwargs
62
- )
63
- if (
64
- result.returncode == 255
65
- ): # 255 is what ssh returns if it has an error with ssh itself
66
- raise SSHConnectionError(
67
- self.host.hostname, result.returncode, result.stdout, result.stderr
68
- )
69
- return result
70
- except ShellException as ex:
71
- if ex.returncode == 255:
72
- raise SSHConnectionError(
73
- self.host.hostname, ex.returncode, ex.stdout, ex.stderr
74
- ) from ex
75
- raise
@@ -1,47 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- from dataclasses import dataclass
3
- from io import RawIOBase
4
-
5
- from shelltastic.exception import ShellException
6
-
7
-
8
- @dataclass
9
- class ShellResult[SO: RawIOBase | bytes | None, SE: RawIOBase | bytes | None]:
10
- cmd: str | list[str]
11
- returncode: int
12
- stdout: SO
13
- stderr: SE
14
-
15
- def check_returncode(self):
16
- """If returncode is not 0, raise ShellException"""
17
- if self.returncode != 0:
18
- raise ShellException(self.cmd, self.returncode, self.stdout, self.stderr)
19
-
20
-
21
- class ShellResultAsync[SO: RawIOBase | None, SE: RawIOBase | None](ABC):
22
- def __init__(self, cmd: str | list[str], stdout: SO, stderr: SE) -> None:
23
- self.cmd = cmd
24
- self.stdout = stdout
25
- self.stderr = stderr
26
-
27
- def __enter__(self):
28
- return self
29
-
30
- def __exit__(self, exc_type, exc, tb):
31
- self.close()
32
-
33
- def close(self) -> None:
34
- return
35
-
36
- @abstractmethod
37
- def result(self) -> ShellResult[SO, SE]:
38
- raise NotImplementedError()
39
-
40
- def wait(self) -> None:
41
- self.result()
42
-
43
- def returncode(self) -> int:
44
- return self.result().returncode
45
-
46
- def check_returncode(self):
47
- self.result().check_returncode()
File without changes
File without changes