Skip to main content

Python SDK Guide

SDK version: 0.5.9 (pure-Python ctypes SDK) Native engine version: 2.0.3 (PyO3 module) License: Apache-2.0 (SDK); the core engine is AGPL-3.0-or-later Prerequisites: Python 3.9+

Complete guide to SochDB's Python SDK covering the embedded Database, transactions, key-value and SQL operations, namespaces and collections, vector search, priority queues, agent memory, temporal graph, semantic cache, and the native HNSW engine.


⚠️ Two packages both named sochdb​

There are two distinct Python packages that both publish under the PyPI name sochdb, with different versions and mostly disjoint APIs. Knowing which one you imported matters.

Pure-Python SDKNative PyO3 engine
Version0.5.92.0.3
Mechanismctypes FFI + thin gRPC/IPC clientsCompiled Rust extension (sochdb._native)
ScopeBroad embedded + server SDKFocused HNSW / BM25 / RRF / TableDatabase engine
Key classesDatabase, Namespace, Collection, Queue, AgentMemory, VectorIndex, StudioClientHnswIndex, BM25Index, RRFFusion, ThreeLaneHybridIndex, MultiShardHnswIndex, TableDatabase

This guide is primarily about the 0.5.9 pure-Python SDK — it is what you want for general application use. The Native HNSW engine section covers the 2.0.3 module, which exposes the low-level indexing primitives. Throughout this page each code block notes which package a class comes from when there is any ambiguity.

note

Both packages set __version__ and ship Database/Transaction classes, but those classes are not the same — the 0.5.9 Database is the high-level KV/SQL/namespace database; the 2.0.3 Database is a low-level native KV store. Install the one that matches the API you need.


Installation​

pip install sochdb
from sochdb import Database

with Database.open("./my_database") as db:
db.put(b"user:123", b'{"name":"Alice","age":30}')
value = db.get(b"user:123")
print(value.decode())
# {"name":"Alice","age":30}

Pre-built wheels target Linux (x86_64, aarch64), macOS (Intel, Apple Silicon), and Windows (x64). The package requires Python 3.9 or newer.


Quick Start​

Embedded mode (FFI)​

Direct FFI bindings to the native library. No server required — ideal for local development, notebooks, simple apps, and edge deployments.

from sochdb import Database

with Database.open("./mydb") as db:
db.put(b"key", b"value")
value = db.get(b"key")

Configuration presets​

Database.open() accepts an optional config dict. There is also a concurrent-access entry point for multi-process scenarios backed by MVCC.

from sochdb import Database

# Multi-process access to the same database
db = Database.open_concurrent("./shared_db")

For a quick in-memory database (uses a temporary directory), the high-level helpers are handy:

from sochdb import Client, open_collection

client = Client(path=":memory:") # temp dir under the hood
collection = open_collection("docs", dimension=384)

Server mode (gRPC / IPC)​

Thin clients connect to a running SochDB server. Embedded uses filesystem paths; gRPC uses host:port strings. There is no sochdb:// URI scheme.

from sochdb import SochDBClient  # alias: GrpcClient

client = SochDBClient("localhost:50051")
# Signature: put(key, value, namespace="default", ttl_seconds=0)
client.put(b"user:123", b'{"name": "Alice"}', namespace="my_namespace")
value = client.get(b"user:123", namespace="my_namespace")
client.close()

Transactions​

Transaction.commit() returns an HLC-backed monotonic commit timestamp (an int), not a log sequence number. SochDB uses Serializable Snapshot Isolation (SSI); a serialization conflict raises TransactionError on commit.

Automatic transactions​

with db.transaction() as txn:
txn.put(b"account:1:balance", b"1000")
txn.put(b"account:2:balance", b"500")
# Commits on clean exit, aborts on exception

Manual control​

from sochdb.errors import TransactionError

txn = db.begin_transaction() # alias for db.transaction()
try:
txn.put(b"key1", b"value1")
txn.put(b"key2", b"value2")

for key, value in txn.scan(b"key", b"key~"):
print(f"{key.decode()}: {value.decode()}")

commit_ts = txn.commit() # int: HLC commit timestamp
print(f"Committed at HLC {commit_ts}")
except TransactionError:
txn.abort() # idempotent
raise

You can also pass a function with db.with_transaction(fn). Per-transaction operations include put/get/delete, put_path/get_path/delete_path, exists, scan, scan_prefix, scan_prefix_unchecked, scan_batched, and execute(sql).

