snapdir for Python

The Python binding wraps the same Rust core (snapdir-apisnapdir-core) as the CLI, so the manifests and snapshot IDs it produces are bit-identical to snapdir and every other binding. Snapshot IDs are 64-character lowercase hex BLAKE3 digests. The surface is the same everywhere: synchronous id / manifest, plus async push / pull / fetch / diff / sync, in Python's idiom.

Install

The binding is a prebuilt abi3 wheel — the Rust core is compiled in, so there is nothing to build:

pip install snapdir

Requires Python 3.10 or newer. PyPI serves prebuilt manylinux and musllinux wheels (x86-64 + aarch64) and macOS wheels; the musllinux wheel runs on Alpine with no compiler. See Install for the platform matrix.

Usage

id and manifest compute content-addressed data without touching a store; the I/O operations (push, pull, fetch, diff, sync) are async. The store URI is always an argument, wrapped in StoreUri; snapshot IDs are wrapped in SnapshotId. Use file://$PWD/store for a local store and gs://bucket/prefix for Google Cloud.

import asyncio
import os

import snapdir
from snapdir import DiffOptions, SnapshotId, StoreUri


async def main() -> None:
    src = "./project"
    store = StoreUri(f"file://{os.getcwd()}/store")

    # Content-addressed id + manifest (no store I/O).
    snap_id = await snapdir.id(src)
    manifest = await snapdir.manifest(src)
    print(snap_id)  # 64-char lowercase hex, e.g. <snapshot-id>
    for entry in manifest.entries:
        print(entry.path_type, entry.path, entry.size)

    # Stage + upload; returns the snapshot id.
    pushed = await snapdir.push(src, store)

    # Download into the local cache, then materialise into a directory.
    await snapdir.fetch(SnapshotId(pushed), store)
    await snapdir.pull(SnapshotId(pushed), store, "./restored")

    # Diff two pinned snapshots. The diff ref convention is store@id
    # (the last @ splits the URI from the 64-hex id).
    opts = DiffOptions.from_refs(
        [f"{store}@{snap_id}"],
        [f"{store}@{pushed}"],
    )
    for change in await snapdir.diff(opts):
        print(change.status, change.path)  # A / D / M / =


asyncio.run(main())

Swap the file:// store for a gs://…, s3://…, or b2://… URI and the same calls publish to and restore from the cloud. For S3-compatible endpoints, only the endpoint and credentials come from the environment (SNAPDIR_S3_STORE_ENDPOINT_URL, AWS_*) — the store URI stays an argument.

Errors

Every failure raises a subclass of SnapdirError, which carries a stable .code string drawn from the 8-code taxonomy shared by all bindings. Match on the class or the code:

import snapdir
from snapdir import SnapshotId, StoreUri

try:
    await snapdir.pull(SnapshotId("<snapshot-id>"),
                       StoreUri("file:///srv/store"), "./restored")
except snapdir.SnapdirError as err:
    print(err.code)  # e.g. "HASH_MISMATCH"
Code Meaning
IO_ERROR Local filesystem read/write failure.
HASH_MISMATCH Fetched content did not match its checksum.
STORE_ERROR Store-level I/O or protocol error.
IN_FLUX Snapshot was still being written (concurrent modification).
CATALOG_ERROR Local catalog / cache integrity error.
INVALID_ID Malformed snapshot ID (not 64-hex).
INVALID_STORE Malformed or unsupported store URI.
CONFLICT Destination already holds conflicting content.

Platform support

Platform Supported
Linux glibc (x64 + arm64) yes
Linux musl / Alpine (x64 + arm64) yes
macOS (Intel + Apple Silicon) yes
Windows no

The Python wheel runs on Linux (glibc and Alpine/musl, x64 and arm64) and macOS with no libc caveat. Only the Java binding is glibc-only today (musl planned for 1.11.1). Windows is unsupported: snapdir is Unix-only.