ssh-handler 1.0.0__tar.gz → 1.0.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.
Files changed (26) hide show
  1. ssh_handler-1.0.1/LICENSE +21 -0
  2. ssh_handler-1.0.1/PKG-INFO +342 -0
  3. ssh_handler-1.0.1/README.md +314 -0
  4. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/pyproject.toml +36 -26
  5. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/__init__.py +52 -52
  6. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/cli.py +2 -2
  7. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/config.py +4 -3
  8. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/credentials.py +1 -1
  9. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/pyqt_worker.py +2 -2
  10. ssh_handler-1.0.1/ssh_handler.egg-info/PKG-INFO +342 -0
  11. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler.egg-info/SOURCES.txt +1 -0
  12. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/tests/test_offline.py +3 -3
  13. ssh_handler-1.0.0/PKG-INFO +0 -182
  14. ssh_handler-1.0.0/README.md +0 -163
  15. ssh_handler-1.0.0/ssh_handler.egg-info/PKG-INFO +0 -182
  16. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/setup.cfg +0 -0
  17. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/__main__.py +0 -0
  18. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/core.py +0 -0
  19. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/exceptions.py +0 -0
  20. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/ftp.py +0 -0
  21. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/pool.py +0 -0
  22. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler/results.py +0 -0
  23. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler.egg-info/dependency_links.txt +0 -0
  24. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler.egg-info/entry_points.txt +0 -0
  25. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler.egg-info/requires.txt +0 -0
  26. {ssh_handler-1.0.0 → ssh_handler-1.0.1}/ssh_handler.egg-info/top_level.txt +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ssh-handler contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,342 @@
