ferry
Pythonic push / pull between your machine and any storage backend —
GCS, S3, Drive, SFTP, … — in a couple of lines. Zero config for GCS and S3.
import ferry
ferry.pull("gs://my-bucket/weights/llama-405b/", "/workspace/weights/")
ferry.push("results/", "s3://my-bucket/exp/results/")
ferry is a thin wrapper around rclone. It does not move bytes itself: rclone already does diffing, parallelism, multipart, resume, and dozens of backends, far better than a hand-rolled client would. ferry only adds the ergonomic, convention-aware Python surface you actually want to call.
Install
pip install ferry-sync # import name: ferry
ferry needs the rclone binary at runtime. If it's missing, ferry can fetch
the static build itself (into ~/.local/bin, no sudo):
ferry install-rclone # or in Python: ferry.ensure_rclone()
Endpoints
An endpoint is a plain string, in one of three forms:
- local path —
results/. - cloud URL —
gs://bucket/keyors3://bucket/key. Norclone configneeded: ferry maps these to on-the-fly rclone backends that authenticate from the environment — Application Default Credentials orGOOGLE_APPLICATION_CREDENTIALSfor GCS;AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_REGIONfor S3. - rclone endpoint —
name:bucket/keywherenamecomes fromrclone listremotes, for anything you've set up withrclone config(Drive, SFTP, R2, …).
Fresh pod → weights, in three lines
The whole point for ephemeral compute (RunPod, Modal, CI): nothing to configure interactively, credentials come in via env.
import ferry
ferry.ensure_rclone() # no-op if installed
ferry.pull("gs://my-bucket/weights/big-model/", "/workspace/weights/",
transfers=16)
Credentials-wise you have two options. With a scoped service-account key,
export GOOGLE_APPLICATION_CREDENTIALS on the pod and gs:// URLs just
work. Without one, ferry.gcs_pod_env() — run on your local machine —
mints a short-lived (~1 h) access token from your gcloud ADC and returns env
vars that define rclone remote gcs: wherever they're exported (e.g.
bellhop's RunSpec(env=ferry.gcs_pod_env())); the pod then uses
gcs:bucket/path endpoints. No long-lived secret leaves your machine and
access self-expires; the flip side is the token can't refresh, so it only
fits jobs that finish (or at least finish their transfers) within the hour.
Preflight before committing to a multi-hundred-GB transfer:
ferry doctor gs://my-bucket/weights/big-model/ # binary? creds? endpoint listable?
ferry size gs://my-bucket/weights/big-model/ # how much am I about to pull?
Semantics
- Additive by default.
push/pullrunrclone copy: nothing on the destination is deleted. Interrupted transfers are safe to re-run — rclone skips what's already there and picks up the rest. mirror=Truerunsrclone syncinstead — the destination becomes an exact mirror, which deletes files on the destination that are absent at the source. Use with care; pair withdry_run=Truefirst.
ferry.push("results/", "gs://bkt/exp/", dry_run=True) # preview
ferry.push("results/", "gs://bkt/exp/", mirror=True) # exact mirror
ferry.push("results/", "gs://bkt/exp/", excludes=["*.tmp", ".git/**"])
ferry.push("results/", "gs://bkt/exp/", transfers=16, checkers=32) # parallelism
Any extra rclone flags pass straight through:
ferry.pull("gs://bkt/x/", "x/", flags=["--checksum", "--fast-list"])
Big-transfer knobs (200 GB and up)
rclone's defaults are already resume-safe and multipart; the knobs that matter at weight-scale are parallelism:
ferry.pull("gs://bkt/weights/", "w/",
transfers=16, # parallel files
flags=["--multi-thread-streams", "8"]) # parallel chunks per big file
For S3, --s3-chunk-size 128M raises multipart chunk size (fewer requests);
--fast-list speeds up listing prefixes with many objects.
Helpers
ferry.ls("gs://bkt/exp/") # entries at top level, dirs suffixed "/"
ferry.size("gs://bkt/exp/") # {"count": 1234, "bytes": 217871234567}
ferry.listremotes() # names from rclone config
ferry.ensure_rclone() # path to rclone, downloading it if absent
Bound remotes
Remote fixes a base prefix so calls take a relative path mapped under it —
the "I never want to retype the bucket" ergonomic:
exp = ferry.Remote(
"gs://my-bucket/experiments/foo",
defaults={"excludes": ["*.tmp"], "transfers": 16}, # applied to every call
)
exp.push("results/") # -> gs://my-bucket/experiments/foo/results/
exp.pull("checkpoints/") # <- gs://my-bucket/experiments/foo/checkpoints/
exp.child("logs").push("./") # -> gs://my-bucket/experiments/foo/logs/
exp.ls() # entries under the base
exp.size() # totals under the base
CLI
The same thing from the shell:
ferry pull gs://bkt/exp/ results/
ferry push results/ gs://bkt/exp/ --transfers 16
ferry pull gs://bkt/exp/ results/ --mirror --dry-run
ferry ls gs://bkt/exp/
ferry size gs://bkt/exp/
ferry doctor [gs://bkt/exp/]
ferry install-rclone
ferry remotes
Stress testing
scripts/stress.py is a repeatable three-tier stress suite (bulk throughput,
kill-9 + resume with byte-level rclone check, many-small-files) run against
gs:// corpora you point it at. Reference numbers from the 2026-08-15 devbox
pass are in its docstring — headline: ~195 MiB/s sustained, kill-resume left
0 byte differences across 110 files, and resume is file-granular (a
partial file restarts from zero — fine for sharded weights, slow for one
giant file). Tiny-file trees are request-rate-bound (~130 objects/s): tar
them before upload.
scripts/stress_pod.py is the pod-tier version — the same suite on a fresh
RunPod pod via bellhop, PyPI install, credentials via gcs_pod_env(). The
2026-08-15 pass (RTX 4090, COMMUNITY cloud): bulk 7.1 GiB at ~57 MiB/s,
kill-resume 110/110 files matching, clean teardown. At community-pod network
speed a 200 GB pull is ~1 h — brushing the token TTL, so prefer a
service-account key for the largest jobs.
What ferry deliberately is not
- Not a new transfer engine — that's rclone's job.
- Not a credential manager — creds come from the environment (ADC,
AWS_*), exactly as your cloud SDKs already expect. - Not a daemon / continuous watcher — it's one-shot
push/pullyou call.
Content-addressed store — ferry.cas
Absorbed from the retired cloudfs
library: a minimal content-addressed file store on GCS. Files are keyed by
the MD5 of their content — uploads are idempotent, identical content is
stored once. Needs the extra: pip install "ferry-sync[gcs]" (auth via
Application Default Credentials, like everything else).
from ferry import cas
file_id = cas.upload("model.safetensors") # -> md5 hex id
cas.download(file_id, "restored.safetensors")
cas.uri(file_id) # gs://<bucket>/<prefix>/<id>
Bucket/prefix/project: FERRY_CAS_BUCKET / FERRY_CAS_PREFIX /
FERRY_CAS_PROJECT (legacy CLOUDFS_* still honored), defaulting to the
same bucket+prefix cloudfs used, so existing ids keep resolving. CLI:
ferry cas upload|download|exists|rm|uri.
Rule of thumb: moving an experiment tree by path → ferry.push/pull;
storing/serving a single artifact by content hash (dedup, stable ids) →
ferry.cas.
Changes in 0.3.0
gs:///s3://URLs accepted everywhere — zero-config, env-authenticated.ferry.ensure_rclone()/ferry install-rclone— bootstrap the binary.ferry doctor [endpoint]— preflight binary, creds, and endpoint access.ferry.ls/ferry.size(+ CLIls/size, +Remote.size).- Breaking:
Remote.ls()now returnslist[str](was a raw string).
Metadata
Release files for ferry-sync 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ferry_sync-0.3.1.tar.gz | 18.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ferry_sync-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 34.5 kB
Release files / ferry_sync-0.3.1.tar.gz
| Download URL | ferry_sync-0.3.1.tar.gz |
|---|---|
| Size | 18.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c2542a55915ce768ab4c07d8481b25c44d17ab31dc4ab5b016ed74e449b42a2f
|
|
BLAKE2b-256 checksum How to use checksums |
9b03d941ffec7b01ac0eae868a4b663645ef954cbb66165840a3fe7aedd0c52f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / ferry_sync-0.3.1-py3-none-any.whl
| Download URL | ferry_sync-0.3.1-py3-none-any.whl |
|---|---|
| Size | 15.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a874809b8757cb730b654052cd850798357b5422af100657afbe4ebcd13a380e
|
|
BLAKE2b-256 checksum How to use checksums |
59cf947a19652098e949fe9f70c2c744a3d6d8362851cec28cf8cc663fe3ce22
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|