Skip to main content
Runic logo

Runic

A type-safe OGM for Cypher graph databases.
Define your models once, run them on any backend.

Version PyPI Python License: MIT

Why Runic • The OGM • Migrations • Graph-RAG • Installation • Docs


Runic maps Python classes to graph nodes and edges. You declare typed Node and Edge models and get change tracking, lazy and eager relationships, a composable query API, and schema migrations — on top of a pluggable driver layer that runs the same model code on FalkorDB, Neo4j, Memgraph, ArcadeDB, Apache AGE, and Amazon Neptune (Database and Analytics). A built-in Graph-RAG SDK layers document ingestion and cited, knowledge-graph-grounded question answering on top of the OGM.

Why Runic

  • Backend-agnostic. One model definition runs on seven backends. Switching from FalkorDB to Neo4j means changing the arguments to create_driver(); your models, queries, and application code don't change.
  • Typed models, no metaclass magic. Node and Edge are plain classes with typed Field descriptors — IDE autocomplete works, and Author.name == "Alice" builds a query predicate.
  • Change tracking. Mutate an object and call commit(); the unit-of-work session computes the diff and writes only what changed. No manual dirty flags, no hand-written SET clauses.
  • First-class relationships. Declare Relation fields for INCOMING/OUTGOING edges, with edge-property models, and choose lazy loading or single-round-trip eager fetch.
  • Native graph types. Vector (vecf32), GeoLocation (point), interned strings, and automatic converters for datetime and Enum — stored without writing serialization code.
  • Migrations included. A migration tool with versioned revisions, a CLI, and rollback snapshots. Revision state lives inside the graph, so there's no external state table.
  • Sync and async. Session/Repository and AsyncSession/AsyncRepository share one API.

The OGM

The two examples below build on one domain — Author, Article, and an AUTHORED edge — and cover the features you'll reach for most.

1. Model a domain, persist it, and wire up relationships

Declare typed Node and Edge classes. Constraints and indexes go inline on Field; native graph types (Vector, GeoLocation, datetime, Enum) get their converters assigned automatically; and Relation declares a traversal with an optional edge-property model. SchemaManager reconciles those declarations with the live graph, a Session handles writes with automatic change tracking, and session.relate() creates the edges between nodes.

from datetime import UTC, datetime
from enum import StrEnum

from runic.ogm import (
    Edge,
    Field,
    GeoLocation,
    Node,
    Relation,
    SchemaManager,
    Session,
    Vector,
    create_driver,
)


class Status(StrEnum):
    DRAFT = "draft"
    PUBLISHED = "published"


class Article(Node, labels=["Article"]):
    id: str = Field(primary_key=True)
    title: str = Field(
        index_type="FULLTEXT"
    )  # fulltext search: FalkorDB/Neo4j/Memgraph
    category: str = Field(
        interned=True
    )  # intern() dedup — FalkorDB only, no-op elsewhere
    status: Status = Status.DRAFT  # EnumConverter auto-assigned
    published_at: datetime | None = None  # DatetimeConverter auto-assigned
    embedding: Vector | None = None  # KNN via vecf32() on FalkorDB; Neo4j/Memgraph
    #   need a pre-created VECTOR INDEX
    origin: GeoLocation | None = None  # point(); updates unsupported on ArcadeDB


class AuthoredEdge(Edge, type="AUTHORED"):
    created_at: datetime


class Author(Node, labels=["Author"]):
    id: str = Field(primary_key=True)
    email: str = Field(unique=True)
    name: str
    articles: list[Article] = Relation(
        relationship="AUTHORED",
        direction="OUTGOING",
        target="Article",
        edge_model=AuthoredEdge,
    )


# Pick a backend here — nothing else in this file changes.
driver = create_driver("falkordb", host="localhost", port=6379, graph="blog")

# Reconcile declared indexes/constraints with the live graph.
schema = SchemaManager(driver)
schema.sync_schema([Author, Article], drop_extra=False)  # create missing; keep extras

with Session(driver) as session:
    alice = Author(id="alice", email="alice@example.com", name="Alice")
    intro = Article(
        id="a1", title="Graphs 101", category="intro", status=Status.PUBLISHED
    )
    session.add_all([alice, intro])
    session.commit()

    # Create the AUTHORED edge, writing properties onto the relationship itself.
    # relate() is MERGE-based: idempotent, and re-calling updates the edge props.
    session.relate(
        alice, Author.articles, intro, edge=AuthoredEdge(created_at=datetime.now(UTC))
    )
    session.commit()

