Skip to content

Latest commit

 

History

History
240 lines (190 loc) · 7.41 KB

File metadata and controls

240 lines (190 loc) · 7.41 KB

Python Quickstart

This guide shows a Python-first developer how to:

  1. build and install the grm-rs Python extension locally
  2. use the extension from Python
  3. run the grm-rs CLI from compiled Rust code

Prerequisites

  • Rust toolchain installed
  • Python 3.9+
  • a virtualenv tool such as venv

Install The Python Extension

From the repo root:

python -m venv .venv
source .venv/bin/activate
pip install maturin
cd grm-python
mkdir -p test-dbs
maturin develop

maturin 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.

Use The Extension From Python

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"])

Data Shape Notes

  • Field definitions are Python dicts with name, type, and required
  • Supported field types are string, int, float, and bool
  • model_create(SomeClass) and node_create(instance) are optional typed-object conveniences; pass id_field="..." or define __grm_id_field__ = "..." on the class
  • The Python method names mostly mirror the CLI commands with _ instead of ., such as model_create, node_find, and edge_update
  • session.batch(...) accepts structured operation dicts for schema, node, and edge creates/updates/deletes; deletes require allow_deletes=True, and node creates can define batch-local refs for later edge endpoints
  • explain_node_find, profile_node_find, explain_edge_find, and profile_edge_find expose the same first-phase query introspection as CLI session.explain / session.profile
  • save_json, save_binary, load_json, and load_binary persist local workspace snapshots, including storage bookkeeping
  • export_json, export_dict, and import_json use the portable grm.interchange graph format described in import-export.md
  • import_json currently requires an empty session; create a fresh Session() 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.autocommit is True and 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: pass via=[{"dir": "out", "link": "AUTHORED", "model": "Post"}], optional end_filters, optional edge_filters, and return_="root", "end", or "edge"
  • node_create, node_find, edge_create, and edge_find return plain Python dictionaries/lists
  • Rust-side failures are raised as grm_rs.GrmError
  • For local scratch session files, prefer keeping them under test-dbs/

Run The CLI From Compiled Code

The Python extension does not wrap the CLI directly. The CLI is still the Rust binary named grm.

To build it:

cargo build --bin grm

Then run the compiled binary:

./target/debug/grm session

If you want an optimized build:

cargo build --release --bin grm
./target/release/grm session

You can also run a setup script through the compiled binary:

./target/debug/grm session --script examples/session_setup.grm

Typical Developer Workflow

For a Python-focused contributor, the common loop is:

  1. activate the virtualenv
  2. run maturin develop after Rust changes that affect the extension
  3. run Python code against grm_rs.Session
  4. build or run grm separately 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