note

Isolation behaviour is exposed through the IsolationLevel enum and tracked client-side. A serialization conflict surfaces as TransactionConflictError (a subclass of TransactionError).


Key-Value Operations​

Basic operations​

db.put(b"key", b"value", ttl_seconds=0)   # ttl_seconds=0 = no expiry
value = db.get(b"key") # bytes | None
db.delete(b"key")

Path API​

db.put_path("users/alice/email", b"alice@example.com")
email = db.get_path("users/alice/email")
db.delete_path("users/alice/email")

Batch operations​

db.put_batch([(b"k1", b"v1"), (b"k2", b"v2")])
values = db.get_batch([b"k1", b"k2"])
db.delete_batch([b"k1", b"k2"])
exists = db.exists(b"k1")

Prefix Scanning​

There are two range/prefix iteration APIs. scan(start, end) walks an explicit byte range, while scan_prefix(prefix) walks all keys under a prefix.

# Multi-tenant data
db.put(b"tenants/acme/users/1", b'{"name":"Alice"}')
db.put(b"tenants/acme/users/2", b'{"name":"Bob"}')
db.put(b"tenants/globex/users/1", b'{"name":"Charlie"}')

# Scan only ACME data
for key, value in db.scan_prefix(b"tenants/acme/"):
print(f"{key.decode()}: {value.decode()}")
Minimum 2-byte prefix

scan_prefix(prefix) enforces a minimum 2-byte prefix and raises ValueError for shorter prefixes. This is a safety guard that prevents an accidental full-database scan and guarantees the scan never crosses into another prefix or tenant. If you genuinely need an empty or 1-byte prefix (a full scan), use scan_prefix_unchecked(prefix) — it skips the guard and is intended for internal/administrative use.

# Explicit full scan (bypasses the 2-byte guard)
for key, value in db.scan_prefix_unchecked(b""):
...

SQL Database​

The 0.5.9 SDK ships a KV-backed SQL engine exposed via db.execute(sql) (with the alias db.execute_sql). It supports a practical subset of SQL.

from sochdb import Database

with Database.open("./sql_db") as db:
db.execute("""
CREATE TABLE users (
id INT,
name TEXT,
email TEXT,
age INT
)
""")

db.execute("INSERT INTO users (id, name, email, age) VALUES (1, 'Alice', 'alice@example.com', 30)")
db.execute("INSERT INTO users (id, name, email, age) VALUES (2, 'Bob', 'bob@example.com', 25)")

result = db.execute("SELECT name, age FROM users WHERE age > 26 ORDER BY age DESC LIMIT 10")
for row in result.rows:
print(row)

db.execute() returns a SQLQueryResult with rows, columns, and rows_affected.

Supported statements: CREATE TABLE, DROP TABLE, INSERT INTO, SELECT ... [WHERE] [ORDER BY] [LIMIT], UPDATE ... SET, DELETE FROM. Column types: INT, TEXT, FLOAT, BOOL, BLOB.

db.execute("UPDATE users SET age = 31 WHERE name = 'Alice'")
db.execute("DELETE FROM users WHERE age < 26")
No CREATE INDEX in SQL

The SQL engine in the 0.5.9 SDK does not support CREATE INDEX / DROP INDEX. Vector indexing is configured through the separate db.create_index(...) method (see Vector indexing on Database), not via SQL DDL. The richer SQL feature set (joins, aggregates, CREATE INDEX) lives in the server-side engine — see the SQL guide.

You can also run SQL inside a transaction together with KV writes for atomicity:

with db.transaction() as txn:
txn.execute("INSERT INTO users (id, name) VALUES (3, 'Carol')")
txn.put(b"user:3:metadata", b'{"verified": true}')
# Atomic SQL + KV commit

Namespaces & Collections​

Type-safe multi-tenant isolation with vector collections.

Creating namespaces​

from sochdb import Database, NamespaceConfig

with Database.open("./multi_tenant") as db:
config = NamespaceConfig(
name="tenant_123",
display_name="Acme Corp",
labels={"tier": "enterprise"},
)
ns = db.create_namespace(config)

# Or fetch an existing one
ns = db.namespace("tenant_123")
# Or get-or-create
ns = db.get_or_create_namespace("tenant_123")