with Session(driver) as session:
    alice = session.get(Author, "alice")
    alice.name = "Alice Smith"  # tracked automatically — no explicit dirty flag
    session.commit()  # only the diff is written

2. Query and traverse the graph

Read data back with composable, type-safe statements, multi-hop traversals, paginated repositories, or your own Cypher. select() builds a statement independently of any session, so you can assemble it from conditional filters and reuse it across sessions. .traverse() walks a relationship — one hop, or to any depth with hops= — for real-world graph queries.

from runic.ogm import Repository, Session, alias, select


# Compose a query dynamically, then run it three ways.
stmt = select(Article).where(Article.status == Status.PUBLISHED)
if category:
    stmt = stmt.where(Article.category == category)

with Session(driver) as session:
    articles: list[Article] = session.scalars(stmt)  # list[Article]
    latest: Article | None = session.scalar(stmt)  # Article | None
    n: int = session.count(stmt)  # int

    # Single-hop traversal with an edge-property filter — published articles
    # Alice authored after a cutoff date.
    e, art = alias(AuthoredEdge, "e"), alias(Article, "art")
    recent = (
        session.query(Author, "a")
        .where(Author.id == "alice")
        .traverse(Author.articles, to=art, edge=e)
        .where(e.created_at >= cutoff)
        .where(art.status == Status.PUBLISHED)
        .return_target(art)
        .all()
    )

    # Variable-length traversal — every article reachable within 3 AUTHORED hops
    # (e.g. co-authorship chains). hops=(1, None) means unbounded.
    network = (
        session.query(Author, "a")
        .where(Author.id == "alice")
        .traverse(Author.articles, to="reached", hops=(1, 3))
        .all()
    )

    # Paginate through a repository.
    page = Repository(session, Article).find_all(skip=0, limit=20)

    # Or load relationships off an entity: lazy by default, eager on request.
    author = session.get(Author, "alice")
    author.articles  # lazy — queried on first access
    eager = session.get(Author, "alice", fetch=["articles"])
    eager.articles  # already loaded, no extra query


# Subclass Repository to drop down to typed Cypher when you need it.
class ArticleRepository(Repository[Article]):
    def by_author_email(self, email: str) -> list[Article]:
        return self.cypher(
            "MATCH (:Author {email: $email})-[:AUTHORED]->(a:Article) RETURN a",
            {"email": email},
            returns=Article,
        )

Migrations

runic.migrate is a migration tool with a CLI for versioned schema evolution. It stores revision state inside the graph, so there's no external state table to manage.

runic init
runic revision -m "create user index"

Edit the generated file in runic/versions/:

revision = "1975ea83b712"
down_revision = None


def upgrade(op) -> None:
    op.create_range_index("User", "email")


def downgrade(op) -> None:
    op.drop_range_index("User", "email")

Apply or roll back:

runic upgrade            # apply all pending revisions
runic downgrade          # roll back one step
runic downgrade 1975e    # roll back to a specific revision (prefix is enough)

Baseline an existing graph without re-running anything — introspect, generate a root revision, and stamp it. The generated revision rebuilds the full schema on an empty graph, so it's safe to replay for CI, cloning, or new tenants:

runic baseline -m "baseline"   # introspect, generate root revision, stamp it
runic current                  # verify it is now tracked
runic upgrade head             # rebuild full schema on a fresh graph

Programmatic SDK — drive migrations from code, against any backend:

from pathlib import Path
from runic import Runic, init
from runic.migrate.adapters import create_adapter

init(Path("runic/"))

adapter = create_adapter(
    "falkordb", url="falkor://localhost:6379", graph_name="my_graph"
)
# adapter = create_adapter("neo4j", host="localhost", port=7687,
#                          database="neo4j", username="neo4j", password="secret")

runic = Runic(adapter, script_location=Path("runic/"))
runic.migrate.upgrade("head")
print("current:", runic.migrate.current())

Graph-RAG

