KineDB Labs

kinedb-client (0.0.0.dev1795+g9fdbe1e5326f)

Published 2026-09-11 10:06:18 +00:00 by skadefro

Installation

pip install --index-url https://git.kinedb.com/api/packages/kinedb/pypi/simple/ --extra-index-url https://pypi.org/simple kinedb-client

About this package

The direct client of kinedb. JSON over HTTP, binary TLV over WebSocket.

kinedb-client

The direct client of kinedb: JSON over HTTP, binary TLV over a WebSocket. It speaks to a running kinedb-server.

It is the Python twin of @kinedb/client. The wire is the same, the API surface is the same, and the names are the same in snake case.

The two shapes of every kinedb SDK

kinedb ships each SDK twice, in every language. The names are fixed, and the API surface is identical, so an application changes ONE import line and nothing else.

role Rust crate npm distribution (pip install) import status
wire types + TLV/frame codec kinedb-protocol @kinedb/protocol kinedb-protocol kinedb.protocol not split out yet
direct client (remote server) (none yet) @kinedb/client kinedb-client kinedb.client this package
embedded engine + sync kinedb @kinedb/embedded kinedb-embedded kinedb.embedded reserved, not built
from kinedb.client import KineDB      # talks to a remote kinedb-server
from kinedb.embedded import KineDB    # talks to a local engine that syncs

kinedb-embedded will bundle the kinedb engine. The SDK talks to that local embedded database, and the embedded database replicates to a real kinedb elsewhere. It does not exist yet. The name is reserved so that no application has to be rewritten when it lands.

Two names, one namespace

Python gives every package two names, and PyPI does not link them. The distribution name is what pip install takes, and it mirrors the crate name character for character. PEP 503 treats kinedb-client, kinedb_client and kinedb.client as one name, so any of the three installs this package; the hyphen is the form we write.

The import name lives under one namespace package, kinedb, which is the Python form of the @kinedb/ npm scope.

No kinedb distribution may ever ship kinedb/__init__.py. kinedb is a PEP 420 namespace package: a directory with NO __init__.py. A regular package there shadows the namespace and breaks every other kinedb-* install in the same environment. tests/test_namespace.py enforces the rule.

Install

The packages live on the forge, under the kinedb organisation: https://git.kinedb.com/kinedb/-/packages. There is no pypi.org copy.

pip:

pip install --index-url https://git.kinedb.com/api/packages/kinedb/pypi/simple kinedb-client

uv, in the pyproject.toml of your application:

[[tool.uv.index]]
name = "kinedb"
url = "https://git.kinedb.com/api/packages/kinedb/pypi/simple"
explicit = true

[tool.uv.sources]
kinedb-client = { index = "kinedb" }

Use the forge as an EXPLICIT index, never --extra-index-url alone. All four kinedb names are free on pypi.org, so a second index may answer first and hand your application a stranger's package.

The index needs a Gitea token when the organisation is private. pip takes it in the URL (https://<user>:<token>@git.kinedb.com/...); uv takes UV_INDEX_KINEDB_USERNAME and UV_INDEX_KINEDB_PASSWORD.

Versions are commits, not releases

kinedb has released no SDK, so no package carries a version number that promises compatibility. A build is named by the commit that produced it:

0.0.0.dev<N>+g<12-char sha>        N = git rev-list --count HEAD

0.0.0 and .dev both say that nothing is released. +g<sha> is the PEP 440 local version label, and it is the commit: kinedb-client==0.0.0.dev1234+g77df86eaede3 is the client of the image kinedb:77df86eaede3.

PyPI has no dist-tags, unlike npm. There is no latest to move and no g<sha> to install by. An unpinned install means "the highest version", and a hex sha does not order by time, so the ordering rides on <N>: the commit count only grows on main, so the newest merge is always the highest version. That is what latest does for the npm package.

Publishing: the forge publishes on every merge, like the image. By hand, from the repository root:

bun scripts/sdk-publish.ts sdk/python/client

It refuses a dirty tree, because a commit-named build must name a commit that actually holds the published bytes. The newest 10 versions are kept.

Use

import asyncio
from kinedb.client import KineDB, rows_as_objects

async def main():
    db = KineDB("http://localhost:4820")

    print(await db.health())
    print(await db.sql("SHOW DATABASES"))    # one statement over HTTP

    sock = await db.connect()                # a persistent WebSocket
    await sock.authenticate("root", "hunter2")
    rows = await sock.sql("SELECT * FROM users")
    print(rows_as_objects(rows))

    sub = await sock.watch("users", lambda notify: print(notify))
    await sub.cancel()
    sock.close()

asyncio.run(main())

base_url is required. Unlike the browser client, this one has no page to derive a URL from. ws_url is derived from base_url when it is not given: http becomes ws, https becomes wss, and /ws is appended to the path.

A result is a plain dict, tagged the way the server tags its own JSON: resp["type"] is rows, created, inserted, backoff, health and so on. A typed result set also carries resp["types"] and resp["schema_id"].

On integers. The JavaScript client hands back a BigInt above 2^53, because a JavaScript number cannot hold an integer exactly past that point. A Python int is unbounded, so that distinction has no Python half: an Int column decodes to a plain int and keeps every bit, always.

Credentials

The client holds no session. An application installs two hooks once, and every KineDB(...) in that application picks them up:

from kinedb.client import set_auth_hooks

set_auth_hooks(
    get_token=lambda: store.token,           # read on EVERY request, never cached
    on_unauthorized=lambda: store.logout(),  # runs on a 401, BEFORE the raise
)

Either hook may be a coroutine function; the client awaits the result.

One client can override them, which is what a program that talks to a second server with a different credential needs:

KineDB(url, get_token=lambda: other_token)

With neither, the client sends no bearer and bounces nobody. That is the right default for a consumer that never logs in. GET /health stays bare in every case, because it is open by design and a liveness probe has no credentials.

set_auth_hooks returns the hooks it replaced, so a test or a one-off task can put them back:

previous = set_auth_hooks(get_token=borrowed)
...
set_auth_hooks(**previous)

Retrying

Off by default: a server-flagged transient rejection raises at once, with err.retryable set so a caller can build its own loop.

db = KineDB(url, retry=True)
db = KineDB(url, retry={"max_retries": 3, "base_delay_ms": 20, "max_delay_ms": 500})

With it on, the client absorbs those rejections behind a random full-jitter wait, and honours the server's own retry_ms pacing hint on a backoff response. The CLIENT retries; the server never does.

No runtime dependencies

The package installs nothing else. The HTTP transport is urllib.request on a worker thread, and the WebSocket client is our own RFC 6455 codec over asyncio streams (kinedb.client.ws). An SDK that pulled websockets or httpx would force a version range on every application that installs it.

Pass http_request= or ws_connect= to KineDB(...) to drive it from a test with no server.

Test

cd sdk/python/client && uv run --with pytest pytest

pytest is the only development dependency. There is no pytest-asyncio: an async case runs under asyncio.run(...) through the async_test helper in tests/conftest.py.

A live test runs only when a server is named:

KINEDB_URL=http://127.0.0.1:4820 uv run --with pytest pytest tests/test_live.py -v

The wire format this package mirrors is the Rust one, crates/kinedb-protocol/src/{frame,tlv,codec,messages}.rs. That is the truth; this package follows it. The JavaScript client, sdk/js/client/src/index.js, is the validated reference implementation of the same wire.

Requirements

Requires Python: >=3.10