1
+ Metadata-Version: 2.4
2
+ Name: ssh-handler
3
+ Version: 1.0.1
4
+ Summary: Extensive SSH/SFTP/SCP/FTP handler built on Paramiko, for test automation, CLIs and PyQt5 tools.
5
+ Author: ssh-handler contributors
6
+ License-Expression: MIT
7
+ Keywords: ssh,sftp,scp,ftp,paramiko,automation,pyqt5,testing
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: System :: Networking
11
+ Classifier: Topic :: Software Development :: Testing
12
+ Classifier: Intended Audience :: Developers
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: paramiko>=3.0
17
+ Provides-Extra: secure
18
+ Requires-Dist: keyring>=23.0; extra == "secure"
19
+ Provides-Extra: scp
20
+ Requires-Dist: scp>=0.14; extra == "scp"
21
+ Provides-Extra: gui
22
+ Requires-Dist: PyQt5>=5.15; extra == "gui"
23
+ Provides-Extra: all
24
+ Requires-Dist: keyring>=23.0; extra == "all"
25
+ Requires-Dist: scp>=0.14; extra == "all"
26
+ Requires-Dist: PyQt5>=5.15; extra == "all"
27
+ Dynamic: license-file
28
+
29
+ # ssh-handler
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/ssh-handler.svg)](https://pypi.org/project/ssh-handler/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/ssh-handler.svg)](https://pypi.org/project/ssh-handler/)
33
+
34
+ An extensive **SSH / SFTP / SCP / FTP** automation handler built on
35
+ [Paramiko](https://www.paramiko.org/). One package, three ways to use it:
36
+
37
+ - **Test-automation framework** — raise-on-error API, pytest fixtures, parallel fleet ops.
38
+ - **Standalone CLI** — `python -m ssh_handler …`, fully argument-driven.
39
+ - **PyQt5 tool** — safe mode + log streaming over Qt signals, runs off the GUI thread.
40
+
41
+ Passwords are wrapped in a `Secret` and stored in the **OS credential vault** — they
42
+ never appear in logs, reprs, tracebacks, or on disk in plaintext.
43
+
44
+ ```bash
45
+ pip install ssh-handler
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Table of contents
51
+
52
+ - [Why this package](#why-this-package)
53
+ - [Install](#install)
54
+ - [Quick start](#quick-start)
55
+ - [What you can do](#what-you-can-do) — full capability list
56
+ - [Domain / Windows (RDP) hosts](#domain--windows-rdp-hosts)
57
+ - [Confidential credentials](#confidential-credentials)
58
+ - [Performance](#performance)
59
+ - [Error handling: two styles](#error-handling-two-styles)
60
+ - [Result objects](#result-objects)
61
+ - [CLI reference](#cli-reference)
62
+ - [PyQt5 integration](#pyqt5-integration)
63
+ - [Parallel fleet operations](#parallel-fleet-operations)
64
+ - [FTP / FTPS](#ftp--ftps)
65
+ - [API map](#api-map)
66
+ - [Releasing](#releasing)
67
+
68
+ ---
69
+
70
+ ## Why this package
71
+
72
+ Paramiko is powerful but low-level: you manage clients, transports, channels,
73
+ SFTP sessions, timeouts, retries, host-key policies and error handling yourself,
74
+ and you repeat that boilerplate in every project. `ssh-handler` wraps all of it
75
+ behind one object that:
76
+
77
+ - auto-selects the right authentication strategy (password, key, agent, empty password),
78
+ - retries connections and transparently reconnects dropped sessions,
79
+ - returns **structured results** for every action instead of raw strings,
80
+ - keeps **passwords confidential** end-to-end,
81
+ - and exposes the same surface whether you're in a test, a CLI, or a GUI.
82
+
83
+ ## Install
84
+
85
+ ```bash
86
+ pip install ssh-handler # core (paramiko only)
87
+ pip install "ssh-handler[secure]" # + keyring (OS credential vault)
88
+ pip install "ssh-handler[scp]" # + scp (SCP-protocol transfers)
89
+ pip install "ssh-handler[gui]" # + PyQt5 (the GUI worker)
90
+ pip install "ssh-handler[all]" # everything
91
+ ```
92
+
93
+ `scp`, `keyring`, and `PyQt5` are optional — the core works without them, and
94
+ those features raise a clear, actionable message if you use them without the extra.
95
+
96
+ ## Quick start
97
+
98
+ ```python
99
+ from ssh_handler import SSHHandler, SSHConfig
100
+
101
+ with SSHHandler(SSHConfig(host="10.0.0.5", username="root", password="pw")) as ssh:
102
+ print(ssh.run("uptime").stdout)
103
+ ssh.run("systemctl restart nginx", check=True) # raises on non-zero exit
104
+ ssh.push("local.txt", "/tmp/remote.txt") # SFTP upload
105
+ ssh.pull("/etc/nginx", "./backup", recursive=True) # recursive download
106
+ ```
107
+
108
+ ## What you can do
109
+
110
+ **Connection & session**
111
+ - Connect with password, private key (+ passphrase), SSH agent, auto-discovered
112
+ keys, or an **empty-password** account — all auto-tried in a smart order.
113
+ - **Auto-retry** connects with backoff; **auto-reconnect** if a session drops.
114
+ - Keepalives, per-command and connection timeouts, optional compression.
115
+ - **Jump host / bastion** chaining (ProxyJump-style) via `SSHConfig(jump_host=…)`.
116
+ - Host-key policy: `auto` / `reject` / `warn`, with an optional `known_hosts` file.
117
+ - Remote-OS awareness (`detect_os()`, `is_windows`) for Linux **and** Windows targets.
118
+ - Raw escape hatch: `ssh.client` and `ssh.transport` expose the underlying Paramiko objects.
119
+
120
+ **Command execution**
121
+ - `run()` — timeout, `check` (raise on non-zero), PTY allocation, custom environment.
122
+ - `run_many()` — batch with stop-on-error.
123
+ - `sudo()` — runs `sudo -S` and feeds the password on stdin.
124
+ - `open_shell()` — a persistent interactive `ShellSession` with `send` /
125
+ `read_until` (send-expect) / `read_available`.
126
+
127
+ **File operations (SFTP) — full Paramiko parity**
128
+ - Transfers: `push` / `pull` (single file **or** recursive directory, with progress
129
+ callbacks and transfer statistics), plus `scp_push` / `scp_pull` (SCP protocol).
130
+ - Listing & metadata: `listdir`, `listdir_attr`, `stat`, `lstat`, `exists`, `isdir`, `walk`.
131
+ - Directories: `mkdir`, `makedirs` (recursive `mkdir -p`), `rmdir`.
132
+ - Files: `remove`, `rename`, `open` (remote file object), `read_text`, `write_text`.
133
+ - Permissions & links: `chmod`, `chown`, `symlink`, `readlink`.
134
+
135
+ **Other protocols**
136
+ - **FTP / FTPS** via `FTPHandler` (standard-library `ftplib`, no extra dependency):
137
+ connect, login, TLS, `push`, `pull`, `listdir`, `cwd`, `pwd`, `mkdir`, `rmdir`,
138
+ `remove`, `rename`, `size`, `exists`.
139
+
140
+ **Scale & integration**
141
+ - `SSHPool` — run the same command/transfer across many hosts in parallel threads.
142
+ - Safe mode + log callback for GUIs; structured result objects everywhere.
143
+ - Confidential credential storage in the OS vault (`CredentialStore`, `Secret`, `mask`).
144
+
145
+ ## Domain / Windows (RDP) hosts
146
+
147
+ The Windows machines you normally RDP into can be driven over SSH once **OpenSSH
148
+ Server** is enabled on them:
149
+
150
+ ```powershell
151
+ Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
152
+ Start-Service sshd ; Set-Service -Name sshd -StartupType Automatic
153
+ ```
154
+
155
+ Then log in with your normal domain credentials. **Pass `domain` and `username`
156
+ separately** — never hard-code a single `"DOMAIN\user"` Python string, because a
157
+ backslash escape (e.g. `\n`, `\t`) silently becomes a control character. The
158
+ handler builds the `DOMAIN\user` login string for you:
159
+
160
+ ```python
161
+ from ssh_handler import SSHHandler, SSHConfig, CredentialStore
162
+
163
+ store = CredentialStore(service="my_test_lab")
164
+ cfg = SSHConfig(
165
+ host="10.20.30.40",
166
+ domain="CORP", username="myuser", # -> login "CORP\myuser"
167
+ password=store.get("CORP\\myuser"), # a Secret pulled from the OS vault
168
+ remote_os="windows", # skip the OS probe
169
+ fast_auth=True, # skip key probing -> faster login
170
+ )
171
+ with SSHHandler(cfg) as ssh:
172
+ print(ssh.run("whoami").stdout) # CORP\myuser
173
+ print(ssh.run("powershell Get-Service sshd").stdout)
174
+ ssh.push("report.xlsx", "C:/Users/myuser/Desktop/report.xlsx")
175
+ ```
176
+
177
+ Store the password **once** so it never appears in code again:
178
+
179
+ ```python
180
+ from ssh_handler import CredentialStore, prompt_password
181
+ CredentialStore("my_test_lab").set("CORP\\myuser", prompt_password())
182
+ ```
183
+ ```bash
184
+ # …or from the CLI:
185
+ python -m ssh_handler store-credential --user myuser --domain CORP --service my_test_lab
186
+ ```
187
+
188
+ ## Confidential credentials
189
+
190
+ | Mechanism | What it does |
191
+ |-----------|--------------|
192
+ | `Secret` | wraps a password; `str()`/`repr()`/logs show `********`; only `.reveal()` exposes it |
193
+ | `mask()` | redacts secret values from any string (applied automatically to all logging) |
194
+ | `CredentialStore` | stores/reads passwords in the OS vault (Windows Credential Manager / macOS Keychain / Secret Service) via `keyring` — **no plaintext on disk** |
195
+ | `prompt_password()` | hidden terminal input, returns a `Secret` |
196
+
197
+ Pass a `Secret` (or a plain string, which is wrapped automatically) anywhere a
198
+ password is accepted. It stays redacted across the whole stack.
199
+
200
+ ## Performance
201
+
202
+ - **`fast_auth`** (default on): when a password is supplied, the slow key/agent
203
+ probing is skipped — faster logins and no "Too many authentication failures"
204
+ from the server's `MaxAuthTries`.
205
+ - One SFTP channel is opened lazily and **reused** across operations.
206
+ - SFTP downloads use Paramiko **prefetch** for high throughput.
207
+ - `remote_os="windows"|"linux"` skips the one-time OS-detection probe.
208
+ - `compress=True` for slow/high-latency links; keepalives keep long sessions alive.
209
+ - `SSHPool` parallelizes across hosts with a thread pool.
210
+
211
+ ## Error handling: two styles
212
+
213
+ **Raise (default)** — best for tests/scripts. Typed exceptions, all subclasses of
214
+ `SSHError`:
215
+
216
+ ```
217
+ SSHConnectionError SSHAuthenticationError SSHTimeoutError
218
+ SSHCommandError SSHTransferError SSHNotConnectedError
219
+ FTPError CredentialError
220
+ ```
221
+
222
+ **Safe mode** (`SSHHandler(cfg, safe=True)`) — best for GUIs. Every call returns an
223
+ `OperationResult` instead of raising, so an event loop never dies:
224
+
225
+ ```python
226
+ res = ssh.connect()
227
+ if not res: # OperationResult is falsy on failure
228
+ show_error(res.error)
229
+ else:
230
+ data = res.value # or res.unwrap() to re-raise on failure
231
+ ```
232
+
233
+ Override per call with `safe=True` / `safe=False`.
234
+
235
+ ## Result objects
236
+
237
+ Every action returns structured data, not bare strings:
238
+
239
+ - **`CommandResult`** — `exit_code`, `stdout`, `stderr`, `duration`, `host`, `.ok`, `.as_dict()`
240
+ - **`TransferResult`** — `size_bytes`, `duration`, `speed_bps`, `human_speed`, `human_size`, `files`
241
+ - **`ShellResult`** — `output`, `matched`, `timed_out`, `duration`
242
+ - **`OperationResult`** — safe-mode wrapper: `bool(res)`, `res.value`, `res.error`, `res.unwrap()`
243
+
244
+ ## CLI reference
245
+
246
+ ```bash
247
+ python -m ssh_handler run --host H --user U --domain CORP uptime
248
+ python -m ssh_handler push --host H --user U ./build /tmp/build --recursive
249
+ python -m ssh_handler pull --host H --user U /var/log ./logs --recursive
250
+ python -m ssh_handler info --host H --user U --json
251
+ python -m ssh_handler store-credential --user U --domain CORP --service my_test_lab
252
+ ```
253
+
254
+ Password options: `--password` (hidden prompt), `--use-stored` (read from the OS
255
+ vault), `--key FILE` (private key). Add `--json` for machine-readable output.
256
+ After `pip install`, a `ssh-handler` console script is also available
257
+ (`ssh-handler run --host …`).
258
+
259
+ ## PyQt5 integration
260
+
261
+ `ssh_handler.pyqt_worker.SSHWorker` is a `QObject` wrapping the handler in **safe
262
+ mode**. Move it to a `QThread`, connect its signals, and drive it from the GUI:
263
+
264
+ ```python
265
+ from PyQt5.QtCore import QThread
266
+ from ssh_handler import SSHConfig
267
+ from ssh_handler.pyqt_worker import SSHWorker
268
+
269
+ worker = SSHWorker(SSHConfig(host="10.0.0.5", username="root", password=secret))
270
+ thread = QThread(); worker.moveToThread(thread)
271
+ worker.log.connect(text_edit.append) # live, secret-masked log lines
272
+ worker.command_done.connect(on_command_done)
273
+ worker.progress.connect(progress_bar.setValue) # bytes_done, bytes_total
274
+ thread.started.connect(lambda: worker.run_command("uptime"))
275
+ thread.start()
276
+ ```
277
+
278
+ Signals: `log`, `connected`, `command_done`, `transfer_done`, `progress`, `error`,
279
+ `finished`. The import is lazy, so the rest of the package works where PyQt5 isn't installed.
280
+
281
+ ## Parallel fleet operations
282
+
283
+ ```python
284
+ from ssh_handler import SSHPool, SSHConfig
285
+
286
+ configs = [SSHConfig(host=h, username="root", password="pw")
287
+ for h in ("10.0.0.1", "10.0.0.2", "10.0.0.3")]
288
+
289
+ with SSHPool(configs, max_workers=8) as pool:
290
+ for host, res in pool.run("uptime").items():
291
+ print(host, res.value.stdout.strip() if res else res.error)
292
+ pool.pull("/var/log/syslog", "logs/{host}_syslog.txt") # {host} avoids collisions
293
+ ```
294
+
295
+ ## FTP / FTPS
296
+
297
+ ```python
298
+ from ssh_handler import FTPHandler, FTPConfig
299
+
300
+ with FTPHandler(FTPConfig(host="ftp.example.com", username="u",
301
+ password="p", use_tls=True)) as ftp:
302
+ ftp.push("local.txt", "remote.txt")
303
+ ftp.pull("remote.txt", "copy.txt")
304
+ print(ftp.listdir("/"))
305
+ ```
306
+
307
+ ## API map
308
+
309
+ ```
310
+ ssh_handler/
311
+ config.py SSHConfig, FTPConfig
312
+ credentials.py Secret, CredentialStore, mask, prompt_password
313
+ core.py SSHHandler, ShellSession (SSH + SFTP + SCP)
314
+ ftp.py FTPHandler (FTP / FTPS)
315
+ pool.py SSHPool (parallel multi-host)
316
+ cli.py argparse entry point (python -m ssh_handler / ssh-handler)
317
+ pyqt_worker.py SSHWorker (PyQt5, lazy import)
318
+ results.py CommandResult, TransferResult, ShellResult, OperationResult
319
+ exceptions.py SSHError hierarchy
320
+ examples/examples.py copy-paste recipes
321
+ tests/test_offline.py offline checks (no network needed)
322
+ ```
323
+
324
+ ## Releasing
325
+
326
+ Maintainers: use the helper to build and publish a new version.
327
+
328
+ ```bash
329
+ python scripts/release.py 1.0.1 # bump -> build -> twine check -> upload
330
+ python scripts/release.py 1.0.1 --dry-run # build + check only, no upload
331
+ python scripts/release.py patch # auto-bump patch/minor/major
332
+ ```
333
+
334
+ The token is read from the `TWINE_PASSWORD` environment variable (username
335
+ `__token__`), never hard-coded. See [`scripts/release.py`](scripts/release.py) and
336
+ the optional [GitHub Actions workflow](.github/workflows/publish.yml) (publishes on
337
+ a `v*` tag). PyPI permanently forbids re-uploading an existing version, so each
338
+ release must use a new version number.
339
+
340
+ ## License
341
+
342
+ MIT
@@ -0,0 +1,314 @@
1
+ # ssh-handler
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/ssh-handler.svg)](https://pypi.org/project/ssh-handler/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/ssh-handler.svg)](https://pypi.org/project/ssh-handler/)
5
+
6
+ An extensive **SSH / SFTP / SCP / FTP** automation handler built on
7
+ [Paramiko](https://www.paramiko.org/). One package, three ways to use it:
8
+
9
+ - **Test-automation framework** — raise-on-error API, pytest fixtures, parallel fleet ops.
10
+ - **Standalone CLI** — `python -m ssh_handler …`, fully argument-driven.
11
+ - **PyQt5 tool** — safe mode + log streaming over Qt signals, runs off the GUI thread.
12
+
13
+ Passwords are wrapped in a `Secret` and stored in the **OS credential vault** — they
14
+ never appear in logs, reprs, tracebacks, or on disk in plaintext.
15
+
16
+ ```bash
17
+ pip install ssh-handler
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Table of contents
23
+
24
+ - [Why this package](#why-this-package)
25
+ - [Install](#install)
26
+ - [Quick start](#quick-start)
27
+ - [What you can do](#what-you-can-do) — full capability list
28
+ - [Domain / Windows (RDP) hosts](#domain--windows-rdp-hosts)
29
+ - [Confidential credentials](#confidential-credentials)
30
+ - [Performance](#performance)
31
+ - [Error handling: two styles](#error-handling-two-styles)
32
+ - [Result objects](#result-objects)
33
+ - [CLI reference](#cli-reference)
34
+ - [PyQt5 integration](#pyqt5-integration)
35
+ - [Parallel fleet operations](#parallel-fleet-operations)
36
+ - [FTP / FTPS](#ftp--ftps)
37
+ - [API map](#api-map)
38
+ - [Releasing](#releasing)
39
+
40
+ ---
41
+
42
+ ## Why this package
43
+
44
+ Paramiko is powerful but low-level: you manage clients, transports, channels,
45
+ SFTP sessions, timeouts, retries, host-key policies and error handling yourself,
46
+ and you repeat that boilerplate in every project. `ssh-handler` wraps all of it
47
+ behind one object that:
48
+
49
+ - auto-selects the right authentication strategy (password, key, agent, empty password),
50
+ - retries connections and transparently reconnects dropped sessions,
51
+ - returns **structured results** for every action instead of raw strings,
52
+ - keeps **passwords confidential** end-to-end,
53
+ - and exposes the same surface whether you're in a test, a CLI, or a GUI.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install ssh-handler # core (paramiko only)
59
+ pip install "ssh-handler[secure]" # + keyring (OS credential vault)
60
+ pip install "ssh-handler[scp]" # + scp (SCP-protocol transfers)
61
+ pip install "ssh-handler[gui]" # + PyQt5 (the GUI worker)
62
+ pip install "ssh-handler[all]" # everything
63
+ ```
64
+
65
+ `scp`, `keyring`, and `PyQt5` are optional — the core works without them, and
66
+ those features raise a clear, actionable message if you use them without the extra.
67
+
68
+ ## Quick start
69
+
70
+ ```python
71
+ from ssh_handler import SSHHandler, SSHConfig
72
+
73
+ with SSHHandler(SSHConfig(host="10.0.0.5", username="root", password="pw")) as ssh:
74
+ print(ssh.run("uptime").stdout)
75
+ ssh.run("systemctl restart nginx", check=True) # raises on non-zero exit
76
+ ssh.push("local.txt", "/tmp/remote.txt") # SFTP upload
77
+ ssh.pull("/etc/nginx", "./backup", recursive=True) # recursive download
78
+ ```
79
+
80
+ ## What you can do
81
+
82
+ **Connection & session**
83
+ - Connect with password, private key (+ passphrase), SSH agent, auto-discovered
84
+ keys, or an **empty-password** account — all auto-tried in a smart order.
85
+ - **Auto-retry** connects with backoff; **auto-reconnect** if a session drops.
86
+ - Keepalives, per-command and connection timeouts, optional compression.
87
+ - **Jump host / bastion** chaining (ProxyJump-style) via `SSHConfig(jump_host=…)`.
88
+ - Host-key policy: `auto` / `reject` / `warn`, with an optional `known_hosts` file.
89
+ - Remote-OS awareness (`detect_os()`, `is_windows`) for Linux **and** Windows targets.
90
+ - Raw escape hatch: `ssh.client` and `ssh.transport` expose the underlying Paramiko objects.
91
+
92
+ **Command execution**
93
+ - `run()` — timeout, `check` (raise on non-zero), PTY allocation, custom environment.
94
+ - `run_many()` — batch with stop-on-error.
95
+ - `sudo()` — runs `sudo -S` and feeds the password on stdin.
96
+ - `open_shell()` — a persistent interactive `ShellSession` with `send` /
97
+ `read_until` (send-expect) / `read_available`.
98
+
99
+ **File operations (SFTP) — full Paramiko parity**
100
+ - Transfers: `push` / `pull` (single file **or** recursive directory, with progress
101
+ callbacks and transfer statistics), plus `scp_push` / `scp_pull` (SCP protocol).
102
+ - Listing & metadata: `listdir`, `listdir_attr`, `stat`, `lstat`, `exists`, `isdir`, `walk`.
103
+ - Directories: `mkdir`, `makedirs` (recursive `mkdir -p`), `rmdir`.
104
+ - Files: `remove`, `rename`, `open` (remote file object), `read_text`, `write_text`.
105
+ - Permissions & links: `chmod`, `chown`, `symlink`, `readlink`.
106
+
107
+ **Other protocols**
108
+ - **FTP / FTPS** via `FTPHandler` (standard-library `ftplib`, no extra dependency):
109
+ connect, login, TLS, `push`, `pull`, `listdir`, `cwd`, `pwd`, `mkdir`, `rmdir`,
110
+ `remove`, `rename`, `size`, `exists`.
111
+
112
+ **Scale & integration**
113
+ - `SSHPool` — run the same command/transfer across many hosts in parallel threads.
114
+ - Safe mode + log callback for GUIs; structured result objects everywhere.
115
+ - Confidential credential storage in the OS vault (`CredentialStore`, `Secret`, `mask`).
116
+
117
+ ## Domain / Windows (RDP) hosts
118
+
119
+ The Windows machines you normally RDP into can be driven over SSH once **OpenSSH
120
+ Server** is enabled on them:
121
+
122
+ ```powershell
123
+ Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
124
+ Start-Service sshd ; Set-Service -Name sshd -StartupType Automatic
125
+ ```
126
+
127
+ Then log in with your normal domain credentials. **Pass `domain` and `username`
128
+ separately** — never hard-code a single `"DOMAIN\user"` Python string, because a
129
+ backslash escape (e.g. `\n`, `\t`) silently becomes a control character. The
130
+ handler builds the `DOMAIN\user` login string for you:
131
+
132
+ ```python
133
+ from ssh_handler import SSHHandler, SSHConfig, CredentialStore
134
+
135
+ store = CredentialStore(service="my_test_lab")
136
+ cfg = SSHConfig(
137
+ host="10.20.30.40",
138
+ domain="CORP", username="myuser", # -> login "CORP\myuser"
139
+ password=store.get("CORP\\myuser"), # a Secret pulled from the OS vault
140
+ remote_os="windows", # skip the OS probe
141
+ fast_auth=True, # skip key probing -> faster login
142
+ )
143
+ with SSHHandler(cfg) as ssh:
144
+ print(ssh.run("whoami").stdout) # CORP\myuser
145
+ print(ssh.run("powershell Get-Service sshd").stdout)
146
+ ssh.push("report.xlsx", "C:/Users/myuser/Desktop/report.xlsx")
147
+ ```
148
+
149
+ Store the password **once** so it never appears in code again:
150
+
151
+ ```python
152
+ from ssh_handler import CredentialStore, prompt_password
153
+ CredentialStore("my_test_lab").set("CORP\\myuser", prompt_password())
154
+ ```
155
+ ```bash
156
+ # …or from the CLI:
157
+ python -m ssh_handler store-credential --user myuser --domain CORP --service my_test_lab
158
+ ```
159
+
160
+ ## Confidential credentials
161
+
162
+ | Mechanism | What it does |
163
+ |-----------|--------------|
164
+ | `Secret` | wraps a password; `str()`/`repr()`/logs show `********`; only `.reveal()` exposes it |
165
+ | `mask()` | redacts secret values from any string (applied automatically to all logging) |
166
+ | `CredentialStore` | stores/reads passwords in the OS vault (Windows Credential Manager / macOS Keychain / Secret Service) via `keyring` — **no plaintext on disk** |
167
+ | `prompt_password()` | hidden terminal input, returns a `Secret` |
168
+
169
+ Pass a `Secret` (or a plain string, which is wrapped automatically) anywhere a
170
+ password is accepted. It stays redacted across the whole stack.
171
+
172
+ ## Performance
173
+
174
+ - **`fast_auth`** (default on): when a password is supplied, the slow key/agent
175
+ probing is skipped — faster logins and no "Too many authentication failures"
176
+ from the server's `MaxAuthTries`.
177
+ - One SFTP channel is opened lazily and **reused** across operations.
178
+ - SFTP downloads use Paramiko **prefetch** for high throughput.
179
+ - `remote_os="windows"|"linux"` skips the one-time OS-detection probe.
180
+ - `compress=True` for slow/high-latency links; keepalives keep long sessions alive.
181
+ - `SSHPool` parallelizes across hosts with a thread pool.
182
+
183
+ ## Error handling: two styles
184
+
185
+ **Raise (default)** — best for tests/scripts. Typed exceptions, all subclasses of
186
+ `SSHError`:
187
+
188
+ ```
189
+ SSHConnectionError SSHAuthenticationError SSHTimeoutError
190
+ SSHCommandError SSHTransferError SSHNotConnectedError
191
+ FTPError CredentialError
192
+ ```
193
+
194
+ **Safe mode** (`SSHHandler(cfg, safe=True)`) — best for GUIs. Every call returns an
195
+ `OperationResult` instead of raising, so an event loop never dies:
196
+
197
+ ```python
198
+ res = ssh.connect()
199
+ if not res: # OperationResult is falsy on failure
200
+ show_error(res.error)
201
+ else:
202
+ data = res.value # or res.unwrap() to re-raise on failure
203
+ ```
204
+
205
+ Override per call with `safe=True` / `safe=False`.
206
+
207
+ ## Result objects
208
+
209
+ Every action returns structured data, not bare strings:
210
+
211
+ - **`CommandResult`** — `exit_code`, `stdout`, `stderr`, `duration`, `host`, `.ok`, `.as_dict()`
212
+ - **`TransferResult`** — `size_bytes`, `duration`, `speed_bps`, `human_speed`, `human_size`, `files`
213
+ - **`ShellResult`** — `output`, `matched`, `timed_out`, `duration`
214
+ - **`OperationResult`** — safe-mode wrapper: `bool(res)`, `res.value`, `res.error`, `res.unwrap()`
215
+
216
+ ## CLI reference
217
+
218
+ ```bash
219
+ python -m ssh_handler run --host H --user U --domain CORP uptime
220
+ python -m ssh_handler push --host H --user U ./build /tmp/build --recursive
221
+ python -m ssh_handler pull --host H --user U /var/log ./logs --recursive
222
+ python -m ssh_handler info --host H --user U --json
223
+ python -m ssh_handler store-credential --user U --domain CORP --service my_test_lab
224
+ ```
225
+
226
+ Password options: `--password` (hidden prompt), `--use-stored` (read from the OS
227
+ vault), `--key FILE` (private key). Add `--json` for machine-readable output.
228
+ After `pip install`, a `ssh-handler` console script is also available
229
+ (`ssh-handler run --host …`).
230
+
231
+ ## PyQt5 integration
232
+
233
+ `ssh_handler.pyqt_worker.SSHWorker` is a `QObject` wrapping the handler in **safe
234
+ mode**. Move it to a `QThread`, connect its signals, and drive it from the GUI:
235
+
236
+ ```python
237
+ from PyQt5.QtCore import QThread
238
+ from ssh_handler import SSHConfig
239
+ from ssh_handler.pyqt_worker import SSHWorker
240
+
241
+ worker = SSHWorker(SSHConfig(host="10.0.0.5", username="root", password=secret))
242
+ thread = QThread(); worker.moveToThread(thread)
243
+ worker.log.connect(text_edit.append) # live, secret-masked log lines
244
+ worker.command_done.connect(on_command_done)
245
+ worker.progress.connect(progress_bar.setValue) # bytes_done, bytes_total
246
+ thread.started.connect(lambda: worker.run_command("uptime"))
247
+ thread.start()
248
+ ```
249
+
250
+ Signals: `log`, `connected`, `command_done`, `transfer_done`, `progress`, `error`,
251
+ `finished`. The import is lazy, so the rest of the package works where PyQt5 isn't installed.
252
+
253
+ ## Parallel fleet operations
254
+
255
+ ```python
256
+ from ssh_handler import SSHPool, SSHConfig
257
+
258
+ configs = [SSHConfig(host=h, username="root", password="pw")
259
+ for h in ("10.0.0.1", "10.0.0.2", "10.0.0.3")]
260
+
261
+ with SSHPool(configs, max_workers=8) as pool:
262
+ for host, res in pool.run("uptime").items():
263
+ print(host, res.value.stdout.strip() if res else res.error)
264
+ pool.pull("/var/log/syslog", "logs/{host}_syslog.txt") # {host} avoids collisions
265
+ ```
266
+
267
+ ## FTP / FTPS
268
+
269
+ ```python
270
+ from ssh_handler import FTPHandler, FTPConfig
271
+
272
+ with FTPHandler(FTPConfig(host="ftp.example.com", username="u",
273
+ password="p", use_tls=True)) as ftp:
274
+ ftp.push("local.txt", "remote.txt")
275
+ ftp.pull("remote.txt", "copy.txt")
276
+ print(ftp.listdir("/"))
277
+ ```
278
+
279
+ ## API map
280
+
281
+ ```
282
+ ssh_handler/
283
+ config.py SSHConfig, FTPConfig
284
+ credentials.py Secret, CredentialStore, mask, prompt_password
285
+ core.py SSHHandler, ShellSession (SSH + SFTP + SCP)
286
+ ftp.py FTPHandler (FTP / FTPS)
287
+ pool.py SSHPool (parallel multi-host)
288
+ cli.py argparse entry point (python -m ssh_handler / ssh-handler)
289
+ pyqt_worker.py SSHWorker (PyQt5, lazy import)
290
+ results.py CommandResult, TransferResult, ShellResult, OperationResult
291
+ exceptions.py SSHError hierarchy
292
+ examples/examples.py copy-paste recipes
293
+ tests/test_offline.py offline checks (no network needed)
294
+ ```
295
+
296
+ ## Releasing
297
+
298
+ Maintainers: use the helper to build and publish a new version.
299
+
300
+ ```bash
301
+ python scripts/release.py 1.0.1 # bump -> build -> twine check -> upload
302
+ python scripts/release.py 1.0.1 --dry-run # build + check only, no upload
303
+ python scripts/release.py patch # auto-bump patch/minor/major
304
+ ```
305
+
306
+ The token is read from the `TWINE_PASSWORD` environment variable (username
307
+ `__token__`), never hard-coded. See [`scripts/release.py`](scripts/release.py) and
308
+ the optional [GitHub Actions workflow](.github/workflows/publish.yml) (publishes on
309
+ a `v*` tag). PyPI permanently forbids re-uploading an existing version, so each
310
+ release must use a new version number.
311
+
312
+ ## License
313
+
314
+ MIT