This guide shows a Python-first developer how to:
- build and install the
grm-rsPython extension locally - use the extension from Python
- run the
grm-rsCLI from compiled Rust code
- Rust toolchain installed
- Python 3.9+
- a virtualenv tool such as
venv
From the repo root:
python -m venv .venv
source .venv/bin/activate
pip install maturin
cd grm-python
mkdir -p test-dbs
maturin developmaturin develop compiles the Rust extension and installs it into the active virtualenv, so import grm_rs works immediately in that environment.
Python methods are adapter conveniences over GRM's shared typed runtime
operations. In particular, structured node_find(...) traversal and
explain_node_find(...)/profile_node_find(...) calls are represented inside
the runtime as typed request objects rather than CLI command strings.
Example:
from grm_rs import Session
session = Session()
session.model_create(
"User",
"userId",
[
{"name": "name", "type": "string", "required": True},
{"name": "age", "type": "int", "required": False},
],
)
session.model_create(
"Post",
"postId",
[
{"name": "title", "type": "string", "required": True},
],
)
session.link_create(
"AUTHORED",
"User",
"Post",
"authoredId",
[
{"name": "year", "type": "int", "required": True},
],
)
user = session.node_create("User", {"name": "Alice", "age": 42})
post = session.node_create("Post", {"title": "Hello"})
edge = session.edge_create("AUTHORED", user["id"], post["id"], {"year": 2024})
print(user)
print(edge)
print(session.node_find("User", {"name": "Alice"}))
print(session.edge_find("AUTHORED"))
print(
session.node_find(
"User",
{"name": "Alice"},
via=[
{"dir": "out", "link": "AUTHORED", "model": "Post"},
],
)
)
print(
session.node_find(
"User",
{"name": "Alice"},
via=[
{"dir": "out", "link": "AUTHORED", "model": "Post"},
],
end_filters={"title": "Hello"},
edge_filters={"year": 2024},
return_="edge",
)
)
plan = session.explain_node_find(
"User",
{"name": "Alice"},
via=[{"dir": "out", "link": "AUTHORED", "model": "Post"}],
)
profile = session.profile_edge_find("AUTHORED", {"from": user["id"]})
print(plan["plan"]["steps"])
print(profile["result_rows"], profile["elapsed"]["display"])
session.export_json("test-dbs/users.interchange.json")
portable = session.export_dict()
fresh = Session()
fresh.import_json("test-dbs/users.interchange.json")
print(portable["format"])You can also derive the same structured schema and write calls from typed Python
objects. This is a Python ergonomic layer over the existing typed GRM operations:
it still delegates to model_create, node_create, link_create, and
edge_create rather than defining a new service contract or query language.
from pydantic import BaseModel
from grm_rs import Session
class StatementLine(BaseModel):
__grm_id_field__ = "statementLineId"
date: str
amount: float
cleared: bool = False
session = Session()
session.model_create(StatementLine)
line = StatementLine(date="2026-06-16", amount=12.5, cleared=True)
node = session.node_create(line)
print(node["props"])Typed schema derivation currently supports only primitive field annotations:
str, int, float, and bool. Complex typing such as Optional, Union,
lists, dictionaries, nested models, aliases, validators, computed fields, and
generics is intentionally out of scope for this adapter slice.
Batch several related mutations with one shared result and one autocommit write:
result = session.batch(
[
{
"op": "node_create",
"args": {"model": "User", "props": {"name": "Bob"}, "ref": "bob"},
},
{
"op": "node_create",
"args": {"model": "Post", "props": {"title": "Batch hello"}, "ref": "post"},
},
{
"op": "edge_create",
"args": {
"model": "AUTHORED",
"from": "bob",
"to": "post",
"props": {"year": 2026},
},
},
],
atomic=True,
response="detailed",
)
print(result["counts"])- Field definitions are Python dicts with
name,type, andrequired - Supported field types are
string,int,float, andbool model_create(SomeClass)andnode_create(instance)are optional typed-object conveniences; passid_field="..."or define__grm_id_field__ = "..."on the class- The Python method names mostly mirror the CLI commands with
_instead of., such asmodel_create,node_find, andedge_update session.batch(...)accepts structured operation dicts for schema, node, and edge creates/updates/deletes; deletes requireallow_deletes=True, and node creates can define batch-local refs for later edge endpointsexplain_node_find,profile_node_find,explain_edge_find, andprofile_edge_findexpose the same first-phase query introspection as CLIsession.explain/session.profilesave_json,save_binary,load_json, andload_binarypersist local workspace snapshots, including storage bookkeepingexport_json,export_dict, andimport_jsonuse the portablegrm.interchangegraph format described inimport-export.mdimport_jsoncurrently requires an empty session; create a freshSession()before importing interchange files- Autocommit is off by default; enable it at construction time with
Session(autocommit=True, autocommit_path="test-dbs/session.json") - When autocommit is enabled,
session.autocommitisTrueand successful mutating operations persist through the shared append-log/checkpoint durability path. Normal schema/node/edge writes append durable records; load/import-style operations checkpoint the session file. - Python traversal mirrors CLI
node.find ... via=...semantics, but uses structured inputs: passvia=[{"dir": "out", "link": "AUTHORED", "model": "Post"}], optionalend_filters, optionaledge_filters, andreturn_="root","end", or"edge" node_create,node_find,edge_create, andedge_findreturn plain Python dictionaries/lists- Rust-side failures are raised as
grm_rs.GrmError - For local scratch session files, prefer keeping them under
test-dbs/
The Python extension does not wrap the CLI directly. The CLI is still the Rust binary named grm.
To build it:
cargo build --bin grmThen run the compiled binary:
./target/debug/grm sessionIf you want an optimized build:
cargo build --release --bin grm
./target/release/grm sessionYou can also run a setup script through the compiled binary:
./target/debug/grm session --script examples/session_setup.grmFor a Python-focused contributor, the common loop is:
- activate the virtualenv
- run
maturin developafter Rust changes that affect the extension - run Python code against
grm_rs.Session - build or run
grmseparately when working with the interactive CLI
If you are editing both the Python bindings and the CLI/runtime code, it is normal to use both of these during development:
cd grm-python && maturin develop
cargo build --bin grm