kinedb-client (0.0.0.dev1799+gca3bd1f6f170)
Installation
pip install --index-url https://git.kinedb.com/api/packages/kinedb/pypi/simple/ --extra-index-url https://pypi.org/simple kinedb-clientAbout 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.