Skip to main content

Cloud-native graph database on object storage — Python bindings

Project description

namidb (Python bindings)

Python wrapper for the NamiDB storage + query engine. Backed by Rust via 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 are published for Linux (x86_64, aarch64), macOS (arm64), and Windows (x86_64) via the python-wheels.yml workflow on every py-v* tag — pyarrow >= 14 is a hard transitive dependency. Intel macOS users fall back to the sdist (slower install, same runtime behaviour).

Build from source

pip install maturin
cd crates/namidb-py
maturin develop --release --extras test

Once maturin develop finishes, the namidb module is importable from any Python ≥ 3.9 environment:

import uuid
import namidb as tg

client = tg.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 returns 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}

Cypher writes (CREATE / SET / DELETE / MERGE / REMOVE) are durably committed (WAL append + manifest CAS) before cypher() returns — the executor calls commit_batch() internally at the end of every write plan. Call client.flush() periodically to push the memtable into L0 SSTs. This is different from the upsert_node / upsert_edge / tombstone_* API, which stages mutations and requires an explicit client.commit().

Async API

The same surface is available as a Python coroutine via Client.acypher for asyncio / FastAPI / aiohttp integration:

import asyncio
import namidb as tg


async def main() -> None:
    client = tg.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 is driven by 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 the same Client is safe.

Type mapping (Cypher ↔ 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 µs) 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 intentionally checked before int so that Python True / False do not silently round-trip as Integer(1) / Integer(0).

Bulk inserts

Client.merge_nodes and Client.merge_edges batch many writes under a single tokio-runtime + mutex round-trip. They are the right ingestion path when you have thousands of rows (Cypher CREATE parses + plans + executes per call):

import uuid
import namidb as tg

client = tg.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_*) — call client.commit() to make the mutations durable.

Arrow / pandas / polars output

pyarrow >= 14 ships as a hard dependency. Every QueryResult can materialise as a pyarrow.Table; pandas / polars conversions delegate to it.

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 yields columns ["name", "age"] even when zero rows match.

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 pointing at the install command.

For label-wide scans you can skip the Cypher round-trip entirely:

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 (wraps LocalFileSystem and adds manifest CAS via flock + atomic rename) 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 via per-namespace flock + atomic rename — passes the same concurrency test suite as s3://.

import namidb as tg

client = tg.Client("file:///var/lib/namidb?ns=prod")
# or relative
client = tg.Client("file://./data?ns=dev")

AWS S3

import namidb as tg

client = tg.Client(
    "s3://my-bucket/data?ns=prod"
    "&region=us-west-2"
)

Credentials are read from the standard AWS environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_DEFAULT_REGION). Query-string region=... overrides the env.

Cloudflare R2

import namidb as tg

client = tg.Client(
    "s3://my-bucket?ns=prod"
    "&endpoint=https://<ACCOUNT_ID>.r2.cloudflarestorage.com"
    "&region=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 = tg.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 = tg.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 = tg.Client(
    "s3://namidb-dev?ns=local"
    "&endpoint=http://localhost:4566"
    "&allow_http=true"
    "&region=us-east-1"
)

The allow_http=true flag is required because LocalStack does not serve TLS by default.

