sftp_helper package
Submodules
Module contents
SFTP Helper — public API surface.
Re-exports the utility functions from sftp_helper.main so that
downstream code can simply write import sftp_helper as sftph and reach
every supported operation (credentials loading, upload, download, exists,
delete, mkdir -p, remote temp file with auto-cleanup) without knowing about
the module layout.
Backed by the system OpenSSH sftp client with strict host-key
verification. See the module docs for the full policy — there is no flag to
disable verification.
Usage Example
>>> import sftp_helper as sftph
>>> cred = sftph.credentials("settings.yaml")
>>> sftph.upload("local.txt", cred, "/remote/base/local.txt")
>>> assert sftph.remote_file_exists("/remote/base/local.txt", cred)
>>> sftph.download("/remote/base/local.txt", cred, "roundtrip.txt")
>>> sftph.delete("/remote/base/local.txt", cred)
- sftp_helper.credentials(config_path=None)[source]
Retrieve SFTP credentials from a configuration file, folder, or environment.
Only
sftp_host,sftp_loginandsftp_httpsare mandatory. Authentication (sftp_passwd/sftp_key) and the write location (sftp_destination_path, default"/") are optional and fall back to documented defaults — so a key-based login writing to the server root needs just the three required fields.- Parameters:
config_path (str) – Path to a JSON/YAML file, a directory containing one, or
Noneto fall back to environment variables /.env.- Returns:
Dictionary with the three required keys always present, plus whichever optional keys were supplied.
sftp_destination_pathis always set (defaulting to"/").- Return type:
- sftp_helper.delete(sftp_address, cred)[source]
Delete a remote file. Returns True if the file is gone afterwards (including the case where it never existed).
- Parameters:
- Returns:
Always
Trueon success — deleting an absent file is a no-op, which makes the operation idempotent.- Return type:
- Raises:
Exception – Wrapped with the address if the connection or removal fails.
- sftp_helper.download(sftp_address, cred, local_path='', *, retries=3, sha256=None, overwrite=True, resume=True, progress=True)[source]
Download a remote SFTP file (or directory) to
local_path: resumable, retried, atomic, verified.If sftp_address is a remote directory, every file under it is downloaded (recursively) via
download_many()instead — see_download_folder(). sha256 is meaningless there (there is no single file to verify) and must be leftNone; the return value is local_path itself rather than a single file’s local path.Mirrors
os_helper.download_file’s design exactly, on the SFTP side (single-file case): bytes land in a<local_path>.partsidecar viareget(OpenSSH’s resume-download command), retried with exponential backoff — each retry continuing rather than restarting — and onlyos.replace-d ontolocal_pathonce size-verified (and, ifsha256is given, content-verified too). See_download_resumable()for the full design.- Parameters:
sftp_address (str) – Full
sftp://address or a plain remote path (file or directory) to fetch.cred (dict) – Credentials dict.
local_path (str, optional) – Destination on the local disk. Defaults to the remote basename.
retries (int, optional) – Extra attempts on a failed or size-mismatched transfer, with exponential backoff (default
3, so up to four tries total). Each retry resumes rather than restarts.sha256 (str or None, optional) – Expected lowercase hex SHA-256. When given, the finished file is verified and a mismatch raises
ValueError(the bad sidecar is discarded). Only valid for a single-file download.overwrite (bool, optional) – Re-download even if
local_pathalready exists (defaultTrue, matching this function’s historical behaviour). SetFalseto reuse a complete destination without re-downloading — its digest is re-checked whensha256is given. Ignored for a folder download.resume (bool, optional) – Continue a partial
.partsidecar (defaultTrue). SetFalseto discard any existing partial and download from scratch.progress (bool, optional) – Show a bar on an interactive terminal (default
True). SetFalseto suppress it even on a TTY (e.g. to keep a bulk loop’s output to one summary line instead of N bars).
- Returns:
The local path of the downloaded file, or local_path itself for a folder download.
- Return type:
- Raises:
ValueError – If
sha256is given and the finished file’s digest does not match, or ifsha256is given for a directory sftp_address.Exception – If every attempt fails. On total failure the
.partsidecar is left in place so a later call todownload()for the same destination resumes instead of starting over.
- sftp_helper.download_many(files, cred, *, retries=3, archive=None, overwrite=True, resume=True, progress=True)[source]
Download several files in one bulk operation, archive-accelerated when possible.
The download-side mirror of
upload_many(): on a server that also accepts exec, this collapses N downloads into one remote stage+zip, oneget, and one local unzip — avoiding even the reduced per-file overhead ControlMaster reuse leaves behind (one get conversation per file otherwise)._probe_exec()detects that capability up front; a server without it (most SFTP-only hosting accounts) falls back to the ordinary per-filedownload()loop, unaffected, since ControlMaster reuse still applies there.- Parameters:
files (list of (str, str)) –
(sftp_address, local_path)pairs to download — mirrorsdownload()’s own argument order.cred (dict) – Credentials dict.
retries (int, optional) – Forwarded to
download()for the per-file fallback path (the archive path has its own stage+zip+get+unzip flow — a failure there falls all the way back to per-file, which does retry).archive (bool or None, optional) – Force the archive path (
True), force the per-file path (False), or auto-detect via_probe_exec()(None, default). Ignored (treated asFalse) when overwrite isFalse— see below.overwrite (bool, optional) – Re-download every file unconditionally (default
True). SetFalseto skip files already present locally — turning a repeat call into an incremental sync. This forces the per-file path regardless of archive: the archive path downloads and extracts the whole batch in one shot with no per-entry check, so it has no way to honour a per-file skip.resume (bool) – Forwarded to
download()for the per-file fallback path.progress (bool) – Forwarded to
download()for the per-file fallback path.
- Returns:
{sftp_address: local_path}for every file.- Return type:
dict of str to str
- sftp_helper.get_client_sftp(cred)[source]
Deprecated compatibility shim — validate the connection and yield
cred.The module used to hand back a live
paramiko.SFTPClient. Now every operation runs as its own short-livedsftpbatch, so there is no persistent client object to expose. This context manager is kept only so olderwith get_client_sftp(cred) as ...:call sites keep importing; it performs a cheap connectivity check (anlsof the server root, which also validates auth and the host key) and yields the credentials dict.
- sftp_helper.list_dir(ftp_dir, cred, *, recursive=False)[source]
List a remote directory’s contents.
The missing counterpart to
upload()for tooling that needs to know what is already on the server before deciding what to transfer (e.g. a mirror-style sync that skips or removes files, rather thanupload()’s unconditional overwrite).- Parameters:
ftp_dir (str) – Full
sftp://address or a plain remote directory path.cred (dict) – Credentials dict.
recursive (bool, optional) – When
True, walks sub-directories too and returns paths relative to ftp_dir (e.g."css/app.css"). WhenFalse(default), returns only the immediate entries (one path component each).
- Returns:
Entry names/relative paths, in the order the server returned them.
- Return type:
- Raises:
Exception – Wrapped with the address if the connection fails or ftp_dir does not exist.
Examples
>>> import sftp_helper as sftph >>> cred = sftph.credentials("settings.yaml") >>> sftph.list_dir("/uploads", cred) ['2026-06', 'readme.txt'] >>> sftph.list_dir("/uploads", cred, recursive=True) ['readme.txt', '2026-06/report.pdf']
- sftp_helper.list_dir_stat(ftp_dir, cred)[source]
Recursively stat every file under a remote directory, in one walk.
The bulk counterpart to
stat(): builds a full{relative_path: {"size", "mtime"}}map for an entire remote tree in one pass (one round trip per directory level, not per file), so a sync tool can decide what to upload by comparing against local files entirely in memory — no per-file remote round trip needed.- Parameters:
- Returns:
{relative_posix_path: {"size": int, "mtime": datetime}}, one entry per file found anywhere under ftp_dir (directories are walked into, not listed). Empty if ftp_dir has no files.- Return type:
dict of str to dict
- Raises:
Exception – Wrapped with the address if the connection fails or ftp_dir does not exist.
Examples
>>> import sftp_helper as sftph >>> cred = sftph.credentials("settings.yaml") >>> sftph.list_dir_stat("/uploads", cred) {'readme.txt': {'size': 1234, 'mtime': ...}, '2026-06/report.pdf': {'size': 98765, 'mtime': ...}}
- sftp_helper.make_remote_directory(ftp_directory, cred)[source]
Ensure the specified remote directory exists, creating intermediate levels as needed.
- Parameters:
- Raises:
AssertionError – If the target directory is still absent after the create loop.
- Return type:
None
- sftp_helper.normalize_path(path)[source]
Normalize a remote path: ensure single leading ‘/’, strip trailing slashes.
- Parameters:
path (str) – A raw remote path, possibly missing the leading slash or carrying redundant trailing slashes.
- Returns:
The canonical form (single leading ‘/’, no trailing ‘/’); the root
"/"is preserved rather than collapsed to the empty string.- Return type:
Examples
>>> normalize_path("foo/bar///") '/foo/bar'
- Raises:
ValueError – If path contains a newline, carriage return, NUL byte, or double quote — see
_assert_safe_path(). Every remote-path-accepting function in this module funnels through here (directly or viastrip_sftp_path()), so this is the one choke point that keeps such a path from ever reaching ansftp -bbatch command or a remotesshexec string, where it could break out of the quoting.- Parameters:
path (str)
- Return type:
- sftp_helper.remote_stat(sftp_address, cred)[source]
Return
{"size": int, "mtime": datetime}for a remote file, orNone.Parses the server’s
ls -loutput (see_parse_ls_long_line());mtimehas minute resolution and no timezone (the server sends none over this interface), so treat it as an approximate local-clock reading good enough for “is the source newer” comparisons, not for anything requiring second-level or timezone-aware precision.- Parameters:
- Returns:
{"size": int, "mtime": datetime}, orNoneif the path does not exist (or is a directory — this is a file stat, not list_dir).- Return type:
dict or None
- Raises:
Exception – Wrapped with the address if the connection fails.
Examples
>>> import sftp_helper as sftph >>> cred = sftph.credentials("settings.yaml") >>> sftph.remote_stat("/uploads/readme.txt", cred) {'size': 1234, 'mtime': datetime.datetime(2026, 6, 19, 10, 30)}
- sftp_helper.remote_tempfile(cred, ext='', subdir='')[source]
Reserve a unique remote path under
cred['sftp_destination_path']and delete it on exit.- Parameters:
- Yields:
(sftp_address, https_url) – The reserved remote location – the file does not exist yet; the caller is expected to upload to it (or skip entirely, in which case cleanup is a no-op).
Cleanup
——-
The remote file is deleted in
finally. Cleanup failures re-raise onlyif no other exception is already propagating; otherwise they are logged
so the original error survives.
- Return type:
Example
>>> with remote_tempfile(cred, ext="txt") as (addr, url): ... upload("local.txt", cred, addr) ... assert osh.is_working_url(url)
- sftp_helper.strip_sftp_path(sftp_address, cred)[source]
Strip
sftp://and the host from an SFTP address.Idempotent: passing an already-stripped path returns it unchanged (modulo normalization).
- sftp_helper.upload(local_path, cred, sftp_address='', *, retries=3, overwrite=True, resume=True, progress=True)[source]
Upload a local file (or directory) to the SFTP server: resumable, retried, atomic, size-verified.
If
local_pathis a directory, every non-hidden file under it is uploaded (recursively) viaupload_many()instead — see_upload_folder().sftp_addressis then required (there is no single file to derive a content-hashed default from) and names the destination directory; the return value is sftp_address itself rather than a single file’s address.If
sftp_addressis empty (single-file case), a content-hashed name undercred['sftp_destination_path']is used.Mirrors
os_helper.download_file’s smart-transfer design: the file is sent into a temp remote path viareput(OpenSSH’s resume-upload command), retried with exponential backoff on failure — each retry continuing rather than restarting — and only published (renamed) ontosftp_addressonce its remote size matches the local file exactly. A caller only ever observessftp_addressabsent or complete, never truncated. See_upload_resumable()for the full design (including its one honest gap versusdownload_file: size is verified, not a full content hash — that would need a remote exec channel many SFTP-only accounts don’t expose).- Parameters:
local_path (str) – Path to the local file or directory to upload.
cred (dict) – Credentials dict.
sftp_address (str, optional) – Destination address (or destination directory, for a folder upload). When empty for a single file, a deterministic content-hashed name is generated so identical files map to the same remote path.
retries (int, optional) – Extra attempts on a failed or size-mismatched transfer, with exponential backoff (default
3, so up to four tries total). Each retry resumes rather than restarts.overwrite (bool, optional) – Re-upload even if sftp_address already exists (default
True, matchingdownload()’s own default). SetFalseto reuse an already-present destination without re-transferring: when sftp_address was auto-derived from the file’s content hash, mere presence already proves the right bytes are there (no further check needed — see the derivation above); when it was given explicitly, the skip only applies if the remote size already matches (upload has no cheap way to content-verify a remote file — see_upload_resumable()).resume (bool, optional) – Continue a partial temp remote file (default
True). SetFalseto discard any existing partial and upload from scratch.progress (bool, optional) – Show a bar on an interactive terminal (default
True). SetFalseto suppress it even on a TTY (e.g. to keep a bulk loop’s output to one summary line instead of N bars).
- Returns:
The full
sftp://address (or plain remote path) of the file, or sftp_address itself for a folder upload.- Return type:
- Raises:
ValueError – If local_path is a directory and sftp_address is empty.
Exception – If every attempt fails, or the atomic publish fails. On total failure the file is left in place at its temp remote path — see
_UPLOAD_TMP_SUFFIX— so a later call toupload()for the same destination resumes instead of starting over.
- sftp_helper.upload_many(files, cred, *, retries=3, archive=None, overwrite=True, resume=True, progress=True)[source]
Upload several files in one bulk operation, archive-accelerated when possible.
Every SFTP command in this package pays a fresh SSH handshake unless ControlMaster reuse kicks in (see
_control_path()); on a server that also accepts exec, this collapses N file transfers into one zip upload plus one remote unzip — the biggest possible win, since it avoids even the reduced per-file overhead ControlMaster leaves behind (one put conversation, one directory-creation round trip, one publish-rename per file)._probe_exec()detects that capability up front; a server that does not have it (most SFTP-only hosting accounts) falls back to the ordinary per-fileupload()loop, unaffected, since ControlMaster reuse still applies there.- Parameters:
files (list of (str, str)) –
(local_path, sftp_address)pairs to upload.cred (dict) – Credentials dict.
retries (int, optional) – Forwarded to
upload()for the per-file fallback path (the archive path has its own upload+unzip retry-free flow — a failure there falls all the way back to per-file, which does retry).archive (bool or None, optional) – Force the archive path (
True), force the per-file path (False), or auto-detect via_probe_exec()(None, default). Ignored (treated asFalse) when overwrite isFalse— see below.overwrite (bool, optional) – Re-upload every file unconditionally (default
True). SetFalseto skip files already present with a matching size — turning a repeat call into an incremental sync. This forces the per-file path regardless of archive: the archive path zips and unzips the whole batch in one shot with no per-entry stat, so it has no way to honour a per-file skip.resume (bool) – Forwarded to
upload()for the per-file fallback path.progress (bool) – Forwarded to
upload()for the per-file fallback path.
- Returns:
{local_path: sftp_address}for every file.- Return type:
dict of str to str