Other namespace methods on Database: use_namespace(name), list_namespaces(), and delete_namespace(name, force=False).

Creating collections​

from sochdb import CollectionConfig, DistanceMetric

config = CollectionConfig(
name="documents",
dimension=384, # None = auto-infer from first vector
metric=DistanceMetric.COSINE,
)
collection = ns.create_collection(config)

# Or the simple form
collection = ns.create_collection("embeddings", dimension=768)

Vector operations​

# Insert a single vector with metadata
collection.insert(
vector=[0.1, 0.2, 0.3], # 384-dim in practice
metadata={"source": "web", "url": "https://example.com"},
id="doc_001", # optional; auto-generated if omitted
)

# Batch insert
collection.insert_batch(
vectors=[[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]],
metadatas=[{"type": "a"}, {"type": "b"}, {"type": "c"}],
ids=["doc_1", "doc_2", "doc_3"],
)

The unified search(SearchRequest) API returns an iterable SearchResults, and there are convenience wrappers for the common cases.

from sochdb import SearchRequest

# Unified request
results = collection.search(
SearchRequest(vector=query_embedding, k=10, filter={"source": "web"})
)
for result in results:
print(f"ID: {result.id}, Score: {result.score:.4f}")

# Convenience methods
results = collection.vector_search(query_embedding, k=10)
results = collection.keyword_search("neural networks", k=10)
results = collection.hybrid_search(query_embedding, "deep learning", k=10)

# Exact (brute-force) search variants
results = collection.vector_search_exact(query_embedding, k=10)

# Tune recall/latency at query time
collection.set_ef_search(128)

Additional Collection methods include insert_multi, add, upsert, query, get(id), delete(id), count(), and info().


VectorIndex / BatchAccumulator (embedded HNSW via ctypes)​

VectorIndex is the ctypes-backed embedded HNSW index in the 0.5.9 SDK.

from sochdb import VectorIndex
import numpy as np

index = VectorIndex(dimension=768, metric="cosine") # cosine | euclidean | dot_product

embeddings = np.random.randn(10000, 768).astype(np.float32)
ids = list(range(len(embeddings)))

# Batch insert (much faster than per-vector insert)
index.insert_batch(ids, embeddings)

query = np.random.randn(768).astype(np.float32)
for vec_id, distance in index.search(query, k=10):
print(f"{vec_id}: {distance:.4f}")

# Tune recall/latency
index.ef_search = 128

VectorIndex also offers insert(id, vector), insert_batch_fast, search_fast, search_ultra, search_exact, search_exact_f64, build_flat_cache(), and dimension().

For large ingest, BatchAccumulator buffers inserts and flushes them efficiently:

from sochdb import VectorIndex, BatchAccumulator

index = VectorIndex(dimension=768)
acc = BatchAccumulator(index, estimated_size=1_000_000)
for vec_id, vec in stream_vectors():
acc.add_single(vec_id, vec)
acc.flush()
acc.save("./index_dir")

Vector indexing on Database​

Database also exposes helper methods to create and query a named HNSW index directly:

# ef_construction defaults to 256, max_connections (M) defaults to 32
db.create_index("docs", dimension=768, max_connections=32, ef_construction=256)
db.insert_vectors("docs", ids=[1, 2, 3], vectors=[[...], [...], [...]])
results = db.search("docs", query=[...], k=10)

Native HNSW engine (2.0.3)​

These classes come from the native PyO3 package (sochdb 2.0.3), not the 0.5.9 SDK. They are the lowest-level, highest-throughput indexing primitives. If the compiled extension is missing, importing them raises ImportError.

HnswIndex​

from sochdb import HnswIndex, recommended_hnsw_params
import numpy as np

# Defaults: m=32, ef_construction=200, metric="cosine", precision="f32"
index = HnswIndex(dimension=768, m=32, ef_construction=200, metric="cosine")

vectors = np.random.randn(100_000, 768).astype(np.float32) # must be C-contiguous
index.insert_batch(vectors) # auto-generates sequential IDs

query = np.random.randn(768).astype(np.float32)
ids, distances = index.search(query, k=10) # returns two numpy arrays

Supported metric values: "cosine", "euclidean"/"l2", "dot"/"dot_product"/"inner_product". Supported precision: "f32"/"float32", "f16"/"float16", "bf16"/"bfloat16".

