This release is a pre-release and may not be stable for production use.
sqlite-ursula
SQLite databases replicated through Ursula streams, for the stdlib sqlite3 module of CPython 3.11 or later. The Python peer of the npm package @tonbo/sqlite-ursula.
import sqlite_ursula
db = sqlite_ursula.open("https://streams.example.com/acme/notes")
db.execute("CREATE TABLE IF NOT EXISTS notes(id INTEGER PRIMARY KEY, body TEXT)")
db.execute("INSERT INTO notes(body) VALUES (?)", ("hello",))
db.commit() # committed on this host
db.synced() # this database's commits are in the stream, and this process still owns it
send(reply)
The one rule: before anything outside the process acts on what you wrote or read (a response, a queue acknowledgement, a call to another service), call db.synced(), await db.asynced(), or wrap the work in with gate(db) or async with agate(db).
A commit is local. The extension appends it to a log it owns and replicates the log to the stream in the background, on threads of its own. synced() returns once the stream holds every commit the process made on that database before the call, and a round trip that started after the call found this process still the owner. Scripts call it before they exit.
Hosts
The extension loads into the sqlite3 module's own SQLite and becomes its default VFS, so every sqlite3.connect(path) of a bound database in the process replicates, a framework's included. That takes a Python whose sqlite3 loads extensions and runs a SQLite the extension supports. The extension's refusal of an older one names the version it needs.
| Python | hosts the extension |
|---|---|
uv-managed CPython (uv python install), Homebrew, conda-forge |
yes |
python:3.x-alpine (Alpine 3.23 or newer) |
yes, on x64 |
python:3.x-slim, Debian and Ubuntu apt, RHEL and UBI, Amazon Linux, Colab |
no: SQLite older than the extension supports |
the python.org macOS installer, Apple's /usr/bin/python3, pyenv's default build |
no: built without loadable extensions |
| Windows | no: use WSL |
On a Python that cannot host it, open() raises UrsulaSetupError with kind host_unsupported, whose fix says what to do for that reason: a Python that can host it, WSL on Windows, or SQLITE_URSULA_VFS where the install carries no library. python -m sqlite_ursula runs the same check and prints what it found.
Docker
Switch the base image in one line:
-FROM python:3.13-slim
+FROM python:3.13-alpine
recipes/Dockerfile is a complete example. To keep a Debian base, on arm64, or with the langgraph extra, which does not install on Alpine, run a uv-managed CPython instead, as recipes/Dockerfile.uv does.
On macOS, use uv or Homebrew.
open
open(url, *, token, dir, read_only, recreate, max_pending_bytes, timeout, busy_timeout, **connect_kwargs) returns an UrsulaConnection, a sqlite3.Connection subclass, so every library that takes one takes it.
- The first open of a stream in the process binds its cache in
dir(by default<user cache>/sqlite-ursula/0.8), catches it up and claims the stream: the last open that reaches the stream owns it. Later opens in the process return another connection on the same binding, with no network. Concurrent opens from any thread share one. tokenis the bearer token requests carry. One token per process:set_token(token)replaces it, andset_token(None)goes back toURSULA_VFS_TOKEN_FILE.- The open gives up after
timeoutseconds (30 by default) withUrsulaUnavailableError, whose kind is the last condition it met. Ctrl-C cancels it too. busy_timeoutissqlite3.connect'stimeout, 5 seconds by default, since a binding's connections share its files. The other keywords go tosqlite3.connect.check_same_threadisFalseunless given.autocommit=False(Python 3.12 or newer) is refused withinvalid_argument. In that mode the connection is always in a transaction: CPython's ownROLLBACKafter a refusedCOMMITwould hide the refusal, and a connection kept open would hold a read snapshot that stops checkpoints. Use the default transaction control,isolation_level=Noneorautocommit=True.read_only=Trueclaims nothing and never fences the owner, and writes fail withSQLITE_READONLY.db.close()closes the connection. Once a binding's last connection closes, replication drains in the background and the binding retires. Keepdbopen while a framework usesdb.path.
UrsulaConnection adds name (the stream's path, such as /acme/notes), path (the local database file, which frameworks open by path), synced(timeout=None), asynced(timeout=None) (asyncio), status() and on_reset(callback) / off_reset(callback).
db = sqlite_ursula.open(url)
other = sqlite3.connect(db.path) # a framework's own connection: it replicates too
synced() covers every connection of the process on this database (db.path), a framework's included, and no other: gate each database an output depends on. A negative or non-numeric timeout is invalid_argument. It raises TimeoutError after timeout seconds while the commits keep replicating, and UrsulaResetError on a reset. Network failures never raise from it: it waits. Ctrl-C interrupts it.
import_sqlite(url, file, *, token, timeout) makes a plain SQLite file with 4096-byte pages the first snapshot of a new stream, then open() it as any other.
asyncio
aopen() takes the arguments open() takes and returns the same UrsulaConnection, and db.asynced(timeout=None) is synced(). Both wait on the event loop instead of blocking it, and neither uses the loop's default executor.
db = await sqlite_ursula.aopen(url)
db.execute("INSERT INTO notes(body) VALUES (?)", ("hello",))
db.commit()
await db.asynced()
send(reply)
Cancelling asynced(), as asyncio.timeout around it does, ends the wait while the commits keep replicating. Cancelling aopen() cancels the bind, so it never goes on to claim the stream. Statements still run on the calling thread, as they do with sqlite3.
Gates
gate(*dbs, timeout=None) and agate(*dbs, timeout=None) wrap a block of work: leaving the block waits for a synced() call made after it ended, on every database given, before the result or the block's error goes on.
with sqlite_ursula.gate(db):
reply = handle(request)
send(reply)
async with sqlite_ursula.agate(db, other):
reply = await handle(request)
await send(reply)
- A block that only read waits too, since it may have read another connection's pending commit.
- An
UrsulaErrorthe block raises passes at once, as the block raised it, also when a library wrapped it (as__cause__, as SQLAlchemy's.orig, or in anExceptionGroup). So does a rawsqlite3error of a framework's own connection that the extension classifies, also wrapped. With one database, a bare raw error that the extension names a refused write or a reset raises as thatUrsulaError, which is asqlite3.OperationalErrortoo. - Every other error waits as a result does, then passes. Ctrl-C and cancellations pass at once.
- The wait covers every database at once. A reset during it raises the
UrsulaResetErrorinstead of the outcome, and a failure on one database ends the wait on the others. timeoutbounds the wait, not the block, and raisesTimeoutError.
Effects inside the block, such as a call to another service, are not gated: call db.synced() before them.
LangGraph
sqlite_ursula.langgraph holds LangGraph's SQLite checkpointers and stores on a replicated database. Install it with the langgraph extra, which pins langgraph-checkpoint-sqlite to the releases CI tests (3.0.3 up to 3.2):
pip install 'sqlite-ursula[langgraph]'
import sqlite_ursula
from sqlite_ursula.langgraph import UrsulaSaver
db = sqlite_ursula.open(url)
with UrsulaSaver(db) as saver:
graph = builder.compile(checkpointer=saver)
reply = graph.invoke(inputs, {"configurable": {"thread_id": "1"}})
send(reply) # every checkpoint of the run is in the stream
| class | LangGraph's | closed by |
|---|---|---|
UrsulaSaver(db) |
SqliteSaver |
close() or with |
await AsyncUrsulaSaver.open(db) |
AsyncSqliteSaver |
aclose() or async with |
UrsulaStore(db) |
SqliteStore |
close() or with |
await AsyncUrsulaStore.open(db) |
AsyncSqliteStore |
aclose() or async with |
- Each class opens a connection of its own on
db's binding, the async ones through aiosqlite, and closing it closes that connection. LangGraph writes checkpoints from threads of its own while the graph's nodes run, so the application keepsdbfor its own statements. The savers' connections keep SQLite's default transaction control, which makes each checkpoint, each task's writes and each deleted thread one transaction, whateverdb's is. The stores' connections open no transactions of their own, sinceSqliteStoreruns its ownBEGINandCOMMIT. Each connection is bound asdbis: read-only whendbis, withdb's busy timeout, and its errors are the package's. - Every write returns once
db.synced()(orasynced()) returned after it: a checkpoint, a task's writes, a deleted thread, a store put or delete, a store get or search that refreshes TTLs (refresh_on_read), a TTL sweep. LangGraph waits for a run's checkpoint writes beforeinvokereturns, so the return is gated in every durability mode. Withdurability="sync", each step also waits for the one before it, so what a node does rests on acknowledged checkpoints. - Reads do not wait, and neither does the setup that creates the tables, which LangGraph runs before the first read: the next write's wait covers it.
AsyncUrsulaStorewaits inaputandadelete, not in the task that runs its batches, so a read never waits behind another call's write. Itsabatchandbatch, which take operations, are LangGraph's own: calldb.asynced()after them. stream()andastream()yield a step's chunks before that step's checkpoint is acknowledged, and only the end of the iteration is gated. Calldb.synced()orawait db.asynced()before acting on a chunk.- A write refused with
backlog_fullcommitted nothing: it waits forsynced()and runs again. That is safe because each of these writes is one transaction. AnAsyncUrsulaStoreread batched with a refused write runs again at once. LangGraph's setup commits its statements one by one, and a store's migrations are not safe to run twice, so there each refused statement waits and runs again by itself. In the async adapters that wait blocks aiosqlite's thread until the stream acknowledges the commits pending before it, even when the caller of the setup is cancelled. - A reset raises the
UrsulaResetError, from a write or a read. Keepdbopen while the graph runs. sqlite-vec, whichlanggraph-checkpoint-sqliterequires, has no wheel for musl, so the extra does not install on Alpine. Use a Debian base with a uv-managed CPython, asrecipes/Dockerfile.uvdoes.
Errors
Four classes with kind and action (retry, reopen, stop, fix), whose cause (__cause__) is the SQLite error they replace. Branch on action. The extension says which failure is which (ursula_classify).
| class | raised by | kinds |
|---|---|---|
UrsulaWriteRefusedError |
a write or COMMIT, nothing committed, and a gate for a framework connection's refusal | backlog_full (retry after synced()), unsupported, too_large |
UrsulaResetError |
every statement after a reset, synced(), asynced(), a gate, on_reset callbacks |
fenced (stop), stale, rewound, deleted, local_damage, stream_state, internal |
UrsulaUnavailableError |
open() or aopen() past its timeout |
the last condition: unreachable, unavailable, rate_limited, unauthorized, payment_required, forbidden, or cancelled |
UrsulaSetupError |
open(), aopen(), import_sqlite(), the calls the package guards in a forked child (Forks) |
host_unsupported, forked, invalid_url, invalid_argument, not_found, in_use, bound_elsewhere, local_setup, and the rest of the extension's |
UrsulaWriteRefusedError and UrsulaResetError are also sqlite3.OperationalError, as the error they replace was, so existing except sqlite3.OperationalError clauses keep catching them, and sqlite_errorcode and sqlite_errorname are SQLite's. A transaction helper's ROLLBACK after one, or its COMMIT in a finally block, which finds no transaction since SQLite rolled it back, does not hide it.
A reset rejects the database's pending synced() calls first, then runs its on_reset callbacks once, on a thread of the package's.
Forks
Open databases after a fork, never before: the extension's replication does not survive fork(). In a child forked after its parent opened a database, open(), aopen(), import_sqlite() and set_token() raise forked, and so do the statements, transactions, synced(), asynced(), status() and on_reset() of an inherited UrsulaConnection, whose close() does nothing. Its other sqlite3.Connection methods (backup, blobopen, serialize and the like) and a framework's own connections on db.path are not the package's to guard: the extension fails their I/O on a bound file in the child. Importing the package before the fork is fine. Use gunicorn's post_fork hook, Celery's worker_process_init signal, or multiprocessing's spawn or forkserver start method.
The extension
The package loads the extension its wheel carries, or the one SQLITE_URSULA_VFS names, built from clients/sqlite-vfs. Wheels carry it for Linux x64 and arm64 with glibc 2.28 or newer, Linux x64 with musl, and macOS 11 or newer on arm64. Elsewhere the installer picks the wheel without a library, and open() raises host_unsupported unless SQLITE_URSULA_VFS names one. The design is docs/architecture/sqlite-vfs.md.
Metadata
Release files for sqlite-ursula 0.11.3.dev0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Actions Reset | |||
|---|---|---|---|---|
| sqlite_ursula-0.11.3.dev0-py3-none-musllinux_1_2_x86_64.whl | Python 3 | none | Linux musl 1.2+ x86-64 | Details for sqlite_ursula-0.11.3.dev0-py3-none-musllinux_1_2_x86_64.whl |
| sqlite_ursula-0.11.3.dev0-py3-none-manylinux_2_28_x86_64.whl | Python 3 | none | Linux glibc 2.28+ x86-64 | Details for sqlite_ursula-0.11.3.dev0-py3-none-manylinux_2_28_x86_64.whl |
| sqlite_ursula-0.11.3.dev0-py3-none-manylinux_2_28_aarch64.whl | Python 3 | none | Linux glibc 2.28+ ARM64 | Details for sqlite_ursula-0.11.3.dev0-py3-none-manylinux_2_28_aarch64.whl |
| sqlite_ursula-0.11.3.dev0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details for sqlite_ursula-0.11.3.dev0-py3-none-macosx_11_0_arm64.whl |
| sqlite_ursula-0.11.3.dev0-py3-none-any.whl | Python 3 | none | any | Details for sqlite_ursula-0.11.3.dev0-py3-none-any.whl |
Total release size: 9.7 MB
Release files / sqlite_ursula-0.11.3.dev0-py3-none-musllinux_1_2_x86_64.whl
| Download URL | sqlite_ursula-0.11.3.dev0-py3-none-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 2.6 MB |
| Tags | Linux musl 1.2+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
ff670dc2eabd04f671ba6cb37f783a3e2f45f3c3f6cb65b12319ff35ecd1eb08
|
|
BLAKE2b-256 checksum How to use checksums |
11ab81d1a1f1bddb32d8c7c75c6cca6baefbbae5e9dbe2c291cbc22f3c167798
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logRelease files / sqlite_ursula-0.11.3.dev0-py3-none-manylinux_2_28_x86_64.whl
| Download URL | sqlite_ursula-0.11.3.dev0-py3-none-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 2.5 MB |
| Tags | Linux glibc 2.28+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
6e567fc5c62c00e8de0e76fdbf07a6ea1114fd37778d974be4575efeb153a9df
|
|
BLAKE2b-256 checksum How to use checksums |
8e5ab96a854896b5e3109d5f51b669562108ee478393178752ab9f14a4abdebe
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logRelease files / sqlite_ursula-0.11.3.dev0-py3-none-manylinux_2_28_aarch64.whl
| Download URL | sqlite_ursula-0.11.3.dev0-py3-none-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 2.4 MB |
| Tags | Linux glibc 2.28+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
bfe4b822b71fe5d230cb6db281460683adc4191bd11cf6e9fe6c36c141165a4b
|
|
BLAKE2b-256 checksum How to use checksums |
dc9fe83f1ad837b0074afad7a2239b3ea11a8029d61cbe37fee610f29b5e0091
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logRelease files / sqlite_ursula-0.11.3.dev0-py3-none-macosx_11_0_arm64.whl
| Download URL | sqlite_ursula-0.11.3.dev0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 2.1 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
448562b04f30dcb40cef7b7dcaba6898667d1b1d2762bf00c1f074449df7b9d3
|
|
BLAKE2b-256 checksum How to use checksums |
1a3ca2cf68aa08d4d9d528ffa3d5db972c9b7006376f9b3dc69b65eba2023789
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logRelease files / sqlite_ursula-0.11.3.dev0-py3-none-any.whl
| Download URL | sqlite_ursula-0.11.3.dev0-py3-none-any.whl |
|---|---|
| Size | 45.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
68ab578cee707f3b7d9ffc9063ae9c922fcac3b3186cc2d541d617c361615dc0
|
|
BLAKE2b-256 checksum How to use checksums |
f090643b6d61c48f1e193b8a9f6a4418c5bcc0931a53a623847f3f407c7280e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logSecurity for sqlite-ursula 0.11.3.dev0
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.
Full provenance
All 5 files in this release have been uploaded with provenance metadata.
Attestation for sqlite_ursula-0.11.3.dev0-py3-none-musllinux_1_2_x86_64.whl
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.
Transparency logA 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.