The single place Forge turns a user-supplied source string into something the
format readers can consume. It lets every command that accepts a dataset
path also accept cloud object-store URIs (s3://, gs://) alongside local
paths and hf:// URLs.
Forge's format readers are built around pathlib.Path and local file access —
h5py.File, av.open, pq.read_table, Path.glob, open() — and format
detection probes the directory tree with local Path operations. Rather than
teach every reader to speak fsspec, all path resolution is funneled through one
utility: paths.py. Remote datasets are downloaded to a
temporary directory on first access and cleaned up automatically, so every
format behaves identically whether the data is local or in a bucket.
source string ──▶ localize() ──▶ local Path ──▶ existing readers (unchanged)
s3://… (temp dir, auto-cleaned at exit)
gs://…
/local/path (returned as-is)
hf://org/repo (handled by forge.hub, not here)
Design note: most operations download the remote source in full before processing — this keeps readers that seek randomly (video keyframes, HDF5 chunks, rosbag indexes) working without modification. The exception is
forge inspecton LeRobot-v3 / Zarr, which reads metadata over the network via range requests (DataSource) — inspecting a multi-GB cloud dataset fetches only kilobytes. Streaming the ingest/scoring paths (parquet column reads) is the next step.
pip install "forge-robotics[s3]" # Amazon S3 (s3://) → pulls in s3fs
pip install "forge-robotics[gcs]" # Google Cloud (gs://) → pulls in gcsfsfsspec itself is a core dependency; only the provider backend is optional. If
you pass an s3:// URI without s3fs installed, Forge fails with the exact
pip install command to run.
Any command that takes a dataset path works with a cloud URI:
forge inspect s3://my-bucket/datasets/run_0413
forge convert gs://lab-data/rosbags ./out --format lerobot-v3
forge quality s3://my-bucket/datasets/droid --report report.html
forge stats gs://lab-data/pusht.zarrWriting outputs to a cloud URI (forge convert … s3://bucket/out) is not
supported yet — commands fail fast with a clear message. Write locally and
upload afterwards.
from forge.io import localize, is_remote_uri, get_filesystem
# Resolve any source to a local Path (downloads if remote, passthrough if local)
local_path = localize("s3://my-bucket/datasets/run_0413")
# ... hand local_path to any forge reader ...
is_remote_uri("s3://bucket/key") # True
is_remote_uri("/data/run") # False
is_remote_uri("hf://org/repo") # False (handled by forge.hub)
# Drop down to the underlying fsspec filesystem if you need raw access
fs, path = get_filesystem("s3://my-bucket/datasets/run_0413")
fs.ls(path)localize() registers each temp directory for removal at process exit; call
forge.io.cleanup_localized() to remove them eagerly (e.g. in a long-running
service that resolves many datasets).
Forge never handles credentials itself — it relies entirely on each provider's default credential chain.
| Provider | How credentials are found (in order) |
|---|---|
S3 (s3://) |
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars → AWS_PROFILE in ~/.aws/credentials & ~/.aws/config → EC2/ECS/EKS instance IAM role |
GCS (gs://) |
GOOGLE_APPLICATION_CREDENTIALS (service-account key) → gcloud auth application-default login → attached service account on GCP |
Docs: AWS credentials · GCP Application Default Credentials.
Work top-to-bottom — the checks are ordered from "wrong install" to "wrong
permissions". The fastest way to isolate a problem is to reproduce it with the
underlying tool (aws/gcloud or s3fs/gcsfs) directly, since Forge just
delegates to them.
The backend isn't installed. Run the exact command in the message
([s3] for s3://, [gcs] for gs://).
This bypasses Forge entirely and tests just the filesystem + credentials:
from forge.io import get_filesystem
fs, path = get_filesystem("s3://my-bucket/datasets/run_0413")
print(fs.exists(path)) # False → wrong path or no read permission
print(fs.ls(path)[:5]) # lists objects if you can see the prefixOr with the provider CLI (which uses the same credential chain):
aws s3 ls s3://my-bucket/datasets/run_0413/
gcloud storage ls gs://lab-data/rosbags/If these fail, the problem is credentials/permissions/path — not Forge.
| Symptom | Likely cause | Fix |
|---|---|---|
NoCredentialsError / Unable to locate credentials |
No credentials on the machine | aws configure / export AWS_PROFILE=… / gcloud auth application-default login |
AccessDenied / 403 Forbidden |
Credentials work but lack s3:GetObject/s3:ListBucket (or storage.objects.get/list) on that prefix |
Grant read on the bucket/prefix, or use a profile/role that has it |
FileNotFoundError: Remote source not found (or empty) |
Path typo, wrong bucket, or an empty prefix | Verify with aws s3 ls / gcloud storage ls; check for a trailing-slash or region mismatch |
NoSuchBucket / The specified bucket does not exist |
Wrong bucket name or wrong region | Check the name; set AWS_DEFAULT_REGION / AWS_REGION |
EndpointConnectionError / connection timeout |
No network, VPC egress blocked, or a custom endpoint needed | Check connectivity; for S3-compatible stores (MinIO, R2) set AWS_ENDPOINT_URL |
ExpiredToken / InvalidToken |
Temporary/STS credentials expired | Re-authenticate (aws sso login, refresh the assumed role) |
Wrong/expired GCP creds despite gcloud working |
GOOGLE_APPLICATION_CREDENTIALS points at a stale key that overrides ADC |
Unset it to fall back to ADC, or point it at the right key |
| Download is slow / appears to hang | Whole dataset is being downloaded to a temp dir (expected) | Use a smaller sample where supported (--sample), or a machine closer to the bucket region |
Point botocore/s3fs at the custom endpoint via the standard env var — no Forge config needed:
export AWS_ENDPOINT_URL=https://<your-endpoint>
export AWS_ACCESS_KEY_ID=… AWS_SECRET_ACCESS_KEY=…
forge inspect s3://my-bucket/my-datasetfsspec/botocore/gcsfs emit useful debug logs. Enable them and resolve a path directly:
import logging
logging.basicConfig(level=logging.DEBUG) # botocore, s3fs, gcsfs all log here
from forge.io import localize
localize("s3://my-bucket/dataset") # watch the request/response traceFor a quick sign of life you can also check where a resolved dataset landed —
Forge prints Downloaded to: /tmp/forge-cloud-… before it starts reading.
Cloud access is tested without any real provider — see
tests/test_io_paths.py:
- fsspec's in-process
memory://filesystem (needs only corefsspec; always runs) for scheme detection, download-to-temp, cleanup, and end-to-end lerobot-v3 / zarr resolution. - a
motomock S3 server for the reals3://code path (skipped ifmoto[server]/s3fsaren't installed).
pip install -e ".[dev]"
pytest tests/test_io_paths.py -vTo drive the real forge CLI against a real S3-compatible server (no AWS
account needed), see TESTING_MINIO.md — a step-by-step guide
to running a local MinIO and testing cloud support against it.
| Name | Purpose |
|---|---|
localize(uri, *, console=None) |
Resolve uri to a local Path, downloading if remote. Local paths & hf:// returned as-is. |
is_remote_uri(uri) |
True for s3:///gs:///other fsspec schemes; False for local paths and hf://. |
get_scheme(uri) |
The lowercased URL scheme ("" for local paths). |
get_filesystem(uri) |
(fsspec_filesystem, path) for raw access; raises MissingDependencyError with the pip hint if the backend is missing. |
cleanup_localized() |
Remove all temp dirs created for downloaded datasets (also runs at exit). |
RemoteWriteNotSupportedError |
Raised when a cloud URI is given as an output destination. |