This release is a pre-release and may not be stable for production use.
tether
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.0betas). 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 Creads 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,pullare 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 movemain; read what upstream has now and commit it.- tether is never in the data path.
openreturns the system's native handle -- an Icechunk session, a Postgres URL, aDeltaTable-- 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:
openhands 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/repaircover 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.0b3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| tether_vcs-0.1.0b3.tar.gz | 239.1 kB | Without provenance | Details for tether_vcs-0.1.0b3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| tether_vcs-0.1.0b3-py3-none-any.whl | Python 3 | none | any | Details for tether_vcs-0.1.0b3-py3-none-any.whl |
Total release size: 508.9 kB
Release files / tether_vcs-0.1.0b3.tar.gz
| Download URL | tether_vcs-0.1.0b3.tar.gz |
|---|---|
| Size | 239.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6a30bb97625b0dfb37216f9e63c1125395a755284df65b494319ee615bd71db4
|
|
BLAKE2b-256 checksum How to use checksums |
024ff932ec824247038f9fca9272aa7e11b13fe713147f62996f43b2f10e2235
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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.0b3-py3-none-any.whl
| Download URL | tether_vcs-0.1.0b3-py3-none-any.whl |
|---|---|
| Size | 269.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c7daaa4cb1052afc3217bad60592977e50dacf50cb00da688ded5d782387bd36
|
|
BLAKE2b-256 checksum How to use checksums |
1b3237df6c4719913ee30d88834eaf3ec80925fa628998071ba2153e9c247b90
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","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.0b3
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.