runic.rag turns unstructured text into a knowledge graph and answers questions over it with citations — chunking, entity/relation extraction, embedding, storage, and hybrid (vector + fulltext + graph-expansion) retrieval are handled for you. It builds on the same OGM and driver layer, so it runs on any supported backend. OpenAI is the default for the LLM and embeddings; point it at Ollama for a fully local run.

from runic.ogm import create_driver
from runic.rag import GraphRAG, Ontology, RagSettings

settings = RagSettings()
driver = create_driver(
    "falkordb", host="localhost", port=6379, graph=settings.falkordb_graph
)

# with_defaults() wires the full adapter stack from your environment:
# paragraph chunker, extractor, embedder, resolver, retrievers, reranker, synthesizer.
rag = GraphRAG.with_defaults(driver, settings=settings, ontology=Ontology.default())
rag.bootstrap_schema()  # create entity types + indexes (idempotent)

# Ingest: chunk -> extract entities/relations -> embed -> resolve -> write the graph.
rag.ingest_text(
    "Ada Lovelace worked with Charles Babbage on the Analytical Engine in London.",
    source="inline-demo",
)

# mode="auto" classifies the question and picks the retrieval strategy automatically.
answer = rag.query("Who worked on the Analytical Engine?")
print(answer.text)
for citation in answer.citations:
    print(f"  - [{citation.source}] {citation.text[:80]}...")

Every stage is a port with a default adapter you can swap — custom ontologies, chunkers, extractors, or retrievers — and the optional runic-rag-docling add-on plugs Docling in for layout-aware parsing and chunking of PDFs and office documents.

uv add "runic-py[graphrag,falkordb]"   # Graph-RAG extras + a backend driver

Installation

Install the core package plus the extra for your backend. The core has no graph-driver dependency — you only pull in what you use.

Backend Extra Driver installed
FalkorDB falkordb falkordb
Neo4j neo4j neo4j
Memgraph memgraph neo4j (Bolt)
ArcadeDB arcadedb neo4j (Bolt)
Apache AGE age psycopg[binary]
Amazon Neptune Database neptune neo4j (Bolt) + botocore
Amazon Neptune Analytics neptune-analytics boto3
All backends all all of the above
Graph-RAG graphrag pydantic-ai, pymupdf (combine with a backend)
uv add "runic-py[falkordb]"   # FalkorDB
uv add "runic-py[neo4j]"      # Neo4j
uv add "runic-py[memgraph]"   # Memgraph (Bolt)
uv add "runic-py[arcadedb]"   # ArcadeDB (Bolt)
uv add "runic-py[age]"        # Apache AGE (PostgreSQL extension)
uv add "runic-py[neptune]"    # Amazon Neptune Database (Bolt + IAM auth)
uv add "runic-py[neptune-analytics]"   # Amazon Neptune Analytics (HTTPS)
uv add "runic-py[all]"        # everything
uv add "runic-py[graphrag,falkordb]"   # Graph-RAG SDK + a backend driver

Documentation

Full conceptual overview, async usage, advanced CLI flags, the Graph-RAG guides, and the complete API reference live at the Runic Documentation.

License

Released under the MIT License.

Disclaimer

The code and the documentation are generated with AI. AI generated

Metadata

Release files for runic-py 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for runic-py 0.6.0
File Size Uploaded
runic_py-0.6.0.tar.gz 201.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for runic-py 0.6.0
File Interpreter ABI Platform
runic_py-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 461.9 kB

Release files / runic_py-0.6.0.tar.gz

Download URL runic_py-0.6.0.tar.gz
Size 201.2 kB
Tags Source
SHA-256 checksum
How to use checksums
fe5fef73c04c00588ba9a191f40b1257d167ee13d18ea42a843aa89ab0c0d0cb
BLAKE2b-256 checksum
How to use checksums
8d10205186d1f07d498a4d044b18772f86c50280988316c704516ccc087705cf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / runic_py-0.6.0-py3-none-any.whl

Download URL runic_py-0.6.0-py3-none-any.whl
Size 260.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7de8c46aca24f2cc5dd2c09e3d75c14971437e8dac85509c2dc60e83251202d3
BLAKE2b-256 checksum
How to use checksums
86e97358707fd8012b0265c1dee86336e46ed6566881090ee03a4eabf8a12b04
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

0.5.3

2 release files

0.5.0

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.7

2 release files

0.3.6

2 release files

0.3.5

2 release files

0.3.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

1 release file

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page