> Fetch the index of every Tonbo docs page as Markdown at https://tonbo.dev/llms.txt

# Connect from Python

Last updated 29 October 2026

Connect a Python program to a database once you have created one: open it by its URL with an API key, commit, and call `synced()` before anything outside the process acts.

`sqlite_ursula.open()`, from the [sqlite-ursula](https://pypi.org/project/sqlite-ursula/0.11.3.dev0/) package, returns a subclass of [`sqlite3.Connection`](https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection), so your SQL, and a library that takes a `sqlite3` connection, work as they do on a local file. Install the package as the [Python quickstart](https://tonbo.dev/docs/getting-started/quickstarts/sqlite-python.md#install) does, and put the database’s URL in `NOTES_URL`:

```shell
export NOTES_URL=$(tonbo database show notes --json | jq -r .url)
```

**app.py**

```python
import os

import sqlite_ursula

# Opens notes by its URL with your API key; db is a sqlite3.Connection.
db = sqlite_ursula.open(
    os.environ["NOTES_URL"],
    token=os.environ["TONBO_API_KEY"],
    dir="./notes-cache",
)
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 machine
db.synced()  # Tonbo now holds every commit made so far
print(db.execute("SELECT count(*) FROM notes").fetchone()[0], db.status()["commits"])
db.close()
```

```shell
TONBO_API_KEY=tbo_... python app.py
```

A database’s URL looks like `https://db.tonbo.dev/<account-id>/notes`: `tonbo database show`, the database’s page in the console and the Management API show it (see [Regions and URLs](https://tonbo.dev/docs/databases/manage.md#regions)).

## Requirements

- CPython 3.11 or later, the free-threaded builds included, in a process that keeps the database open while it works. A short-lived process works too, and rebuilds the local copy at each start (see [Short-lived processes](https://tonbo.dev/docs/databases/cold-starts.md#short-lived)). See [Where it runs](https://tonbo.dev/docs/databases.md#where) for the machines that fit.
- A Python whose [sqlite3](https://docs.python.org/3/library/sqlite3.html) module can load SQLite extensions and runs a recent SQLite, since the package runs inside it. The table below says which do.
- One of the platforms the package ships for (below). On any other, such as an Intel Mac, pip installs it without the part that runs inside SQLite, and `open()` fails.
- A local disk for the cache directory, the `dir` argument, not a network filesystem.
- `sqlite-ursula==0.11.3.dev0`, pinned exactly. Python uses the pre-release sqlite-ursula 0.11.3.dev0 until 0.11.3 is published to PyPI.

| Python | Runs the package |
| --- | --- |
| uv-managed CPython (uv python install), Homebrew, conda-forge | Yes |
| The python:3.x-alpine images (Alpine 3.23 or later), on x64 | Yes |
| The python:3.x-slim images, Debian’s and Ubuntu’s apt Python, RHEL and UBI, Amazon Linux, Colab | No: their SQLite is too old |
| The python.org macOS installer, Apple’s /usr/bin/python3, pyenv’s default build | No: built without loadable SQLite extensions |
| Windows | No: run it under WSL |

| Wheel | Platform |
| --- | --- |
| `macosx_11_0_arm64` | macOS 11 or later, Apple silicon |
| `manylinux_2_28_x86_64` | Linux x64, glibc 2.28 or later |
| `manylinux_2_28_aarch64` | Linux arm64, glibc 2.28 or later |
| `musllinux_1_2_x86_64` | Linux x64, musl (Alpine 3.23 or later) |

`python -m sqlite_ursula` checks the Python it runs in and prints `result: ok` or what to use instead. On a Python that cannot run the package, `open()` raises `UrsulaSetupError` with the kind `host_unsupported`, whose `fix` says the same. On a laptop, a [uv-managed CPython](https://docs.astral.sh/uv/guides/install-python/) is the usual answer. In Docker, switch the base image to Alpine:

**Dockerfile**

```text
-FROM python:3.13-slim
+FROM python:3.13-alpine
```

To keep a Debian base, on arm64, or with LangGraph, which does not install on Alpine, run a uv-managed CPython in the image instead, as the package’s [Docker notes](https://github.com/tonbo-io/ursula/blob/8423f6ebf065b370c9d915ad8b4a31432fa5a2aa/clients/sqlite-ursula-python/README.md#docker) show.

## The API key

Pass the key as `token`. A key that acts as you is sent to Tonbo as it is, with no exchange (see [Sending the key directly](https://tonbo.dev/docs/platform/api-keys.md#direct)), so a string is all `open()` needs, and it serves for as long as the key lives. The process holds one token for every database it opens: open each with the same key, or call `sqlite_ursula.set_token(key)` to replace it.

A key that acts as an Agent is never accepted directly: Tonbo refuses it with `401` `exchange_required`. Exchange it first at `POST https://tonbo.dev/api/iam/token` for an access token that lasts 10 minutes, and pass that token as `token` (see [The token exchange](https://tonbo.dev/docs/platform/api-keys.md#exchange)). The package does not renew it: before it expires, exchange the key again and pass the new token to `sqlite_ursula.set_token()`, or commits wait until you do.

## open()

The first `open()` of a database in a process catches its local copy up from Tonbo and makes this process the database’s writer. Its arguments, after the URL:

- `token`: the key, or an access token exchanged from it (above).
- `dir`: the cache directory, where the local copy lives. By default it is under your user cache directory.
- `timeout`: how long the open may take, 30 seconds by default. Past it, `open()` raises `UrsulaUnavailableError`, whose `kind` says what it last met, such as `unreachable` or `unauthorized`. Ctrl-C cancels it too.
- `read_only=True`: read without becoming the writer. Writes fail with `SQLITE_READONLY`, and the process writing the database is left alone.
- `busy_timeout`: how long a statement waits for another connection’s lock, 5 seconds by default. Every other keyword goes to `sqlite3.connect`, and `check_same_thread` is `False` unless you pass it.

Keep the default transaction control, `isolation_level=None` or `autocommit=True`: `autocommit=False` is refused, since it would hide a refused commit. `db.close()` closes the connection; once the last one closes, the commits left keep replicating in the background. The package’s [README](https://github.com/tonbo-io/ursula/blob/8423f6ebf065b370c9d915ad8b4a31432fa5a2aa/clients/sqlite-ursula-python/README.md) lists every argument.

## synced()

A commit returns once it is on this machine. `db.synced()` returns once Tonbo holds every commit the process made on the database before the call. Call it before anything outside the process acts on what you wrote or read, such as a response or a queue acknowledgement:

```python
db.execute("INSERT INTO notes(body) VALUES (?)", (body,))
db.commit()
db.synced()  # returns once Tonbo holds the commit
send(reply)
```

It does not raise for a network failure: it waits until Tonbo is reachable again. `db.synced(timeout=5)` raises `TimeoutError` after 5 seconds while the commits keep replicating. `sqlite_ursula.gate(db)` does the same wait for a block of work: leaving the block waits for a `synced()` made after it ended, on every database you name, before the result or the block’s error goes on.

```python
with sqlite_ursula.gate(db):
    reply = handle(request)  # commits through db
send(reply)  # runs once Tonbo holds every commit the block made
```

See [When a commit is durable](https://tonbo.dev/docs/databases/how-it-works.md#commit). To make a call to another service once across crashes and second instances, see [Run side effects once](https://tonbo.dev/docs/databases/side-effects.md), and for the other rules, [Rules for databases](https://tonbo.dev/docs/databases/rules.md).

## asyncio

`aopen()` takes the arguments of `open()`, and `await db.asynced()` is `synced()`. Both wait on the event loop instead of blocking it, and `async with sqlite_ursula.agate(db)` is the gate. Statements still run on the calling thread, as they do with `sqlite3`.

```python
async def main() -> None:
    db = await sqlite_ursula.aopen(
        os.environ["NOTES_URL"],
        token=os.environ["TONBO_API_KEY"],
        dir="./notes-cache",
    )
    db.execute("INSERT INTO notes(body) VALUES (?)", ("hello",))
    db.commit()
    await db.asynced()  # waits on the event loop, without blocking it
    await send(reply)
```

## Connections and frameworks

Each later `open()` of the database in the process returns another connection to it with no network, and any thread may call it. A framework that opens its own `sqlite3` connection by path works on the same database: give it `db.path`, and keep `db` open while it works. Its commits replicate too, and `db.synced()` covers them:

```python
other = sqlite3.connect(db.path)  # replicates too, and db.synced() covers it
```

## Worker processes

Open databases after a fork, never before: replication does not survive `fork()`, and in a child forked after its parent opened a database, the package raises `UrsulaSetupError` with the kind `forked`. Open them in gunicorn’s `post_fork` hook or Celery’s `worker_process_init` signal, or use multiprocessing’s `spawn` or `forkserver` start method. Importing the package before the fork is fine.

## Open on another host

Stop the process on the old host, then open the same database on the new one, with the same key. The open rebuilds the local copy from Tonbo and makes the new process the writer. If the old process still runs, its statements and `synced()` raise `UrsulaResetError` from then on. Do not copy the cache directory to another host. See [Open on another host](https://tonbo.dev/docs/databases/connect.md#another-host) for Kubernetes.

## LangGraph

`sqlite_ursula.langgraph` holds LangGraph’s SQLite checkpointers and stores on a database: `UrsulaSaver`, `AsyncUrsulaSaver`, `UrsulaStore` and `AsyncUrsulaStore`. Install them with the `langgraph` extra:

```shell
uv pip install 'sqlite-ursula[langgraph]==0.11.3.dev0'
```

```python
from sqlite_ursula.langgraph import UrsulaSaver

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 Tonbo
```

Every checkpoint write waits for Tonbo, so when `invoke` returns, the run’s checkpoints are durable. A streamed chunk is not: call `db.synced()` before acting on one. The package’s [LangGraph](https://github.com/tonbo-io/ursula/blob/8423f6ebf065b370c9d915ad8b4a31432fa5a2aa/clients/sqlite-ursula-python/README.md#langgraph) section has the details.

## Errors

Every error the package raises has a `kind`, what failed, and an `action`, what to do: `retry`, `reopen`, `stop` or `fix`. Branch on `action`.

| Class | Raised by |
| --- | --- |
| `UrsulaWriteRefusedError` | A write or a commit Tonbo refused, with nothing committed, such as `backlog_full`: call `synced()`, then retry. |
| `UrsulaResetError` | Every statement after the database was reset, such as when another process took it over, and `synced()`. |
| `UrsulaUnavailableError` | `open()` past its `timeout`. |
| `UrsulaSetupError` | `open()` on a Python that cannot run the package, a wrong argument or URL, or a database that does not exist. |

`UrsulaWriteRefusedError` and `UrsulaResetError` are also `sqlite3.OperationalError`, so existing handlers keep catching them. [Troubleshooting](https://tonbo.dev/docs/databases/troubleshooting.md) says what each kind means and what to do about it.

## Status

`db.status()` returns the database’s state as a `dict`, with the fields [Status](https://tonbo.dev/docs/reference/nodejs/status.md) lists, such as `commits.pending`, commits on this machine that Tonbo does not hold yet, and `replication.condition`, why they wait.