Scope (v0)

  • Six storage backends: memory://, file://, s3://, gs://, az://. All five non-memory backends share the same manifest CAS protocol (If-Match on object stores, flock + atomic rename on the filesystem) and the same single-writer-per-namespace epoch fencing.
  • Synchronous Python API + async coroutine API (acypher). Under the hood every call drives a tokio runtime owned by the Client; the first call you make per process pays the bootstrap cost.
  • The same SST + bloom cache used by the Rust read path (SstCache) is exposed via client.cache_stats() so application-level dashboards can graph hit rate.
  • Cypher coverage matches the Rust engine: LDBC SNB Interactive IC01 through IC12, factorized execution toggleable via NAMIDB_FACTORIZE=1. See the project README for the engine's surface and the RFCs in docs/rfc/ for 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 enable it:

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

  1. Bump version in crates/namidb-py/pyproject.toml and crates/namidb-py/Cargo.toml (they must match).
  2. Update CHANGELOG.md (or this README's release notes section).
  3. Commit, then tag and push:
    git tag py-v0.2.0
    git push origin py-v0.2.0
    
  4. python-wheels.yml builds 4 wheels (Linux x86_64/aarch64, macOS arm64, Windows x86_64) + sdist, smoke-tests one wheel on Python 3.9 and 3.13, then publishes to PyPI via OIDC trusted publishing (configured once per account at https://pypi.org/manage/account/publishing/).

py-v* tag prefix keeps Python releases separate from any future v* tags that mark engine / crate releases.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

namidb-0.4.1.tar.gz (415.9 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

namidb-0.4.1-cp39-abi3-win_amd64.whl (5.9 MB view details)

Uploaded CPython 3.9+Windows x86-64

namidb-0.4.1-cp39-abi3-manylinux_2_28_x86_64.whl (6.5 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.28+ x86-64

namidb-0.4.1-cp39-abi3-manylinux_2_28_aarch64.whl (6.0 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.28+ ARM64

namidb-0.4.1-cp39-abi3-macosx_11_0_arm64.whl (5.5 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

File details

Details for the file namidb-0.4.1.tar.gz.

File metadata

  • Download URL: namidb-0.4.1.tar.gz
  • Upload date:
  • Size: 415.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for namidb-0.4.1.tar.gz
Algorithm Hash digest
SHA256 3f6273faeeb9e9b8b41286ffb185b2070cada0b927b92dc895fe4fbe82c5417f
MD5 22e238ebaffb684746df0f3b45b77fd4
BLAKE2b-256 48e722d74feea1d3749b5c2026918c2f567402191b0b5bdd6b8373c429c909de

See more details on using hashes here.

Provenance

The following attestation bundles were made for namidb-0.4.1.tar.gz:

Publisher: python-wheels.yml on namidb/namidb

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file namidb-0.4.1-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: namidb-0.4.1-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 5.9 MB
  • Tags: CPython 3.9+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for namidb-0.4.1-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 14568b7dbf365ee37210af2fe73228cd2105b66560a206eb19010b4637c07ca6
MD5 ce476960b3c0830002be5906bc37a46c
BLAKE2b-256 1759071caeccf7b7153880882b780d6ed4cfd3fe2de6696e129685f003d12ad8

See more details on using hashes here.

Provenance

The following attestation bundles were made for namidb-0.4.1-cp39-abi3-win_amd64.whl:

Publisher: python-wheels.yml on namidb/namidb

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file namidb-0.4.1-cp39-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for namidb-0.4.1-cp39-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 b573e3ec784b415bf292594e1579217344b55e12277e6b0ed0c2ac52002f6c71
MD5 2684d6b591fa156a32e86fa4c838dddc
BLAKE2b-256 966940664980cf5b051505c2e650bde83f47758575c10b3ac2dd26376c21e0c7

See more details on using hashes here.

Provenance

The following attestation bundles were made for namidb-0.4.1-cp39-abi3-manylinux_2_28_x86_64.whl:

Publisher: python-wheels.yml on namidb/namidb

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file namidb-0.4.1-cp39-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for namidb-0.4.1-cp39-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 4925f775df20900d88bbac1ada7e12bc48c673a6c93385e5146042971bd75a6c
MD5 8062a21827aed5f630c9b8974d505b8f
BLAKE2b-256 ac903939eab47e9f337ef7f7019c4f42b8dea2dcf631f569dc9bcfcde6db14e2

See more details on using hashes here.

Provenance

The following attestation bundles were made for namidb-0.4.1-cp39-abi3-manylinux_2_28_aarch64.whl:

Publisher: python-wheels.yml on namidb/namidb

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file namidb-0.4.1-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for namidb-0.4.1-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 69aa6c96e731acde58fa7e7477b5f5919618d93d329c58fba512ae130f3d29fc
MD5 df66b35bad371ca844d13afdc0d9f7ce
BLAKE2b-256 16fa3288521242aca384ac65edd39dfb5d5a0857534fdbb06eed87fe47cfd458

See more details on using hashes here.

Provenance

The following attestation bundles were made for namidb-0.4.1-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: python-wheels.yml on namidb/namidb

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page