Metadata-Version: 2.5
Name: kinedb-client
Version: 0.0.0.dev1795+g9fdbe1e5326f
Summary: The direct client of kinedb. JSON over HTTP, binary TLV over WebSocket.
Project-URL: Homepage, https://git.kinedb.com/kinedb/kinedb
Project-URL: Source, https://git.kinedb.com/kinedb/kinedb
License: UNLICENSED
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# 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     |

```python
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.

<!-- #region install -->
## 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:

```toml
[[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`.

<!-- #endregion install -->

### 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.

<!-- #region use -->
## Use

```python
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:

```python
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:

```python
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:

```python
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.

```python
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.

<!-- #endregion use -->

## 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.
