Skip to main content

fsspec backend for Ethereum Swarm (bzz://) via the Bee HTTP API

Project description

swarmfs

tests PyPI license

An fsspec backend for Ethereum Swarm, talking to a Bee node (or public gateway) over its HTTP API. Installing it makes Swarm a first-class storage backend for the Python data ecosystem — pandas, dask, zarr, xarray, pyarrow, DuckDB — via URLs like bzz://<reference>/path/to/file.parquet.

Status: v2. Read-only bzz:// access, transactional copy-on-write writes (postage stamps, every commit a snapshot), and mutable feed-backed bzzf:// mounts. See the roadmap.

New to swarmfs? This README is a quick reference — the User Guide walks through a worked example for every library above (pandas, all three Dask collection types, Zarr, xarray, PyArrow, DuckDB) and explains the content-addressing model in plain terms.

Install

pip install swarmfs            # or: pip install "swarmfs[feeds]" for signed feeds

Upload and download a file

You need a running Bee light node (http://localhost:1633 by default) and, for uploads, a usable postage stamp:

import fsspec

fs = fsspec.filesystem("bzz", stamp="auto")

ref = fs.upload("photo.jpg")                    # → "c0ffee…" (64 hex chars)
fs.download(f"bzz://{ref}/photo.jpg", "copy.jpg")

On Swarm the address of new content is the result of a write, not its input — upload returns the new reference, and that reference is permanent: it names this exact content forever. Directories work the same way and come back as a single reference for the whole tree:

ref = fs.upload("dataset/")
fs.ls(f"bzz://{ref}")
fs.download(f"bzz://{ref}", "dataset-copy/", recursive=True)

upload accepts content_type= (otherwise guessed from the filename), encrypt=True (single files; the returned 128-hex reference includes the decryption key), and redundancy=0–4 (erasure coding, default 2). The stamp is validated before any byte moves, so a missing or expired stamp fails immediately with an actionable error.

The data ecosystem

The point of being an fsspec backend: everything that speaks fsspec now speaks Swarm, with zero extra code.

import pandas as pd

df = pd.read_parquet("bzz://<64-hex-reference>/data.parquet")

# local caching via URL chaining
df = pd.read_parquet("simplecache::bzz://<reference>/big.parquet")
import fsspec

fs = fsspec.filesystem("bzz")          # api_url=..., default $BEE_API_URL or localhost:1633
fs.ls("bzz://<reference>/")            # client-side Mantaray trie walk
fs.find("bzz://<reference>/dataset/")  # recursive listing (dask uses this)
fs.cat("bzz://<reference>/hello.txt")

with fs.open("bzz://<reference>/big.parquet", block_size=2**20) as f:
    f.seek(-8, 2)                      # range requests: only the bytes you touch
    f.read(8)

Same story for any other fsspec-based tool — Intake, DVC, Kedro, pyxet, Hugging Face Datasets, petl, and more (see the User Guide).

Transactional writes

For anything beyond a one-shot upload — building a dataset in place, changing one file inside a large collection — writes are copy-on-write commits: each commit patches the manifest trie client-side, re-uploads only what changed, and yields a new root. Old roots are untouched, so every commit is a snapshot.

fs = fsspec.filesystem("bzz", stamp="auto")
with fs.transaction:
    fs.pipe_file("bzz://new/dataset/a.parquet", data_a)
    fs.pipe_file("bzz://new/dataset/b.parquet", data_b)
root = fs.latest("new")          # share this reference; it never changes

Mutable feeds (bzzf://)

A feed gives you a stable URL whose contents you can update — the mutable filesystem on top of immutable commits:

ffs = fsspec.filesystem("bzzf", stamp="auto", signer="<private key hex>")
ffs.pipe_file(f"bzzf://{owner}/my-app/config.json", b'{"v": 2}')
# readers need no keys — and the URL never changes

Addressing content offline, and buying stamps

Two things you can do without uploading anything:

import swarmfs

ref = swarmfs.content_address(open("photo.jpg", "rb").read())   # no node needed
root, chunks = swarmfs.split(data)   # the whole chunk tree, keyed by address

content_address computes the reference Bee would return for those bytes, so you can check whether the network already has content, name blobs by their Swarm address in a local store, or build test fixtures at real addresses. It needs the feeds extra (keccak256).

One caveat that bites in practice: this is the reference for a plain upload. Erasure coding changes every intermediate chunk and therefore the root, and parity is the node's to generate — so a redundant upload's reference cannot be predicted offline. swarmfs writes with redundancy=2 by default and many nodes default to redundancy too, so pass redundancy=0 if you want the uploaded reference to match the one you computed.

Stamps can also be handled programmatically — selection is automatic (stamp="auto" picks the usable batch with the longest life), and spending is available but never implicit. Every plan_* call is a pure question; only the verbs move money:

from swarmfs.stamps import StampManager, depth_for_addresses, stamped_chunks

plan = await mgr.plan(size_bytes, ttl_secs)   # depth, amount, cost in xBZZ
batch = await mgr.buy(plan.amount, plan.depth)  # spends the node wallet's xBZZ

# depth depends on how you will upload: erasure parity and encryption add
# stamped chunks, and a batch dies of a full *bucket*, not of full capacity
plan = await mgr.plan(size_bytes, ttl_secs, redundancy=4, encrypted=True)
# ...or skip the statistics entirely — a plain upload's addresses are known
root, chunks = swarmfs.split(data)
plan = await mgr.plan(size_bytes, ttl_secs, depth=depth_for_addresses(chunks))

# renewal: extend BY a duration, TO a total, or for at most a budget
plan = await mgr.plan_topup(batch, ttl_secs=30 * 86400)
print(plan.cost_bzz, plan.total_ttl_secs, plan.warning)
info = await mgr.topup(batch, plan.added_amount)   # waits until the node applies it

# capacity, not time: dilution costs gas, and is paid for in TTL
print((await mgr.plan_dilute(batch, 20)).ttl_after_secs)   # ~halved per step

Four things about renewal that are easy to get wrong, so swarmfs encodes them: a topup adds to the remaining life rather than restarting it; a nearly-full immutable batch should be diluted before topping up, or the dilution halves away part of what you just bought (plan_topup().warning says so); remaining life is the node's batchTTL and never amount / currentPrice (that field describes lifetime from the creation block, and is local bookkeeping that can revert); and an expired batch cannot be revived, so renew while it lives. The node also takes ~40 s to index a topup — topup() polls, because reading straight after the transaction shows the old value and looks like a silent failure.

For monitoring, mgr.list_batches() plus StampInfo.problem(min_ttl) turns "still usable" into "needs renewing" at whatever threshold you choose, and mgr.buckets(batch) reports the true per-bucket headroom that bounds the next upload — utilizationRatio only summarises it.

plan sizes the batch for your upload (bucket-overflow-aware) and prices it from the chain; buy purchases and waits until the batch is usable. Nothing in swarmfs buys on its own — deciding to spend is the caller's.

Which API should I use?

Three tiers, all backed by the same endpoint resolution (api_url=...$BEE_API_URLhttp://localhost:1633):

  • SwarmFileSystem / fsspec URLs — the default. Filesystem semantics, transactions, verification, and the whole data ecosystem for free.

  • swarmfs.SyncSwarmClient / swarmfs.SwarmClient — direct calls against the Bee API (upload a blob, fetch bytes, post a feed update) without filesystem semantics. SyncSwarmClient is the blocking twin for plain scripts; SwarmClient is the same surface as coroutines for asyncio code:

    from swarmfs import SyncSwarmClient
    
    with SyncSwarmClient() as client:            # async? use SwarmClient + await
        ref = client.bzz_post(open("photo.jpg", "rb"), stamp=batch_id)
        data = client.bzz_get(ref, "photo.jpg")
    
  • Raw HTTP — the Bee API is plain HTTP; no library needed:

    curl -X POST -H "Swarm-Postage-Batch-Id: <batch>" \
         --data-binary @photo.jpg http://localhost:1633/bzz?name=photo.jpg
    

    What the library adds over this: stamp validation up front, chunk verification, gateway policy, better errors — the edge cases.

Nodes, gateways, verification

The recommended setup is a local light node — reads then come straight from the network with nothing to trust in between. Pointing api_url at a public gateway is discouraged and requires an explicit allow_gateway=True — on that path swarmfs verifies every fetched chunk client-side against its BMT address (a Swarm reference is the content hash), so even an untrusted gateway can't tamper with what you read. Verification can also be forced on/off with verify=True/False.

How it works

Swarm has no server-side directory listing today, so swarmfs parses the binary Mantaray manifest trie itself, fetching nodes on demand via /bytes (see swarmfs/mantaray/ — a self-contained pure-Python codec). File reads resolve the path to its data reference once, then use HTTP range requests against /bytes, which is what makes Parquet predicate pushdown and zarr chunk reads viable. When Bee grows a server-side listing endpoint (ethersphere/bee#5535) it will slot in behind the existing capability seam with no API change.

Compared to ipfsspec

ipfsspec, the closest analog in the fsspec ecosystem, is read-only by its own admission. Postage stamps make paid writes tractable on Swarm, so swarmfs adds a transactional write path and, via bzzf:// feeds, a stable URL you can actually mutate — not just read.

Development

pip install -e ".[test]"
pytest                                   # offline unit tests (no node needed)
SWARMFS_TEST_BEE=http://localhost:1633 \
SWARMFS_TEST_STAMP=<batch-id> pytest tests/test_integration.py

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

swarmfs-0.4.0.tar.gz (121.8 kB view details)

Uploaded Source

Built Distribution

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

swarmfs-0.4.0-py3-none-any.whl (59.7 kB view details)

Uploaded Python 3

File details

Details for the file swarmfs-0.4.0.tar.gz.

File metadata

  • Download URL: swarmfs-0.4.0.tar.gz
  • Upload date:
  • Size: 121.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for swarmfs-0.4.0.tar.gz
Algorithm Hash digest
SHA256 55e75fe05fcf11637c64315f077bed22890a34199244086ccbc8bd63a6781f15
MD5 df48f1eb6503bb3830710dae1555a1bc
BLAKE2b-256 d0f2120697c4986098025951c2b2d83531eb747a5ae15ebe9d0445db7c801dcb

See more details on using hashes here.

Provenance

The following attestation bundles were made for swarmfs-0.4.0.tar.gz:

Publisher: publish.yml on petfold/swarmfs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file swarmfs-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: swarmfs-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 59.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for swarmfs-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 98feed7a12d0db19db27c3e3664efae8738d03608f3dcf55885b6fea8c9bcdd0
MD5 28b2bf8b52f6d3e3bd5fdd328ebaba4d
BLAKE2b-256 e792be8290113e1c22f611b71181a335802381ed0722079474a3578298423597

See more details on using hashes here.

Provenance

The following attestation bundles were made for swarmfs-0.4.0-py3-none-any.whl:

Publisher: publish.yml on petfold/swarmfs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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