open() and aopen()

Last updated 3 November 2026

View .md

open() opens a database by its URL and makes this process its writer; aopen() is the same call for asyncio. This page lists every parameter, what they return and what they raise.

A database’s URL comes from tonbo database show <name> --json, the console or the Management API. Connect from Python walks through a first open.

open()

def open(
    url: str,
    *,
    token: str | None = None,
    dir: str | os.PathLike[str] | None = None,
    read_only: bool = False,
    recreate: bool = False,
    max_pending_bytes: int | None = None,
    timeout: float = DEFAULT_OPEN_S,
    busy_timeout: float = DEFAULT_BUSY_S,
    **connect_kwargs: Any,
) -> UrsulaConnection: ...
import os

import sqlite_ursula

db = sqlite_ursula.open(
    os.environ["NOTES_URL"],
    token=os.environ["TONBO_API_KEY"],
    dir="./notes-cache",
    timeout=10,
)

The first open of a database in the process binds its local cache, catches it up from Tonbo and claims the database, so any earlier writer is fenced: from then on its statements raise fenced. Later opens of the database in the process return another connection on the same binding, with no network, and concurrent opens, from any thread, share one bind and its outcome. It blocks the calling thread for at most timeout.

Parameters

ParameterTypeDefaultWhat it does
urlstrRequiredThe database’s URL, such as https://…/acme/notes, as tonbo database show --json prints it. A string; anything else raises invalid_argument, and a string that is not a database’s URL invalid_url.
tokenstr | NoneNone: the token the process holds, else URSULA_VFS_TOKEN_FILE’sThe bearer token every request of the process carries from now on: a key that acts as you, or an access token exchanged from a key (see Credentials). It is applied with set_token() before the open, so it replaces the token of every database the process has open. A string only: for a token that changes, call set_token() with each new one.
dirstr | os.PathLike[str] | NoneNone: <user cache>/sqlite-ursula/0.8The cache directory, where the local copy lives; created when missing. Use a local disk, not a network filesystem. A database is open under one dir at a time in the process: an open under another waits for it to retire, and raises bound_elsewhere while it has connections open. The user cache is $XDG_CACHE_HOME, else ~/.cache on Linux and ~/Library/Caches on macOS.
read_onlyboolFalseOpen without becoming the writer, so the process writing the database is left alone; writes fail with SQLITE_READONLY. See Read-only opens. A boolean, and not with recreate.
recreateboolFalseWhen the database’s stream is missing and the cache holds data, create the stream again and move the cache into salvage/ (see Salvage), instead of raising not_found. Tonbo creates a database’s stream itself, so on Tonbo create the database first and leave this False.
max_pending_bytesint | NoneNone: 64 MiBHow many bytes of compressed commits may wait for Tonbo before writes raise backlog_full. A whole number of bytes, zero or more.
timeoutfloat30: secondsHow many seconds the open may take: a positive number, at most threading.TIMEOUT_MAX. Past it the open cancels its request and raises UrsulaUnavailableError: the last condition it met, or cancelled. Ctrl-C cancels it too.
busy_timeoutfloat5: secondssqlite3.connect’s timeout: how long a statement waits for another connection’s lock, zero or more seconds. The connections of a database share its files, hence the default.
**connect_kwargsAnyNoneEvery other keyword goes to sqlite3.connect, such as isolation_level or detect_types. check_same_thread is False unless given. A factory must be a subclass of UrsulaConnection. database, uri and autocommit=False raise invalid_argument.

Read-only opens

In a process that owns the database, read_only=True returns a read-only connection on the owner’s binding: it sees the owner’s latest commits, and its synced() waits as the owner’s does. In any other process it builds or catches up a reader cache of its own to the database as of the open, claims nothing and does not follow later commits, and its synced() returns at once. Close every read-only connection of the process and open again to read newer commits.

Credentials

token is a key that acts as you, sent as it is (see Sending the key directly), or an access token exchanged from a key (see The token exchange). A key that acts as an Agent must be exchanged first: sent as it is, the open raises unauthorized once its timeout passes (see an Agent’s key). The key must reach the database, and a read-only key opens it only with read_only=True, since claiming the database writes to it.

Returns

An UrsulaConnection, a sqlite3.Connection subclass, or the factory class given. Its status()["state"] is live, or provisional when Tonbo was unreachable and a trusted cache that claimed the database before served the open: reads come from the cache, commits wait, and the state turns live once Tonbo answers (see state).

Errors

Retries

An open is safe to call again: it never creates a database (unless recreate recreates a missing one), and each call either binds, joins a bind under way or returns a connection on the binding there is. Call it again after an error whose action is retry, with a backoff. Never call it in a loop after fenced: each open takes the database back from the other writer.

import time

import sqlite_ursula

for attempt in range(5):
    try:
        db = sqlite_ursula.open(url, token=key)
        break
    except sqlite_ursula.UrsulaError as error:
        if error.action != "retry":
            raise  # fix, reopen or stop: retrying the same call cannot help
        time.sleep(2**attempt)

aopen()

async def aopen(
    url: str,
    *,
    token: str | None = None,
    dir: str | os.PathLike[str] | None = None,
    read_only: bool = False,
    recreate: bool = False,
    max_pending_bytes: int | None = None,
    timeout: float = DEFAULT_OPEN_S,
    busy_timeout: float = DEFAULT_BUSY_S,
    **connect_kwargs: Any,
) -> UrsulaConnection: ...
import asyncio
import os

import sqlite_ursula


async def main() -> None:
    async with asyncio.timeout(10):  # cancels the open, too
        db = await sqlite_ursula.aopen(
            os.environ["NOTES_URL"], token=os.environ["TONBO_API_KEY"]
        )
    db.execute("INSERT INTO notes(body) VALUES (?)", ("hello",))
    db.commit()
    await db.asynced()

open() for asyncio: the same parameters, the same connection and the same errors, and it waits for the bind on the event loop instead of blocking it. Cancelling it, as asyncio.timeout around it does, cancels the bind and raises CancelledError, so no bind goes on to claim the database after the caller gave up. Statements on the connection it returns still run on the calling thread, as they do with sqlite3; wait for Tonbo with asynced() or agate().