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).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ferry_sync-0.3.1.tar.gz.
File metadata
- Download URL: ferry_sync-0.3.1.tar.gz
- Upload date:
- Size: 18.8 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2542a55915ce768ab4c07d8481b25c44d17ab31dc4ab5b016ed74e449b42a2f
|
|
| MD5 |
8646000e5f0de1368f74e5751ba985fa
|
|
| BLAKE2b-256 |
9b03d941ffec7b01ac0eae868a4b663645ef954cbb66165840a3fe7aedd0c52f
|
File details
Details for the file ferry_sync-0.3.1-py3-none-any.whl.
File metadata
- Download URL: ferry_sync-0.3.1-py3-none-any.whl
- Upload date:
- Size: 15.7 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a874809b8757cb730b654052cd850798357b5422af100657afbe4ebcd13a380e
|
|
| MD5 |
7e9a41d69bcbfd420ee85a1a5dbfaa09
|
|
| BLAKE2b-256 |
59cf947a19652098e949fe9f70c2c744a3d6d8362851cec28cf8cc663fe3ce22
|