Skip to main content

kvgit 🔀

Git-style versioning for your data. Commits, branches, and merges -- backed by a dict-like MutableMapping.

Features Description
Dict interface MutableMapping[str, Any] -- reads and writes work like a dict
Commits Immutable, content-addressable snapshots with rollback
Branches Cheap forks with CAS-based optimistic concurrency
Tags Immutable names for commits; a tagged commit outlives every branch that reached it, in every kvgit version
Three-way merge Auto-merges non-overlapping changes; pluggable merge fns for conflicts
Pluggable backends In-memory, disk (diskcache), IndexedDB (Pyodide/browser), or bring your own KVStore
Chunked codecs Optional content-addressed dedup for large numpy arrays and pandas DataFrames -- equal buffers stored once across keys, commits, and branches

Install

pip install kvgit              # in-memory only
pip install kvgit[disk]        # adds disk backend via diskcache
pip install kvgit[scientific]  # adds chunked codecs for numpy / pandas
# IndexedDB backend is available automatically in Pyodide (browser) environments

Quick example

import kvgit

main = kvgit.store()

main["user"] = "alice"
main["score"] = 0
main.commit()

# Branch and diverge
dev = main.create_branch("dev")
dev["score"] = 999
dev.commit()

print(main["score"])  # 0   (main unchanged)
print(dev["score"])   # 999 (dev branch)

# Tag a commit by name -- immutable, and safe from garbage collection
main.tag("v1")
print(main.peek("score", tag="v1"))  # 0

Merging

Staged.merge() merges another head -- usually another branch's HEAD -- into the current branch: lowest common ancestor, three-way resolve, and a two-parent merge commit guarded on your own head:

dev["score"] = 500
dev.commit()

result = main.merge(dev.current_commit)  # True when merged
print(main["score"])  # 500 (fast-forward: main hadn't diverged)

Overlapping changes need a merge function per key, or a default_merge fallback. text_merge() resolves line-oriented text with git-style <<<<<<< markers (its ours_label / theirs_label arguments name them); anything it cannot mark -- binary, non-UTF-8, oversized -- raises CantMark, filed as an ordinary conflict:

from kvgit import text_merge

main["notes"] = "alpha\nbeta\n"
main.commit()

edits = main.create_branch("edits")
edits["notes"] = "alpha\nBETA\n"
edits.commit()

main["notes"] = "ALPHA\nbeta\n"
main.commit()

main.merge(edits.current_commit, default_merge=text_merge())
print(main["notes"])  # "ALPHA\nBETA\n" -- both edits kept

kvgit.merges.text is the same merge one level down, over raw bytes, for VersionedKV. A Staged decodes each side before calling a merge function, so register text_merge() there unless the key's values are already bytes.

Registrations resolve most-specific-first: a key takes its exact-key registration, else the longest registered prefix it starts with, else default_merge. Prefixes cover keys whose names are not known when the policy is set:

from kvgit import MergeChoice, text_merge

main.set_merge_prefix("runs/", MergeChoice.OURS)   # this branch owns runs/
main.set_merge_fn("runs/index", text_merge())      # except this one key

A registration holds either a merge function or a MergeChoice, and the two reach differently:

  • A merge function is consulted only where both sides changed a key. A change only one side made is applied as it always was.
  • A MergeChoice is a standing policy: it hands that side every key either side changed under it. So OURS also drops a key the other side added and keeps one the other side removed -- what a branch that owns a whole namespace needs. Nothing is read or decoded for those keys.

kvgit.merges.ours and kvgit.merges.theirs are the merge-function form, for keys where one side wins only when both sides collide. They keep that side's stored value as it stands rather than rewriting it, so the merge writes no new blob -- and if the chosen side removed the key, the merge removes it.

commit() and merge() take merge_fns= / merge_prefixes= / default_merge= for one call, layered over what is registered.

Keys both sides changed to the same bytes merge cleanly with no merge function, even though each side wrote its own copy.

A post_check(key, merged_bytes) predicate runs over every merge-produced value; returning False files that key as conflicted. on_conflict="abandon" leaves the branch untouched instead of raising. Merging refuses with ValueError when the staging buffer holds uncommitted changes -- commit or reset first.

Chunked codecs (numpy / pandas)

Large numpy arrays and pandas DataFrames -- and any sliced views of them -- can be stored once and shared across keys, commits, and branches:

import kvgit
import numpy as np

s = kvgit.store(codecs="scientific")

big = np.arange(1_000_000, dtype="float64")  # ~8 MB
s["full"] = big
s["head"] = big[:100_000]
s["tail"] = big[-100_000:]
s.commit()
# All three keys reference the same chunk on disk -- ~8 MB total, not ~24 MB.

Pandas DataFrames piggyback on the numpy codec via their underlying block ndarrays. See docs/quick-start.md and the API reference.

Part of the agex stack

kvgit provides versioned agent memory in agex with branching and rollback. It also works as a versioned backing store for monkeyfs virtual filesystems -- pass a Staged instance anywhere a dict is expected.

Development

uv sync --extra dev
uv run pytest

Documentation

See docs/ for detailed documentation:

  • Quick Start -- common patterns with runnable examples
  • API Reference -- full reference for all classes, methods, and types
  • Browser persistence (Pyodide) -- choosing between the IndexedDB and OPFS-mounted-disk backends, plus the syncfs flush requirement and recommended host-side patterns

Metadata

Release files for kvgit 0.3.9

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

Source distribution (sdist)

Source distribution for kvgit 0.3.9
File Size Uploaded
kvgit-0.3.9.tar.gz 108.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for kvgit 0.3.9
File Interpreter ABI Platform
kvgit-0.3.9-py3-none-any.whl Python 3 none any Details

Total release size: 201.9 kB

Release files / kvgit-0.3.9.tar.gz

Download URL kvgit-0.3.9.tar.gz
Size 108.6 kB
Tags Source
SHA-256 checksum
How to use checksums
8d02e5697294fa4d688e2d784ab78df27d8c3795ec768342d7953ea341da66f1
BLAKE2b-256 checksum
How to use checksums
9f0e8a3ac3abad835006fa754ac0d6dd481daef569fe57cde7b739b1433e9ce4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release files / kvgit-0.3.9-py3-none-any.whl

Download URL kvgit-0.3.9-py3-none-any.whl
Size 93.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fb37f23fcc8eccecd7f307735e3da86bbb3360cf1c658e49339c15d692cbedb6
BLAKE2b-256 checksum
How to use checksums
a526356549505dd4f5bdbef94beec2ccc670d2901c5d85be093c62a4a4609821
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.14

Release history Release notifications | RSS feed

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

This release

0.3.9 This release

2 release files

0.3.8

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

1 release file

0.1.3

2 release files

0.1.2

2 release files

0.1.0

2 release files

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