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