aws-ssh-utils 0.2.7__tar.gz → 0.3.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 (21) hide show
  1. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/PKG-INFO +13 -7
  2. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/README.md +10 -6
  3. aws_ssh_utils-0.3.0/aws_ssh_utils/__init__.py +1 -0
  4. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/commands/app.py +1 -1
  5. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/commands/ec2.py +1 -1
  6. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/commands/emr.py +5 -3
  7. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/connection.py +41 -38
  8. aws_ssh_utils-0.3.0/aws_ssh_utils/session.py +128 -0
  9. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/terminal.py +6 -5
  10. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/pyproject.toml +2 -0
  11. aws_ssh_utils-0.2.7/aws_ssh_utils/__init__.py +0 -1
  12. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/.gitignore +0 -0
  13. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/LICENSE +0 -0
  14. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/cli_utils.py +0 -0
  15. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/commands/__init__.py +0 -0
  16. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/commands/emr_all.py +0 -0
  17. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/commands/healthcheck.py +0 -0
  18. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/ec2_utils.py +0 -0
  19. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/emr_utils.py +0 -0
  20. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/py.typed +0 -0
  21. {aws_ssh_utils-0.2.7 → aws_ssh_utils-0.3.0}/aws_ssh_utils/ssh.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: aws-ssh-utils
3
- Version: 0.2.7
3
+ Version: 0.3.0
4
4
  Summary: Easy AWS SSHing
5
5
  Project-URL: Homepage, https://github.com/mvanderlee/aws-ssh-utils
6
6
  Author-email: Michiel Vanderlee <jmt.vanderlee@gmail.com>
@@ -26,7 +26,9 @@ Requires-Dist: boto3
26
26
  Requires-Dist: click
27
27
  Requires-Dist: loguru
28
28
  Requires-Dist: paramiko
29
+ Requires-Dist: ptyprocess>=0.7.0; sys_platform != 'win32'
29
30
  Requires-Dist: pyte>=0.8.2
31
+ Requires-Dist: pywinpty>=3.0.0; sys_platform == 'win32'
30
32
  Requires-Dist: questionary
31
33
  Requires-Dist: requests>=2.34.2
32
34
  Requires-Dist: rich>=15.0.0
