s4fs — fsspec filesystem for S4 objects (no gateway required)
s4fs lets pandas / pyarrow / DuckDB / Polars (anything fsspec-aware) read
S4 gateway-written objects directly
from the S3 backend. Objects are transparently decompressed on read,
ls/info report the original (decompressed) sizes, and range reads use the
<key>.s4index sidecar to fetch + decode only the frames that overlap the
requested range. Objects that never went through the gateway pass through
byte-for-byte. This is the lock-in escape hatch: if you stop running the
gateway, your data stays readable.
Writes are supported too (opt-in, write_enabled=True): s4fs encodes the
body in the exact format the gateway's single-PUT path produces — S4F2
frames with the gateway's chunk-size policy, the five manifest metadata
keys, and an ETag-bound .s4index sidecar for multi-frame bodies — so
gateway GET / Range GET, s4 verify-sidecar and s4fs itself all read the
result back.
Install
pip install -e python/s4fs[s3] # from a source checkout
# requires the s4-codec wheel: cd crates/s4-codec-py && maturin build --release
Use
import pandas as pd
opts = {"target_options": {"endpoint_url": "http://backend:9000"}}
df = pd.read_parquet("s4://bucket/data.parquet", storage_options=opts)
import fsspec, pyarrow.parquet as pq
fs = fsspec.filesystem("s4", target_options={"endpoint_url": "http://backend:9000"})
table = pq.read_table("bucket/data.parquet", filesystem=fs)
import duckdb
con = duckdb.connect(); con.register_filesystem(fs)
con.sql("SELECT count(*) FROM read_parquet('s4://bucket/data.parquet')")
Any underlying fsspec filesystem can be injected instead of s3fs:
S4FileSystem(fs=my_fs) (used by the unit tests with an in-memory stub).
Write (opt-in)
import pandas as pd
opts = {
"write_enabled": True, # writes are refused without this
"target_options": {"endpoint_url": "http://backend:9000"},
}
df.to_parquet("s4://bucket/data.parquet", storage_options=opts)
df2 = pd.read_parquet("s4://bucket/data.parquet", storage_options=opts)
import fsspec
fs = fsspec.filesystem(
"s4", write_enabled=True, target_options={"endpoint_url": "http://backend:9000"}
)
fs.pipe_file("bucket/key.bin", b"payload") # or fs.open(..., "wb")
fs.put_file("local.csv", "bucket/data.csv")
What a write does (mirrors the gateway single-PUT path, byte-compatible):
- compress into S4F2 frames (
cpu-zstdlevel 3 by default; chunk size follows the gateway policy — 1 MiB for bodies ≤ 1 MiB, 4 MiB up to 100 MiB, 16 MiB above) or store raw withwrite_codec="passthrough"; - PUT the body with the gateway's manifest metadata (
s4-codec,s4-original-size,s4-compressed-size,s4-crc32c,s4-framed); - for multi-frame bodies, PUT a
<key>.s4indexsidecar bound to the body's backend ETag + size (the binding gateway Range GET ands4 verify-sidecarcheck).
Write constraints:
- Opt-in. Without
write_enabled=Trueevery write API raisesNotImplementedError(the pre-1.2 read-only contract). - Metadata-capable underlying fs required. The manifest metadata stamp
is what makes the gateway decode the object; a framed body without it
would be served as raw compressed bytes. s3fs works out of the box;
other filesystems are refused with
S4MetadataUnsupportedErrorunless they declare as4fs_metadata_pipe_kwargattribute naming thepipe_file()keyword that accepts a{str: str}metadata dict. - Create / overwrite only. Append (
mode="ab") raisesNotImplementedError.open(path, "wb")buffers the whole object in memory and uploads on close. - Codecs:
cpu-zstd(default) andpassthrough. SSE encryption, zstd-dictionary compression (cpu-zstd-dict),cpu-gzipand GPU codecs raiseNotImplementedError— write through the gateway for those. - No gateway versioning. A direct backend overwrite does not advance a
gateway-side version chain; if you rely on
--versioning, write through the gateway.
Decoded formats
- S4F2-framed objects (single-PUT and multipart), S4P1 padding skipped
- codecs:
passthrough,cpu-zstd,cpu-gzip,cpu-zstd-dict(dictionaries are fetched from.s4dict/<id>and fingerprint-verified) - unframed gateway objects carrying a metadata manifest (
cpu-gzip, legacy raw zstd,passthrough) .s4indexsidecars v1/v2/v3 with ETag staleness checks (a stale sidecar falls back to a full-object read)
Limitations
- Writes are opt-in and scoped. Default is read-only; see
Write (opt-in) above for what
write_enabled=Truesupports and refuses. Copy / move / delete stay unsupported (the gateway owns reserved-metadata propagation and sidecar/version cleanup). - GPU frames are refused loudly.
nvcomp-*/dietgpu-ansframes raiseNotImplementedError(decode them through the gateway); s4fs never returns silently-wrong bytes. - SSE-encrypted objects are refused loudly. Reads raise
NotImplementedError(the keyring / KMS / SSE-C key lives in the gateway — read encrypted objects through the gateway). Detection is threefold: thes4-encryptedobject metadata stamp, the sidecar's v3 SSE binding, and theS4E1–S4E6envelope magic in the body; s4fs never returns ciphertext as if it were data. - Exact-size resolution in
ls/infomay cost one extra backend request per object (sidecar GET or metadata HEAD); results are cached per filesystem instance. - Range reads on framed objects without a usable sidecar fall back to a full-object read (with a warning when the object is multi-frame). Legacy v1 sidecars (no source ETag/size binding) are treated as unusable — they cannot be tied to the live object.
open()refuses framed objects whose original size is inexact (no usable sidecar, nos4-original-sizemetadata) instead of silently truncating buffered reads at the compressed size; opt back in withS4FileSystem(allow_inexact_open=True).cat_file()is unaffected.
Tests
pytest python/s4fs/tests # unit (gateway-captured fixtures)
pytest python/s4fs/tests/test_e2e_minio.py -m e2e # read e2e: docker + MinIO + real gateway
pytest python/s4fs/tests/test_e2e_s4fs_write.py -m e2e # write e2e: gateway GET / verify-sidecar / Range GET
Release files for s4fs 1.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| s4fs-1.5.1.tar.gz | 32.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| s4fs-1.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:50.8 kB
Release files / s4fs-1.5.1.tar.gz
| Download URL | s4fs-1.5.1.tar.gz |
|---|---|
| Size | 32.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3357e2c78f38ba428c3f9e60a57bcc30b451e56fbdd9f633ca1cefc13e794368
|
|
BLAKE2b-256 checksum How to use checksums |
3bb7514c8156647d3340b90ca14466f116226f0fa5073b94c51ec25adb580651
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / s4fs-1.5.1-py3-none-any.whl
| Download URL | s4fs-1.5.1-py3-none-any.whl |
|---|---|
| Size | 17.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
04e4dd2fdd6740c7643e7d93af29fb3c4dd8e6afbc16a4b3b939db2d1482e533
|
|
BLAKE2b-256 checksum How to use checksums |
734e51f8f66635d67d82ad3ce69c879bf2c699453cd383155156c5641df81895
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|