Key methods: insert_batch_with_ids(ids, vectors), search_batch(queries, k, ef_search), search_filtered(query, k, filter, ef_search) (AND-semantics metadata filter), set_metadata/set_metadata_batch, optimize(), refine_graph(), refine_graph_additive(), repair(), diagnose(), save(path)/HnswIndex.load(path), and stats().

from sochdb import recommended_hnsw_params, build_index_from_numpy

params = recommended_hnsw_params(dimension=768, n_vectors=1_000_000, target_recall=0.95)
# -> {"m": ..., "ef_construction": ..., "ef_search": ..., "note": ...}

# Build an index, auto-selecting params when m/ef_construction are omitted
index = build_index_from_numpy(vectors, ids=ids)

recommended_hnsw_params picks M by dimension (≤128 → 16, 129–512 → 24, 513+ → 32), sets ef_construction = max(200, M*8), and scales ef_search to the requested recall.

There is also build_index(embeddings, m=32, ef_construction=200, metric="cosine", ids=None) and build_index_from_file(...). bulk_build_index(...) is deprecated and now warns.

MultiShardHnswIndex​

Python-only wrapper

MultiShardHnswIndex is a pure-Python scatter-gather wrapper in the 2.0.3 package, built on top of HnswIndex for very large (100M–1B) vector sets. It uses Python threads and per-shard locks and routes by id % n_shards. It is not a core-engine type — the Rust core has no multi-shard HNSW. Do not expect it server-side.

from sochdb import MultiShardHnswIndex

idx = MultiShardHnswIndex(dimension=768, n_shards=8, target_recall=0.95)
idx.insert_batch_with_ids(ids, vectors)
result_ids, distances = idx.search(query, k=10, failure_policy="raise")
paths = idx.save("./big_index") # writes ./big_index_shard_{i}.hnsw

Hybrid indexes (BM25 + RRF)​

The native package also exposes BM25Index, RRFFusion, the pure-Python HybridSearchIndex (composes HNSW + BM25 + RRF), and the native ThreeLaneHybridIndex (grep + BM25 + HNSW fused with RRF).

from sochdb import HybridSearchIndex

# adaptive_rrf_k is a Python-SDK feature (the core RRF-k is fixed at 60.0)
hybrid = HybridSearchIndex(dimension=768, bm25_weight=0.4, vector_weight=0.6, adaptive_rrf_k=True)
hybrid.build(doc_ids, texts, embeddings)
hits = hybrid.search(query_text, query_embedding, k=10)
note

adaptive_rrf_k=True only adjusts the RRF constant inside this Python HybridSearchIndex. The Rust/core fusion uses a fixed RRF k of 60.0 — there is no adaptive RRF-k in the engine.


Priority Queue​

First-class queue API with ordered-key task entries and a configurable visibility timeout.

from sochdb import Database
from sochdb.queue import create_queue, QueueConfig

db = Database.open("./queue_db")

# Convenience factory
queue = create_queue(db, queue_id="tasks", visibility_timeout_ms=30000, max_attempts=3)

You can also build a PriorityQueue directly with PriorityQueue.from_database(db, ...), from_client(...), or from_backend(...), and configure it via QueueConfig().with_visibility_timeout(...).with_max_attempts(...).with_dead_letter_queue(...).

Enqueue, dequeue, acknowledge​

task_id = queue.enqueue(
priority=1, # lower = higher priority
payload=b'{"action": "process_order", "order_id": 123}',
metadata={"source": "api"},
)

task = queue.dequeue()
if task:
try:
process(task.payload)
queue.ack(task.task_id)
except Exception:
queue.nack(task.task_id) # retry or dead-letter

Additional methods: enqueue_batch, extend_visibility(task_id, ms), peek(), stats() (returns QueueStats), and list_tasks(limit=100).

stats = queue.stats()
print(stats)

StreamingTopK is available for efficient ordered top-k accumulation:

from sochdb.queue import StreamingTopK

topk = StreamingTopK()
for item in items:
topk.push(item)
ordered = topk.get_sorted()

Backends include FFIQueueBackend, GrpcQueueBackend, and InMemoryQueueBackend.


Agent Memory​

Added in v0.5.8/0.5.9, AgentMemory provides episodic write + bi-temporal retrieval for agent workloads.

from sochdb import SochDBClient
from sochdb.memory import AgentMemory, QueryLanes, create_agent_memory

