namidb (Python bindings)
Python wrapper around the NamiDB storage and query engine. The heavy
lifting is Rust, bridged with pyo3 and built with
maturin.
Install
pip install namidb # released wheel, Python >= 3.9, abi3
pip install 'namidb[pandas]' # + DataFrame interop
pip install 'namidb[polars]' # + polars interop
Wheels go out for Linux (x86_64, aarch64), macOS (arm64), and Windows
(x86_64) from the python-wheels.yml workflow on every py-v* tag.
pyarrow >= 14 is a hard transitive dependency. Intel macOS falls back
to the sdist, which is slower to install but behaves the same at runtime.
Build from source
pip install maturin
cd crates/namidb-py
maturin develop --release --extras test
Once maturin develop finishes, namidb imports from any Python >= 3.9
environment:
import uuid
import namidb
client = namidb.Client("memory://acme")
alice = str(uuid.uuid7())
bob = str(uuid.uuid7())
client.upsert_node("Person", alice, {"name": "Alice", "age": 30})
client.upsert_node("Person", bob, {"name": "Bob"})
client.upsert_edge("KNOWS", alice, bob, {"since": 2020})
client.commit()
client.flush()
print(client.lookup_node("Person", alice))
# {'id': '...', 'label': 'Person', 'lsn': 1, 'schema_version': 0,
# 'properties': {'name': 'Alice', 'age': 30}}
print(client.scan_label("Person"))
print(client.out_edges("KNOWS", alice))
print(client.cache_stats())
Cypher queries
Client.cypher(query, params=None) runs a Cypher query against the
current namespace and hands back a QueryResult:
client.cypher("CREATE (a:Person {name: 'Alice', age: 30})")
client.cypher("CREATE (a:Person {name: 'Bob', age: 25})")
client.commit()
result = client.cypher(
"MATCH (p:Person) WHERE p.age > $min RETURN p.name AS name, p.age AS age",
params={"min": 26},
)
print(result.columns) # ['name', 'age']
print(len(result)) # 1
print(result.first()) # {'name': 'Alice', 'age': 30}
for row in result.rows():
print(row) # {'name': 'Alice', 'age': 30}
One thing to keep straight: Cypher writes (CREATE / SET / DELETE / MERGE
/ REMOVE) are durably committed (WAL append plus manifest CAS) before
cypher() returns, because the executor calls commit_batch() at the
end of every write plan. You still want to call client.flush() now and
then to push the memtable into L0 SSTs. That's different from the
upsert_node / upsert_edge / tombstone_* API, which stages mutations
and waits for an explicit client.commit().
Vector and full-text indexes
Published wheels include the same persistent Vamana ANN and BM25 index features as the server image. Their DDL works directly through the embedded client:
client.cypher(
"CREATE VECTOR INDEX doc_emb ON :Doc(embedding) "
"METRIC cosine DIMENSION 384"
)
client.cypher("CREATE FULLTEXT INDEX doc_text ON :Doc(title, body)")
client.cypher(
"CREATE INDEX IF NOT EXISTS FOR (d:Doc) ON (d.tenant_id)"
)
client.cypher(
"CREATE INDEX IF NOT EXISTS FOR (d:Doc) ON (d.vigente)"
)
Index bodies are built by an authoritative compaction. compact() prepares
the merge and the .vg / .ft bodies without holding the foreground writer
mutex, then briefly re-acquires it to install the new manifest:
client.flush()
report = client.compact()
print(report["applied"], report["l0_before"], report["l0_after"])
await client.acompact() is the coroutine equivalent. Until a body is
materialized, and whenever freshness cannot be proven, vector and BM25
queries automatically use their exact flat fallback.
search.vector can apply metadata eligibility before the returned k, so a
selective slice does not get truncated out of an unfiltered top-k:
hits = client.cypher(
"""
CALL search.vector({
label: 'Doc', property: 'embedding', query: $q, k: 10,
filter: { tenant_id: $tenant, vigente: true }
})
YIELD node, score
RETURN node, score
""",
params={"q": [0.1] * 384, "tenant": "laboral"},
).rows()
A scalar filter value means equality, a list means IN, and different keys
AND-combine. Indexed String/Boolean equality and IN clauses use native
vector-ordinal postings; unsupported or range clauses retain the exact
widening/flat-scan fallback. The index changes the cost, not the result.
Async API
The same surface is available as a coroutine through Client.acypher,
for asyncio / FastAPI / aiohttp:
import asyncio
import namidb
async def main() -> None:
client = namidb.Client("memory://acme")
await client.acypher("CREATE (p:Person {name: 'Alice'})")
client.commit()
result = await client.acypher(
"MATCH (p:Person {name: $name}) RETURN p.name AS name",
params={"name": "Alice"},
)
print(result.rows())
asyncio.run(main())
acypher rides the pyo3-async-runtimes tokio bridge. Every call runs
on the same multi-threaded tokio runtime that backs the synchronous API,
so mixing the two from one Client is fine.
Type mapping (Cypher and Python)
Both cypher parameters and QueryResult.rows() follow the same
mapping:
Cypher RuntimeValue |
Python type |
|---|---|
Null |
None |
Bool |
bool |
Integer |
int |
Float |
float |
String |
str |
Bytes |
bytes |
Vector(Vec<f32>) |
list[float] |
List |
list |
Map |
dict[str, ...] |
Date |
datetime.date |
DateTime (UTC, microseconds) |
datetime.datetime UTC |
Node(NodeValue) |
{"_kind": "node", "id", "label", "properties"} |
Rel(RelValue) |
{"_kind": "rel", "edge_type", "src", "dst", "properties"} |
Path |
list[Node|Rel] alternating |
bool is checked before int on purpose, so that Python True /
False don't quietly round-trip as Integer(1) / Integer(0).
Bulk inserts
Client.merge_nodes and Client.merge_edges batch a lot of writes
behind a single tokio-runtime plus mutex round-trip. They're the right
ingestion path once you have thousands of rows, since Cypher CREATE
parses, plans and executes once per call:
import uuid
import namidb
client = namidb.Client("memory://acme")
# Bulk insert: each row needs an "id" UUID string + arbitrary properties.
client.merge_nodes(
"Person",
[{"id": str(uuid.uuid4()), "name": f"p{i}", "age": 20 + i} for i in range(10_000)],
)
# Edges: each row needs "src" + "dst" UUIDs.
client.merge_edges(
"KNOWS",
[
{"src": "uuid-a", "dst": "uuid-b", "since": 2020},
{"src": "uuid-b", "dst": "uuid-c", "since": 2021},
],
)
client.commit() # WAL + manifest CAS
client.flush() # memtable -> L0 SSTs
merge_nodes / merge_edges stage into the current batch (same
lifecycle as upsert_*), so call client.commit() to make the
mutations durable.
Arrow / pandas / polars output
pyarrow >= 14 is a hard dependency. Every QueryResult can
materialise as a pyarrow.Table, and the pandas / polars conversions
just delegate to that.
result = client.cypher(
"MATCH (p:Person) RETURN p.name AS name, p.age AS age ORDER BY p.age DESC"
)
table = result.to_arrow() # pyarrow.Table
df = result.to_pandas() # pandas.DataFrame (needs pandas)
pl_df = result.to_polars() # polars.DataFrame (needs polars)
Column order follows the RETURN projection from the parsed plan, not
the runtime row's BTreeMap ordering, so RETURN p.name AS name, p.age AS age always gives you columns ["name", "age"] even when nothing
matches.
pandas and polars are optional extras:
pip install 'namidb[pandas]'
pip install 'namidb[polars]'
Calling to_polars() without the polars extra raises a clear
ImportError that points at the install command.
For label-wide scans you can skip the Cypher round-trip:
table = client.scan_label_arrow("Person")
# Columns: id, label, lsn, schema_version, then the union of property
# keys across the scanned views (missing keys filled with None).
Storage backends
| URI scheme | Backend | Status |
|---|---|---|
memory://<ns> |
object_store::memory::InMemory |
Stable. Ephemeral, single-process. |
file:///abs/dir?ns=<ns> (or file://./rel?ns=<ns>) |
NamiDB LocalFileObjectStore over LocalFileSystem; Create-only pointer CAS uses O_CREAT|O_EXCL |
Stable. |
s3://<bucket>[/<prefix>]?ns=<ns>... |
object_store::aws::AmazonS3 |
Stable. AWS S3, Cloudflare R2, MinIO, Tigris, LocalStack, any S3-compatible service. |
gs://<bucket>[/<prefix>]?ns=<ns> |
object_store::gcp::GoogleCloudStorage |
Stable. Auth via GOOGLE_APPLICATION_CREDENTIALS or ?service_account=.... |
az://<account>/<container>[/<prefix>]?ns=<ns> |
object_store::azure::MicrosoftAzure |
Stable. Auth via AZURE_STORAGE_* env vars; ?use_emulator=true for Azurite. |
Local filesystem
For development, single-machine deployments, and CI fixtures. Full
manifest CAS uses the same Create-only versioned pointer family as the cloud
backends, with local O_CREAT|O_EXCL providing PUT-if-absent. It passes the
same concurrency test suite as s3://.
import namidb
client = namidb.Client("file:///var/lib/namidb?ns=prod")
# or relative
client = namidb.Client("file://./data?ns=dev")
AWS S3
import namidb
client = namidb.Client(
"s3://my-bucket/data?ns=prod"
"®ion=us-west-2"
)
Credentials come from the standard AWS environment variables
(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN,
AWS_DEFAULT_REGION). A query-string region=... overrides the env.
Cloudflare R2
import namidb
client = namidb.Client(
"s3://my-bucket?ns=prod"
"&endpoint=https://<ACCOUNT_ID>.r2.cloudflarestorage.com"
"®ion=auto"
)
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY should hold the R2 API
token credentials.
Google Cloud Storage
import os
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/etc/gcs-key.json"
client = namidb.Client("gs://my-bucket/data?ns=prod")
Azure Blob Storage
import os
os.environ["AZURE_STORAGE_ACCOUNT_NAME"] = "myacct"
os.environ["AZURE_STORAGE_ACCESS_KEY"] = "..."
client = namidb.Client("az://myacct/mycontainer?ns=prod")
LocalStack (local persistent storage)
docker run -p 4566:4566 -e SERVICES=s3 localstack/localstack
aws --endpoint-url=http://localhost:4566 s3 mb s3://namidb-dev
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
client = namidb.Client(
"s3://namidb-dev?ns=local"
"&endpoint=http://localhost:4566"
"&allow_http=true"
"®ion=us-east-1"
)
You need allow_http=true because LocalStack doesn't serve TLS by
default.
Scope
- Five storage backends:
memory://,file://,s3://,gs://,az://. All four non-memory backends share the same Create-only versioned manifest-pointer protocol: eachp<N>.jsonis written once with PUT-if-absent (If-None-Match: *on object stores,O_CREAT|O_EXCLlocally), the highestNis current, and the same single-writer-per-namespace epoch fencing applies. - A synchronous Python API plus async coroutine APIs (
acypher,acompact). Under the hood every call drives a tokio runtime owned by theClient; the first call per process pays the bootstrap cost. - Persistent Vamana vector and BM25 full-text indexes, including their Cypher DDL and off-writer-lock compaction lifecycle.
- The same SST plus bloom cache the Rust read path uses
(
SstCache) is exposed throughclient.cache_stats(), so application dashboards can graph the hit rate. - Cypher coverage matches the Rust engine: LDBC SNB Interactive IC01
through IC12, with factorized execution toggled by
NAMIDB_FACTORIZE=1. See the projectREADMEfor the engine's surface and the RFCs indocs/rfc/for the design details.
Running the integration test (optional)
The pytest suite under tests/ ships a LocalStack round-trip test that
is @pytest.mark.skipif-guarded on the NAMIDB_TEST_LOCALSTACK_BUCKET
env var. To turn it on:
docker run -p 4566:4566 -e SERVICES=s3 localstack/localstack &
aws --endpoint-url=http://localhost:4566 s3 mb s3://namidb-it
export NAMIDB_TEST_LOCALSTACK_BUCKET=namidb-it
export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test
.venv/bin/pytest tests/test_uri.py::test_s3_localstack_round_trip -v
Releasing to PyPI
Run every command below from the repository root.
- Set the same
X.Y.Zin the rootCargo.toml(workspace.package.versionand every internal workspace dependency pin) and incrates/namidb-py/pyproject.toml. Every crate, includingnamidb-py, inherits its Rust version from the root workspace manifest. - Refresh
Cargo.lockand updateCHANGELOG.md. - Validate the complete release metadata before creating an immutable tag:
VERSION=X.Y.Z python scripts/check-release-metadata.py \ --tag "v$VERSION" --tag-kind engine python scripts/check-release-metadata.py \ --tag "py-v$VERSION" --tag-kind python
- Commit and push the release commit and wait for
ciandpython-wheelsto pass onmain. Before creating an immutable tag, manually dispatchrelease-binaries.yml(full OS/architecture release dry-run) anddocker-image.ymlwith an emptyversion(native amd64/arm64 build-check) on that same commit, and wait for both to pass. - Create both annotated tags on that exact commit:
git tag -a "v$VERSION" -m "NamiDB $VERSION" git tag -a "py-v$VERSION" -m "namidb Python $VERSION" test "$(git rev-parse "v$VERSION^{commit}")" = \ "$(git rev-parse "py-v$VERSION^{commit}")" git push --atomic origin "v$VERSION" "py-v$VERSION"
python-wheels.ymlbuilds 4 wheels (Linux x86_64/aarch64, macOS arm64, Windows x86_64) plus the sdist, smoke-tests one wheel on Python 3.9 and 3.13, then publishes to PyPI via OIDC trusted publishing (set up once per account at https://pypi.org/manage/account/publishing/).
The v* tag creates the GitHub Release, prebuilt binaries, and container
images. The py-v* tag publishes the Python distribution to PyPI. The
release workflows reject either tag unless it exactly matches the Cargo,
lockfile, Python version, dated changelog entry, and bundled license.
Metadata
Release files for namidb 2.6.19
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| namidb-2.6.19.tar.gz | 1.8 MB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| namidb-2.6.19-cp39-abi3-win_amd64.whl | CPython 3.9 | abi3 | Windows x86-64 | Details |
| namidb-2.6.19-cp39-abi3-manylinux_2_28_x86_64.whl | CPython 3.9 | abi3 | Linux glibc 2.28+ x86-64 | Details |
| namidb-2.6.19-cp39-abi3-manylinux_2_28_aarch64.whl | CPython 3.9 | abi3 | Linux glibc 2.28+ ARM64 | Details |
| namidb-2.6.19-cp39-abi3-macosx_11_0_arm64.whl | CPython 3.9 | abi3 | macOS 11.0+ ARM64 | Details |
Total release size: 41.0 MB
Release files / namidb-2.6.19.tar.gz
| Download URL | namidb-2.6.19.tar.gz |
|---|---|
| Size | 1.8 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0abd920f85378bc959336221837d75ab073ddb7ceccab8db0b3a031a077407d5
|
|
BLAKE2b-256 checksum How to use checksums |
52a6103948263a0e1e3cad0fe72d678b2aad48b447a220bbdd45b1943d08bce0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / namidb-2.6.19-cp39-abi3-win_amd64.whl
| Download URL | namidb-2.6.19-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 9.8 MB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
3cd8581387714433694bf1a61ea19240bcc14e18b397293dcbf4fd0ae802fa84
|
|
BLAKE2b-256 checksum How to use checksums |
b1ab80a44975f3b7f08539539928d77468f667c1c7a75ab87870d49a1b4c7853
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / namidb-2.6.19-cp39-abi3-manylinux_2_28_x86_64.whl
| Download URL | namidb-2.6.19-cp39-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 10.6 MB |
| Tags | CPython 3.9 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
3b5b3bb8ed82766b9848853fcbbd5b2fbf1f5bb052d24791f590a7fa5f595979
|
|
BLAKE2b-256 checksum How to use checksums |
9284bc0cb1096a083b374a89b3e19962fb3bfad6ff7af9e370f4472b7ea6966b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / namidb-2.6.19-cp39-abi3-manylinux_2_28_aarch64.whl
| Download URL | namidb-2.6.19-cp39-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 9.8 MB |
| Tags | CPython 3.9 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
2ac8ca37ec7f0c92d6dbf309defbcf71bbb78606876269a582eaf1991779673a
|
|
BLAKE2b-256 checksum How to use checksums |
f1adafe574c5a49b88c7847ae26c31219a9a7ec02baf06eb0adb7205627bf4e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency logRelease files / namidb-2.6.19-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | namidb-2.6.19-cp39-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 9.0 MB |
| Tags | CPython 3.9 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
94def3264e7e5e547cec3fc4509ba8fb9e58aa700ec239e89c09cedc2e7891fc
|
|
BLAKE2b-256 checksum How to use checksums |
9fe1d499f65a7402932dc7a3bd8dbee118059bdbf3fc1765839ecf1684253412
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.
Transparency log