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.
Files changed (44) hide show
  1. swarmfs-0.1.0/.github/workflows/publish.yml +36 -0
  2. swarmfs-0.1.0/.github/workflows/tests.yml +52 -0
  3. swarmfs-0.1.0/.gitignore +9 -0
  4. swarmfs-0.1.0/CLAUDE.md +302 -0
  5. swarmfs-0.1.0/LICENSE +28 -0
  6. swarmfs-0.1.0/PKG-INFO +210 -0
  7. swarmfs-0.1.0/README.md +175 -0
  8. swarmfs-0.1.0/docs/USER_GUIDE.md +484 -0
  9. swarmfs-0.1.0/docs/bee-feature-request.md +137 -0
  10. swarmfs-0.1.0/docs/roadmap.md +141 -0
  11. swarmfs-0.1.0/pyproject.toml +57 -0
  12. swarmfs-0.1.0/swarmfs/__init__.py +27 -0
  13. swarmfs-0.1.0/swarmfs/_client.py +428 -0
  14. swarmfs-0.1.0/swarmfs/_listing.py +100 -0
  15. swarmfs-0.1.0/swarmfs/bmt.py +54 -0
  16. swarmfs-0.1.0/swarmfs/commit.py +134 -0
  17. swarmfs-0.1.0/swarmfs/core.py +920 -0
  18. swarmfs-0.1.0/swarmfs/exceptions.py +35 -0
  19. swarmfs-0.1.0/swarmfs/feedfs.py +182 -0
  20. swarmfs-0.1.0/swarmfs/feeds.py +200 -0
  21. swarmfs-0.1.0/swarmfs/join.py +164 -0
  22. swarmfs-0.1.0/swarmfs/mantaray/__init__.py +40 -0
  23. swarmfs-0.1.0/swarmfs/mantaray/build.py +186 -0
  24. swarmfs-0.1.0/swarmfs/mantaray/node.py +261 -0
  25. swarmfs-0.1.0/swarmfs/mantaray/walk.py +180 -0
  26. swarmfs-0.1.0/swarmfs/stamps.py +183 -0
  27. swarmfs-0.1.0/tests/capture_fixture.py +107 -0
  28. swarmfs-0.1.0/tests/conftest.py +307 -0
  29. swarmfs-0.1.0/tests/fixtures/real_manifest.json +79 -0
  30. swarmfs-0.1.0/tests/test_bmt.py +53 -0
  31. swarmfs-0.1.0/tests/test_codec.py +181 -0
  32. swarmfs-0.1.0/tests/test_exceptions.py +51 -0
  33. swarmfs-0.1.0/tests/test_feedfs.py +161 -0
  34. swarmfs-0.1.0/tests/test_fs.py +240 -0
  35. swarmfs-0.1.0/tests/test_integration.py +346 -0
  36. swarmfs-0.1.0/tests/test_patch.py +189 -0
  37. swarmfs-0.1.0/tests/test_real_fixture.py +79 -0
  38. swarmfs-0.1.0/tests/test_stamps.py +173 -0
  39. swarmfs-0.1.0/tests/test_sync_client.py +56 -0
  40. swarmfs-0.1.0/tests/test_upload_download.py +129 -0
  41. swarmfs-0.1.0/tests/test_verify.py +252 -0
  42. swarmfs-0.1.0/tests/test_walk.py +144 -0
  43. swarmfs-0.1.0/tests/test_write_fs.py +226 -0
  44. 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/
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .coverage
9
+ .claude/
@@ -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
+ ```