Skip to main content

A modern Python ORM for graph databases (RedisGraph/FalkorDB) with type safety, query builder, and automatic property management

Project description

GraphORM

Python Version License codecov

GraphORM is a modern Python ORM for graph databases, specifically designed for RedisGraph and FalkorDB. It provides a simple, intuitive API with type safety, automatic property management, and a powerful fluent query builder.

Features

  • Type-safe node and edge definitions with Python type hints
  • Automatic property management with validation
  • Fluent Query Builder API with intuitive syntax
  • Transaction support for atomic operations
  • Bulk operations for efficient data insertion
  • Lazy loading of relationships using Relationship descriptors
  • Automatic index creation from class definitions
  • Support for explicit labels and relation names
  • Batch operations with configurable batch size
  • Isolated properties system (separates user properties from internal attributes)

Requirements

  • Python 3.10, 3.11, 3.12, or 3.13
  • RedisGraph 2.x or FalkorDB
  • Redis server with graph module enabled

Installation

Install from PyPI:

pip install graphorm

Install from source:

git clone https://github.com/hello-tmst/graphorm.git
cd graphorm
pip install -e .

Quick Start

Defining Nodes

from graphorm import Node, Graph

class Page(Node):
    __primary_key__ = ["path"]
    
    path: str
    parsed: bool = False
    title: str = ""

# Create a graph instance
graph = Graph("my_graph", host="localhost", port=6379)
graph.create()

# Create and add nodes
page = Page(path="/home", parsed=True, title="Home Page")
graph.add_node(page)
graph.flush()

Defining Edges

from graphorm import Edge

class Linked(Edge):
    pass

page1 = Page(path="/page1")
page2 = Page(path="/page2")

graph.add_node(page1)
graph.add_node(page2)

link = Linked(page1, page2)
graph.add_edge(link)
graph.flush()

Query Builder API

GraphORM provides a fluent query builder API:

from graphorm import select, count, indegree, outdegree

# Find unparsed pages
stmt = select().match(Page.alias("p")).where(
    (Page.alias("p").parsed == False) & 
    (Page.alias("p").error.is_null())
).limit(10)

result = graph.execute(stmt)

# Find pages with highest degree
total_degree = indegree(Page.alias("p")) + outdegree(Page.alias("p"))
stmt = select().match(Page.alias("p")).where(
    outdegree(Page.alias("p")) > 0
).returns(
    Page.alias("p"),
    total_degree.label("degree")
).orderby(total_degree.desc()).limit(20)

result = graph.execute(stmt)
for row in result.result_set:
    page, degree = row
    print(f"{page.properties['path']}: {degree} connections")

Transactions

Use transactions to group operations atomically:

with graph.transaction() as tx:
    tx.add_node(page1)
    tx.add_node(page2)
    tx.add_edge(Linked(page1, page2))
    # Automatically flushed on exit

Bulk Operations

Efficiently insert large amounts of data using bulk operations:

pages_data = [
    {"path": f"/page{i}", "domain": "example.com", "parsed": False}
    for i in range(10000)
]

result = graph.bulk_upsert(Page, pages_data, batch_size=1000)

Relationships

Lazy load related nodes using Relationship descriptors:

from graphorm import Relationship

class Page(Node):
    __primary_key__ = ["path"]
    path: str
    
    linked_pages = Relationship("Linked", direction="outgoing")
    linked_from = Relationship("Linked", direction="incoming")

page = graph.get_node(Page(path="/home"))
if page:
    for linked_page in page.linked_pages:
        print(linked_page.properties['path'])

Indexes

Automatically create indexes on node properties:

class Page(Node):
    __primary_key__ = ["path"]
    __indexes__ = ["path", "parsed", "domain"]
    path: str
    parsed: bool = False
    domain: str = ""

graph.create()  # Automatically creates indexes from __indexes__

Managing Labels and Relations

Default Behavior

By default, GraphORM uses the class name as-is for labels and relations:

class Page(Node):
    # Label will be "Page" (not "page")
    pass

class MyLink(Edge):
    # Relation will be "MyLink" (not "myLink")
    pass

Explicit Labels and Relations

You can explicitly specify labels and relation names using class attributes:

class CustomPage(Node):
    __label__ = "Page"  # Explicit label
    __primary_key__ = ["path"]
    path: str

