Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

tether

PyPI Python CI Coverage Docs License

Version control (fingerprints, pins, & forks) for heterogeneous datasets.

A dataset rarely lives in one place: an Icechunk repository, a directory of raw files, a Postgres database, an Iceberg table, and the code that produced them. tether tracks all of them from one git or jj repository. Each commit records every object's exact state -- and, where the system allows it, pins that state with a native ref so it stays readable, and forks a writable branch off it when you want to change things without touching main.

Think of it as DVC for branchable systems: like DVC it commits small manifests to your repository, but where DVC only fingerprints files, tether also pins and forks live systems.

Status: pre-release (0.1.0 betas). See the changelog and ROADMAP.md for what is tested against real services and what is still experimental. Vibe coded with Claude Fable 5.1. USE AT YOUR OWN RISK.

Install

pip install "tether-vcs[cli]"                 # core + CLI
pip install "tether-vcs[cli,icechunk,lance]"  # add the backends you use
pip install "tether-vcs[all]"                 # everything

The distribution is tether-vcs; the package you import and the command you run are both tether. Python 3.11 or newer; git and/or jj on PATH. Extras: objectstore (S3/GCS/Azure for file), icechunk, neon, iceberg, delta, lance, lakefs, ducklake, dolt, postgres, all.

Quickstart

tether lives inside a git or jj repository. Nothing below needs a cloud account: point it at an Icechunk repository and a directory you have on disk.

jj git init my-dataset && cd my-dataset      # or: git init my-dataset
tether init
tether add zarr/imaging --kind icechunk ../data/imaging.icechunk
tether add raw/plate1   --kind file     ../data/raw/plate1/

tether status                       # new / clean / modified per object
tether commit -m "Baseline"         # pin the Icechunk snapshot; record the directory; jj/git commit
  pinned zarr/imaging -> tether.4e9503fc.73e341e966c83d38
  recorded raw/plate1 (not recoverable)
committed b55326ca55a0

That commit id names the exact state of every object, forever: tether open zarr/imaging --rev b55326ca55a0 opens the pinned snapshot read-only, and TETHER_REV=b55326ca55a0 python report.py makes every repo.open() in a script do the same.

To change data without touching main, work on a bookmark:

tether new -b relabel               # one branch per system, created on first write
python relabel.py                   # writes through repo.open("zarr/imaging")
tether commit -m "Relabel plate1"   # pin the branch head; move the bookmark
tether promote                      # fast-forward main in every system, then move the main bookmark
tether verify --all-history         # every pin any commit ever named still resolves
from tether import Repo

repo = Repo.find(".")
h = repo.open("zarr/imaging")  # writable IcechunkHandle on this bookmark's branch
ro = repo.open("zarr/imaging", rev="main")  # read-only at main's pin

Every command that writes to a store takes --dry-run (and --plan FILE / --from-plan FILE) so the writes can be reviewed first. The Getting Started guide runs this walkthrough with full output; the user guide takes it from there.

How it works

  • A dataset is a git/jj repository with one small manifest per object. History, branching, workspaces, and sharing are the VCS's.
  • Commits hold pins. Each dataset commit records, per object, an exact state and -- where the system allows -- a pin: a native, GC-proof ref (an Icechunk tag, a protected Neon branch, a git tag) that holds that state. tether open KEY --rev C reads it back.
  • Bookmarks hold branches. The trunk bookmark (main) stands for every object's upstream branch; working on it writes there. Any other bookmark stands for one branch per system, tether.ws.<dataset>.<bookmark>, forked from the pins where it started and created on the first write.
  • commit, promote, pull are the three verbs: pin the bookmark's branch heads and move the bookmark; fast-forward (or merge) each system's upstream branch to the bookmark's and move main; read what upstream has now and commit it.
  • tether is never in the data path. open returns the system's native handle -- an Icechunk session, a Postgres URL, a DeltaTable -- and steps aside.

What the VCS cannot see -- what tether did to the stores -- is in tether's own operation log (tether ops, tether undo, tether repair).

What each backend can do

Every backend is fingerprinted; the rest depends on what the system offers.

Backend Recover Pin Fork Promote Merge Diff History
local files and directories; object-store prefixes (S3, GCS, Azure) ❌ ❌ ❌ ❌ ❌ ✅ ❌
single object-store objects with versioning enabled 🟡 ❌ ❌ ❌ ❌ ❌ ❌
git / jj repositories ✅ ✅ ✅ ✅ ✅ ✅ ✅
Icechunk repositories ✅ ✅ ✅ ✅ ❌ ✅ ✅
Lance datasets ✅ ✅ ✅ ❌ ❌ ✅ ✅
Delta Lake tables 🟡 ❌ ❌ ❌ ❌ ✅ ✅
Neon Postgres databases (experimental) ✅ ✅ ✅ ❌ ❌ ❌ ❌
Apache Iceberg tables (experimental) ✅ ✅ ✅ ✅ ❌ ✅ ✅
lakeFS repositories (experimental) ✅ ✅ ✅ ✅ ✅ ✅ ✅
DuckLake catalogs (experimental) 🟡 ❌ ❌ ❌ ❌ ✅ ✅
Dolt databases (experimental) ✅ ✅ ✅ ✅ ✅ ✅ ✅

