Skip to main content

ferry

PyPI Python CI License: MIT

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 pathresults/.
  • cloud URLgs://bucket/key or s3://bucket/key. No rclone config needed: ferry maps these to on-the-fly rclone backends that authenticate from the environment — Application Default Credentials or GOOGLE_APPLICATION_CREDENTIALS for GCS; AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGION for S3.
  • rclone endpointname:bucket/key where name comes from rclone listremotes, for anything you've set up with rclone 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/pull run rclone 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=True runs rclone sync instead — the destination becomes an exact mirror, which deletes files on the destination that are absent at the source. Use with care; pair with dry_run=True first.
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/pull you 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 (+ CLI ls/size, + Remote.size).
  • Breaking: Remote.ls() now returns list[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

ferry_sync-0.3.1.tar.gz (18.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ferry_sync-0.3.1-py3-none-any.whl (15.7 kB view details)

Uploaded Python 3

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

Hashes for ferry_sync-0.3.1.tar.gz
Algorithm Hash digest
SHA256 c2542a55915ce768ab4c07d8481b25c44d17ab31dc4ab5b016ed74e449b42a2f
MD5 8646000e5f0de1368f74e5751ba985fa
BLAKE2b-256 9b03d941ffec7b01ac0eae868a4b663645ef954cbb66165840a3fe7aedd0c52f

See more details on using hashes here.

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

Hashes for ferry_sync-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a874809b8757cb730b654052cd850798357b5422af100657afbe4ebcd13a380e
MD5 7e9a41d69bcbfd420ee85a1a5dbfaa09
BLAKE2b-256 59cf947a19652098e949fe9f70c2c744a3d6d8362851cec28cf8cc663fe3ce22

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.3.1 This release

2 files

0.3.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page