β—† Python Β· S3 client

Data in and out of any S3 store, in Python.

Nine verbs that read and write DataFrames, dicts, text and files by extension, on a Rust core that signs, retries and verifies every transfer. pip install s3pe, two environment variables, done.

PyPI CI License MIT DOI Python 3.9+

Built to be trusted with the data

A client that moves the files a pipeline depends on has one job: either the bytes arrived, or you hear about it. Everything here serves that.

πŸ—‚οΈ

Formats by extension

.json β†’ dict, .csv/.csv.gz/.tsv/.parquet/.xlsx β†’ DataFrame, text files β†’ str, anything else β†’ bytes. format= overrides; extra arguments go to pandas.

πŸ¦€

A Rust core

Signing, retries and checks happen in the s3pe crate; the GIL is released during transfers. Wheels for Linux (x86_64, aarch64), macOS and Windows, Python 3.9 or later.

πŸ”

Signed the way every store accepts

AWS Signature V4 over the real SHA-256 of the body. No unsigned payloads, no aws-chunked trailers (which some stores reject), path-style URLs.

πŸ”

Retries that know when to stop

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

βœ…

Every byte accounted for

ETag against the local MD5 after a PUT; Content-Length and MD5 after a GET. Downloads land in a .s3pe-part file and are renamed when complete.

🧭

Typed exceptions

NotFoundError, AccessDeniedError, HttpError (with .status, .code), TransportError, ConfigError, IntegrityError, all under S3Error.

The nine verbs, and four more

Module-level verbs over a default client (s3pe.configure(bucket="my-bucket") sets it), and the same verbs as methods of s3pe.Client. Files above 64 MiB go up as a multipart upload (parts in parallel, aborted cleanly on failure); metadata and headers ride along on writes. No ACLs and no versioning, by design.

VerbDoes
read_s3GET, decoded by extension or format=
write_s3PUT a DataFrame, dict, list, str, bytes, file-like or Path
download_s3GET to a file, streaming; refuses to overwrite unless asked
upload_s3PUT from a file, streaming
list_s3every S3Object under a prefix, all pages
exists_s3bool
head_s3S3Object or None: size, type, date, ETag, metadata
delete_s3one key or a list (batches of 1000)
buckets_s3list of Bucket
copy_s3server-side copy
list_s3(delimiter="/")one level: objects and prefixes (is_prefix)
presign_s3a GET or PUT URL without credentials, up to 7 days
sync_up_s3 / sync_down_s3a directory and a prefix mirrored by ETag, a manifest last

A key is s3://bucket/key, or a bare key when the bucket is configured (S3PE_BUCKET or the client). Endpoint: S3PE_ENDPOINT (https://host[:port], required); credentials: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.

What happens in every call

The same six steps in the four editions.

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

Resolve and hash

s3://bucket/key or bucket plus key; SHA-256 for the signature, MD5 for the check.

02

Sign and send

SigV4 over host, date and payload hash; path-style, Content-Length, no chunked trailers.

03

Retry

Connection failures and 429/5xx, three tries, 1 s then 2 s. Other 4xx come straight back.

04

Verify and report

ETag and Content-Length checked; one line on stderr; typed errors otherwise.

Install & run

Python 3.9 or later; pandas and pyarrow only for tables. Changes between versions in the changelog.

$ pip install s3pe              # bytes, str, dict/list and files
$ pip install "s3pe[pandas]"     # also CSV, Parquet and Excel as DataFrames
# an explicit client
s3 = s3pe.Client(endpoint="http://127.0.0.1:9000",
                 bucket="my-bucket",
                 access_key="AKIA…", secret_key="…")
s3.read("data/manifest.json")
s3.write(df, "reports/sales.csv", index=False)
s3.download("data/regions.geojson", "pe.geojson")
s3.upload("figure1.png", "reports/")
s3.head("etl/sales.parquet")      # S3Object or None

s3pe.quiet()                        # or S3PE_QUIET=1
# errors and formats
try:
    s3pe.read_s3("s3://my-bucket/missing.json")
except s3pe.NotFoundError:
    ...
except s3pe.HttpError as e:
    print(e.status, e.code)

raw = s3pe.read_s3("x.csv", bucket="b", format="bytes")
df  = s3pe.read_s3("x.csv", bucket="b", sep=";", dtype=str)
s3pe.write_s3({"ok": True}, "s3://b/manifest.json")
s3pe.write_s3(df, "s3://b/series.csv.gz")   # gzipped on the way

Architecture

The Python package is a thin layer: formats and logging in Python, everything on the wire in the Rust crate through PyO3. R and Go reimplement the same specification; the four signers reproduce the AWS documentation vector. One repository, one version, one DESIGN.md.

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 and talk to any S3-compatible store.

R

install.packages("s3pe")

Rust

cargo add s3pe

Go

go get github.com/StrategicProjects/s3pe