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.2.1.tar.gz (392.5 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.2.1-cp39-abi3-win_amd64.whl (5.8 MB view details)

Uploaded CPython 3.9+Windows x86-64

namidb-0.2.1-cp39-abi3-manylinux_2_28_x86_64.whl (6.4 MB view details)

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

namidb-0.2.1-cp39-abi3-manylinux_2_28_aarch64.whl (5.9 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.28+ ARM64

namidb-0.2.1-cp39-abi3-macosx_11_0_arm64.whl (5.4 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

File details

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

File metadata

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

File hashes

Hashes for namidb-0.2.1.tar.gz
Algorithm Hash digest
SHA256 fac217694b18fe43b9cf0e2341b890a0d1599bd00aeaca12e3807c14dc70d111
MD5 f07cd1f4ec377d942218cf389b6056c2
BLAKE2b-256 95e38f656d39cf43cdd9199fd6fafe44ee4ef01d9cf86638c6adbe1b0070ca36

See more details on using hashes here.

Provenance

The following attestation bundles were made for namidb-0.2.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.2.1-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: namidb-0.2.1-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 5.8 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.2.1-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 985d17bdb83cceb8ee199fc32bd4d16b5c1fb82d7efa17e7a61ec58dc4bee80b
MD5 d6e3837adb788cea18aff45394f2b4d7
BLAKE2b-256 1aefb1e687a82a7ea803b6aa1c2a0b770f105f42d31181b8c05581e5de74209f

See more details on using hashes here.

Provenance

The following attestation bundles were made for namidb-0.2.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.2.1-cp39-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for namidb-0.2.1-cp39-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 e1cdb2ba2a25c3c822ee4a2ba144111a6b4eabcbe56b0dacd0b6ad81ae85a3bc
MD5 b63c8c807b4326e7ab27a10d1f1b8a04
BLAKE2b-256 2e0653d023752b6379e6f4830799cd2accfc00f0ef7e001b498f6c7148f259d2

See more details on using hashes here.

Provenance

The following attestation bundles were made for namidb-0.2.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.2.1-cp39-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for namidb-0.2.1-cp39-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 db49209bba638bfe3978c8125a2330b9357a6ee8f1a2ada27d56efd6e4f6b3fa
MD5 e6fe420108de849d53aaae053577abdb
BLAKE2b-256 30ac3b1fb48026965146528fa37f2e8469c71a3c42ad011fb68c8b2ef03bca30

See more details on using hashes here.

Provenance

The following attestation bundles were made for namidb-0.2.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.2.1-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for namidb-0.2.1-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a8a32ddb187854162f5b2803db20893ee622aad3a08948eee2deb274476d9296
MD5 7ed6efa376f2f636a49c98f3acdb3fa0
BLAKE2b-256 ceb6394619427418401c4542a0d0d5e1ea3b84c9d3b26513c880717080f97f3b

See more details on using hashes here.

Provenance

The following attestation bundles were made for namidb-0.2.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