ssh-handler 1.0.0__py3-none-any.whl

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.
ssh_handler/results.py ADDED
@@ -0,0 +1,127 @@
1
+ """Rich, structured result objects returned by every action."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass, field, asdict
7
+ from typing import Optional
8
+
9
+
10
+ def _human_size(num: float) -> str:
11
+ for unit in ("B", "KB", "MB", "GB", "TB"):
12
+ if abs(num) < 1024.0:
13
+ return f"{num:.1f}{unit}"
14
+ num /= 1024.0
15
+ return f"{num:.1f}PB"
16
+
17
+
18
+ @dataclass
19
+ class CommandResult:
20
+ """Result of a remote command execution."""
21
+
22
+ command: str
23
+ exit_code: int
24
+ stdout: str
25
+ stderr: str
26
+ duration: float
27
+ host: str = ""
28
+ started_at: float = field(default_factory=time.time)
29
+
30
+ @property
31
+ def ok(self) -> bool:
32
+ return self.exit_code == 0
33
+
34
+ def __bool__(self) -> bool:
35
+ return self.ok
36
+
37
+ def as_dict(self) -> dict:
38
+ return asdict(self)
39
+
40
+ def __str__(self) -> str:
41
+ return (
42
+ f"<CommandResult host={self.host} exit={self.exit_code} "
43
+ f"ok={self.ok} dur={self.duration:.2f}s cmd={self.command!r}>"
44
+ )
45
+
46
+
47
+ @dataclass
48
+ class TransferResult:
49
+ """Result of a file/dir transfer (push/pull via SFTP or SCP)."""
50
+
51
+ source: str
52
+ dest: str
53
+ direction: str # "push" or "pull"
54
+ protocol: str # "sftp" or "scp"
55
+ size_bytes: int
56
+ duration: float
57
+ files: int = 1
58
+
59
+ @property
60
+ def speed_bps(self) -> float:
61
+ return self.size_bytes / self.duration if self.duration > 0 else 0.0
62
+
63
+ @property
64
+ def human_speed(self) -> str:
65
+ return f"{_human_size(self.speed_bps)}/s"
66
+
67
+ @property
68
+ def human_size(self) -> str:
69
+ return _human_size(self.size_bytes)
70
+
71
+ def as_dict(self) -> dict:
72
+ d = asdict(self)
73
+ d.update(speed_bps=self.speed_bps, human_speed=self.human_speed)
74
+ return d
75
+
76
+ def __str__(self) -> str:
77
+ return (
78
+ f"<TransferResult {self.direction}/{self.protocol} "
79
+ f"{self.source} -> {self.dest} {self.human_size} "
80
+ f"in {self.duration:.2f}s ({self.human_speed}), files={self.files}>"
81
+ )
82
+
83
+
84
+ @dataclass
85
+ class ShellResult:
86
+ """Result of an interactive shell read/expect operation."""
87
+
88
+ output: str
89
+ matched: Optional[str]
90
+ timed_out: bool
91
+ duration: float
92
+
93
+ def __bool__(self) -> bool:
94
+ return not self.timed_out
95
+
96
+ def __str__(self) -> str:
97
+ return (
98
+ f"<ShellResult matched={self.matched!r} "
99
+ f"timed_out={self.timed_out} dur={self.duration:.2f}s>"
100
+ )
101
+
102
+
103
+ @dataclass
104
+ class OperationResult:
105
+ """
106
+ Returned by safe-mode operations. Wraps a value or an error so GUI handlers
107
+ never have to try/except. Falsy on failure.
108
+ """
109
+
110
+ success: bool
111
+ action: str = ""
112
+ value: object = None
113
+ error: Optional[Exception] = None
114
+
115
+ def __bool__(self) -> bool:
116
+ return self.success
117
+
118
+ def unwrap(self):
119
+ """Return value on success, else re-raise the captured error."""
120
+ if self.success:
121
+ return self.value
122
+ raise self.error if self.error else RuntimeError(f"{self.action} failed")
123
+
124
+ def __str__(self) -> str:
125
+ if self.success:
126
+ return f"<OperationResult {self.action} ok value={self.value!r}>"
127
+ return f"<OperationResult {self.action} error={self.error!r}>"
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: ssh-handler
3
+ Version: 1.0.0
4
+ Summary: Extensive SSH/SFTP/SCP/FTP handler built on Paramiko, for test automation, CLIs and PyQt5 tools.
5
+ Author: Naveen
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: paramiko>=3.0
9
+ Provides-Extra: secure
10
+ Requires-Dist: keyring>=23.0; extra == "secure"
11
+ Provides-Extra: scp
12
+ Requires-Dist: scp>=0.14; extra == "scp"
13
+ Provides-Extra: gui
14
+ Requires-Dist: PyQt5>=5.15; extra == "gui"
15
+ Provides-Extra: all
16
+ Requires-Dist: keyring>=23.0; extra == "all"
17
+ Requires-Dist: scp>=0.14; extra == "all"
18
+ Requires-Dist: PyQt5>=5.15; extra == "all"
19
+
20
+ # ssh_handler
21
+
22
+ An extensive **SSH / SFTP / SCP / FTP** handler built on [Paramiko](https://www.paramiko.org/),
23
+ packaged so the *same* code works in three places:
24
+
25
+ - a **test-automation framework** (raise-on-error, pytest fixtures, parallel fleet ops),
26
+ - a **standalone CLI** (`python -m ssh_handler …`, argument-driven),
27
+ - a **PyQt5 tool** (safe mode + log streaming via Qt signals, off the GUI thread).
28
+
29
+ Passwords are wrapped in a `Secret` and stored in the **OS credential vault** — they
30
+ never appear in logs, reprs, tracebacks, or on disk in plaintext.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install -e . # core (paramiko)
36
+ pip install -e ".[all]" # + keyring, scp, PyQt5
37
+ # or just the pieces you need:
38
+ pip install -e ".[secure]" # keyring (confidential password storage)
39
+ pip install -e ".[scp]" # scp (SCP-protocol transfers)
40
+ pip install -e ".[gui]" # PyQt5 (the worker)
41
+ ```
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ from ssh_handler import SSHHandler, SSHConfig
47
+
48
+ with SSHHandler(SSHConfig(host="10.0.0.5", username="root", password="pw")) as ssh:
49
+ print(ssh.run("uptime").stdout)
50
+ ssh.run("systemctl restart nginx", check=True) # raises on non-zero exit
51
+ ssh.push("local.txt", "/tmp/remote.txt") # SFTP upload
52
+ ssh.pull("/etc/nginx", "./backup", recursive=True) # recursive download
53
+ ```
54
+
55
+ ## Connecting to your RDP / Windows hosts (domain login `EU\nkennedy`)
56
+
57
+ Those Windows machines need **OpenSSH Server** enabled (Settings → Optional Features →
58
+ "OpenSSH Server", or `Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0`).
59
+ Then you log in with your normal domain credentials.
60
+
61
+ **Pass `domain` and `username` separately** — never hard-code `"EU\nkennedy"` as a Python
62
+ string, because `\n` becomes a newline. The handler builds `EU\nkennedy` for you:
63
+
64
+ ```python
65
+ from ssh_handler import SSHHandler, SSHConfig, CredentialStore
66
+
67
+ store = CredentialStore(service="my_test_lab")
68
+ cfg = SSHConfig(
69
+ host="10.20.30.40",
70
+ domain="EU", username="nkennedy",
71
+ password=store.get("EU\\nkennedy"), # a Secret pulled from the OS vault
72
+ remote_os="windows", # skip the OS probe
73
+ fast_auth=True, # skip key probing -> faster login
74
+ )
75
+ with SSHHandler(cfg) as ssh:
76
+ print(ssh.run("whoami").stdout) # EU\nkennedy
77
+ print(ssh.run("powershell Get-Service sshd").stdout)
78
+ ssh.push("report.xlsx", "C:/Users/nkennedy/Desktop/report.xlsx")
79
+ ```
80
+
81
+ Store the password **once** so it's never typed or committed again:
82
+
83
+ ```python
84
+ CredentialStore("my_test_lab").set("EU\\nkennedy", prompt_password())
85
+ # or from the CLI:
86
+ python -m ssh_handler store-credential --user nkennedy --domain EU --service my_test_lab
87
+ ```
88
+
89
+ ## Confidential credentials
90
+
91
+ | Mechanism | What it does |
92
+ |-----------|--------------|
93
+ | `Secret` | wraps a password; `str()`/`repr()`/logs show `********`; only `.reveal()` exposes it |
94
+ | `mask()` | redacts secret values from any log line (applied automatically to all logging) |
95
+ | `CredentialStore` | stores/reads passwords in Windows Credential Manager (or macOS Keychain / Secret Service) via `keyring` — **no plaintext on disk** |
96
+ | `prompt_password()` | hidden terminal input |
97
+
98
+ ## Speed
99
+
100
+ - **`fast_auth`** (default on): when a password is supplied, key/agent probing is skipped —
101
+ faster logins and no "Too many authentication failures" from the server's `MaxAuthTries`.
102
+ - One SFTP channel is opened lazily and **reused** across operations.
103
+ - SFTP downloads use Paramiko **prefetch** for high throughput.
104
+ - **`SSHPool`** runs the same command/transfer across many hosts in parallel threads.
105
+ - `remote_os="windows"|"linux"` skips the one-time OS-detection probe.
106
+ - `compress=True` for slow/high-latency links; keepalives keep long sessions alive.
107
+
108
+ ## What's covered (Paramiko parity, plus extras)
109
+
110
+ - **Exec**: `run` (timeout, `check`, pty, env), `run_many`, `sudo` (password via stdin)
111
+ - **Interactive shell**: `open_shell()` → `ShellSession` with `send` / `read_until` / `read_available`
112
+ - **SFTP**: `listdir`, `listdir_attr`, `stat`, `lstat`, `exists`, `isdir`, `mkdir`, `makedirs`,
113
+ `rmdir`, `remove`, `rename`, `chmod`, `chown`, `symlink`, `readlink`, `open`,
114
+ `read_text`, `write_text`, `walk`, and `push`/`pull` (recursive, progress, stats)
115
+ - **SCP**: `scp_push` / `scp_pull` (needs the `scp` extra)
116
+ - **FTP/FTPS**: `FTPHandler` (stdlib `ftplib`, no extra dependency)
117
+ - **Jump host / bastion**: `SSHConfig(jump_host=…)` (ProxyJump-style)
118
+ - **Escape hatch**: `ssh.client` and `ssh.transport` expose the raw Paramiko objects
119
+
120
+ ## Result objects
121
+
122
+ Every action returns structured data, not bare strings:
123
+
124
+ - `CommandResult` — `exit_code`, `stdout`, `stderr`, `duration`, `host`, `.ok`, `.as_dict()`
125
+ - `TransferResult` — `size_bytes`, `duration`, `speed_bps`, `human_speed`, `files`
126
+ - `ShellResult` — `output`, `matched`, `timed_out`, `duration`
127
+ - `OperationResult` — safe-mode wrapper: `bool(res)`, `res.value`, `res.error`, `res.unwrap()`
128
+
129
+ ## Two error-handling styles
130
+
131
+ - **Raise (default)** — for tests/scripts. Typed exceptions: `SSHConnectionError`,
132
+ `SSHAuthenticationError`, `SSHTimeoutError`, `SSHCommandError`, `SSHTransferError`,
133
+ `FTPError`, `CredentialError` — all subclasses of `SSHError`. **Auto-retry** on connect
134
+ and **auto-reconnect** on dropped sessions are built in.
135
+ - **Safe mode** (`SSHHandler(cfg, safe=True)`) — for GUIs. Every call returns an
136
+ `OperationResult` instead of raising, so the event loop never dies. Override per call
137
+ with `safe=True/False`.
138
+
139
+ ## CLI
140
+
141
+ ```bash
142
+ python -m ssh_handler run --host H --user U --domain EU uptime
143
+ python -m ssh_handler push --host H --user U ./build /tmp/build --recursive
144
+ python -m ssh_handler pull --host H --user U /var/log ./logs --recursive
145
+ python -m ssh_handler info --host H --user U --json
146
+ python -m ssh_handler run --host H --user U --use-stored uptime # vault password
147
+ ```
148
+ Password options: `--password` (hidden prompt), `--use-stored` (OS vault), `--key FILE`.
149
+ Add `--json` for machine-readable output.
150
+
151
+ ## PyQt5
152
+
153
+ `ssh_handler.pyqt_worker.SSHWorker` is a `QObject` wrapping the handler in **safe mode**.
154
+ `moveToThread()` it, connect its signals (`log`, `connected`, `command_done`,
155
+ `transfer_done`, `progress`, `error`, `finished`), and drive it from the GUI — see recipe
156
+ in [examples/examples.py](examples/examples.py).
157
+
158
+ ## Project layout
159
+
160
+ ```
161
+ ssh_handler/
162
+ config.py SSHConfig, FTPConfig
163
+ credentials.py Secret, CredentialStore, mask, prompt_password
164
+ core.py SSHHandler, ShellSession (SSH + SFTP + SCP)
165
+ ftp.py FTPHandler (FTP/FTPS)
166
+ pool.py SSHPool (parallel multi-host)
167
+ cli.py argparse entry point (python -m ssh_handler)
168
+ pyqt_worker.py SSHWorker (PyQt5, lazy import)
169
+ results.py CommandResult, TransferResult, ShellResult, OperationResult
170
+ exceptions.py SSHError hierarchy
171
+ examples/examples.py
172
+ tests/test_offline.py (no network needed)
173
+ ```
174
+
175
+ ## Tests
176
+
177
+ `tests/test_offline.py` validates secret masking, domain-login building, fast-auth
178
+ kwargs, recursive-mkdir logic, transfer math, and both error-handling modes — all offline.
179
+
180
+ ```bash
181
+ python tests/test_offline.py
182
+ ```
@@ -0,0 +1,16 @@
1
+ ssh_handler/__init__.py,sha256=ZY-5qIgixXRCKs_AcONVIFegIOPYX3ImCZK-CyKHg5M,1737
2
+ ssh_handler/__main__.py,sha256=y-JCII5ssoxfEktEawpfYKBz-CCgxob8Q3b0AK76Lqo,122
3
+ ssh_handler/cli.py,sha256=AXG8u2hmRQXG2BsShwC_XFrHXPXf83CoE26DS-JOIrc,5926
4
+ ssh_handler/config.py,sha256=ElXOX-Al_o8PVv_i8cZponb1cdd1nkDBlwJjbV3g_oI,3982
5
+ ssh_handler/core.py,sha256=wGyr22I3MeuGXvBx4hi9Sv9Bm9ZLdwvpD1zPMB0VCCc,33491
6
+ ssh_handler/credentials.py,sha256=dhdun4U4lhngjbvT_Udv2Xpf9_NCmnB9Nk-S4ucOlNY,4848
7
+ ssh_handler/exceptions.py,sha256=O2Ksw1r0mTQkqd_w36KYwuL6me_bKFFXFJ9o8WO6Ul0,1319
8
+ ssh_handler/ftp.py,sha256=TOEJ3qHlo49A7rxf5iSunlr0zZGgxRpzbt27unHKWjA,7346
9
+ ssh_handler/pool.py,sha256=L7_dN75GYZYGedS3DHumgNmc6rYxJeIDmJ6r03QjxNk,3822
10
+ ssh_handler/pyqt_worker.py,sha256=FE1KiTC34fJTQxzVwo5CvKbb_j7FJes2f-aMgX3auXg,3636
11
+ ssh_handler/results.py,sha256=BRZ10UmIvuaLwOC1YSdDarNKJ7rz0jcOmm_5mVNi_8k,3262
12
+ ssh_handler-1.0.0.dist-info/METADATA,sha256=BCILCNpa6XbORrY2sMqRScUT988prMxHtg78Lov3q0U,7998
13
+ ssh_handler-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
14
+ ssh_handler-1.0.0.dist-info/entry_points.txt,sha256=AuXs00bXUYsd1ABut1If8dnWB7S8U9ZaWpsPClAHf2A,53
15
+ ssh_handler-1.0.0.dist-info/top_level.txt,sha256=QQ4IFeUXYOGWDo2uNzt6PEzQU_jZIBSONnD8oh5fvNg,12
16
+ ssh_handler-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ssh-handler = ssh_handler.cli:main
@@ -0,0 +1 @@
1
+ ssh_handler