# AgentMemory talks to a running SochDB server (ContextService), not an
# embedded Database — pass a SochDBClient, not Database.open(...).
client = SochDBClient("localhost:50051")
memory = AgentMemory(client, namespace="default", session_id="sess-1", token_limit=4096)
# Or: memory = create_agent_memory(client, namespace="default", token_limit=4096)

# Write an episode (t_valid_from is optional; bi-temporal)
memory.write_episode("User asked about pricing tiers.", metadata={"role": "user"})

# Retrieve context, optionally as-of a point in time (unix ms)
ctx = memory.search(
"pricing",
lanes=QueryLanes.HYBRID, # lexical | three_lane | hybrid | bm25 | trigram
token_limit=2048,
as_of=None, # int unix-ms for point-in-time recall
)
print(ctx)

QueryLanes constants: LEXICAL="lexical", THREE_LANE="three_lane", HYBRID="hybrid", BM25="bm25", TRIGRAM="trigram". Other AgentMemory methods: get_episode(doc_id), compile_context(sections, ...), estimate_tokens(content), and format_context(content).

Lower-level building blocks are also importable from sochdb.memory: ExtractionPipeline, Consolidator, HybridRetriever, NamespaceManager, and dataclasses Entity, Relation, Assertion, CanonicalFact, RetrievalResult.

Example patterns, not SDK classes

Higher-level agent scaffolding — context builders, policy hooks, tool routing, and graph-overlay helpers — are example patterns that live in the separate sochdb-python-examples repository (e.g. ContextQueryBuilder/ContextComponent, the validate_user/redact_pii policy hooks, the tool-routing demo). They are demonstration scripts, not importable classes in the sochdb package. Build them yourself using the primitives above, or copy the examples.


Temporal Graph​

Time-aware relationships support point-in-time and range queries.

from sochdb import Database

with Database.open("./temporal_db") as db:
db.add_temporal_edge(
namespace="org",
from_id="alice",
edge_type="WORKS_AT",
to_id="acme_corp",
valid_from=1704067200000, # 2024-01-01 (unix ms)
valid_until=1735689600000, # 2025-01-01
properties={"role": "Engineer"},
)

db.add_temporal_edge(
namespace="org",
from_id="alice",
edge_type="WORKS_AT",
to_id="globex_inc",
valid_from=1735689600000,
valid_until=0, # 0 = no end (current)
properties={"role": "Senior Engineer"},
)

Querying​

# Point in time: "Where did Alice work on 2024-06-15?"
results = db.query_temporal_graph(
namespace="org",
node_id="alice",
mode="POINT_IN_TIME", # CURRENT | POINT_IN_TIME | RANGE
timestamp=1718409600000,
edge_type="WORKS_AT",
)
for edge in results:
print(edge)

End an edge with db.end_temporal_edge(...). A plain (non-temporal) graph API is also available: add_node, add_edge, traverse, delete_node, delete_edge, get_neighbors(direction="outgoing"), and find_path(from, to, max_depth=10).


Semantic Cache​

An embedding-aware cache. Entries are stored under a cache name + key with an associated embedding; lookups match by embedding similarity (cosine) above a threshold.

# cache_put(cache_name, key, value, embedding, ttl_seconds=0) -> bool
db.cache_put(
"answers",
key="q:pricing",
value="Our pricing has three tiers...", # str, not bytes
embedding=[0.1, 0.2, 0.3],
ttl_seconds=3600,
)

# cache_get matches by embedding similarity, not by key:
# cache_get(cache_name, query_embedding, threshold=0.85) -> str | None
hit = db.cache_get("answers", [0.1, 0.2, 0.3], threshold=0.85)

db.cache_delete("answers", "q:pricing")
db.cache_clear("answers")
stats = db.cache_stats("answers")

Hosted Studio​

StudioClient talks to the hosted SochDB Studio service for event ingestion and health checks.

from sochdb import StudioClient

studio = StudioClient(base_url="https://studio.example.com", api_key="...", timeout=30.0)
print(studio.health())
result = studio.ingest_events(events)

Errors surface as StudioAPIError(status_code, message); ingest_events returns a StudioEventIngestResult.


TOON Format​

SochDB's native, token-efficient output notation. TOON typically uses 40-66% fewer tokens than equivalent JSON for tabular data, which makes it well suited for LLM context.

from sochdb import Database

records = [
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"},
]

toon_str = db.to_toon("users", records, ["name", "email"])
print(toon_str)

