ssh-handler 1.4.1__tar.gz → 1.5.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 (29) hide show
  1. {ssh_handler-1.4.1/ssh_handler.egg-info → ssh_handler-1.5.0}/PKG-INFO +1 -1
  2. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/pyproject.toml +1 -1
  3. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/__init__.py +1 -1
  4. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/core.py +25 -6
  5. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/results.py +24 -9
  6. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/serial_handler.py +8 -2
  7. {ssh_handler-1.4.1 → ssh_handler-1.5.0/ssh_handler.egg-info}/PKG-INFO +1 -1
  8. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/LICENSE +0 -0
  9. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/README.md +0 -0
  10. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/setup.cfg +0 -0
  11. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/__main__.py +0 -0
  12. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/cli.py +0 -0
  13. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/config.py +0 -0
  14. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/credentials.py +0 -0
  15. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/exceptions.py +0 -0
  16. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/ftp.py +0 -0
  17. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/openssh/OpenSSH-ARM64.zip +0 -0
  18. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/openssh/OpenSSH-Win32.zip +0 -0
  19. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/openssh/OpenSSH-Win64.zip +0 -0
  20. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/pool.py +0 -0
  21. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/pyqt_worker.py +0 -0
  22. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/setup_openssh_server.ps1 +0 -0
  23. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler/winrm_bootstrap.py +0 -0
  24. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler.egg-info/SOURCES.txt +0 -0
  25. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler.egg-info/dependency_links.txt +0 -0
  26. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler.egg-info/entry_points.txt +0 -0
  27. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler.egg-info/requires.txt +0 -0
  28. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/ssh_handler.egg-info/top_level.txt +0 -0
  29. {ssh_handler-1.4.1 → ssh_handler-1.5.0}/tests/test_offline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssh-handler
3
- Version: 1.4.1
3
+ Version: 1.5.0
4
4
  Summary: Extensive SSH/SFTP/SCP/FTP handler built on Paramiko, for test automation, CLIs and PyQt5 tools.
5
5
  Author: ssh-handler contributors
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ssh-handler"
7
- version = "1.4.1"
7
+ version = "1.5.0"
8
8
  description = "Extensive SSH/SFTP/SCP/FTP handler built on Paramiko, for test automation, CLIs and PyQt5 tools."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -22,7 +22,7 @@ dependencies (PyQt5 / scp) don't break the core package.
22
22
 
23
23
  from __future__ import annotations
24
24
 
25
- __version__ = "1.4.1"
25
+ __version__ = "1.5.0"
26
26
 
27
27
  from .config import SSHConfig, FTPConfig
28
28
  from .core import SSHHandler, ShellSession
@@ -603,7 +603,7 @@ class SSHHandler:
603
603
  # Continuous / streaming output (tail -f, slog2info -w, journalctl -f…)
604
604
  # ------------------------------------------------------------------ #
605
605
  def iter_lines(self, command: str, *, timeout: Optional[float] = None,
606
- stop_event=None, get_pty: bool = True, encoding: str = "utf-8",
606
+ stop_event=None, get_pty: bool = False, encoding: str = "utf-8",
607
607
  idle_poll: float = 0.4):
608
608
  """
609
609
  Run a (possibly never-ending) command and yield its stdout **line by
@@ -655,7 +655,8 @@ class SSHHandler:
655
655
 
656
656
  def stream(self, command: str, *, on_line=None, on_match=None, match=None,
657
657
  stop_on_match: bool = False, save_to: Optional[str] = None,
658
- append: bool = True, timeout: Optional[float] = None,
658
+ append: bool = True, clean: bool = True,
659
+ timeout: Optional[float] = None,
659
660
  stop_event=None, encoding: str = "utf-8", safe: Optional[bool] = None):
660
661
  """
661
662
  Consume a streaming command with built-in matching and file logging.
@@ -669,6 +670,7 @@ class SSHHandler:
669
670
  :returns: dict with 'lines' (count) and 'matches' (list).
670
671
  """
671
672
  import re
673
+ from .results import strip_ansi
672
674
 
673
675
  def _do():
674
676
  pat = re.compile(match) if isinstance(match, str) else match
@@ -678,6 +680,8 @@ class SSHHandler:
678
680
  try:
679
681
  for line in self.iter_lines(command, timeout=timeout,
680
682
  stop_event=stop_event, encoding=encoding):
683
+ if clean:
684
+ line = strip_ansi(line)
681
685
  count += 1
682
686
  if fh:
683
687
  fh.write(line + "\n")
@@ -708,8 +712,9 @@ class SSHHandler:
708
712
  def serial_stream(self, device: str = "/dev/ttyUSB0", *, baudrate: int = 115200,
709
713
  mode: str = "auto", on_line=None, on_match=None, match=None,
710
714
  stop_on_match: bool = False, save_to: Optional[str] = None,
711
- timeout: Optional[float] = None, stop_event=None,
712
- configure: bool = True, safe: Optional[bool] = None):
715
+ clean: bool = True, timeout: Optional[float] = None,
716
+ stop_event=None, configure: bool = True,
717
+ safe: Optional[bool] = None):
713
718
  """
714
719
  Stream a serial port attached to the **remote** host, over SSH — so it
715
720
  works through a jump host (laptop -> RDP machine -> target). Same live
@@ -753,7 +758,7 @@ class SSHHandler:
753
758
  else:
754
759
  cmd = f"cat {dev}"
755
760
  return self.stream(cmd, on_line=on_line, on_match=on_match, match=match,
756
- stop_on_match=stop_on_match, save_to=save_to,
761
+ stop_on_match=stop_on_match, save_to=save_to, clean=clean,
757
762
  timeout=timeout, stop_event=stop_event, safe=safe)