Recover: can a committed state be opened again later? ✅ held by a pin; 🟡 re-openable by version id for as long as the system keeps that version; ❌ tether can only tell you it changed. Pin: a durable native ref created at commit time. Fork: a writable branch off a pin for tether new. Promote / Merge: fast-forward, or three-way merge, the upstream branch to the fork. Diff: tether diff --content describes what changed inside the object. History: tether log lists the object's own snapshots. Experimental backends have run against a fake of the service, not the service itself. The full matrix -- state fields, flags, per-backend caveats -- is in the backends guide.

jj or tether?

A tether command exists where an operation has two halves -- one in the VCS, one in the stores -- that must happen together: commit, new, pull, promote, restore, abandon, drop, forget-workspace, and undo / repair for what tether itself did. Everything that only touches files and history -- describe, squash, rebase, push -- is the VCS's, and tether notices what it needs to: a bookmark deleted, renamed, or moved by hand shows up in status with what to do. The concepts guide has the table.

Non-goals

  • Sitting in the data path: open hands back the system's native handle and steps aside.
  • Running between commands: no daemon, no watcher; states are compared when a command asks.
  • Reimplementing history, branching, or sharing: that is the VCS's job. tether ops / undo / repair cover only what tether did to the stores.
  • Cross-system transactions or a query layer.

Documentation

https://evanlyall.com/tether/ -- getting started, concepts, worked examples, CLI guide, backends, and the generated API and CLI reference.

Why this exists

Every existing tool versions a single layer:

Tool Scope Relationship to tether
DVC (lakeFS-owned) files/objects in git closest structural analog; no pin/fork of live systems
lakeFS, Quilt, Oxen, DataChain objects / files analog of our file objects; lakeFS is also a backend
Dolt, pgGit, Neon, Databricks Lakebase one database vendor-bound; no external-object pins; Dolt and Neon are backends
Nessie, Bauplan Iceberg catalog branching Iceberg-only
Icechunk, Lance, Delta, DuckLake one dataset / table / catalog we use them as backends
Yggdrasil cross-system snapshot / branch / merge over Git, ZFS, IPFS, Iceberg, lakeFS, Dolt, ... the closest conceptual sibling; a JVM library, not adoptable from Python
Dagster observable assets staleness detection analog of our snapshot/drift step; no branching

Nothing provides unified version control across files + Icechunk + Postgres + Iceberg with pinning and forking. So tether borrows jj's working-copy and operation-log ideas (new REV, stale-working-copy detection, undo), DVC's manifests-in-VCS layout, and Yggdrasil's observe-then-record shape: a workspace that watches independent systems and records their snapshots, rather than a store that holds the data.

Development

uv sync --all-extras
uv run ruff format --check . && uv run ruff check .
uv run ty check
uv run pytest

The publish / import tests start an ephemeral PostgreSQL cluster through pytest-postgresql; they need pg_ctl on PATH or a Homebrew / Debian install (brew install postgresql@16; GitHub's Ubuntu runners ship it) and skip otherwise. The use-cases story (tests/test_use_cases.py) regenerates the guide's command output with TETHER_UPDATE_DOCS=1.

License

Apache-2.0. See LICENSE.

Metadata

Release files for tether-vcs 0.1.0b2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tether-vcs 0.1.0b2
File Size Uploaded Provenance Actions
tether_vcs-0.1.0b2.tar.gz 238.8 kB Without provenance Details for tether_vcs-0.1.0b2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for tether-vcs 0.1.0b2
File Interpreter ABI Platform Actions
tether_vcs-0.1.0b2-py3-none-any.whl Python 3 none any Details for tether_vcs-0.1.0b2-py3-none-any.whl

Total release size: 508.2 kB

Release files / tether_vcs-0.1.0b2.tar.gz

Download URL tether_vcs-0.1.0b2.tar.gz
Size 238.8 kB
Tags Source
SHA-256 checksum
How to use checksums
024963bda2fda0bd2feed7cb3dcd49c594e40b32d7b0bfa604f77ccf8d8eced4
BLAKE2b-256 checksum
How to use checksums
ed11a3af7b1bd98f9261bd98121b2bbd9d9c59c4ec663b6f83eb1488bbb1fb5f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / tether_vcs-0.1.0b2-py3-none-any.whl

Download URL tether_vcs-0.1.0b2-py3-none-any.whl
Size 269.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e143c3fc0015e747bcfb695f2aea61f28e5259673baf7e2f6d83264e0912592e
BLAKE2b-256 checksum
How to use checksums
3d403afa8e7badd72f817a60189ac21d5c545e7186835fd3bcca9bf51150bde4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for tether-vcs 0.1.0b2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page