# Round-trip back. from_toon returns a (table_name, fields, records) tuple.
table_name, fields, records_again = db.from_toon(toon_str)

Database also exposes to_json and from_json. The wire/context format enums (WireFormat, ContextFormat, CanonicalFormat) live in sochdb.format.


CLI Tools​

Three console scripts are installed with the package:

sochdb-server        # embedded/IPC server
sochdb-bulk # bulk vector index build/query
sochdb-grpc-server # gRPC server

Bulk index build example:

sochdb-bulk build-index \
--input embeddings.npy \
--output index.hnsw \
--dimension 768 \
--max-connections 32 \
--ef-construction 256 \
--metric cosine

Error Handling​

The SDK provides a structured error hierarchy. All errors derive from SochDBError, which carries an optional code and context and offers .to_dict().

from sochdb.errors import (
SochDBError, # base
ConnectionError,
DatabaseError,
ProtocolError,

# Transactions
TransactionError,
TransactionConflictError,

# Namespaces
NamespaceError,
NamespaceNotFoundError,
NamespaceExistsError,
NamespaceAccessError,

# Collections
CollectionError,
CollectionNotFoundError,
CollectionExistsError,
CollectionConfigError,

# Validation
ValidationError,
DimensionMismatchError,
InvalidMetadataError,
ScopeViolationError,

# Queries
QueryError,
QueryTimeoutError,
EmbeddingError,

# Locks
LockError,
DatabaseLockedError,
LockTimeoutError,
EpochMismatchError,
SplitBrainError,
)

Handling lock errors​

from sochdb import Database
from sochdb.errors import DatabaseLockedError, LockTimeoutError

try:
db = Database.open("./shared_db")
except DatabaseLockedError as e:
print(f"Locked by another process: {e}")
db = Database.open_concurrent("./shared_db") # retry concurrently
except LockTimeoutError as e:
print(f"Timed out waiting for lock: {e}")

Handling dimension errors​

from sochdb.errors import DimensionMismatchError

try:
collection.insert([1.0, 2.0, 3.0]) # 3-dim into a 384-dim collection
except DimensionMismatchError as e:
print(f"Expected {e.expected} dimensions, got {e.actual}")

Best Practices​

Use context managers​

# Automatic cleanup
with Database.open("./db") as db:
db.put(b"key", b"value")

Use prefix scans for multi-tenancy​

for key, value in db.scan_prefix(f"tenants/{tenant_id}/".encode()):
process(key, value)

Batch your writes​

# Fast: one batched call
db.put_batch(items)

# For vectors, accumulate then flush
acc = BatchAccumulator(index)
for vid, vec in stream():
acc.add_single(vid, vec)
acc.flush()

Use TOON for LLM context​

records = [dict(zip(result.columns, r)) for r in result.rows]
toon_context = db.to_toon("users", records, ["name", "email"])
# Send TOON to the LLM instead of JSON — saves 40-66% tokens

Complete Example: Multi-Tenant SaaS with SQL + KV​

from sochdb import Database
import json

def main():
with Database.open("./saas_db") as db:
db.execute("""
CREATE TABLE tenants (
id INT,
name TEXT,
created_at TEXT
)
""")
db.execute("INSERT INTO tenants (id, name, created_at) VALUES (1, 'ACME Corp', '2026-01-01')")
db.execute("INSERT INTO tenants (id, name, created_at) VALUES (2, 'Globex Inc', '2026-01-01')")

# Per-tenant KV data
db.put(b"tenants/1/users/alice", b'{"role":"admin","email":"alice@acme.com"}')
db.put(b"tenants/1/users/bob", b'{"role":"user","email":"bob@acme.com"}')
db.put(b"tenants/2/users/charlie", b'{"role":"admin","email":"charlie@globex.com"}')

result = db.execute("SELECT id, name FROM tenants ORDER BY name")
for row in result.rows:
tenant_id, tenant_name = row[0], row[1]
prefix = f"tenants/{tenant_id}/".encode()
users = list(db.scan_prefix(prefix))
print(f"\n{tenant_name} ({len(users)} users):")
for key, value in users:
u = json.loads(value.decode())
print(f" {key.decode()}: {u['email']} ({u['role']})")

if __name__ == "__main__":
main()

Resources​


Last updated: June 2026 — SDK 0.5.9, native engine 2.0.3.