n6k-duckdb
In-process bridge between DuckDB connections with permission-based access control.
Install
pip install n6k-duckdb
Usage
import duckdb
from n6k_server.bridge import bridge
cfg = {"allow_unsigned_extensions": "true"}
source = duckdb.connect(config=cfg)
source.sql("CREATE TABLE users(id INTEGER, name VARCHAR)")
source.sql("INSERT INTO users VALUES (1, 'alice'), (2, 'bob')")
target = duckdb.connect(config=cfg)
bridge(source, target, "app", permissions={"users": "readwrite"})
# Query through the bridge
target.sql("SELECT * FROM app.users").show()
# Insert through the bridge (requires 'readwrite')
target.sql("INSERT INTO app.users VALUES (3, 'charlie')")
# Update and delete also work with 'readwrite'
target.sql("UPDATE app.users SET name = 'Alice' WHERE id = 1")
target.sql("DELETE FROM app.users WHERE id = 2")
Permissions
| Permission | Allows |
|---|---|
'read' |
SELECT only |
'readwrite' |
SELECT, INSERT, UPDATE, DELETE |
Tables not listed in permissions are inaccessible.
How it works
bridge() automatically installs and loads the n6k_bridge DuckDB extension from the n6k extension repository. The extension creates an in-process bridge between two DuckDB database instances using a token-based handshake.
No network server is required — data flows directly between connections in the same process.
Server framework (optional extra)
This package also ships a reusable FastAPI framework for serving the n6k
protocol — install with pip install "n6k-duckdb[test-server]".
The protocol itself is not implemented in Python. It is served by the
n6k_server DuckDB extension (C++); this package accepts the WebSocket, decides
which catalogs to serve, hands the socket to the extension, and shuttles bytes.
It never decodes a frame.
Minimal server with a DuckDB backend:
import duckdb
from fastapi import FastAPI, WebSocket
from n6k_server.extension import load_n6k_server
from n6k_server.pump import WsReject
from n6k_server.server_fastapi.register import register
app = FastAPI()
def open_db(ws: WebSocket, **_):
catalog = ws.query_params.get("catalog")
if not catalog:
raise WsReject(4400, "missing catalog")
con = duckdb.connect(config={"allow_unsigned_extensions": "true"})
load_n6k_server(con)
con.execute(f'ATTACH \':memory:\' AS "{catalog}"')
# ... populate tables in `con` ...
return con
register(app, "", connect=open_db)
# Run: `uvicorn your_module:app --port 8099`
connect runs once per WebSocket and returns the connection to serve it from.
That is the entire contract — every catalog it ATTACHed is served, and the
connection's own default database (memory, for a plain duckdb.connect()) is
not.
con is surrendered to the serving call for the connection's lifetime and closed
on teardown, so nothing else may touch it. connect may be sync, async, or an
async generator — yield when you need a hook on both ends:
async def open_db(ws):
con = duckdb.connect()
...
live[id(ws)] = con.cursor() # a way into the data while it is being served
try:
yield con # served here; resumes when the socket drops
finally:
del live[id(ws)]
A worked example — auth, per-WS observability, /debug/* hooks — lives in
src/n6k_server/test_server/app.py. Run it with
python -m n6k_server.test_server --port 8099.
Auth
Two credentials arrive on two channels, and each is checked by whichever side can see it.
Transport — Authorization: Bearer or a query param, on the HTTP upgrade.
This process terminates the upgrade, so only it can read them. Check them in
connect and raise WsReject(4401, "unauthorized").
Handshake frame — where a browser must put its token, since a browser cannot
set a header on a WebSocket upgrade. Define a function named n6k_authorize on
the connection and the extension calls it for every handshake:
con.create_function(
"n6k_authorize",
lambda token, catalog: token == expected, # or raise, to say why
[duckdb.sqltype("VARCHAR"), duckdb.sqltype("VARCHAR")],
duckdb.sqltype("BOOLEAN"),
)
Registering it is the whole opt-in — there is no auth setting. False refuses the
session; raising sends your message to the client. Reading that token in Python
would mean parsing frames, which is the extension's job.
Notifying clients of stale catalog entries
When the server's catalog state changes outside of a client's own DDL (e.g. another writer dropped a view), peers' cached table listings go stale. Call
CALL n6k_serve_push_invalidate('<catalog>', 'main')
on any connection to the same database (a cursor() of the served connection
works) to send one FT_PUSH(OP_CATALOG_INVALIDATED) to every session serving
that catalog; each client drops its cached entries for the named schemas and
refetches on next access. It returns how many sessions it reached.
Nothing fires this automatically — the server decides when a catalog changed.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file n6k_duckdb-0.8.1.tar.gz.
File metadata
- Download URL: n6k_duckdb-0.8.1.tar.gz
- Upload date:
- Size: 79.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
746d8816e5892fe3d48765c3bf7b68c702c798f629a33f24e31f5d68a703d165
|
|
| MD5 |
25798a40fedb2fddb5c2b0fe48fb924b
|
|
| BLAKE2b-256 |
a4cac6775b02cb883b9609bcf72c25970a48a850f205b2f1f1b0cbc656b33fc4
|
Provenance
The following attestation bundles were made for n6k_duckdb-0.8.1.tar.gz:
Publisher:
release-python.yml on n6k-io/duckdb-extension
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
n6k_duckdb-0.8.1.tar.gz -
Subject digest:
746d8816e5892fe3d48765c3bf7b68c702c798f629a33f24e31f5d68a703d165 - Sigstore transparency entry: 2495238122
- Sigstore integration time:
-
Permalink:
n6k-io/duckdb-extension@0cdbcdef4839fc7548cb0dc62fdd06852077d03f -
Branch / Tag:
refs/tags/python/v0.8.1 - Owner: https://github.com/n6k-io
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@0cdbcdef4839fc7548cb0dc62fdd06852077d03f -
Trigger Event:
push
-
Statement type:
File details
Details for the file n6k_duckdb-0.8.1-py3-none-any.whl.
File metadata
- Download URL: n6k_duckdb-0.8.1-py3-none-any.whl
- Upload date:
- Size: 101.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e446f503caa5b41a32315fa33ff33da4b30835f0ea04566d430a897686b0430
|
|
| MD5 |
692f8cabc4613d58b9ec87401f0b3208
|
|
| BLAKE2b-256 |
3b23fb0d442627bebfc5249fd36b2964550690e12633d68617aa8a1330587380
|
Provenance
The following attestation bundles were made for n6k_duckdb-0.8.1-py3-none-any.whl:
Publisher:
release-python.yml on n6k-io/duckdb-extension
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
n6k_duckdb-0.8.1-py3-none-any.whl -
Subject digest:
4e446f503caa5b41a32315fa33ff33da4b30835f0ea04566d430a897686b0430 - Sigstore transparency entry: 2495238125
- Sigstore integration time:
-
Permalink:
n6k-io/duckdb-extension@0cdbcdef4839fc7548cb0dc62fdd06852077d03f -
Branch / Tag:
refs/tags/python/v0.8.1 - Owner: https://github.com/n6k-io
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@0cdbcdef4839fc7548cb0dc62fdd06852077d03f -
Trigger Event:
push
-
Statement type: