shelltastic 0.3.1__tar.gz → 0.4.1__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.
- {shelltastic-0.3.1 → shelltastic-0.4.1}/PKG-INFO +1 -1
- {shelltastic-0.3.1 → shelltastic-0.4.1}/pyproject.toml +1 -1
- shelltastic-0.4.1/src/shelltastic/__init__.py +21 -0
- shelltastic-0.4.1/src/shelltastic/backend/__init__.py +35 -0
- shelltastic-0.4.1/src/shelltastic/backend/base.py +47 -0
- shelltastic-0.4.1/src/shelltastic/backend/default/common.py +196 -0
- shelltastic-0.4.1/src/shelltastic/backend/default/local.py +14 -0
- shelltastic-0.4.1/src/shelltastic/backend/default/remote.py +65 -0
- shelltastic-0.4.1/src/shelltastic/display.py +99 -0
- shelltastic-0.4.1/src/shelltastic/enum.py +23 -0
- {shelltastic-0.3.1 → shelltastic-0.4.1}/src/shelltastic/exception.py +15 -0
- shelltastic-0.4.1/src/shelltastic/frontend/__init__.py +0 -0
- {shelltastic-0.3.1 → shelltastic-0.4.1}/src/shelltastic/frontend/git.py +14 -11
- {shelltastic-0.3.1 → shelltastic-0.4.1}/src/shelltastic/frontend/scp.py +6 -6
- {shelltastic-0.3.1 → shelltastic-0.4.1}/src/shelltastic/frontend/shell.py +23 -15
- shelltastic-0.4.1/src/shelltastic/result.py +16 -0
- shelltastic-0.3.1/src/shelltastic/__init__.py +0 -9
- shelltastic-0.3.1/src/shelltastic/backend/__init__.py +0 -119
- shelltastic-0.3.1/src/shelltastic/backend/base.py +0 -35
- shelltastic-0.3.1/src/shelltastic/enum.py +0 -12
- {shelltastic-0.3.1 → shelltastic-0.4.1}/LICENSE +0 -0
- {shelltastic-0.3.1 → shelltastic-0.4.1}/README.md +0 -0
- {shelltastic-0.3.1/src/shelltastic/frontend → shelltastic-0.4.1/src/shelltastic/backend/default}/__init__.py +0 -0
- {shelltastic-0.3.1 → shelltastic-0.4.1}/src/shelltastic/frontend/common.py +0 -0
- {shelltastic-0.3.1 → shelltastic-0.4.1}/src/shelltastic/host.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from shelltastic.enum import CaptureMode, DisplayMode, SystemType
|
|
2
|
+
from shelltastic.frontend.git import LocalGitFrontend
|
|
3
|
+
from shelltastic.frontend.scp import SCP
|
|
4
|
+
from shelltastic.frontend.shell import LocalShellFrontend
|
|
5
|
+
from shelltastic.host import Host
|
|
6
|
+
from shelltastic.result import ShellResult
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"shell",
|
|
10
|
+
"scp",
|
|
11
|
+
"git",
|
|
12
|
+
"DisplayMode",
|
|
13
|
+
"CaptureMode",
|
|
14
|
+
"SystemType",
|
|
15
|
+
"Host",
|
|
16
|
+
"ShellResult",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
shell = LocalShellFrontend()
|
|
20
|
+
scp = SCP()
|
|
21
|
+
git = LocalGitFrontend()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from shelltastic.backend.base import LocalShellBackend, RemoteShellBackend
|
|
4
|
+
from shelltastic.backend.default.local import DefaultLocalBackend
|
|
5
|
+
from shelltastic.backend.default.remote import DefaultRemoteBackend
|
|
6
|
+
from shelltastic.host import Host
|
|
7
|
+
|
|
8
|
+
_default_local_backend: Type[LocalShellBackend] = DefaultLocalBackend
|
|
9
|
+
_default_remote_backend: Type[RemoteShellBackend] = DefaultRemoteBackend
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_default_local_backend(backend: Type[LocalShellBackend]):
|
|
13
|
+
global _default_local_backend
|
|
14
|
+
_default_local_backend = backend
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_default_remote_backend(backend: Type[RemoteShellBackend]):
|
|
18
|
+
global _default_remote_backend
|
|
19
|
+
_default_remote_backend = backend
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_local_backend(
|
|
23
|
+
*, backend: Type[LocalShellBackend] | None = None
|
|
24
|
+
) -> LocalShellBackend:
|
|
25
|
+
if backend:
|
|
26
|
+
return backend.local()
|
|
27
|
+
return _default_local_backend.local()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_remote_backend(
|
|
31
|
+
host: Host, *, backend: Type[RemoteShellBackend] | None = None
|
|
32
|
+
) -> RemoteShellBackend:
|
|
33
|
+
if backend:
|
|
34
|
+
return backend.for_host(host)
|
|
35
|
+
return _default_remote_backend.for_host(host)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from shelltastic.display import IODisplay
|
|
7
|
+
from shelltastic.enum import CaptureMode, DisplayMode
|
|
8
|
+
from shelltastic.host import Host
|
|
9
|
+
from shelltastic.result import ShellResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ShellBackend(ABC):
|
|
13
|
+
__slots__ = ()
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def run(
|
|
17
|
+
self,
|
|
18
|
+
cmd: str | list[str],
|
|
19
|
+
*,
|
|
20
|
+
check: bool = True,
|
|
21
|
+
stdout: CaptureMode | None = None,
|
|
22
|
+
stderr: CaptureMode | None = None,
|
|
23
|
+
stdout_display: DisplayMode | IODisplay | None = None,
|
|
24
|
+
stderr_display: DisplayMode | IODisplay | None = None,
|
|
25
|
+
cwd: str | Path | None = None,
|
|
26
|
+
echo_cmd: DisplayMode | IODisplay | bool | None = None,
|
|
27
|
+
**kwargs,
|
|
28
|
+
) -> ShellResult:
|
|
29
|
+
raise NotImplementedError()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LocalShellBackend(ShellBackend):
|
|
33
|
+
__slots__ = ()
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def local() -> LocalShellBackend:
|
|
38
|
+
raise NotImplementedError()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RemoteShellBackend(ShellBackend):
|
|
42
|
+
__slots__ = ()
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def for_host(host: Host) -> RemoteShellBackend:
|
|
47
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import pathlib
|
|
5
|
+
import shlex
|
|
6
|
+
import subprocess
|
|
7
|
+
from multiprocessing.pool import ThreadPool
|
|
8
|
+
from typing import IO, Literal
|
|
9
|
+
|
|
10
|
+
from shelltastic import display
|
|
11
|
+
from shelltastic.backend.base import ShellBackend
|
|
12
|
+
from shelltastic.enum import CaptureMode, DisplayMode
|
|
13
|
+
from shelltastic.result import ShellResult
|
|
14
|
+
|
|
15
|
+
LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _output_and_collect(
|
|
19
|
+
io: IO[bytes] | None, display: display.IODisplay | None, collect: bool = True
|
|
20
|
+
) -> bytes | None:
|
|
21
|
+
if io is None:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
if collect:
|
|
25
|
+
if display:
|
|
26
|
+
return_bytes = bytes()
|
|
27
|
+
for line in io:
|
|
28
|
+
return_bytes += line
|
|
29
|
+
display.printbytes(line)
|
|
30
|
+
return return_bytes
|
|
31
|
+
else:
|
|
32
|
+
return io.read()
|
|
33
|
+
else:
|
|
34
|
+
if display:
|
|
35
|
+
for line in io:
|
|
36
|
+
display.printbytes(line)
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _determine_subprocess_output_mode(
|
|
41
|
+
capture_mode: CaptureMode | None,
|
|
42
|
+
display_mode: DisplayMode | display.IODisplay | None,
|
|
43
|
+
output_name: Literal["stdout", "stderr"],
|
|
44
|
+
) -> int | None:
|
|
45
|
+
# If DisplayMode is None, we act like subpress does
|
|
46
|
+
if display_mode is None:
|
|
47
|
+
if capture_mode == CaptureMode.PIPE:
|
|
48
|
+
return subprocess.PIPE
|
|
49
|
+
elif capture_mode == CaptureMode.STDOUT:
|
|
50
|
+
return subprocess.STDOUT
|
|
51
|
+
elif capture_mode == CaptureMode.DEVNULL:
|
|
52
|
+
return subprocess.DEVNULL
|
|
53
|
+
else:
|
|
54
|
+
assert capture_mode is None
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
# Don't print
|
|
58
|
+
if display_mode == DisplayMode.DEVNULL:
|
|
59
|
+
if capture_mode == CaptureMode.PIPE:
|
|
60
|
+
return subprocess.PIPE
|
|
61
|
+
elif capture_mode == CaptureMode.STDOUT:
|
|
62
|
+
if output_name == "stdout":
|
|
63
|
+
# stdout
|
|
64
|
+
return subprocess.PIPE
|
|
65
|
+
else:
|
|
66
|
+
# stderr
|
|
67
|
+
return subprocess.STDOUT
|
|
68
|
+
elif capture_mode == CaptureMode.DEVNULL:
|
|
69
|
+
return subprocess.DEVNULL
|
|
70
|
+
else:
|
|
71
|
+
assert capture_mode is None
|
|
72
|
+
return subprocess.DEVNULL
|
|
73
|
+
|
|
74
|
+
# Print Output
|
|
75
|
+
# display_mode == STDOUT, STDERR, LOG, or is an IODisplay
|
|
76
|
+
if capture_mode == CaptureMode.PIPE:
|
|
77
|
+
return subprocess.PIPE
|
|
78
|
+
elif capture_mode == CaptureMode.STDOUT:
|
|
79
|
+
return subprocess.STDOUT
|
|
80
|
+
elif capture_mode == CaptureMode.DEVNULL:
|
|
81
|
+
# We need to PIPE to be able to print the output
|
|
82
|
+
return subprocess.PIPE
|
|
83
|
+
else:
|
|
84
|
+
assert capture_mode is None
|
|
85
|
+
# We need to PIPE to be able to print the output
|
|
86
|
+
return subprocess.PIPE
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CommonDefaultBackend(ShellBackend):
|
|
90
|
+
__slots__ = ()
|
|
91
|
+
|
|
92
|
+
def run(
|
|
93
|
+
self,
|
|
94
|
+
cmd: str | list[str],
|
|
95
|
+
*,
|
|
96
|
+
check: bool = True,
|
|
97
|
+
stdout: CaptureMode | None = None,
|
|
98
|
+
stderr: CaptureMode | None = None,
|
|
99
|
+
stdout_display: DisplayMode | display.IODisplay | None = None,
|
|
100
|
+
stderr_display: DisplayMode | display.IODisplay | None = None,
|
|
101
|
+
cwd: str | pathlib.Path | None = None,
|
|
102
|
+
echo_cmd: DisplayMode | display.IODisplay | bool | None = None,
|
|
103
|
+
**kwargs,
|
|
104
|
+
) -> ShellResult:
|
|
105
|
+
if kwargs:
|
|
106
|
+
raise TypeError(f"Uknown argument/s {list(kwargs.keys())}")
|
|
107
|
+
|
|
108
|
+
# If cmd is a str, run as shell
|
|
109
|
+
if isinstance(cmd, str):
|
|
110
|
+
shell = True
|
|
111
|
+
else:
|
|
112
|
+
shell = False
|
|
113
|
+
|
|
114
|
+
# Echo command based on setting
|
|
115
|
+
# Defaults to DEBUG_LOG
|
|
116
|
+
# False to disable
|
|
117
|
+
if isinstance(echo_cmd, bool):
|
|
118
|
+
if echo_cmd:
|
|
119
|
+
echo_cmd = DisplayMode.STDOUT
|
|
120
|
+
else:
|
|
121
|
+
echo_cmd = DisplayMode.DEVNULL
|
|
122
|
+
|
|
123
|
+
if echo_cmd is None:
|
|
124
|
+
echo_cmd = DisplayMode.DEBUG_LOG
|
|
125
|
+
|
|
126
|
+
if isinstance(echo_cmd, display.IODisplay):
|
|
127
|
+
echo_display = echo_cmd
|
|
128
|
+
else:
|
|
129
|
+
echo_display = display.from_mode(echo_cmd)
|
|
130
|
+
|
|
131
|
+
if isinstance(cmd, str):
|
|
132
|
+
echo_display.printline("%s> %s", cwd if cwd else "", cmd)
|
|
133
|
+
else:
|
|
134
|
+
echo_display.printline("%s> %s", cwd if cwd else "", shlex.join(cmd))
|
|
135
|
+
|
|
136
|
+
# Determine PIPE mode for stdout
|
|
137
|
+
stdout_sub_mode = _determine_subprocess_output_mode(
|
|
138
|
+
stdout, stdout_display, "stdout"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Determine PIPE mode for stderr
|
|
142
|
+
stderr_sub_mode = _determine_subprocess_output_mode(
|
|
143
|
+
stderr, stderr_display, "stderr"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Run subprocess command
|
|
147
|
+
with subprocess.Popen(
|
|
148
|
+
cmd, shell=shell, stdout=stdout_sub_mode, stderr=stderr_sub_mode, cwd=cwd
|
|
149
|
+
) as popen:
|
|
150
|
+
with ThreadPool(2) as display_pool:
|
|
151
|
+
# Print out and capture stdout
|
|
152
|
+
stdout_result = display_pool.apply_async(
|
|
153
|
+
_output_and_collect,
|
|
154
|
+
args=[
|
|
155
|
+
popen.stdout,
|
|
156
|
+
(
|
|
157
|
+
stdout_display
|
|
158
|
+
if stdout_display is None
|
|
159
|
+
or isinstance(stdout_display, display.IODisplay)
|
|
160
|
+
else display.from_mode(stdout_display)
|
|
161
|
+
),
|
|
162
|
+
False if stdout == CaptureMode.DEVNULL else True,
|
|
163
|
+
],
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Print out and capture stderr
|
|
167
|
+
stderr_result = display_pool.apply_async(
|
|
168
|
+
_output_and_collect,
|
|
169
|
+
args=[
|
|
170
|
+
popen.stderr,
|
|
171
|
+
(
|
|
172
|
+
stderr_display
|
|
173
|
+
if stderr_display is None
|
|
174
|
+
or isinstance(stderr_display, display.IODisplay)
|
|
175
|
+
else display.from_mode(stderr_display)
|
|
176
|
+
),
|
|
177
|
+
False if stderr == CaptureMode.DEVNULL else True,
|
|
178
|
+
],
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Wait on command completion
|
|
182
|
+
returncode = popen.wait()
|
|
183
|
+
|
|
184
|
+
# Build our result
|
|
185
|
+
result = ShellResult(
|
|
186
|
+
cmd=cmd,
|
|
187
|
+
stdout=stdout_result.get(),
|
|
188
|
+
stderr=stderr_result.get(),
|
|
189
|
+
returncode=returncode,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# If check is True, raise exception if returncode is not 0
|
|
193
|
+
if check:
|
|
194
|
+
result.check_returncode()
|
|
195
|
+
|
|
196
|
+
return result
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from shelltastic.backend.base import LocalShellBackend
|
|
6
|
+
from shelltastic.backend.default.common import CommonDefaultBackend
|
|
7
|
+
|
|
8
|
+
LOGGER = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DefaultLocalBackend(CommonDefaultBackend, LocalShellBackend):
|
|
12
|
+
@staticmethod
|
|
13
|
+
def local() -> LocalShellBackend:
|
|
14
|
+
return DefaultLocalBackend()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import pathlib
|
|
5
|
+
import shlex
|
|
6
|
+
|
|
7
|
+
from shelltastic.backend.base import RemoteShellBackend
|
|
8
|
+
from shelltastic.backend.default.common import CommonDefaultBackend
|
|
9
|
+
from shelltastic.exception import ShellException, SSHConnectionError
|
|
10
|
+
from shelltastic.host import Host
|
|
11
|
+
from shelltastic.result import ShellResult
|
|
12
|
+
|
|
13
|
+
LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DefaultRemoteBackend(CommonDefaultBackend, RemoteShellBackend):
|
|
17
|
+
def __init__(self, host: Host) -> None:
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.host: Host = host
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def for_host(host: Host) -> RemoteShellBackend:
|
|
23
|
+
return DefaultRemoteBackend(host)
|
|
24
|
+
|
|
25
|
+
def run(
|
|
26
|
+
self,
|
|
27
|
+
cmd: str | list[str],
|
|
28
|
+
*,
|
|
29
|
+
subshell: bool = True,
|
|
30
|
+
cwd: str | pathlib.Path | None = None,
|
|
31
|
+
**kwargs,
|
|
32
|
+
) -> ShellResult:
|
|
33
|
+
"""
|
|
34
|
+
Execute a remote command using SSH.
|
|
35
|
+
"""
|
|
36
|
+
if isinstance(cmd, list):
|
|
37
|
+
cmd = shlex.join(cmd)
|
|
38
|
+
|
|
39
|
+
# Wrap user command in sh -c call
|
|
40
|
+
if subshell:
|
|
41
|
+
cmd = shlex.join(["bash", "-c", cmd])
|
|
42
|
+
|
|
43
|
+
# cd into cwd first if cwd is set
|
|
44
|
+
if cwd:
|
|
45
|
+
cmd = shlex.join(["cd", str(cwd)]) + " && " + cmd
|
|
46
|
+
|
|
47
|
+
port_flag = ["-p", str(self.host.port)] if self.host.port else []
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
result = super().run(
|
|
51
|
+
["ssh", *port_flag, self.host.host_specifier(), cmd], **kwargs
|
|
52
|
+
)
|
|
53
|
+
if (
|
|
54
|
+
result.returncode == 255
|
|
55
|
+
): # 255 is what ssh returns if it has an error with ssh itself
|
|
56
|
+
raise SSHConnectionError(
|
|
57
|
+
self.host.hostname, result.returncode, result.stdout, result.stderr
|
|
58
|
+
)
|
|
59
|
+
return result
|
|
60
|
+
except ShellException as ex:
|
|
61
|
+
if ex.returncode == 255:
|
|
62
|
+
raise SSHConnectionError(
|
|
63
|
+
self.host.hostname, ex.returncode, ex.stdout, ex.stderr
|
|
64
|
+
) from ex
|
|
65
|
+
raise
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from shelltastic.enum import DisplayMode
|
|
6
|
+
|
|
7
|
+
LOGGER = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IODisplay(ABC):
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def printbytes(self, line: bytes):
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def printline(self, msg: object, *args: object):
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DevNullDisplay(IODisplay):
|
|
21
|
+
def printbytes(self, line: bytes):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def printline(self, msg, *args):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StdoutDisplay(IODisplay):
|
|
29
|
+
def printbytes(self, line: bytes):
|
|
30
|
+
print(line.decode(), end="")
|
|
31
|
+
|
|
32
|
+
def printline(self, msg, *args):
|
|
33
|
+
print(str(msg) % args)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StderrDisplay(IODisplay):
|
|
37
|
+
def printbytes(self, line: bytes):
|
|
38
|
+
print(line.decode(), end="", file=sys.stderr)
|
|
39
|
+
|
|
40
|
+
def printline(self, msg, *args):
|
|
41
|
+
print(str(msg) % args, file=sys.stderr)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LogDisplay(IODisplay):
|
|
45
|
+
def __init__(self, log_level) -> None:
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.log_level = log_level
|
|
48
|
+
|
|
49
|
+
def printbytes(self, line: bytes):
|
|
50
|
+
LOGGER.log(self.log_level, line.decode().rstrip(), stacklevel=2)
|
|
51
|
+
|
|
52
|
+
def printline(self, msg: object, *args: object):
|
|
53
|
+
LOGGER.log(self.log_level, msg, *args, stacklevel=2)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CriticalLogDisplay(LogDisplay):
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
super().__init__(logging.CRITICAL)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ErrorLogDisplay(LogDisplay):
|
|
62
|
+
def __init__(self) -> None:
|
|
63
|
+
super().__init__(logging.ERROR)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class WarningLogDisplay(LogDisplay):
|
|
67
|
+
def __init__(self) -> None:
|
|
68
|
+
super().__init__(logging.WARNING)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class InfoLogDisplay(LogDisplay):
|
|
72
|
+
def __init__(self) -> None:
|
|
73
|
+
super().__init__(logging.INFO)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DebugLogDisplay(LogDisplay):
|
|
77
|
+
def __init__(self) -> None:
|
|
78
|
+
super().__init__(logging.DEBUG)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def from_mode(mode: DisplayMode) -> IODisplay:
|
|
82
|
+
"""Get a IODisplay for the given DisplayMode"""
|
|
83
|
+
if mode == DisplayMode.DEVNULL:
|
|
84
|
+
return DevNullDisplay()
|
|
85
|
+
if mode == DisplayMode.STDOUT:
|
|
86
|
+
return StdoutDisplay()
|
|
87
|
+
if mode == DisplayMode.STDERR:
|
|
88
|
+
return StderrDisplay()
|
|
89
|
+
if mode == DisplayMode.CRITICAL_LOG:
|
|
90
|
+
return CriticalLogDisplay()
|
|
91
|
+
if mode == DisplayMode.ERROR_LOG:
|
|
92
|
+
return ErrorLogDisplay()
|
|
93
|
+
if mode == DisplayMode.WARNING_LOG:
|
|
94
|
+
return WarningLogDisplay()
|
|
95
|
+
if mode == DisplayMode.INFO_LOG:
|
|
96
|
+
return InfoLogDisplay()
|
|
97
|
+
if mode == DisplayMode.DEBUG_LOG:
|
|
98
|
+
return DebugLogDisplay()
|
|
99
|
+
raise ValueError(f"Unknown DisplayMode {mode}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SystemType(Enum):
|
|
5
|
+
DEBIAN_BASED = auto()
|
|
6
|
+
UNKNOWN = auto()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CaptureMode(Enum):
|
|
10
|
+
PIPE = auto()
|
|
11
|
+
STDOUT = auto()
|
|
12
|
+
DEVNULL = auto()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DisplayMode(Enum):
|
|
16
|
+
STDOUT = auto()
|
|
17
|
+
STDERR = auto()
|
|
18
|
+
DEVNULL = auto()
|
|
19
|
+
CRITICAL_LOG = auto()
|
|
20
|
+
ERROR_LOG = auto()
|
|
21
|
+
WARNING_LOG = auto()
|
|
22
|
+
INFO_LOG = auto()
|
|
23
|
+
DEBUG_LOG = auto()
|
|
@@ -10,3 +10,18 @@ class SSHConnectionError(Exception):
|
|
|
10
10
|
self.returncode = returncode
|
|
11
11
|
self.stdout = stdout
|
|
12
12
|
self.stderr = stderr
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ShellException(Exception):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
cmd: str | list[str],
|
|
19
|
+
returncode: int,
|
|
20
|
+
stdout: bytes | None,
|
|
21
|
+
stderr: bytes | None,
|
|
22
|
+
):
|
|
23
|
+
super().__init__(f"Shell execution failed with returncode {returncode}")
|
|
24
|
+
self.cmd = cmd
|
|
25
|
+
self.returncode = returncode
|
|
26
|
+
self.stdout = stdout
|
|
27
|
+
self.stderr = stderr
|
|
File without changes
|
|
@@ -2,9 +2,9 @@ import logging
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import overload
|
|
4
4
|
|
|
5
|
-
from shelltastic
|
|
5
|
+
from shelltastic import backend
|
|
6
6
|
from shelltastic.backend.base import ShellBackend
|
|
7
|
-
from shelltastic.enum import
|
|
7
|
+
from shelltastic.enum import CaptureMode
|
|
8
8
|
from shelltastic.frontend.common import RemoteFrontend, RemoteFrontendFactory
|
|
9
9
|
from shelltastic.host import Host
|
|
10
10
|
|
|
@@ -26,9 +26,10 @@ class GitFrontend:
|
|
|
26
26
|
def get_remote_url(self, repo_path: str | Path, remote: str = "origin") -> str:
|
|
27
27
|
result = self._backend.run(
|
|
28
28
|
["git", "remote", "get-url", remote],
|
|
29
|
-
stdout=
|
|
29
|
+
stdout=CaptureMode.PIPE,
|
|
30
30
|
cwd=repo_path,
|
|
31
31
|
)
|
|
32
|
+
assert result.stdout is not None
|
|
32
33
|
return result.stdout.decode().strip()
|
|
33
34
|
|
|
34
35
|
def set_remote_url(
|
|
@@ -53,9 +54,10 @@ class GitFrontend:
|
|
|
53
54
|
result = self._backend.run(
|
|
54
55
|
["git", "config", "--global", "--get-all", key],
|
|
55
56
|
check=False,
|
|
56
|
-
stdout=
|
|
57
|
-
stderr=
|
|
57
|
+
stdout=CaptureMode.PIPE,
|
|
58
|
+
stderr=CaptureMode.PIPE,
|
|
58
59
|
)
|
|
60
|
+
assert result.stdout is not None
|
|
59
61
|
configs = [i.strip() for i in result.stdout.decode().split("\n") if i.strip()]
|
|
60
62
|
return configs
|
|
61
63
|
|
|
@@ -68,8 +70,8 @@ class GitFrontend:
|
|
|
68
70
|
self._backend.run(
|
|
69
71
|
["git", "config", "unset", "--global", "--all", key],
|
|
70
72
|
check=False,
|
|
71
|
-
stdout=
|
|
72
|
-
stderr=
|
|
73
|
+
stdout=CaptureMode.DEVNULL,
|
|
74
|
+
stderr=CaptureMode.DEVNULL,
|
|
73
75
|
)
|
|
74
76
|
if isinstance(value, str):
|
|
75
77
|
value = [value]
|
|
@@ -82,12 +84,13 @@ class GitFrontend:
|
|
|
82
84
|
response = self._backend.run(
|
|
83
85
|
"cat ~/.git-credentials",
|
|
84
86
|
check=False,
|
|
85
|
-
stdout=
|
|
86
|
-
stderr=
|
|
87
|
+
stdout=CaptureMode.PIPE,
|
|
88
|
+
stderr=CaptureMode.DEVNULL,
|
|
87
89
|
)
|
|
88
90
|
if response.returncode != 0:
|
|
89
91
|
return []
|
|
90
92
|
else:
|
|
93
|
+
assert response.stdout is not None
|
|
91
94
|
gitcredentials = response.stdout.decode()
|
|
92
95
|
|
|
93
96
|
return gitcredentials.splitlines()
|
|
@@ -97,7 +100,7 @@ class RemoteGitFrontend(GitFrontend, RemoteFrontend):
|
|
|
97
100
|
__slots__ = ()
|
|
98
101
|
|
|
99
102
|
def __init__(self, host: Host) -> None:
|
|
100
|
-
super().__init__(
|
|
103
|
+
super().__init__(backend.get_remote_backend(host))
|
|
101
104
|
super(GitFrontend, self).__init__(host)
|
|
102
105
|
|
|
103
106
|
|
|
@@ -105,7 +108,7 @@ class LocalGitFrontend(GitFrontend, RemoteFrontendFactory[RemoteGitFrontend]):
|
|
|
105
108
|
__slots__ = ()
|
|
106
109
|
|
|
107
110
|
def __init__(self) -> None:
|
|
108
|
-
super().__init__(
|
|
111
|
+
super().__init__(backend.get_local_backend())
|
|
109
112
|
|
|
110
113
|
def _create_remote_frontend(self, host: Host) -> RemoteGitFrontend:
|
|
111
114
|
return RemoteGitFrontend(host)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
from shelltastic
|
|
5
|
-
from shelltastic.enum import
|
|
4
|
+
from shelltastic import backend
|
|
5
|
+
from shelltastic.enum import CaptureMode
|
|
6
6
|
from shelltastic.frontend.common import RemoteFrontend, RemoteFrontendFactory
|
|
7
7
|
from shelltastic.host import Host
|
|
8
8
|
|
|
@@ -12,8 +12,8 @@ LOGGER = logging.getLogger(__name__)
|
|
|
12
12
|
class SCPFrontend(RemoteFrontend):
|
|
13
13
|
def __init__(self, host: Host) -> None:
|
|
14
14
|
super().__init__(host)
|
|
15
|
-
self._local_backend =
|
|
16
|
-
self._remote_backend =
|
|
15
|
+
self._local_backend = backend.get_local_backend()
|
|
16
|
+
self._remote_backend = backend.get_remote_backend(host)
|
|
17
17
|
|
|
18
18
|
def _scp_flags(self, recursive) -> list[str]:
|
|
19
19
|
# Set Port
|
|
@@ -58,7 +58,7 @@ class SCPFrontend(RemoteFrontend):
|
|
|
58
58
|
source_path,
|
|
59
59
|
dest,
|
|
60
60
|
],
|
|
61
|
-
stdout=
|
|
61
|
+
stdout=CaptureMode.DEVNULL,
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
# Set mode if configured
|
|
@@ -93,7 +93,7 @@ class SCPFrontend(RemoteFrontend):
|
|
|
93
93
|
src,
|
|
94
94
|
destination_path,
|
|
95
95
|
],
|
|
96
|
-
stdout=
|
|
96
|
+
stdout=CaptureMode.DEVNULL,
|
|
97
97
|
)
|
|
98
98
|
if mode:
|
|
99
99
|
LOGGER.info("chmod %s to %s", destination_path, mode)
|
|
@@ -2,13 +2,14 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import shutil
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from subprocess import CompletedProcess
|
|
6
5
|
|
|
7
|
-
from shelltastic
|
|
6
|
+
from shelltastic import backend
|
|
8
7
|
from shelltastic.backend.base import ShellBackend
|
|
9
|
-
from shelltastic.
|
|
8
|
+
from shelltastic.display import IODisplay
|
|
9
|
+
from shelltastic.enum import CaptureMode, DisplayMode, SystemType
|
|
10
10
|
from shelltastic.frontend.common import RemoteFrontend, RemoteFrontendFactory
|
|
11
11
|
from shelltastic.host import Host
|
|
12
|
+
from shelltastic.result import ShellResult
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class ShellFrontend:
|
|
@@ -17,11 +18,13 @@ class ShellFrontend:
|
|
|
17
18
|
|
|
18
19
|
def which(self, command: str) -> Path | None:
|
|
19
20
|
"""Return the path to the command or None if the command is not on the path."""
|
|
20
|
-
result = self.run(["which", command], check=False, stdout=
|
|
21
|
+
result = self.run(["which", command], check=False, stdout=CaptureMode.PIPE)
|
|
21
22
|
|
|
22
23
|
if result.returncode != 0:
|
|
23
24
|
return None
|
|
24
25
|
|
|
26
|
+
assert result.stdout is not None
|
|
27
|
+
|
|
25
28
|
return Path(result.stdout.decode().strip())
|
|
26
29
|
|
|
27
30
|
def file_exists(self, path: str | Path) -> bool:
|
|
@@ -29,8 +32,8 @@ class ShellFrontend:
|
|
|
29
32
|
result = self.run(
|
|
30
33
|
f'stat "{path}"',
|
|
31
34
|
check=False,
|
|
32
|
-
stdout=
|
|
33
|
-
stderr=
|
|
35
|
+
stdout=CaptureMode.DEVNULL,
|
|
36
|
+
stderr=CaptureMode.DEVNULL,
|
|
34
37
|
)
|
|
35
38
|
if result.returncode == 0:
|
|
36
39
|
return True
|
|
@@ -39,8 +42,9 @@ class ShellFrontend:
|
|
|
39
42
|
def home_dir(self) -> Path:
|
|
40
43
|
"""Return the path to the current user's home directory"""
|
|
41
44
|
result = self.run(
|
|
42
|
-
"echo ${HOME}", stdout=
|
|
45
|
+
"echo ${HOME}", stdout=CaptureMode.PIPE, stderr=CaptureMode.DEVNULL
|
|
43
46
|
)
|
|
47
|
+
assert result.stdout is not None
|
|
44
48
|
return Path(result.stdout.decode().strip())
|
|
45
49
|
|
|
46
50
|
def system_type(self) -> SystemType:
|
|
@@ -49,8 +53,8 @@ class ShellFrontend:
|
|
|
49
53
|
self.run(
|
|
50
54
|
"stat /etc/debian_version",
|
|
51
55
|
check=False,
|
|
52
|
-
stdout=
|
|
53
|
-
stderr=
|
|
56
|
+
stdout=CaptureMode.DEVNULL,
|
|
57
|
+
stderr=CaptureMode.DEVNULL,
|
|
54
58
|
).returncode
|
|
55
59
|
== 0
|
|
56
60
|
):
|
|
@@ -62,18 +66,22 @@ class ShellFrontend:
|
|
|
62
66
|
cmd: str | list[str],
|
|
63
67
|
*,
|
|
64
68
|
check: bool = True,
|
|
65
|
-
stdout:
|
|
66
|
-
stderr:
|
|
69
|
+
stdout: CaptureMode | None = None,
|
|
70
|
+
stderr: CaptureMode | None = None,
|
|
71
|
+
stdout_display: DisplayMode | IODisplay | None = None,
|
|
72
|
+
stderr_display: DisplayMode | IODisplay | None = None,
|
|
67
73
|
cwd: str | Path | None = None,
|
|
68
|
-
echo_cmd: bool =
|
|
74
|
+
echo_cmd: DisplayMode | IODisplay | bool | None = None,
|
|
69
75
|
**kwargs,
|
|
70
|
-
) ->
|
|
76
|
+
) -> ShellResult:
|
|
71
77
|
"""Run a shell command"""
|
|
72
78
|
return self._backend.run(
|
|
73
79
|
cmd,
|
|
74
80
|
check=check,
|
|
75
81
|
stdout=stdout,
|
|
76
82
|
stderr=stderr,
|
|
83
|
+
stdout_display=stdout_display,
|
|
84
|
+
stderr_display=stderr_display,
|
|
77
85
|
cwd=cwd,
|
|
78
86
|
echo_cmd=echo_cmd,
|
|
79
87
|
**kwargs,
|
|
@@ -84,7 +92,7 @@ class RemoteShellFrontend(ShellFrontend, RemoteFrontend):
|
|
|
84
92
|
__slots__ = ()
|
|
85
93
|
|
|
86
94
|
def __init__(self, host: Host) -> None:
|
|
87
|
-
super().__init__(
|
|
95
|
+
super().__init__(backend.get_remote_backend(host))
|
|
88
96
|
super(ShellFrontend, self).__init__(host)
|
|
89
97
|
|
|
90
98
|
|
|
@@ -92,7 +100,7 @@ class LocalShellFrontend(ShellFrontend, RemoteFrontendFactory[RemoteShellFronten
|
|
|
92
100
|
__slots__ = ()
|
|
93
101
|
|
|
94
102
|
def __init__(self) -> None:
|
|
95
|
-
super().__init__(
|
|
103
|
+
super().__init__(backend.get_local_backend())
|
|
96
104
|
|
|
97
105
|
def _create_remote_frontend(self, host: Host) -> RemoteShellFrontend:
|
|
98
106
|
return RemoteShellFrontend(host)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from shelltastic.exception import ShellException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class ShellResult:
|
|
8
|
+
cmd: str | list[str]
|
|
9
|
+
returncode: int
|
|
10
|
+
stdout: bytes | None
|
|
11
|
+
stderr: bytes | None
|
|
12
|
+
|
|
13
|
+
def check_returncode(self):
|
|
14
|
+
"""If returncode is not 0, raise ShellException"""
|
|
15
|
+
if self.returncode != 0:
|
|
16
|
+
raise ShellException(self.cmd, self.returncode, self.stdout, self.stderr)
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from shelltastic.frontend.git import LocalGitFrontend
|
|
2
|
-
from shelltastic.frontend.scp import SCP
|
|
3
|
-
from shelltastic.frontend.shell import LocalShellFrontend
|
|
4
|
-
|
|
5
|
-
__all__ = ["shell", "scp", "git"]
|
|
6
|
-
|
|
7
|
-
shell = LocalShellFrontend()
|
|
8
|
-
scp = SCP()
|
|
9
|
-
git = LocalGitFrontend()
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import pathlib
|
|
5
|
-
import shlex
|
|
6
|
-
import subprocess
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
from shelltastic.backend.base import RemoteShellBackend, ShellBackend
|
|
10
|
-
from shelltastic.enum import OutputMode
|
|
11
|
-
from shelltastic.exception import SSHConnectionError
|
|
12
|
-
from shelltastic.host import Host
|
|
13
|
-
|
|
14
|
-
LOGGER = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class LocalShellBackend(ShellBackend):
|
|
18
|
-
__slots__ = ()
|
|
19
|
-
|
|
20
|
-
def run(
|
|
21
|
-
self,
|
|
22
|
-
cmd: str | list[str],
|
|
23
|
-
*,
|
|
24
|
-
check: bool = True,
|
|
25
|
-
stdout: OutputMode = OutputMode.PRINT,
|
|
26
|
-
stderr: OutputMode = OutputMode.PRINT,
|
|
27
|
-
cwd: str | pathlib.Path | None = None,
|
|
28
|
-
echo_cmd: bool = False,
|
|
29
|
-
**kwargs,
|
|
30
|
-
) -> subprocess.CompletedProcess[bytes]:
|
|
31
|
-
if kwargs:
|
|
32
|
-
raise TypeError(f"Uknown argument/s {list(kwargs.keys())}")
|
|
33
|
-
|
|
34
|
-
if isinstance(cmd, str):
|
|
35
|
-
LOGGER.debug("%s> %s", cwd if cwd else "", cmd)
|
|
36
|
-
else:
|
|
37
|
-
LOGGER.debug("%s> %s", cwd if cwd else "", shlex.join(cmd))
|
|
38
|
-
|
|
39
|
-
if echo_cmd:
|
|
40
|
-
print(cmd if isinstance(cmd, str) else shlex.join(cmd))
|
|
41
|
-
|
|
42
|
-
cmd_to_run = {}
|
|
43
|
-
cmd_to_run["args"] = cmd
|
|
44
|
-
cmd_to_run["check"] = check
|
|
45
|
-
cmd_to_run["cwd"] = cwd
|
|
46
|
-
|
|
47
|
-
if isinstance(cmd, str):
|
|
48
|
-
cmd_to_run["shell"] = True
|
|
49
|
-
|
|
50
|
-
# stdout
|
|
51
|
-
if stdout == OutputMode.CAPTURE:
|
|
52
|
-
cmd_to_run["stdout"] = subprocess.PIPE
|
|
53
|
-
elif stdout == OutputMode.DEVNULL:
|
|
54
|
-
cmd_to_run["stdout"] = subprocess.DEVNULL
|
|
55
|
-
|
|
56
|
-
# stderr
|
|
57
|
-
if stderr == OutputMode.CAPTURE:
|
|
58
|
-
cmd_to_run["stderr"] = subprocess.PIPE
|
|
59
|
-
elif stderr == OutputMode.DEVNULL:
|
|
60
|
-
cmd_to_run["stderr"] = subprocess.DEVNULL
|
|
61
|
-
|
|
62
|
-
# Flush stdout/stderr
|
|
63
|
-
# This fixes out of order prints between python and commands
|
|
64
|
-
sys.stdout.flush()
|
|
65
|
-
sys.stderr.flush()
|
|
66
|
-
|
|
67
|
-
return subprocess.run(**cmd_to_run)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class SSHShellBackend(LocalShellBackend, RemoteShellBackend):
|
|
71
|
-
def __init__(self, host: Host) -> None:
|
|
72
|
-
super().__init__()
|
|
73
|
-
self.host: Host = host
|
|
74
|
-
|
|
75
|
-
@staticmethod
|
|
76
|
-
def for_host(host: Host) -> RemoteShellBackend:
|
|
77
|
-
return SSHShellBackend(host)
|
|
78
|
-
|
|
79
|
-
def run(
|
|
80
|
-
self,
|
|
81
|
-
cmd: str | list[str],
|
|
82
|
-
*,
|
|
83
|
-
subshell: bool = True,
|
|
84
|
-
cwd: str | pathlib.Path | None = None,
|
|
85
|
-
**kwargs,
|
|
86
|
-
) -> subprocess.CompletedProcess[bytes]:
|
|
87
|
-
"""
|
|
88
|
-
Execute a remote command using SSH.
|
|
89
|
-
"""
|
|
90
|
-
if isinstance(cmd, list):
|
|
91
|
-
cmd = shlex.join(cmd)
|
|
92
|
-
|
|
93
|
-
# Wrap user command in sh -c call
|
|
94
|
-
if subshell:
|
|
95
|
-
cmd = shlex.join(["bash", "-c", cmd])
|
|
96
|
-
|
|
97
|
-
# cd into cwd first if cwd is set
|
|
98
|
-
if cwd:
|
|
99
|
-
cmd = shlex.join(["cd", str(cwd)]) + " && " + cmd
|
|
100
|
-
|
|
101
|
-
port_flag = ["-p", str(self.host.port)] if self.host.port else []
|
|
102
|
-
|
|
103
|
-
try:
|
|
104
|
-
result = super().run(
|
|
105
|
-
["ssh", *port_flag, self.host.host_specifier(), cmd], **kwargs
|
|
106
|
-
)
|
|
107
|
-
if (
|
|
108
|
-
result.returncode == 255
|
|
109
|
-
): # 255 is what ssh returns if it has an error with ssh itself
|
|
110
|
-
raise SSHConnectionError(
|
|
111
|
-
self.host.hostname, result.returncode, result.stdout, result.stderr
|
|
112
|
-
)
|
|
113
|
-
return result
|
|
114
|
-
except subprocess.CalledProcessError as ex:
|
|
115
|
-
if ex.returncode == 255:
|
|
116
|
-
raise SSHConnectionError(
|
|
117
|
-
self.host.hostname, ex.returncode, ex.stdout, ex.stderr
|
|
118
|
-
) from ex
|
|
119
|
-
raise
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import pathlib
|
|
4
|
-
import subprocess
|
|
5
|
-
from abc import ABC, abstractmethod
|
|
6
|
-
|
|
7
|
-
from shelltastic.enum import OutputMode
|
|
8
|
-
from shelltastic.host import Host
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ShellBackend(ABC):
|
|
12
|
-
__slots__ = ()
|
|
13
|
-
|
|
14
|
-
@abstractmethod
|
|
15
|
-
def run(
|
|
16
|
-
self,
|
|
17
|
-
cmd: str | list[str],
|
|
18
|
-
*,
|
|
19
|
-
check: bool = True,
|
|
20
|
-
stdout: OutputMode = OutputMode.PRINT,
|
|
21
|
-
stderr: OutputMode = OutputMode.PRINT,
|
|
22
|
-
cwd: str | pathlib.Path | None = None,
|
|
23
|
-
echo_cmd: bool = False,
|
|
24
|
-
**kwargs,
|
|
25
|
-
) -> subprocess.CompletedProcess[bytes]:
|
|
26
|
-
raise NotImplementedError()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class RemoteShellBackend(ShellBackend):
|
|
30
|
-
__slots__ = ()
|
|
31
|
-
|
|
32
|
-
@staticmethod
|
|
33
|
-
@abstractmethod
|
|
34
|
-
def for_host(host: Host) -> RemoteShellBackend:
|
|
35
|
-
raise NotImplementedError()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|