swarmfs 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swarmfs-0.1.0/.github/workflows/publish.yml +36 -0
- swarmfs-0.1.0/.github/workflows/tests.yml +52 -0
- swarmfs-0.1.0/.gitignore +9 -0
- swarmfs-0.1.0/CLAUDE.md +302 -0
- swarmfs-0.1.0/LICENSE +28 -0
- swarmfs-0.1.0/PKG-INFO +210 -0
- swarmfs-0.1.0/README.md +175 -0
- swarmfs-0.1.0/docs/USER_GUIDE.md +484 -0
- swarmfs-0.1.0/docs/bee-feature-request.md +137 -0
- swarmfs-0.1.0/docs/roadmap.md +141 -0
- swarmfs-0.1.0/pyproject.toml +57 -0
- swarmfs-0.1.0/swarmfs/__init__.py +27 -0
- swarmfs-0.1.0/swarmfs/_client.py +428 -0
- swarmfs-0.1.0/swarmfs/_listing.py +100 -0
- swarmfs-0.1.0/swarmfs/bmt.py +54 -0
- swarmfs-0.1.0/swarmfs/commit.py +134 -0
- swarmfs-0.1.0/swarmfs/core.py +920 -0
- swarmfs-0.1.0/swarmfs/exceptions.py +35 -0
- swarmfs-0.1.0/swarmfs/feedfs.py +182 -0
- swarmfs-0.1.0/swarmfs/feeds.py +200 -0
- swarmfs-0.1.0/swarmfs/join.py +164 -0
- swarmfs-0.1.0/swarmfs/mantaray/__init__.py +40 -0
- swarmfs-0.1.0/swarmfs/mantaray/build.py +186 -0
- swarmfs-0.1.0/swarmfs/mantaray/node.py +261 -0
- swarmfs-0.1.0/swarmfs/mantaray/walk.py +180 -0
- swarmfs-0.1.0/swarmfs/stamps.py +183 -0
- swarmfs-0.1.0/tests/capture_fixture.py +107 -0
- swarmfs-0.1.0/tests/conftest.py +307 -0
- swarmfs-0.1.0/tests/fixtures/real_manifest.json +79 -0
- swarmfs-0.1.0/tests/test_bmt.py +53 -0
- swarmfs-0.1.0/tests/test_codec.py +181 -0
- swarmfs-0.1.0/tests/test_exceptions.py +51 -0
- swarmfs-0.1.0/tests/test_feedfs.py +161 -0
- swarmfs-0.1.0/tests/test_fs.py +240 -0
- swarmfs-0.1.0/tests/test_integration.py +346 -0
- swarmfs-0.1.0/tests/test_patch.py +189 -0
- swarmfs-0.1.0/tests/test_real_fixture.py +79 -0
- swarmfs-0.1.0/tests/test_stamps.py +173 -0
- swarmfs-0.1.0/tests/test_sync_client.py +56 -0
- swarmfs-0.1.0/tests/test_upload_download.py +129 -0
- swarmfs-0.1.0/tests/test_verify.py +252 -0
- swarmfs-0.1.0/tests/test_walk.py +144 -0
- swarmfs-0.1.0/tests/test_write_fs.py +226 -0
- swarmfs-0.1.0/tests/test_zarr.py +72 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
- run: pip install -e ".[test]"
|
|
16
|
+
- run: pytest
|
|
17
|
+
- run: pip install build twine
|
|
18
|
+
- run: python -m build
|
|
19
|
+
- run: twine check dist/*
|
|
20
|
+
- uses: actions/upload-artifact@v4
|
|
21
|
+
with:
|
|
22
|
+
name: dist
|
|
23
|
+
path: dist/
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
needs: build
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
environment: pypi
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write # required for PyPI trusted publishing (OIDC) — no API token stored
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/download-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: pip install -e ".[test]"
|
|
21
|
+
# SWARMFS_TEST_BEE is unset, so tests/test_integration.py's
|
|
22
|
+
# live-node tests skip themselves — this runs the offline suite only.
|
|
23
|
+
- run: pytest
|
|
24
|
+
|
|
25
|
+
package:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.12"
|
|
32
|
+
- run: pip install build twine
|
|
33
|
+
- run: python -m build
|
|
34
|
+
- run: twine check dist/*
|
|
35
|
+
# regression guard: the sdist must never bundle local tooling config
|
|
36
|
+
# (it did once — a leaked postage-batch ID and shell history) or ship
|
|
37
|
+
# without the LICENSE file hatchling is expected to auto-include.
|
|
38
|
+
- name: verify sdist contents
|
|
39
|
+
run: |
|
|
40
|
+
SDIST=$(ls dist/*.tar.gz)
|
|
41
|
+
if tar tzf "$SDIST" | grep -q '/\.claude/'; then
|
|
42
|
+
echo "::error::sdist bundles .claude/ — local tooling config must not ship"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
if ! tar tzf "$SDIST" | grep -q '/LICENSE$'; then
|
|
46
|
+
echo "::error::sdist is missing LICENSE"
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
- uses: actions/upload-artifact@v4
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
swarmfs-0.1.0/.gitignore
ADDED
swarmfs-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# swarmfs — CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Persistent brief for Claude Code. Read this first, every session. Keep it updated as
|
|
4
|
+
decisions change; treat it as the source of truth over any single conversation.
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
`swarmfs` is an [fsspec](https://filesystem-spec.readthedocs.io/) backend for
|
|
9
|
+
[Ethereum Swarm](https://docs.ethswarm.org/), talking to a [Bee](https://github.com/ethersphere/bee)
|
|
10
|
+
node over its HTTP API. Installing it makes Swarm a first-class storage backend for the
|
|
11
|
+
entire Python data ecosystem — pandas, dask, zarr, xarray, pyarrow, DuckDB — via URLs like
|
|
12
|
+
`bzz://<reference>/path/to/file.parquet`.
|
|
13
|
+
|
|
14
|
+
## Primary audience (drives priorities)
|
|
15
|
+
|
|
16
|
+
"Data people who should be able to ignore that it's Swarm." The read path and the
|
|
17
|
+
pandas/dask/zarr experience matter most. Swarm-native mutable-filesystem use
|
|
18
|
+
(feed-mounted writes) is a real but secondary audience — build the read story first.
|
|
19
|
+
|
|
20
|
+
## Names and identifiers (decided)
|
|
21
|
+
|
|
22
|
+
- Package / import name: `swarmfs` (confirmed unclaimed on PyPI).
|
|
23
|
+
- Main class: `SwarmFileSystem`.
|
|
24
|
+
- Protocols: `bzz://` (immutable, content-addressed) and `bzzf://` (feed-backed, mutable).
|
|
25
|
+
- File class: `SwarmFile`.
|
|
26
|
+
|
|
27
|
+
## The core impedance mismatches (why this isn't just another HTTP backend)
|
|
28
|
+
|
|
29
|
+
1. **Content-addressing vs. mutable paths.** Writing to Swarm produces a *new* reference;
|
|
30
|
+
the old root is unchanged. Resolution: copy-on-write commit model. Writes are staged,
|
|
31
|
+
then a commit builds a new Mantaray manifest, uploads only changed nodes+data, and
|
|
32
|
+
yields a new root reference. Map this onto fsspec's existing `transaction` context
|
|
33
|
+
manager. Every commit is automatically a snapshot (free versioning/rollback).
|
|
34
|
+
2. **Stable identity.** A root hash that changes on every write is hostile to config files
|
|
35
|
+
and pipelines. Feeds provide a stable pointer: `bzzf://<owner-or-ens>/<topic>/path`
|
|
36
|
+
resolves through a feed to the latest root manifest; commit updates the feed. This is
|
|
37
|
+
the mutable filesystem; `bzz://` is immutable.
|
|
38
|
+
3. **Payment.** Writing costs money and needs a valid postage stamp (batch). Stamps live in
|
|
39
|
+
`storage_options`. A stamp manager checks usability/TTL *before* a commit and fails early
|
|
40
|
+
with a useful error, never a mid-write 402.
|
|
41
|
+
|
|
42
|
+
## The listing problem (CRITICAL architectural point)
|
|
43
|
+
|
|
44
|
+
Bee has **no server-side manifest-listing endpoint** today. To implement `ls`/`find`/`glob`
|
|
45
|
+
we must traverse the Mantaray trie **client-side**, fetching nodes via `/bytes` chunk by
|
|
46
|
+
chunk. This is the single biggest piece of real engineering in the project.
|
|
47
|
+
|
|
48
|
+
- A feature request for a server-side listing (+ mutation) endpoint has been filed upstream
|
|
49
|
+
(full text in `docs/bee-feature-request.md`), tracked as **ethersphere/bee#5535**
|
|
50
|
+
(https://github.com/ethersphere/bee/issues/5535). Open, no maintainer reply when
|
|
51
|
+
filed; a read-only listing implementation is being prototyped on a bee fork (Go —
|
|
52
|
+
new `pkg/api` handler over the existing `WalkNode`/`LookupNode` primitives, scoped to
|
|
53
|
+
the read path with mutation as a follow-up). It is NOT on this project's critical path.
|
|
54
|
+
- **This whole approach assumes CURRENT Bee features (client-side trie traversal).** It should
|
|
55
|
+
be revised depending on the status of issue #5535: if/when the server-side manifest listing
|
|
56
|
+
(and mutation) endpoint ships, the design becomes significantly more efficient — listing
|
|
57
|
+
collapses from O(trie nodes) round trips to O(pages), and the client-side Mantaray walk
|
|
58
|
+
becomes a fallback rather than the primary path. Check the issue's status at the start of
|
|
59
|
+
planning any listing/write work, and update this file and `docs/roadmap.md` accordingly.
|
|
60
|
+
- **Design for a dual read path with capability detection.** Probe for the server-side
|
|
61
|
+
endpoint (via Bee version from `/health`, or by trying it once and caching the result per
|
|
62
|
+
filesystem instance). If present, use it. If absent, fall back to client-side trie walking.
|
|
63
|
+
When the endpoint eventually ships, the speedup arrives with no swarmfs release needed.
|
|
64
|
+
- v0 ships on the client-side path so it works against **today's** network and public gateways.
|
|
65
|
+
|
|
66
|
+
## The two hard engineering artifacts
|
|
67
|
+
|
|
68
|
+
1. **A Python Mantaray codec** (`swarmfs/mantaray/`). Bee gives no "list manifest" endpoint,
|
|
69
|
+
so we parse and build the binary Mantaray trie ourselves. Needed for: listing (walk the
|
|
70
|
+
trie via `/bytes`), and writes (patch the trie so changing one file in a big collection
|
|
71
|
+
only re-uploads the affected path). Consider extracting as a standalone `mantaray-py`
|
|
72
|
+
package later — it's independently useful. Reference implementations to study:
|
|
73
|
+
`ethersphere/mantaray-js` and Bee's own `pkg/manifest/mantaray` (Go).
|
|
74
|
+
2. **The commit engine** (`swarmfs/commit.py`). Staging strategy (memory + local spool),
|
|
75
|
+
parallel chunk uploads with tags for progress, building/patching the manifest, the
|
|
76
|
+
feed-update step for `bzzf://`.
|
|
77
|
+
|
|
78
|
+
## v1 write semantics (decided, implemented)
|
|
79
|
+
|
|
80
|
+
- **Copy-on-write staged commits.** Writes stage on the filesystem instance; a commit
|
|
81
|
+
validates the stamp first (fail early, never a mid-write 402), uploads data blobs in
|
|
82
|
+
parallel, patches the Mantaray trie client-side (O(path depth) node re-uploads — proven
|
|
83
|
+
against the real-Bee fixture), and yields a new root. Old roots are untouched: every
|
|
84
|
+
commit is a snapshot.
|
|
85
|
+
- **Autocommit vs. transaction.** Outside a transaction every write op commits
|
|
86
|
+
immediately. Inside ``with fs.transaction:`` everything is one commit per manifest
|
|
87
|
+
lineage; rollback on exception discards staging having uploaded nothing.
|
|
88
|
+
- **Where does the new root go?** (old open decision — resolved: neither loudly nor
|
|
89
|
+
quietly, but *queryably*.) The instance keeps an old→new root map: reads through the
|
|
90
|
+
original URL see the latest committed state (read-your-writes), `fs.latest(ref)`
|
|
91
|
+
returns the current head, `fs.commit_log` the history. Fresh manifests start at the
|
|
92
|
+
pseudo-reference `bzz://new/...` (or `new-<suffix>` for several in one instance).
|
|
93
|
+
- **Lineage discipline.** Staging is keyed by each lineage's *origin* root and commits
|
|
94
|
+
are serialized per instance, so concurrent writers (zarr writes chunks concurrently)
|
|
95
|
+
extend one lineage instead of forking it. Content-addressing corner: committing
|
|
96
|
+
identical content yields an identical root — never record an identity mapping
|
|
97
|
+
(it makes head-resolution loop forever; found via xarray's double group-metadata write).
|
|
98
|
+
- **Metadata on write** (old open decision — resolved): emit bee-style `Content-Type`
|
|
99
|
+
(guessed from the filename unless given) + `Filename`, matching what bee's own
|
|
100
|
+
uploader produces (verified against the captured fixture).
|
|
101
|
+
- `mkdir`/`makedirs` are no-ops (directories are implicit in manifests). Write spool:
|
|
102
|
+
`tempfile.SpooledTemporaryFile`, 16 MiB memory threshold (old open decision — resolved).
|
|
103
|
+
- Removing a directory's last file prunes the empty intermediate nodes (deliberate,
|
|
104
|
+
small deviation from bee's Remove, which leaves empty nodes behind).
|
|
105
|
+
|
|
106
|
+
## Convenience surface & API tiers (decided, implemented)
|
|
107
|
+
|
|
108
|
+
- **`fs.upload(local_path) -> str` / `fs.download(rpath, lpath)`** are the
|
|
109
|
+
hello-world one-liners; the README leads with them (the data-stack story comes
|
|
110
|
+
second — nobody trusts the killer feature until the trivial round trip works).
|
|
111
|
+
`upload` embraces the Swarm-native shape: the destination address is the
|
|
112
|
+
*result* of a write, returned as the value. A single file is one direct
|
|
113
|
+
`POST /bzz` through `SwarmClient` — deliberately NOT routed through the
|
|
114
|
+
commit engine or fsspec's generic machinery (they add nothing for one file);
|
|
115
|
+
a directory reuses the commit engine as a fresh manifest. Both paths hit
|
|
116
|
+
`StampManager` first (fail early) and respect gateway policy via `_setup`.
|
|
117
|
+
`upload(lpath, rpath)` (rpath given) keeps fsspec's base-class alias-of-put
|
|
118
|
+
contract; `download` is an alias of `get`.
|
|
119
|
+
- **Generic `fs.put(local, "bzz://...")`** must never succeed in a way where the
|
|
120
|
+
caller can't recover the reference. A bare/invalid destination raises a
|
|
121
|
+
ValueError pointing at `fs.upload()` (and `bzz://new/…` + `fs.latest`). Put
|
|
122
|
+
into an existing manifest path works normally (stage + commit). The generic
|
|
123
|
+
`_get_file`/`_put_file` contract stays correct and tested — dask/rsync/
|
|
124
|
+
third-party code calls it without knowing it's Swarm.
|
|
125
|
+
- **Three-tier public API**: raw HTTP (documented curl example, no shame in it)
|
|
126
|
+
→ `swarmfs.SwarmClient` (exported; direct async Bee calls with the shared
|
|
127
|
+
endpoint resolution, no filesystem semantics) → `SwarmFileSystem`/fsspec.
|
|
128
|
+
The middle tier has a blocking twin, `SyncSwarmClient` — the sync methods
|
|
129
|
+
are generated from SwarmClient's coroutines (same signatures/docs, kept in
|
|
130
|
+
lockstep by a test) and run on fsspec's shared background loop, the same
|
|
131
|
+
trick fsspec uses for the fs object. Client-tier open items, deliberately
|
|
132
|
+
not done yet: `stamp="auto"` resolution at this tier (safe — delegate to
|
|
133
|
+
the same StampManager, explicit stamp skips resolution; just not needed
|
|
134
|
+
yet) and exporting `VerifyingReader` for verified reads over an untrusted
|
|
135
|
+
endpoint (gateway *refusal* stays fs-only by decision: SwarmClient
|
|
136
|
+
endpoints are always explicit, so the silent-fallback risk it guards
|
|
137
|
+
against doesn't exist at this tier).
|
|
138
|
+
Convenience methods reach straight down to `SwarmClient`, skipping the middle
|
|
139
|
+
layer when it adds nothing — but the fs object stays the single enforcement
|
|
140
|
+
point for stamp/gateway/verification policy. No swarmfs CLI: that's
|
|
141
|
+
swarm-cli's job (scope boundary, deliberate).
|
|
142
|
+
- **Exception taxonomy** (`swarmfs/exceptions.py`, exported from the package
|
|
143
|
+
root): `SwarmError(OSError)` is the base for everything node/network —
|
|
144
|
+
OSError so fsspec's and our own `except OSError` seams keep working.
|
|
145
|
+
`BeeAPIError(SwarmError)` carries `.status`/`.url`/`.detail`;
|
|
146
|
+
`BeePermissionError(BeeAPIError, PermissionError)` for 401/403 (gateway
|
|
147
|
+
trust-detection catches it as before); 402 raises `StampError` — one type
|
|
148
|
+
for "no usable stamp" whether caught locally by StampManager or as a node
|
|
149
|
+
402. 404 stays builtin `FileNotFoundError` (fsspec semantics depend on it).
|
|
150
|
+
`StampError` now lives in exceptions.py, re-exported from `swarmfs.stamps`.
|
|
151
|
+
|
|
152
|
+
## `modified()` (decided, implemented)
|
|
153
|
+
|
|
154
|
+
`AbstractFileSystem.modified()` raises `NotImplementedError` by default;
|
|
155
|
+
DuckDB's fsspec bridge calls it unconditionally, so `read_parquet` over a
|
|
156
|
+
registered swarmfs filesystem failed outright until this was overridden.
|
|
157
|
+
`SwarmFileSystem.modified(path)` checks the path exists (like `info`) and
|
|
158
|
+
returns a fixed constant (the epoch) — the honest answer, since `bzz://`
|
|
159
|
+
content is content-addressed and immutable at a fixed reference: there is no
|
|
160
|
+
real last-modified time to report, and a constant can never spuriously
|
|
161
|
+
invalidate a downstream cache. `bzzf://` mounts inherit this unchanged; it
|
|
162
|
+
does **not** reflect a feed's most recent update (the SOC payload's
|
|
163
|
+
timestamp is parsed in `feeds.py` but currently discarded) — a real
|
|
164
|
+
per-feed `modified()` is a reasonable future addition but wasn't in scope
|
|
165
|
+
for this fix.
|
|
166
|
+
|
|
167
|
+
## Base class and async
|
|
168
|
+
|
|
169
|
+
Subclass `fsspec.asyn.AsyncFileSystem` (the s3fs/gcsfs pattern) over `aiohttp`. fsspec
|
|
170
|
+
generates the sync interface automatically. Range requests: Bee supports HTTP Range on
|
|
171
|
+
downloads — implement `_fetch_range` so fsspec block caching / readahead work, which is what
|
|
172
|
+
makes Parquet predicate pushdown and zarr chunk reads viable.
|
|
173
|
+
|
|
174
|
+
## v2 feed semantics (decided, implemented)
|
|
175
|
+
|
|
176
|
+
- **Path model**: `bzzf://<owner>/<topic>/path` — owner is a 40-hex ethereum address
|
|
177
|
+
(0x-prefix tolerated), topic is a human string (keccak256'd, bee-js
|
|
178
|
+
`Topic.fromString` convention) or a raw 64-hex topic. ENS owners deferred.
|
|
179
|
+
- **Read** needs no keys: Bee's server-side sequence lookup (`GET /feeds`, headers only
|
|
180
|
+
via `Swarm-Only-Root-Chunk`) finds the current index; we fetch the SOC chunk at that
|
|
181
|
+
index ourselves and parse the payload — handling bee-js's `timestamp‖ref` format, a
|
|
182
|
+
bare ref, and the wrapped-root-chunk format (via our BMT hasher).
|
|
183
|
+
- **Write** reuses the v1 commit machinery unchanged — a feed is just another lineage
|
|
184
|
+
whose head advances — plus an `_after_commit` hook that publishes a client-side-signed
|
|
185
|
+
SOC feed update (bee-js `timestamp‖ref` format, same postage batch as the commit).
|
|
186
|
+
Requires `signer=<private key hex>` in storage_options and the optional `feeds` extra
|
|
187
|
+
(`eth-keys` + `eth-hash[pycryptodome]`; core deps stay lean). Missing/mismatched
|
|
188
|
+
signers fail at *staging* time, before anything uploads.
|
|
189
|
+
- **`swarmfs/bmt.py`**: BMT chunk addressing in pure Python — required for SOC signing
|
|
190
|
+
(the signature covers the wrapped chunk's address), validated against the real
|
|
191
|
+
references in the captured manifest fixture, and the primitive for the future opt-in
|
|
192
|
+
chunk-verification mode.
|
|
193
|
+
- **Freshness/concurrency**: feed resolution is TTL-cached per instance (`feed_ttl`,
|
|
194
|
+
default 15 s); own commits refresh it immediately; other writers' updates are adopted
|
|
195
|
+
when seen (roots this instance committed are never rolled back by a stale lookup).
|
|
196
|
+
Feeds are last-write-wins — documented, not papered over.
|
|
197
|
+
- **Listings stay in feed coordinates** (`<owner>/<topic>/…`), preserving the stable-URL
|
|
198
|
+
illusion instead of leaking resolved root hashes.
|
|
199
|
+
|
|
200
|
+
## Prior art: ipfsspec (study, don't copy wholesale)
|
|
201
|
+
|
|
202
|
+
`ipfsspec` (IPFS backend in the official fsspec org) is the closest existing analog and
|
|
203
|
+
confirms our core choices: it subclasses `fsspec.asyn.AsyncFileSystem`, implements
|
|
204
|
+
`_cat_file`/`_ls`/etc. over an HTTP gateway, and registers `ipfs://` via entry points —
|
|
205
|
+
exactly our pattern. Two instructive contrasts:
|
|
206
|
+
|
|
207
|
+
1. It has stayed read-only, partly because writing to IPFS is awkward. Feeds + postage
|
|
208
|
+
stamps give us a genuinely writable `bzzf://` — we can *exceed* the IPFS analog, not
|
|
209
|
+
just match it.
|
|
210
|
+
2. Its one big unfinished piece is UnixFS HAMT support (sharded large-directory listing)
|
|
211
|
+
— the direct analog of our Mantaray codec. This independently confirms that the
|
|
212
|
+
manifest/trie codec is the load-bearing, bug-prone part: tests first, never mock the
|
|
213
|
+
trie format.
|
|
214
|
+
|
|
215
|
+
Before designing the v1 commit engine, also look at `ipfspy` (Algovera) — rougher, but it
|
|
216
|
+
has a local-node write path. One ipfsspec pattern we deliberately do NOT adopt: public
|
|
217
|
+
gateway selection/fallback (see next section).
|
|
218
|
+
|
|
219
|
+
## Gateways, light nodes, and content verification (decided, implemented)
|
|
220
|
+
|
|
221
|
+
- **Endpoint resolution order**, consistent across the codebase (same shape as
|
|
222
|
+
ipfsspec's convention): explicit `storage_options` (`api_url`) → an injected client's
|
|
223
|
+
endpoint → `BEE_API_URL` environment variable → default `http://localhost:1633`.
|
|
224
|
+
- **Design stance: encourage running a light node, discourage gateways** — encoded in
|
|
225
|
+
the software. First contact (`_setup`, once per instance) pings `/health`: an
|
|
226
|
+
unreachable endpoint fails with an error pointing at light-node setup, never a silent
|
|
227
|
+
gateway fallback. Trust detection: localhost is trusted; elsewhere the node-owner API
|
|
228
|
+
(`/stamps`) is probed — blocked means "gateway", refused unless `allow_gateway=True`.
|
|
229
|
+
- **Content verification** (`swarmfs/join.py`): a verifying joiner walks the Swarm hash
|
|
230
|
+
tree over `/chunks`, BMT-checking every chunk against the reference it was fetched by;
|
|
231
|
+
range reads descend only the subtrees they need, so Parquet/zarr access stays viable.
|
|
232
|
+
Manifest walks verify too (the listing loader routes through the same reader), and
|
|
233
|
+
bzzf feed updates get full SOC verification (address + owner-signature recovery).
|
|
234
|
+
`verify=None` (default) auto-resolves: **on for gateways, off for a trusted node**;
|
|
235
|
+
either can be forced. Facts learned live: the BMT address covers the stored span
|
|
236
|
+
as-is (erasure-coding level bits included), and intermediate chunks carry parity refs
|
|
237
|
+
after the `ceil(span/unit)` data refs — traversal takes only the data refs. Bare-ref
|
|
238
|
+
reads (`/bzz` index-document resolution) are refused under verification — they resolve
|
|
239
|
+
server-side and cannot be checked.
|
|
240
|
+
|
|
241
|
+
## What falls out for free (validate these as acceptance demos)
|
|
242
|
+
|
|
243
|
+
- `fs.get_mapper("bzz://ref/store")` → MutableMapping → **zarr on Swarm**. This is the
|
|
244
|
+
flagship demo for the data audience.
|
|
245
|
+
- `simplecache::bzz://ref/big.parquet` → local caching via fsspec URL chaining, zero code.
|
|
246
|
+
- Entry-point registration → every fsspec consumer understands `bzz://` after `pip install`.
|
|
247
|
+
|
|
248
|
+
## Constraints / environment
|
|
249
|
+
|
|
250
|
+
- Assume a local Bee node at `http://localhost:1633` by default; configurable per the
|
|
251
|
+
resolution order above. Gateway reads (read-only, no stamp) may exist for the
|
|
252
|
+
"no node of my own" crowd, but only as an explicit opt-in — the answer we lead with is
|
|
253
|
+
"run a light node" (see the gateways section above).
|
|
254
|
+
- Target modern Python (3.11+ — floor raised from the original 3.10+ once CI showed
|
|
255
|
+
`zarr>=3`, a test dependency, has no release supporting 3.10; see Packaging & CI).
|
|
256
|
+
Keep runtime deps lean: `fsspec`, `aiohttp`. Everything else (numpy/zarr/pandas) is
|
|
257
|
+
test/dev-only and optional.
|
|
258
|
+
- Peter's context: comfortable with content-addressed tries over chunks (cf. his OntoDAG
|
|
259
|
+
`recordstore` work). Don't over-explain Swarm internals; do surface API-shape decisions.
|
|
260
|
+
|
|
261
|
+
## Packaging & CI (decided, implemented)
|
|
262
|
+
|
|
263
|
+
- **Version**: `0.1.0` (bumped from the placeholder `0.1.0.dev0` in both
|
|
264
|
+
`pyproject.toml` and `swarmfs/__init__.py` — keep these two in sync on every
|
|
265
|
+
bump). `.devN`/pre-release suffixes are excluded from `pip install` by
|
|
266
|
+
default; `0.1.0` with the existing "Alpha" classifier is the intended shape
|
|
267
|
+
for a first real release — the classifier signals maturity, the version
|
|
268
|
+
string doesn't need to.
|
|
269
|
+
- **CI**: `.github/workflows/tests.yml` runs the offline suite across Python
|
|
270
|
+
3.11–3.12 on push/PR (integration tests self-skip without
|
|
271
|
+
`SWARMFS_TEST_BEE`, so no live Bee node is needed in CI), plus a `package`
|
|
272
|
+
job that builds both artifacts, runs `twine check`, and asserts the sdist
|
|
273
|
+
contains `LICENSE` and never contains `.claude/` — a direct regression
|
|
274
|
+
guard for the packaging leak caught before the `0.1.0` release (see the
|
|
275
|
+
git history around the `LICENSE`/packaging-fixes commit).
|
|
276
|
+
- **Publish**: `.github/workflows/publish.yml` triggers on a published GitHub
|
|
277
|
+
Release, re-runs tests, builds, and publishes via PyPI trusted publishing
|
|
278
|
+
(OIDC — no stored API token). **Requires a one-time manual step only the
|
|
279
|
+
repo owner can do**: register `petfold/swarmfs`, workflow `publish.yml`,
|
|
280
|
+
environment `pypi` as a (pending) trusted publisher at
|
|
281
|
+
https://pypi.org/manage/account/publishing/ before the first release is
|
|
282
|
+
cut — until then the `publish` job will fail at the OIDC exchange step.
|
|
283
|
+
|
|
284
|
+
## Phase plan
|
|
285
|
+
|
|
286
|
+
See `docs/roadmap.md`. Short version:
|
|
287
|
+
- **v0** read-only `bzz://`: client + Mantaray parse + range reads. Enough for pandas/dask.
|
|
288
|
+
- **v1** stamps + immutable writes via the transactional commit engine.
|
|
289
|
+
- **v2** `bzzf://` feed-mounted mutability.
|
|
290
|
+
- **later** encrypted refs (128-hex), ACT, redundancy level as write kwarg, gateway fallback,
|
|
291
|
+
wire up the server-side listing endpoint when it lands.
|
|
292
|
+
|
|
293
|
+
## Working agreements for Claude Code
|
|
294
|
+
|
|
295
|
+
- Update this file and `docs/roadmap.md` when a decision changes. They outlive any chat.
|
|
296
|
+
- Tests first for the Mantaray codec — it's the load-bearing, bug-prone part. Build against
|
|
297
|
+
known fixtures (upload a small collection to a real Bee node, capture the reference, assert
|
|
298
|
+
the codec's parse matches). Don't mock away the trie format; that's where the bugs hide.
|
|
299
|
+
- Keep the capability-detection seam clean: listing/mutation go through an internal interface
|
|
300
|
+
with two implementations (client-side, server-side) so the server path drops in later.
|
|
301
|
+
- Prefer real integration tests against a local Bee over heavy mocking, but keep a fast unit
|
|
302
|
+
layer that runs without a node (fixture-based).
|
swarmfs-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Peter Foldiak
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
swarmfs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swarmfs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: fsspec backend for Ethereum Swarm (bzz://) via the Bee HTTP API
|
|
5
|
+
Project-URL: Homepage, https://github.com/petfold/swarmfs
|
|
6
|
+
Project-URL: Repository, https://github.com/petfold/swarmfs
|
|
7
|
+
Project-URL: Issues, https://github.com/petfold/swarmfs/issues
|
|
8
|
+
Author: Peter Foldiak
|
|
9
|
+
License: BSD-3-Clause
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: bee,bzz,ethereum,filesystem,fsspec,swarm
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: System :: Filesystems
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: aiohttp>=3.8
|
|
21
|
+
Requires-Dist: fsspec>=2023.6.0
|
|
22
|
+
Provides-Extra: feeds
|
|
23
|
+
Requires-Dist: eth-hash[pycryptodome]>=0.5; extra == 'feeds'
|
|
24
|
+
Requires-Dist: eth-keys>=0.4; extra == 'feeds'
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: dask[dataframe]; extra == 'test'
|
|
27
|
+
Requires-Dist: eth-hash[pycryptodome]>=0.5; extra == 'test'
|
|
28
|
+
Requires-Dist: eth-keys>=0.4; extra == 'test'
|
|
29
|
+
Requires-Dist: pandas; extra == 'test'
|
|
30
|
+
Requires-Dist: pyarrow; extra == 'test'
|
|
31
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
32
|
+
Requires-Dist: xarray; extra == 'test'
|
|
33
|
+
Requires-Dist: zarr>=3; extra == 'test'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# swarmfs
|
|
37
|
+
|
|
38
|
+
An [fsspec](https://filesystem-spec.readthedocs.io/) backend for
|
|
39
|
+
[Ethereum Swarm](https://docs.ethswarm.org/), talking to a
|
|
40
|
+
[Bee](https://github.com/ethersphere/bee) node (or public gateway) over its
|
|
41
|
+
HTTP API. Installing it makes Swarm a first-class storage backend for the
|
|
42
|
+
Python data ecosystem — pandas, dask, zarr, xarray, pyarrow, DuckDB — via
|
|
43
|
+
URLs like `bzz://<reference>/path/to/file.parquet`.
|
|
44
|
+
|
|
45
|
+
**Status: v2.** Read-only `bzz://` access, transactional copy-on-write
|
|
46
|
+
writes (postage stamps, every commit a snapshot), and mutable feed-backed
|
|
47
|
+
`bzzf://` mounts. See the [roadmap](docs/roadmap.md).
|
|
48
|
+
|
|
49
|
+
New to swarmfs? This README is a quick reference — the
|
|
50
|
+
**[User Guide](docs/USER_GUIDE.md)** walks through a worked example for every
|
|
51
|
+
library above (pandas, all three Dask collection types, Zarr, xarray,
|
|
52
|
+
PyArrow, DuckDB) and explains the content-addressing model in plain terms.
|
|
53
|
+
|
|
54
|
+
## Upload and download a file
|
|
55
|
+
|
|
56
|
+
You need a running [Bee light node](https://docs.ethswarm.org/docs/bee/installation/getting-started/)
|
|
57
|
+
(`http://localhost:1633` by default) and, for uploads, a usable
|
|
58
|
+
[postage stamp](https://docs.ethswarm.org/docs/develop/access-the-swarm/buy-a-stamp-batch):
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import fsspec
|
|
62
|
+
|
|
63
|
+
fs = fsspec.filesystem("bzz", stamp="auto")
|
|
64
|
+
|
|
65
|
+
ref = fs.upload("photo.jpg") # → "c0ffee…" (64 hex chars)
|
|
66
|
+
fs.download(f"bzz://{ref}/photo.jpg", "copy.jpg")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
On Swarm the address of new content is the *result* of a write, not its
|
|
70
|
+
input — `upload` returns the new reference, and that reference is permanent:
|
|
71
|
+
it names this exact content forever. Directories work the same way and come
|
|
72
|
+
back as a single reference for the whole tree:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
ref = fs.upload("dataset/")
|
|
76
|
+
fs.ls(f"bzz://{ref}")
|
|
77
|
+
fs.download(f"bzz://{ref}", "dataset-copy/", recursive=True)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`upload` accepts `content_type=` (otherwise guessed from the filename),
|
|
81
|
+
`encrypt=True` (single files; the returned 128-hex reference includes the
|
|
82
|
+
decryption key), and `redundancy=0–4` (erasure coding, default 2). The stamp
|
|
83
|
+
is validated before any byte moves, so a missing or expired stamp fails
|
|
84
|
+
immediately with an actionable error.
|
|
85
|
+
|
|
86
|
+
## The data ecosystem
|
|
87
|
+
|
|
88
|
+
The point of being an fsspec backend: everything that speaks fsspec now
|
|
89
|
+
speaks Swarm, with zero extra code.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import pandas as pd
|
|
93
|
+
|
|
94
|
+
df = pd.read_parquet("bzz://<64-hex-reference>/data.parquet")
|
|
95
|
+
|
|
96
|
+
# local caching via URL chaining
|
|
97
|
+
df = pd.read_parquet("simplecache::bzz://<reference>/big.parquet")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import fsspec
|
|
102
|
+
|
|
103
|
+
fs = fsspec.filesystem("bzz") # api_url=..., default $BEE_API_URL or localhost:1633
|
|
104
|
+
fs.ls("bzz://<reference>/") # client-side Mantaray trie walk
|
|
105
|
+
fs.find("bzz://<reference>/dataset/") # recursive listing (dask uses this)
|
|
106
|
+
fs.cat("bzz://<reference>/hello.txt")
|
|
107
|
+
|
|
108
|
+
with fs.open("bzz://<reference>/big.parquet", block_size=2**20) as f:
|
|
109
|
+
f.seek(-8, 2) # range requests: only the bytes you touch
|
|
110
|
+
f.read(8)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Same story for any other fsspec-based tool — Intake, DVC, Kedro, pyxet,
|
|
114
|
+
Hugging Face Datasets, petl, and more (see the [User Guide](docs/USER_GUIDE.md#also-works-with)).
|
|
115
|
+
|
|
116
|
+
## Transactional writes
|
|
117
|
+
|
|
118
|
+
For anything beyond a one-shot upload — building a dataset in place, changing
|
|
119
|
+
one file inside a large collection — writes are copy-on-write commits: each
|
|
120
|
+
commit patches the manifest trie client-side, re-uploads only what changed,
|
|
121
|
+
and yields a new root. Old roots are untouched, so every commit is a snapshot.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
fs = fsspec.filesystem("bzz", stamp="auto")
|
|
125
|
+
with fs.transaction:
|
|
126
|
+
fs.pipe_file("bzz://new/dataset/a.parquet", data_a)
|
|
127
|
+
fs.pipe_file("bzz://new/dataset/b.parquet", data_b)
|
|
128
|
+
root = fs.latest("new") # share this reference; it never changes
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Mutable feeds (`bzzf://`)
|
|
132
|
+
|
|
133
|
+
A feed gives you a stable URL whose contents you can update — the mutable
|
|
134
|
+
filesystem on top of immutable commits:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
ffs = fsspec.filesystem("bzzf", stamp="auto", signer="<private key hex>")
|
|
138
|
+
ffs.pipe_file(f"bzzf://{owner}/my-app/config.json", b'{"v": 2}')
|
|
139
|
+
# readers need no keys — and the URL never changes
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Which API should I use?
|
|
143
|
+
|
|
144
|
+
Three tiers, all backed by the same endpoint resolution
|
|
145
|
+
(`api_url=...` → `$BEE_API_URL` → `http://localhost:1633`):
|
|
146
|
+
|
|
147
|
+
- **`SwarmFileSystem` / fsspec URLs** — the default. Filesystem semantics,
|
|
148
|
+
transactions, verification, and the whole data ecosystem for free.
|
|
149
|
+
- **`swarmfs.SyncSwarmClient` / `swarmfs.SwarmClient`** — direct calls
|
|
150
|
+
against the Bee API (upload a blob, fetch bytes, post a feed update)
|
|
151
|
+
without filesystem semantics. `SyncSwarmClient` is the blocking twin for
|
|
152
|
+
plain scripts; `SwarmClient` is the same surface as coroutines for
|
|
153
|
+
asyncio code:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from swarmfs import SyncSwarmClient
|
|
157
|
+
|
|
158
|
+
with SyncSwarmClient() as client: # async? use SwarmClient + await
|
|
159
|
+
ref = client.bzz_post(open("photo.jpg", "rb"), stamp=batch_id)
|
|
160
|
+
data = client.bzz_get(ref, "photo.jpg")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **Raw HTTP** — the Bee API is plain HTTP; no library needed:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
curl -X POST -H "Swarm-Postage-Batch-Id: <batch>" \
|
|
167
|
+
--data-binary @photo.jpg http://localhost:1633/bzz?name=photo.jpg
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
What the library adds over this: stamp validation up front, chunk
|
|
171
|
+
verification, gateway policy, better errors — the edge cases.
|
|
172
|
+
|
|
173
|
+
## Nodes, gateways, verification
|
|
174
|
+
|
|
175
|
+
The recommended setup is a local light node — reads then come straight from
|
|
176
|
+
the network with nothing to trust in between. Pointing `api_url` at a public
|
|
177
|
+
gateway is discouraged and requires an explicit `allow_gateway=True` — on
|
|
178
|
+
that path swarmfs verifies every fetched chunk client-side against its BMT
|
|
179
|
+
address (a Swarm reference *is* the content hash), so even an untrusted
|
|
180
|
+
gateway can't tamper with what you read. Verification can also be forced
|
|
181
|
+
on/off with `verify=True/False`.
|
|
182
|
+
|
|
183
|
+
## How it works
|
|
184
|
+
|
|
185
|
+
Swarm has no server-side directory listing today, so `swarmfs` parses the
|
|
186
|
+
binary [Mantaray](https://github.com/ethersphere/bee/tree/master/pkg/manifest/mantaray)
|
|
187
|
+
manifest trie itself, fetching nodes on demand via `/bytes` (see
|
|
188
|
+
`swarmfs/mantaray/` — a self-contained pure-Python codec). File reads resolve
|
|
189
|
+
the path to its data reference once, then use HTTP range requests against
|
|
190
|
+
`/bytes`, which is what makes Parquet predicate pushdown and zarr chunk reads
|
|
191
|
+
viable. When Bee grows a server-side listing endpoint
|
|
192
|
+
([ethersphere/bee#5535](https://github.com/ethersphere/bee/issues/5535)) it
|
|
193
|
+
will slot in behind the existing capability seam with no API change.
|
|
194
|
+
|
|
195
|
+
## Compared to ipfsspec
|
|
196
|
+
|
|
197
|
+
[ipfsspec](https://github.com/fsspec/ipfsspec), the closest analog in the
|
|
198
|
+
fsspec ecosystem, is read-only by its own admission. Postage stamps make
|
|
199
|
+
paid writes tractable on Swarm, so swarmfs adds a transactional write path
|
|
200
|
+
and, via `bzzf://` feeds, a stable URL you can actually mutate — not just
|
|
201
|
+
read.
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
pip install -e ".[test]"
|
|
207
|
+
pytest # offline unit tests (no node needed)
|
|
208
|
+
SWARMFS_TEST_BEE=http://localhost:1633 \
|
|
209
|
+
SWARMFS_TEST_STAMP=<batch-id> pytest tests/test_integration.py
|
|
210
|
+
```
|