class CustomLink(Edge):
    __relation_name__ = "Linked"  # Explicit relation name
    pass

Note: Explicit labels and relations must be non-empty strings. Invalid values will raise a ValueError.

Querying with Labels

When writing Cypher queries, use the actual label name (class name or explicit label):

# For class Page (label is "Page")
result = graph.query("MATCH (p:Page) RETURN p")

# For class with explicit label
class MyPage(Node):
    __label__ = "Page"
    __primary_key__ = ["path"]
    path: str

# Query still uses "Page"
result = graph.query("MATCH (p:Page) RETURN p")

Properties Management

GraphORM includes an isolated properties management system that separates user-defined properties from internal attributes.

Accessing Properties

page = Page(path="/test", parsed=True)

# Get all properties (excluding internal attributes)
props = page.properties
# Returns: {'path': '/test', 'parsed': True}

# Properties are isolated from internal attributes
# Internal attributes like __id__, __alias__, etc. are not included

Updating Properties

# Update properties
page.update({"parsed": False, "title": "New Title"})

# Properties are validated based on type annotations

Examples

Complete Example

from graphorm import Node, Edge, Graph

# Define nodes
class Page(Node):
    __primary_key__ = ["path"]
    path: str
    parsed: bool = False

class Website(Node):
    __primary_key__ = ["domain"]
    domain: str

# Define edges
class Linked(Edge):
    pass

# Create graph
graph = Graph("example", host="localhost", port=6379)
graph.create()

# Create nodes
page1 = Page(path="/page1", parsed=False)
page2 = Page(path="/page2", parsed=True)
website = Website(domain="example.com")

graph.add_node(page1)
graph.add_node(page2)
graph.add_node(website)
graph.flush()

# Create edges
link1 = Linked(page1, page2)
link2 = Linked(page1, website)

graph.add_edge(link1)
graph.add_edge(link2)
graph.flush()

# Query
result = graph.query("""
    MATCH (p:Page)-[:Linked]->(target)
    WHERE p.parsed = false
    RETURN p, target
""")

# Cleanup
graph.delete()

More Examples

For more detailed examples, see the examples directory:

Documentation

Development

Running Tests

Run tests with coverage:

pytest --cov=graphorm --cov-report=html --cov-report=term-missing

View coverage report:

# HTML report
open htmlcov/index.html

# Terminal report
pytest --cov=graphorm --cov-report=term-missing

Breaking Changes

Version 0.2.0+

  • Removed dependency on camelcase: Labels and relations now use class names as-is (e.g., Page instead of page)
  • All existing code must be updated: Queries using old lowercase labels need to be updated to use class names

Migration Guide

If you're upgrading from an older version:

  1. Update all Cypher queries to use class names instead of camelcase labels:

    # Old (before 0.2.0)
    query = "MATCH (p:page) RETURN p"
    
    # New (0.2.0+)
    query = "MATCH (p:Page) RETURN p"
    
  2. If you need to maintain old label names, use explicit labels:

    class Page(Node):
        __label__ = "page"  # Maintain old label
        __primary_key__ = ["path"]
        path: str
    

License

This project is licensed under the MIT License - see the LICENSE file for details.

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

graphorm-0.2.1.tar.gz (59.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

graphorm-0.2.1-py3-none-any.whl (41.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: graphorm-0.2.1.tar.gz
  • Upload date:
  • Size: 59.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for graphorm-0.2.1.tar.gz
Algorithm Hash digest
SHA256 df868914ba4eba9dd167797c34e8c950570b2c1bee14cab12c3ef3cd0417ce3f
MD5 8127c3202ee82ac2a0bbf74cb7c33928
BLAKE2b-256 79c13e856e9528a0ff06e547495564f689f06bf195d11fae01441f774c5ccede

See more details on using hashes here.

File details

Details for the file graphorm-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: graphorm-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 41.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for graphorm-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6a862e609d4fdce856d57c89121fbadebccadd8044fdddf46df726ccd38aa3b0
MD5 42ee8889128f5a7a0df784d2198e2149
BLAKE2b-256 ae58ddf8ba2d66dde8dae02540822deb6c0f962e4935254cd80f1b983652ee7e

See more details on using hashes here.

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