Metadata-Version: 2.4
Name: ssh-handler
Version: 1.0.1
Summary: Extensive SSH/SFTP/SCP/FTP handler built on Paramiko, for test automation, CLIs and PyQt5 tools.
Author: ssh-handler contributors
License-Expression: MIT
Keywords: ssh,sftp,scp,ftp,paramiko,automation,pyqt5,testing
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: System :: Networking
Classifier: Topic :: Software Development :: Testing
Classifier: Intended Audience :: Developers
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: paramiko>=3.0
Provides-Extra: secure
Requires-Dist: keyring>=23.0; extra == "secure"
Provides-Extra: scp
Requires-Dist: scp>=0.14; extra == "scp"
Provides-Extra: gui
Requires-Dist: PyQt5>=5.15; extra == "gui"
Provides-Extra: all
Requires-Dist: keyring>=23.0; extra == "all"
Requires-Dist: scp>=0.14; extra == "all"
Requires-Dist: PyQt5>=5.15; extra == "all"
Dynamic: license-file

# ssh-handler

[![PyPI](https://img.shields.io/pypi/v/ssh-handler.svg)](https://pypi.org/project/ssh-handler/)
[![Python](https://img.shields.io/pypi/pyversions/ssh-handler.svg)](https://pypi.org/project/ssh-handler/)

An extensive **SSH / SFTP / SCP / FTP** automation handler built on
[Paramiko](https://www.paramiko.org/). One package, three ways to use it:

- **Test-automation framework** — raise-on-error API, pytest fixtures, parallel fleet ops.
- **Standalone CLI** — `python -m ssh_handler …`, fully argument-driven.
- **PyQt5 tool** — safe mode + log streaming over Qt signals, runs off the GUI thread.

Passwords are wrapped in a `Secret` and stored in the **OS credential vault** — they
never appear in logs, reprs, tracebacks, or on disk in plaintext.

```bash
pip install ssh-handler
```

---

## Table of contents

- [Why this package](#why-this-package)
- [Install](#install)
- [Quick start](#quick-start)
- [What you can do](#what-you-can-do) — full capability list
- [Domain / Windows (RDP) hosts](#domain--windows-rdp-hosts)
- [Confidential credentials](#confidential-credentials)
- [Performance](#performance)
- [Error handling: two styles](#error-handling-two-styles)
- [Result objects](#result-objects)
- [CLI reference](#cli-reference)
- [PyQt5 integration](#pyqt5-integration)
- [Parallel fleet operations](#parallel-fleet-operations)
- [FTP / FTPS](#ftp--ftps)
- [API map](#api-map)
- [Releasing](#releasing)

---

## Why this package

Paramiko is powerful but low-level: you manage clients, transports, channels,
SFTP sessions, timeouts, retries, host-key policies and error handling yourself,
and you repeat that boilerplate in every project. `ssh-handler` wraps all of it
behind one object that:

- auto-selects the right authentication strategy (password, key, agent, empty password),
- retries connections and transparently reconnects dropped sessions,
- returns **structured results** for every action instead of raw strings,
- keeps **passwords confidential** end-to-end,
- and exposes the same surface whether you're in a test, a CLI, or a GUI.

## Install

```bash
pip install ssh-handler                 # core (paramiko only)
pip install "ssh-handler[secure]"       # + keyring  (OS credential vault)
pip install "ssh-handler[scp]"          # + scp      (SCP-protocol transfers)
pip install "ssh-handler[gui]"          # + PyQt5    (the GUI worker)
pip install "ssh-handler[all]"          # everything
```

`scp`, `keyring`, and `PyQt5` are optional — the core works without them, and
those features raise a clear, actionable message if you use them without the extra.

## Quick start

```python
from ssh_handler import SSHHandler, SSHConfig

with SSHHandler(SSHConfig(host="10.0.0.5", username="root", password="pw")) as ssh:
    print(ssh.run("uptime").stdout)
    ssh.run("systemctl restart nginx", check=True)      # raises on non-zero exit
    ssh.push("local.txt", "/tmp/remote.txt")            # SFTP upload
    ssh.pull("/etc/nginx", "./backup", recursive=True)  # recursive download
```

## What you can do

**Connection & session**
- Connect with password, private key (+ passphrase), SSH agent, auto-discovered
  keys, or an **empty-password** account — all auto-tried in a smart order.
- **Auto-retry** connects with backoff; **auto-reconnect** if a session drops.
- Keepalives, per-command and connection timeouts, optional compression.
- **Jump host / bastion** chaining (ProxyJump-style) via `SSHConfig(jump_host=…)`.
- Host-key policy: `auto` / `reject` / `warn`, with an optional `known_hosts` file.
- Remote-OS awareness (`detect_os()`, `is_windows`) for Linux **and** Windows targets.
- Raw escape hatch: `ssh.client` and `ssh.transport` expose the underlying Paramiko objects.

**Command execution**
- `run()` — timeout, `check` (raise on non-zero), PTY allocation, custom environment.
- `run_many()` — batch with stop-on-error.
- `sudo()` — runs `sudo -S` and feeds the password on stdin.
- `open_shell()` — a persistent interactive `ShellSession` with `send` /
  `read_until` (send-expect) / `read_available`.

**File operations (SFTP) — full Paramiko parity**
- Transfers: `push` / `pull` (single file **or** recursive directory, with progress
  callbacks and transfer statistics), plus `scp_push` / `scp_pull` (SCP protocol).
- Listing & metadata: `listdir`, `listdir_attr`, `stat`, `lstat`, `exists`, `isdir`, `walk`.
- Directories: `mkdir`, `makedirs` (recursive `mkdir -p`), `rmdir`.
- Files: `remove`, `rename`, `open` (remote file object), `read_text`, `write_text`.
- Permissions & links: `chmod`, `chown`, `symlink`, `readlink`.

**Other protocols**
- **FTP / FTPS** via `FTPHandler` (standard-library `ftplib`, no extra dependency):
  connect, login, TLS, `push`, `pull`, `listdir`, `cwd`, `pwd`, `mkdir`, `rmdir`,
  `remove`, `rename`, `size`, `exists`.

**Scale & integration**
- `SSHPool` — run the same command/transfer across many hosts in parallel threads.
- Safe mode + log callback for GUIs; structured result objects everywhere.
- Confidential credential storage in the OS vault (`CredentialStore`, `Secret`, `mask`).

## Domain / Windows (RDP) hosts

The Windows machines you normally RDP into can be driven over SSH once **OpenSSH
Server** is enabled on them:

```powershell
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd ; Set-Service -Name sshd -StartupType Automatic
```

Then log in with your normal domain credentials. **Pass `domain` and `username`
separately** — never hard-code a single `"DOMAIN\user"` Python string, because a
backslash escape (e.g. `\n`, `\t`) silently becomes a control character. The
handler builds the `DOMAIN\user` login string for you:

```python
from ssh_handler import SSHHandler, SSHConfig, CredentialStore

store = CredentialStore(service="my_test_lab")
cfg = SSHConfig(
    host="10.20.30.40",
    domain="CORP", username="myuser",        # -> login "CORP\myuser"
    password=store.get("CORP\\myuser"),       # a Secret pulled from the OS vault
    remote_os="windows",                      # skip the OS probe
    fast_auth=True,                           # skip key probing -> faster login
)
with SSHHandler(cfg) as ssh:
    print(ssh.run("whoami").stdout)                       # CORP\myuser
    print(ssh.run("powershell Get-Service sshd").stdout)
    ssh.push("report.xlsx", "C:/Users/myuser/Desktop/report.xlsx")
```

Store the password **once** so it never appears in code again:

```python
from ssh_handler import CredentialStore, prompt_password
CredentialStore("my_test_lab").set("CORP\\myuser", prompt_password())
```
```bash
# …or from the CLI:
python -m ssh_handler store-credential --user myuser --domain CORP --service my_test_lab
```

## Confidential credentials

| Mechanism | What it does |
|-----------|--------------|
| `Secret` | wraps a password; `str()`/`repr()`/logs show `********`; only `.reveal()` exposes it |
| `mask()` | redacts secret values from any string (applied automatically to all logging) |
| `CredentialStore` | stores/reads passwords in the OS vault (Windows Credential Manager / macOS Keychain / Secret Service) via `keyring` — **no plaintext on disk** |
| `prompt_password()` | hidden terminal input, returns a `Secret` |

Pass a `Secret` (or a plain string, which is wrapped automatically) anywhere a
password is accepted. It stays redacted across the whole stack.

## Performance

- **`fast_auth`** (default on): when a password is supplied, the slow key/agent
  probing is skipped — faster logins and no "Too many authentication failures"
  from the server's `MaxAuthTries`.
- One SFTP channel is opened lazily and **reused** across operations.
- SFTP downloads use Paramiko **prefetch** for high throughput.
- `remote_os="windows"|"linux"` skips the one-time OS-detection probe.
- `compress=True` for slow/high-latency links; keepalives keep long sessions alive.
- `SSHPool` parallelizes across hosts with a thread pool.

## Error handling: two styles

**Raise (default)** — best for tests/scripts. Typed exceptions, all subclasses of
`SSHError`:

```
SSHConnectionError  SSHAuthenticationError  SSHTimeoutError
SSHCommandError     SSHTransferError        SSHNotConnectedError
FTPError            CredentialError
```

**Safe mode** (`SSHHandler(cfg, safe=True)`) — best for GUIs. Every call returns an
`OperationResult` instead of raising, so an event loop never dies:

```python
res = ssh.connect()
if not res:                 # OperationResult is falsy on failure
    show_error(res.error)
else:
    data = res.value        # or res.unwrap() to re-raise on failure
```

Override per call with `safe=True` / `safe=False`.

## Result objects

Every action returns structured data, not bare strings:

- **`CommandResult`** — `exit_code`, `stdout`, `stderr`, `duration`, `host`, `.ok`, `.as_dict()`
- **`TransferResult`** — `size_bytes`, `duration`, `speed_bps`, `human_speed`, `human_size`, `files`
- **`ShellResult`** — `output`, `matched`, `timed_out`, `duration`
- **`OperationResult`** — safe-mode wrapper: `bool(res)`, `res.value`, `res.error`, `res.unwrap()`

## CLI reference

```bash
python -m ssh_handler run   --host H --user U --domain CORP uptime
python -m ssh_handler push  --host H --user U ./build /tmp/build --recursive
python -m ssh_handler pull  --host H --user U /var/log ./logs --recursive
python -m ssh_handler info  --host H --user U --json
python -m ssh_handler store-credential --user U --domain CORP --service my_test_lab
```

Password options: `--password` (hidden prompt), `--use-stored` (read from the OS
vault), `--key FILE` (private key). Add `--json` for machine-readable output.
After `pip install`, a `ssh-handler` console script is also available
(`ssh-handler run --host …`).

## PyQt5 integration

`ssh_handler.pyqt_worker.SSHWorker` is a `QObject` wrapping the handler in **safe
mode**. Move it to a `QThread`, connect its signals, and drive it from the GUI:

```python
from PyQt5.QtCore import QThread
from ssh_handler import SSHConfig
from ssh_handler.pyqt_worker import SSHWorker

worker = SSHWorker(SSHConfig(host="10.0.0.5", username="root", password=secret))
thread = QThread(); worker.moveToThread(thread)
worker.log.connect(text_edit.append)            # live, secret-masked log lines
worker.command_done.connect(on_command_done)
worker.progress.connect(progress_bar.setValue)  # bytes_done, bytes_total
thread.started.connect(lambda: worker.run_command("uptime"))
thread.start()
```

Signals: `log`, `connected`, `command_done`, `transfer_done`, `progress`, `error`,
`finished`. The import is lazy, so the rest of the package works where PyQt5 isn't installed.

## Parallel fleet operations

```python
from ssh_handler import SSHPool, SSHConfig

configs = [SSHConfig(host=h, username="root", password="pw")
           for h in ("10.0.0.1", "10.0.0.2", "10.0.0.3")]

with SSHPool(configs, max_workers=8) as pool:
    for host, res in pool.run("uptime").items():
        print(host, res.value.stdout.strip() if res else res.error)
    pool.pull("/var/log/syslog", "logs/{host}_syslog.txt")   # {host} avoids collisions
```

## FTP / FTPS

```python
from ssh_handler import FTPHandler, FTPConfig

with FTPHandler(FTPConfig(host="ftp.example.com", username="u",
                          password="p", use_tls=True)) as ftp:
    ftp.push("local.txt", "remote.txt")
    ftp.pull("remote.txt", "copy.txt")
    print(ftp.listdir("/"))
```

## API map

```
ssh_handler/
  config.py       SSHConfig, FTPConfig
  credentials.py  Secret, CredentialStore, mask, prompt_password
  core.py         SSHHandler, ShellSession   (SSH + SFTP + SCP)
  ftp.py          FTPHandler                 (FTP / FTPS)
  pool.py         SSHPool                    (parallel multi-host)
  cli.py          argparse entry point       (python -m ssh_handler / ssh-handler)
  pyqt_worker.py  SSHWorker                  (PyQt5, lazy import)
  results.py      CommandResult, TransferResult, ShellResult, OperationResult
  exceptions.py   SSHError hierarchy
examples/examples.py        copy-paste recipes
tests/test_offline.py       offline checks (no network needed)
```

## Releasing

Maintainers: use the helper to build and publish a new version.

```bash
python scripts/release.py 1.0.1            # bump -> build -> twine check -> upload
python scripts/release.py 1.0.1 --dry-run  # build + check only, no upload
python scripts/release.py patch            # auto-bump patch/minor/major
```

The token is read from the `TWINE_PASSWORD` environment variable (username
`__token__`), never hard-coded. See [`scripts/release.py`](scripts/release.py) and
the optional [GitHub Actions workflow](.github/workflows/publish.yml) (publishes on
a `v*` tag). PyPI permanently forbids re-uploading an existing version, so each
release must use a new version number.

## License

MIT
