◆ R · Python · Rust · Go

Data in and out of any S3 store, in one line.

The same nine verbs in four languages for any S3-compatible object store: AWS S3, Garage, MinIO, Ceph. Endpoint and credentials from the environment, format from the extension, every transfer signed, retried and checked.

CI CRAN PyPI crates.io Go Reference License MIT DOI

Four editions, one behaviour

Pick the language of the project. Everything else is the same: defaults, signing, retries, integrity checks, log lines and error kinds. One repository, one version, one changelog.

📊

R

Pure R on httr2 and cli, tidyverse style. Tibbles from CSV and Parquet, lists from JSON, sf from GeoJSON.

install.packages("s3pe")
🐍

Python

A binding on the Rust core, wheels for every platform. DataFrames with pandas, dicts from JSON, bytes otherwise.

pip install "s3pe[pandas]"
🦀

Rust

The core: SigV4, retries and checks over ureq and rustls. Also the s3pe command line.

cargo add s3pe
cargo install s3pe
🐹

Go

Standard library only, context on every call, typed errors for errors.Is.

go get github.com/StrategicProjects/s3pe

The nine verbs

A key is s3://bucket/key, or a bare key when the bucket is configured. Nothing else to learn.

VerbDoesR and Python return
read_s3GET an object into memorydecoded by extension: tibble / DataFrame, list / dict, text, raw bytes
write_s3PUT an object from memoryencoded by extension: CSV, CSV.GZ, TSV, JSON, GeoJSON, Parquet, XLSX, RDS, text, bytes
download_s3GET to a file, streamingwritten as .s3pe-part, renamed when complete
upload_s3PUT from a file, streamingcontent type guessed from the key
list_s3every object under a prefix, all pagestibble / list of records: key, size, last_modified, etag
exists_s3HEAD, 404 as falselogical / bool
head_s3HEAD: size, type, date, ETagone record, or nothing
delete_s3DELETE, idempotentnothing
buckets_s3the buckets the credentials seenames and creation dates

Rust and Go expose the same verbs (s3pe::read_s3, s3pe.ReadS3) and a Client for explicit settings; R has s3_config().

What happens in every call

The parts that make the difference between "it usually works" and "it worked, and we know it did".

Six steps: resolve the key, hash the payload, sign with SigV4, send with Content-Length, retry on 429 and 5xx after 1 and 2 seconds, verify the ETag and Content-Length. Success prints one log line; errors are typed.
01

Sign

AWS Signature V4 over the real SHA-256 of the payload. No unsigned payloads, no aws-chunked (some stores reject it). The four signers reproduce the AWS documentation vector.

02

Retry

Connection failures and HTTP 429, 500, 502, 503, 504: three tries, 1 s then 2 s. Never on other 4xx.

03

Verify

After an upload the server's ETag against the local MD5; after a download the byte count and the MD5 of what arrived.

04

Report

One line per transfer, silenced with S3PE_QUIET=1. Errors are typed: not found, access denied, HTTP, transport, configuration, integrity.

🧪

Tested three ways

Each edition runs against a local fake S3 that recomputes and checks every signature, against the AWS documentation vector, and against a real store when S3PE_ENDPOINT and S3PE_TEST_BUCKET are set.

📄

Decisions on record

Why path-style, why our own signer, why no default endpoint: DESIGN.md keeps the reasons.

Architecture

Four implementations, one specification, any store. The Python package is a binding on the Rust crate; R and Go are native. Each edition has its own page: R, Python, Rust, Go.

Four editions (R on httr2 and openssl; Python as a PyO3 binding on the Rust crate; the Rust crate on ureq and rustls with the s3pe command line; Go on net/http) share one specification: resolve the key, sign with SigV4, retry, verify, log one line. All talk to any S3-compatible store through path-style URLs.

Configuration

Argument, then environment, then default. Three variables are enough: the endpoint and the two halves of the key.

~/.Renviron, .env or the shell

S3PE_ENDPOINT=https://s3.example.org
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
S3PE_BUCKET=my-bucket              # optional: bare keys

Never put the key in a script or a repository. In R, use_endpoint() and use_bucket() set the two for the session; in Python, s3pe.configure().

SettingEnvironmentDefault
endpointS3PE_ENDPOINT, AWS_ENDPOINT_URLrequired
regionAWS_REGION, AWS_DEFAULT_REGIONus-east-1
credentialsAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYrequired
bucketS3PE_BUCKETnone: use s3://bucket/key
quietS3PE_QUIET=1one line per transfer