@@ -58,17 +60,21 @@ It utilizes [questionary](https://pypi.org/project/questionary/) to ask you whic
58
60
 
59
61
  ## App
60
62
 
61
- Terminal UI to browse EC2 instances and EMR clusters, with SSH shells in tabs.
63
+ Terminal UI to browse EC2 instances and EMR clusters, with SSM or SSH shells in tabs.
62
64
  It's the default command, so `aws_ssh` and `aws_ssh -p my-profile` launch it too.
63
- Each connection tries, in order: `~/.ssh/config`, SSH over AWS SSM (`session-manager-plugin`), opkssh (via instance/cluster tags),
65
+ Each connection tries, in order: `~/.ssh/config`, a keyless AWS SSM shell, opkssh (via instance/cluster tags),
64
66
  the instance's EC2 key found in `~/.ssh`, then your default keys/ssh-agent.
65
- Unknown host keys are accepted and appended to `~/.ssh/known_hosts`; changed host keys are rejected.
67
+ SSM uses `aws ssm start-session --target INSTANCE_ID` with the selected profile and region. It requires the AWS CLI,
68
+ `session-manager-plugin`, and Session Manager access to the instance, but no SSH key, SSH server, or inbound port 22.
69
+ The shell runs as the Session Manager configured user (usually `ssm-user`); SSH usernames and key options only apply
70
+ to SSH connections. The `ec2` and `emr` commands use the same connection order; `emr-all` still uses SSH in tmux.
71
+ For SSH connections, unknown host keys are accepted and appended to `~/.ssh/known_hosts`; changed host keys are rejected.
66
72
 
67
73
  ```shell
68
74
  $ aws_ssh app --help
69
75
  Usage: aws_ssh app [OPTIONS]
70
76
 
71
- Browse EC2 instances and EMR clusters, and open SSH shells in tabs.
77
+ Browse EC2 instances and EMR clusters, and open SSM or SSH shells in tabs.
72
78
 
73
79
  Options:
74
80
  -p, --profile TEXT Which AWS profile to use
@@ -92,7 +98,7 @@ $ aws_auth ec2 --help
92
98
  Usage: aws_ssh ec2 [OPTIONS]
93
99
 
94
100
  Asks user which EC2 instance they want to connect to, then opens an
95
- interactive SSH session to the instance
101
+ interactive SSM or SSH session to the instance
96
102
 
97
103
  Options:
98
104
  -p, --profile TEXT Which AWS profile to use
@@ -114,7 +120,7 @@ Select the EMR cluster, instance group, and instance to connect to.
114
120
  $ aws_ssh emr --help
115
121
  Usage: aws_ssh emr [OPTIONS]
116
122
 
117
- Asks user which Cluster and EC2 instance they want to connect to, then opens an interactive SSH session to the instance
123
+ Asks user which Cluster and EC2 instance they want to connect to, then opens an interactive SSM or SSH session to the instance
118
124
 
119
125
  Options:
120
126
  -p, --profile TEXT Which AWS profile to use
@@ -22,17 +22,21 @@ It utilizes [questionary](https://pypi.org/project/questionary/) to ask you whic
22
22
 
23
23
  ## App
24
24
 
25
- Terminal UI to browse EC2 instances and EMR clusters, with SSH shells in tabs.
25
+ Terminal UI to browse EC2 instances and EMR clusters, with SSM or SSH shells in tabs.
26
26
  It's the default command, so `aws_ssh` and `aws_ssh -p my-profile` launch it too.
27
- Each connection tries, in order: `~/.ssh/config`, SSH over AWS SSM (`session-manager-plugin`), opkssh (via instance/cluster tags),
27
+ Each connection tries, in order: `~/.ssh/config`, a keyless AWS SSM shell, opkssh (via instance/cluster tags),
28
28
  the instance's EC2 key found in `~/.ssh`, then your default keys/ssh-agent.
29
- Unknown host keys are accepted and appended to `~/.ssh/known_hosts`; changed host keys are rejected.
29
+ SSM uses `aws ssm start-session --target INSTANCE_ID` with the selected profile and region. It requires the AWS CLI,
30
+ `session-manager-plugin`, and Session Manager access to the instance, but no SSH key, SSH server, or inbound port 22.
31
+ The shell runs as the Session Manager configured user (usually `ssm-user`); SSH usernames and key options only apply
32
+ to SSH connections. The `ec2` and `emr` commands use the same connection order; `emr-all` still uses SSH in tmux.
33
+ For SSH connections, unknown host keys are accepted and appended to `~/.ssh/known_hosts`; changed host keys are rejected.
30
34
 
31
35
  ```shell
32
36
  $ aws_ssh app --help
33
37
  Usage: aws_ssh app [OPTIONS]
34
38
 
35
- Browse EC2 instances and EMR clusters, and open SSH shells in tabs.
39
+ Browse EC2 instances and EMR clusters, and open SSM or SSH shells in tabs.
36
40
 
37
41
  Options:
38
42
  -p, --profile TEXT Which AWS profile to use
@@ -56,7 +60,7 @@ $ aws_auth ec2 --help
56
60
  Usage: aws_ssh ec2 [OPTIONS]
57
61
 
58
62
  Asks user which EC2 instance they want to connect to, then opens an
59
- interactive SSH session to the instance
63
+ interactive SSM or SSH session to the instance
60
64
 
61
65
  Options:
62
66
  -p, --profile TEXT Which AWS profile to use
@@ -78,7 +82,7 @@ Select the EMR cluster, instance group, and instance to connect to.
78
82
  $ aws_ssh emr --help
79
83
  Usage: aws_ssh emr [OPTIONS]
80
84
 
81
- Asks user which Cluster and EC2 instance they want to connect to, then opens an interactive SSH session to the instance
85
+ Asks user which Cluster and EC2 instance they want to connect to, then opens an interactive SSM or SSH session to the instance
82
86
 
83
87
  Options:
84
88
  -p, --profile TEXT Which AWS profile to use
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -48,7 +48,7 @@ ANIMATION_INTERVAL = 1 / 12
48
48
  @click.option('-r', '--region', default=None, help='Which AWS region to use')
49
49
  @scrollback_option
50
50
  def app(profile: str | None = None, region: str | None = None, scrollback: int | None = None, **kwargs: Any):
51
- """Browse EC2 instances and EMR clusters, and open SSH shells in tabs."""
51
+ """Browse EC2 instances and EMR clusters, and open SSM or SSH shells in tabs."""
52
52
  try:
53
53
  session = boto3.Session(profile_name=profile, region_name=region)
54
54
  session.client('sts').get_caller_identity()
@@ -41,7 +41,7 @@ def ec2(
41
41
  ):
42
42
  """
43
43
  Asks user which EC2 instance they want to connect to,
44
- then opens an interactive SSH session to the instance
44
+ then opens an interactive SSM or SSH session to the instance
45
45
  """
46
46
  session = boto3.Session(profile_name=profile, region_name=region)
47
47
  ec2_client = session.client('ec2')
@@ -45,7 +45,7 @@ def emr(
45
45
  ):
46
46
  """
47
47
  Asks user which Cluster and EC2 instance they want to connect to,
48
- then opens an interactive SSH session to the instance
48
+ then opens an interactive SSM or SSH session to the instance
49
49
  """
50
50
  session = boto3.Session(profile_name=profile, region_name=region)
51
51
  emr_client = session.client('emr')
@@ -65,9 +65,11 @@ def emr(
65
65
  with spinner():
66
66
  target = emr_target(emr_client, cluster_id, instance_ip.instance_id or '', ip)
67
67
  target = replace(target, user=user or target.user, key_file=key_file)
68
+ env = detect_environment(profile, session.region_name)
68
69
 
69
70
  if (
70
- key_file is None
71
+ not env.ssm_installed
72
+ and key_file is None
71
73
  and target.opkssh_provider is None
72
74
  and find_ssh_key_file(target.key_name) is None
73
75
  and not questionary.confirm(f'Could not find the ssh key {target.key_name}, would you like to continue?').unsafe_ask()
@@ -78,7 +80,7 @@ def emr(
78
80
  postfix = f'[{group_idx}]' if group_idx else ''
79
81
  open_shell(
80
82
  target,
81
- detect_environment(profile, session.region_name),
83
+ env,
82
84
  title=f'{cluster_name} - {group}{postfix}',
83
85
  scrollback=scrollback,
84
86
  initial_input=CORE_NODE_INPUT if group.lower().startswith('core') else b'',
@@ -1,4 +1,4 @@
1
- """SSH connection strategies, tried in order until one succeeds."""
1
+ """SSM and SSH connection strategies, tried in order until one succeeds."""
2
2
 
3
3
  import os
4
4
  import shlex
@@ -18,6 +18,8 @@ import paramiko
18
18
  from loguru import logger
19
19
  from typing_extensions import override
20
20
 
21
+ from aws_ssh_utils.session import ShellChannel, SSMSession
22
+
21
23
  # CSV of issuer,client
22
24
  OPKSSH_PROVIDER_TAG = 'opkssh_provider'
23
25
  # EMR doesn't support comma's in tags.
@@ -54,7 +56,7 @@ class Environment:
54
56
 
55
57
  @dataclass(frozen=True)
56
58
  class Connection:
57
- client: paramiko.SSHClient
59
+ client: paramiko.SSHClient | SSMSession
58
60
  via: str
59
61
  """How the connection was made: AWS SSM, opkssh, a key name, ..."""
60
62
 
@@ -64,7 +66,7 @@ class Attempt:
64
66
  via: str
65
67
  command: str
66
68
  """The equivalent command line, to retry by hand."""
67
- open_client: Callable[[], paramiko.SSHClient] = field(repr=False, compare=False)
69
+ open_client: Callable[[], paramiko.SSHClient | SSMSession] = field(repr=False, compare=False)
68
70
  state: Literal['pending', 'connecting', 'connected', 'failed', 'skipped'] = 'pending'
69
71
  detail: str = ''
70
72
  """The error when failed, the reason when skipped."""
@@ -85,7 +87,7 @@ def connect(
85
87
 
86
88
  Tried in order:
87
89
  1. ~/.ssh/config, when a Host pattern (other than `*`) matches the instance id, an alias or the IP.
88
- 2. SSH over AWS SSM to the instance id, with any existing opkssh/EC2 key plus the default keys.
90
+ 2. A keyless AWS SSM shell, authenticated with the AWS profile.
89
91
  3. opkssh, when the instance/cluster has opkssh tags. May open a browser login.
90
92
  4. The EC2 key pair (or `target.key_file`), when a matching file is found in ~/.ssh.
91
93
  5. The ssh-agent and default ~/.ssh/id_* keys.
@@ -96,6 +98,24 @@ def connect(
96
98
  open_ssh_ = partial(open_ssh, confirm_host_key=confirm_host_key)
97
99
  attempts: list[Attempt] = []
98
100
 
101
+ def try_attempt(i: int) -> Connection | None:
102
+ attempt = attempts[i]
103
+ if attempt.state == 'skipped':
104
+ on_progress(list(attempts))
105
+ return None
106
+ attempts[i] = replace(attempt, state='connecting')
107
+ on_progress(list(attempts))
108
+ try:
109
+ client = attempt.open_client()
110
+ except Exception as e:
111
+ logger.debug(f"Failed to connect via {attempt.via}: {e!r}")
112
+ attempts[i] = replace(attempt, state='failed', detail=str(e) or type(e).__name__)
113
+ on_progress(list(attempts))
114
+ return None
115
+ attempts[i] = replace(attempt, state='connected')
116
+ on_progress(list(attempts))
117
+ return Connection(client, attempt.via)
118
+
99
119
  config = find_ssh_config(target)
100
120
  if config is not None:
101
121
  alias, options = config
@@ -107,21 +127,18 @@ def connect(
107
127
  ),
108
128
  )
109
129
 
110
- ec2_key = target.key_file or find_ssh_key_file(target.key_name)
111
- known_keys = [k for k in (valid_opkssh_key_file(target.opkssh_provider), ec2_key) if k]
112
- ssm_proxy = ssm_command(target.instance_id, env)
113
- ssm = Attempt(
114
- "AWS SSM",
115
- ssh_command(target.user, target.instance_id, known_keys, proxy=ssm_proxy),
116
- lambda: open_ssh_(
117
- target.instance_id,
118
- target.user,
119
- known_keys,
120
- use_defaults=True,
121
- sock=proxy_socket(ssm_proxy, on_output),
122
- ),
130
+ command = ssm_command(target.instance_id, env)
131
+ ssm = Attempt('AWS SSM', shlex.join(command), lambda: SSMSession.open(command))
132
+ attempts.append(
133
+ ssm if env.ssm_installed else replace(ssm, state='skipped', detail="AWS CLI or session-manager-plugin not installed"),
123
134
  )
124
- attempts.append(ssm if env.ssm_installed else replace(ssm, state='skipped', detail="session-manager-plugin not installed"))
135
+ for i in range(len(attempts)):
136
+ if result := try_attempt(i):
137
+ return result
138
+
139
+ # Key discovery is only needed for the remaining SSH fallbacks.
140
+ fallback_start = len(attempts)
141
+ ec2_key = target.key_file or find_ssh_key_file(target.key_name)
125
142
 
126
143
  if (provider := target.opkssh_provider) is not None:
127
144
  opkssh_key = str(opkssh_key_path(provider))
@@ -150,21 +167,9 @@ def connect(
150
167
  ),
151
168
  )
152
169
 
153
- for i, attempt in enumerate(attempts):
154
- if attempt.state == 'skipped':
155
- continue
156
- attempts[i] = replace(attempt, state='connecting')
157
- on_progress(list(attempts))
158
- try:
159
- client = attempt.open_client()
160
- except Exception as e:
161
- logger.debug(f"Failed to connect via {attempt.via}: {e!r}")
162
- attempts[i] = replace(attempt, state='failed', detail=str(e) or type(e).__name__)
163
- on_progress(list(attempts))
164
- continue
165
- attempts[i] = replace(attempt, state='connected')
166
- on_progress(list(attempts))
167
- return Connection(client, attempt.via)
170
+ for i in range(fallback_start, len(attempts)):
171
+ if result := try_attempt(i):
172
+ return result
168
173
  return None
169
174
 
170
175
 
@@ -226,7 +231,9 @@ def ssh_command(
226
231
  return shlex.join([*cmd, f'{user}@{host}'])
227
232
 
228
233
 
229
- def open_shell_channel(client: paramiko.SSHClient) -> paramiko.Channel:
234
+ def open_shell_channel(client: paramiko.SSHClient | SSMSession) -> ShellChannel:
235
+ if isinstance(client, SSMSession):
236
+ return client
230
237
  channel = client.get_transport().open_session() # pyright: ignore[reportOptionalMemberAccess]
231
238
  channel.get_pty(term='xterm-256color')
232
239
  channel.invoke_shell()
@@ -301,10 +308,6 @@ def ssm_command(instance_id: str, env: Environment) -> list[str]:
301
308
  'start-session',
302
309
  '--target',
303
310
  instance_id,
304
- '--document-name',
305
- 'AWS-StartSSHSession',
306
- '--parameters',
307
- 'portNumber=22',
308
311
  ]
309
312
  if env.profile:
310
313
  cmd += ['--profile', env.profile]
@@ -0,0 +1,128 @@
1
+ """Terminal channels for native, keyless Session Manager shells."""
2
+
3
+ import os
4
+ import queue
5
+ import re
6
+ import threading
7
+ import time
8
+ from typing import Protocol, cast
9
+
10
+
11
+ class ShellChannel(Protocol):
12
+ def recv(self, nbytes: int, /) -> bytes: ...
13
+ def send(self, data: bytes, /) -> int: ...
14
+ def resize_pty(self, width: int, height: int) -> None: ...
15
+
16
+
17
+ class ShellClient(Protocol):
18
+ def close(self) -> None: ...
19
+
20
+
21
+ class Pty(Protocol):
22
+ def read(self, size: int) -> str: ...
23
+ def write(self, data: str) -> int: ...
24
+ def setwinsize(self, rows: int, cols: int) -> None: ...
25
+ def close(self, force: bool = False) -> None: ...
26
+
27
+
28
+ def spawn_pty(command: list[str]) -> Pty:
29
+ """SSM needs a real terminal for raw input, control keys, and window sizes."""
30
+ env = {**os.environ, 'TERM': 'xterm-256color', 'AWS_PAGER': '', 'AWS_CLI_AUTO_PROMPT': 'off'}
31
+ if os.name == 'nt':
32
+ from winpty import PtyProcess
33
+
34
+ return cast(Pty, PtyProcess.spawn(command, env=env, dimensions=(24, 80)))
35
+
36
+ from ptyprocess import PtyProcessUnicode
37
+
38
+ return cast(Pty, PtyProcessUnicode.spawn(command, env=env, dimensions=(24, 80), echo=False))
39
+
40
+
41
+ class SSMSession:
42
+ """Adapt an AWS CLI pseudo-terminal to the terminal widget's channel interface."""
43
+
44
+ def __init__(self, process: Pty):
45
+ self.process = process
46
+ self.closed = False
47
+ self._output: queue.Queue[bytes | Exception | None] = queue.Queue()
48
+ self._pending = b''
49
+ self._eof = False
50
+ threading.Thread(target=self._read, daemon=True).start()
51
+
52
+ @classmethod
53
+ def open(cls, command: list[str], timeout: float = 30) -> 'SSMSession':
54
+ session = cls(spawn_pty(command))
55
+ try:
56
+ session._wait_started(timeout)
57
+ except BaseException:
58
+ session.close()
59
+ raise
60
+ return session
61
+
62
+ def _read(self):
63
+ try:
64
+ while not self.closed:
65
+ data = self.process.read(65536)
66
+ if data:
67
+ self._output.put(data.encode('utf-8'))
68
+ except EOFError:
69
+ pass
70
+ except Exception as e:
71
+ if not self.closed:
72
+ self._output.put(e)
73
+ finally:
74
+ self._output.put(None)
75
+
76
+ def _wait_started(self, timeout: float):
77
+ """Keep startup output for the terminal, but report CLI failures to the fallback list."""
78
+ deadline = time.monotonic() + timeout
79
+ output = bytearray()
80
+ try:
81
+ while True:
82
+ data = self._output.get(timeout=max(0, deadline - time.monotonic()))
83
+ if isinstance(data, Exception):
84
+ raise OSError(str(data)) from data
85
+ if data is None:
86
+ raise OSError(output.decode('utf-8', errors='replace').strip() or 'SSM exited before starting a session')
87
+ output.extend(data)
88
+ # ConPTY adds terminal escape sequences to console output on Windows.
89
+ text = re.sub(rb'\x1b\[[0-?]*[ -/]*[@-~]', b'', output)
90
+ if re.search(rb'Starting session with SessionId:\s*\S+', text):
91
+ self._pending = bytes(output)
92
+ return
93
+ except queue.Empty:
94
+ detail = output.decode('utf-8', errors='replace').strip()
95
+ raise TimeoutError(f'Timed out starting SSM session{": " + detail if detail else ""}') from None
96
+
97
+ def recv(self, nbytes: int) -> bytes:
98
+ if nbytes <= 0:
99
+ return b''
100
+ while not self._pending and not self._eof:
101
+ data = self._output.get()
102
+ if isinstance(data, Exception):
103
+ raise OSError(str(data)) from data
104
+ if data is None:
105
+ self._eof = True
106
+ else:
107
+ self._pending = data
108
+ data, self._pending = self._pending[:nbytes], self._pending[nbytes:]
109
+ return data
110
+
111
+ def send(self, data: bytes) -> int:
112
+ try:
113
+ self.process.write(data.decode('utf-8'))
114
+ except EOFError as e:
115
+ raise OSError('SSM session is closed') from e
116
+ return len(data)
117
+
118
+ def resize_pty(self, width: int, height: int):
119
+ if not self.closed:
120
+ self.process.setwinsize(height, width)
121
+
122
+ def close(self):
123
+ if not self.closed:
124
+ self.closed = True
125
+ try:
126
+ self.process.close(force=True)
127
+ finally:
128
+ self._output.put(None)
@@ -1,4 +1,4 @@
1
- """Textual widgets rendering a paramiko shell channel through the pyte VT100 emulator."""
1
+ """Textual widgets rendering SSH and SSM shell channels through the pyte VT100 emulator."""
2
2
 
3
3
  import datetime as dt
4
4
  import time
@@ -7,7 +7,6 @@ from collections.abc import Sequence
7
7
  from functools import lru_cache
8
8
  from typing import Any
9
9
 
10
- import paramiko
11
10
  import pyte
12
11
  from pyte.screens import Char, Margins
13
12
  from rich.segment import Segment
@@ -22,6 +21,8 @@ from textual.widget import Widget
22
21
  from textual.widgets import Static
23
22
  from typing_extensions import override
24
23
 
24
+ from aws_ssh_utils.session import ShellChannel, ShellClient
25
+
25
26
  KEYS = {
26
27
  'enter': '\r',
27
28
  'tab': '\t',
@@ -108,7 +109,7 @@ def cell_style(
108
109
  class ChannelScreen(pyte.Screen):
109
110
  """Answers terminal queries over the channel, and keeps the lines scrolled off the top as history."""
110
111
 
111
- def __init__(self, channel: paramiko.Channel, columns: int, lines: int, scrollback: int = 0):
112
+ def __init__(self, channel: ShellChannel, columns: int, lines: int, scrollback: int = 0):
112
113
  super().__init__(columns, lines)
113
114
  self.channel = channel
114
115
  self.history: deque[tuple[Char, ...]] = deque(maxlen=scrollback)
@@ -145,7 +146,7 @@ class Terminal(Widget, can_focus=True):
145
146
  class Closed(Message):
146
147
  pass
147
148
 
148
- def __init__(self, client: paramiko.SSHClient, channel: paramiko.Channel, scrollback: int = 0, **kwargs: object):
149
+ def __init__(self, client: ShellClient, channel: ShellChannel, scrollback: int = 0, **kwargs: object):
149
150
  super().__init__(**kwargs) # pyright: ignore[reportArgumentType]
150
151
  self.client = client
151
152
  self.channel = channel
@@ -287,7 +288,7 @@ class ShellApp(App[None]):
287
288
  # Its priority ctrl+p binding would steal shell history navigation from the terminal.
288
289
  ENABLE_COMMAND_PALETTE = False
289
290
 
290
- def __init__(self, via: str, client: paramiko.SSHClient, channel: paramiko.Channel, scrollback: int = 0):
291
+ def __init__(self, via: str, client: ShellClient, channel: ShellChannel, scrollback: int = 0):
291
292
  super().__init__()
292
293
  self.via = via
293
294
  self.client = client
@@ -34,6 +34,8 @@ dependencies = [
34
34
  "loguru",
35
35
  "paramiko",
36
36
  "pyte>=0.8.2",
37
+ "ptyprocess>=0.7.0; sys_platform != 'win32'",
38
+ "pywinpty>=3.0.0; sys_platform == 'win32'",
37
39
  "questionary",
38
40
  "requests>=2.34.2",
39
41
  "rich>=15.0.0",
@@ -1 +0,0 @@
1
- __version__ = "0.2.7"
File without changes
File without changes