758
763
 
759
764
  def serial_write(self, device: str, data: str, *, baudrate: int = 115200,
@@ -1027,6 +1032,10 @@ class SSHHandler:
1027
1032
  local_path = os.path.expanduser(local_path)
1028
1033
  sftp = self.sftp()
1029
1034
  start = time.time()
1035
+ # Verify the remote path exists FIRST, so a missing source never leaves
1036
+ # behind an empty local file (sftp.get opens the local file before fetch).
1037
+ if not self._remote_exists(sftp, remote_path):
1038
+ raise SSHTransferError(f"Remote path does not exist: {remote_path}")
1030
1039
  try:
1031
1040
  if self._remote_is_dir(sftp, remote_path):
1032
1041
  if not recursive:
@@ -1039,7 +1048,17 @@ class SSHHandler:
1039
1048
  if parent and not os.path.exists(parent):
1040
1049
  os.makedirs(parent, exist_ok=True)
1041
1050
  self._emit(logging.INFO, f"PULL {remote_path} -> {local_path}")
1042
- sftp.get(remote_path, local_path, callback=callback)
1051
+ existed = os.path.exists(local_path)
1052
+ try:
1053
+ sftp.get(remote_path, local_path, callback=callback)
1054
+ except BaseException:
1055
+ # don't leave a partial/empty local file behind on failure
1056
+ if not existed and os.path.exists(local_path):
1057
+ try:
1058
+ os.remove(local_path)
1059
+ except OSError:
1060
+ pass
1061
+ raise
1043
1062
  size, count = os.path.getsize(local_path), 1
1044
1063
  except SSHTransferError:
1045
1064
  raise
@@ -2,11 +2,28 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import re
5
6
  import time
6
7
  from dataclasses import dataclass, field, asdict
7
8
  from typing import Optional
8
9
 
9
10
 
11
+ # ANSI/VT escape sequences (CSI like ESC[23;80H, OSC like ESC]0;title BEL, etc.)
12
+ _ANSI_RE = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))")
13
+ # control chars except tab(09), newline(0a), carriage-return(0d handled separately)
14
+ _CTRL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
15
+
16
+
17
+ def strip_ansi(text: str) -> str:
18
+ """Remove ANSI/VT escape codes, carriage returns, and other control chars,
19
+ so streamed console output is clean to save, match, and read."""
20
+ if not text:
21
+ return text
22
+ text = _ANSI_RE.sub("", text)
23
+ text = text.replace("\r", "")
24
+ return _CTRL_RE.sub("", text)
25
+
26
+
10
27
  def _human_size(num: float) -> str:
11
28
  for unit in ("B", "KB", "MB", "GB", "TB"):
12
29
  if abs(num) < 1024.0:
@@ -43,10 +60,8 @@ class CommandResult:
43
60
  return asdict(self)
44
61
 
45
62
  def __str__(self) -> str:
46
- return (
47
- f"<CommandResult host={self.host} exit={self.exit_code} "
48
- f"ok={self.ok} dur={self.duration:.2f}s cmd={self.command!r}>"
49
- )
63
+ status = "ok" if self.ok else f"FAILED (exit {self.exit_code})"
64
+ return f"$ {self.command} [{status}, {self.duration:.2f}s]"
50
65
 
51
66
 
52
67
  @dataclass
@@ -79,11 +94,11 @@ class TransferResult:
79
94
  return d
80
95
 
81
96
  def __str__(self) -> str:
82
- return (
83
- f"<TransferResult {self.direction}/{self.protocol} "
84
- f"{self.source} -> {self.dest} {self.human_size} "
85
- f"in {self.duration:.2f}s ({self.human_speed}), files={self.files}>"
86
- )
97
+ verb = "Uploaded" if self.direction == "push" else "Downloaded"
98
+ files = f"{self.files} files, " if self.files != 1 else ""
99
+ return (f"{verb} {self.source} -> {self.dest} "
100
+ f"({files}{self.human_size} in {self.duration:.2f}s, "
101
+ f"{self.human_speed})")
87
102
 
88
103
 
89
104
  @dataclass
@@ -153,12 +153,16 @@ class SerialHandler:
153
153
 
154
154
  def stream(self, *, on_line=None, on_match=None, match=None,
155
155
  stop_on_match: bool = False, save_to: Optional[str] = None,
156
- append: bool = True, timeout: Optional[float] = None, stop_event=None,
156
+ append: bool = True, clean: bool = True,
157
+ timeout: Optional[float] = None, stop_event=None,
157
158
  encoding: str = "utf-8", safe=None):
158
159
  """
159
160
  Read the serial console continuously with built-in matching + file
160
- logging. Same signature/semantics as SSHHandler.stream.
161
+ logging. Same signature/semantics as SSHHandler.stream. ``clean=True``
162
+ strips ANSI escape codes and control chars from each line.
161
163
  """
164
+ from .results import strip_ansi
165
+
162
166
  def _do():
163
167
  pat = re.compile(match) if isinstance(match, str) else match
164
168
  matches, count = [], 0
@@ -167,6 +171,8 @@ class SerialHandler:
167
171
  try:
168
172
  for line in self.iter_lines(stop_event=stop_event, timeout=timeout,
169
173
  encoding=encoding):
174
+ if clean:
175
+ line = strip_ansi(line)
170
176
  count += 1
171
177
  if fh:
172
178
  fh.write(line + "\n")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssh-handler
3
- Version: 1.4.1
3
+ Version: 1.5.0
4
4
  Summary: Extensive SSH/SFTP/SCP/FTP handler built on Paramiko, for test automation, CLIs and PyQt5 tools.
5
5
  Author: ssh-handler contributors
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes