Skip to main content

A prototype ORM for SurrealDB

Project description

Tapestry

A modern, type-safe Python ORM for SurrealDB

Tapestry brings the power of Pydantic validation and Python's type system to SurrealDB, enabling you to build graph-aware applications with confidence. Define your models once, get automatic schema generation, full-text search, and intelligent query building with complete IDE autocomplete support.

Python 3.12+ Type Checked SurrealDB


โœจ Features

  • ๐ŸŽฏ Type-Safe Queries - Full IDE autocomplete and static type checking (to the extent of what Python allows)
  • ๐Ÿ“Š Graph-First Design - Native support for relationships and graph traversals
  • ๐Ÿ” Full-Text Search - Built-in tokenizers for multilingual search (as long as you speak French)
  • ๐Ÿ”„ Auto Schema Generation - Define models in Python, generate SurrealQL DDL (actually works quiet well)
  • โœ… Pydantic Integration - Automatic validation and serialization
  • โšก Async/Await - Built for modern async Python applications
  • ๐ŸŽจ Pythonic API - Clean, intuitive (...really ?) query building

๐Ÿ“ฆ Installation

uv add tapestry-orm

๐ŸŽฏ Quick Start

Define Your Models

from tapestry import Node, Edge
from datetime import date

class Person(Node):
    """A person in our database"""
    first_name: str
    last_name: str
    email: str
    date_of_birth: date

class Company(Node):
    """A company"""
    name: str
    founded: date
    industry: str

class WorksAt(Edge):
    """Relationship: Person works at Company"""
    in_: Person      # Source: the person
    out_: Company    # Target: the company
    position: str
    since: date

Connect and Setup

from surrealdb import AsyncSurreal
from tapestry import Base

async with AsyncSurreal("ws://localhost:8000/rpc") as db:
    await db.signin({"username": "root", "password": "root"})
    await db.use("myapp", "myapp")
    
    # Generate and apply schema automatically
    schema = Base.generate_schema()
    await db.query(schema)

Create Records

# Create a person
alice = Person(
    first_name="Alice",
    last_name="Johnson",
    email="alice@example.com",
    date_of_birth=date(1990, 5, 15)
)
await alice.create(db)

# Batch insert multiple records
people = [
    Person(first_name="Bob", last_name="Smith", email="bob@example.com", date_of_birth=date(1985, 3, 20)),
    Person(first_name="Carol", last_name="Williams", email="carol@example.com", date_of_birth=date(1992, 7, 8))
]
await Person.insert(db, people)

# Create a company
acme = Company(
    name="Acme Corp",
    founded=date(2010, 1, 1),
    industry="Technology"
)
await acme.create(db)

Build Relationships

# Connect Alice to Acme Corp
employment = WorksAt(
    in_=alice,
    out_=acme,
    position="Software Engineer",
    since=date(2020, 6, 1)
)
await employment.relate(db)

Query with Type Safety

from tapestry import Q

# Simple query - returns list[Person]
adults = await Q(Person).where(Person.date_of_birth < date(2000, 1, 1)).execute(db)

# IDE autocomplete works on results!
for person in adults:
    print(f"{person.first_name} {person.last_name}")  # โœ“ Full autocomplete
    print(f"Email: {person.email}")                   # โœ“ Type-safe access

# Query with field selection
emails = await Q(Person).select("email", "first_name").execute(db)

# Get just the values
names = await Q(Person).select("first_name").value().execute(db)

# Complex conditions
tech_workers = await (Q(Person)
    .where(
        (Person.first_name == "Alice") |
        (Person.last_name == "Smith")
    )
    .execute(db)
)

Graph Traversals

# Forward traversal: Find where Alice works
companies = await (
    Q(Person)
    .where(Person.email == "alice@example.com")
    >> WorksAt
    >> Company
).execute(db)

# Backward traversal: Find who works at Acme
employees = await (
    Q(Company)
    .where(Company.name == "Acme Corp")
    << WorksAt
    << Person
).execute(db)

# Conditional edges: Senior positions only
seniors = await (
    Q(Company)
    << WorksAt.where(WorksAt.position == "Senior Engineer")
    << Person
).execute(db)

# Multi-hop traversals
complex_query = (
    Q(Person)
    >> WorksAt
    >> Company
    .where(Company.industry == "Technology")
)

Full-Text Search

from tapestry import Text
from tapestry.tokenizer import EnglishTokenizer

class Article(Node):
    title: str
    content: Text[EnglishTokenizer]
    author: str

# Search articles
results = await Q(Article).where(Article.content @ "machine learning").execute(db)

Update and Delete

# Update an existing record
alice.email = "alice.johnson@newcompany.com"
await alice.save(db)

# Query, modify, and save
person = (await Q(Person).where(Person.email == "bob@example.com").execute(db))[0]
person.last_name = "Johnson-Smith"
await person.save(db)

๐ŸŽจ Advanced Features

Computed Fields

from pydantic import computed_field

class Person(Node):
    first_name: str
    last_name: str
    
    @computed_field
    @property
    def full_name(self) -> str:
        return f"{self.first_name} {self.last_name}"

# Use in queries
people = await Q(Person).execute(db)
for person in people:
    print(person.full_name)

Nested Field Queries

class Role(Node):
    title: str
    department: str

class Person(Node):
    name: str
    role: Role

# Query nested fields
managers = await Q(Person).where(Person.role.title == "Manager").execute(db)

Connection Pooling

from tapestry import create_engine

# Create a connection pool
engine = create_engine(
    "ws://localhost:8000/rpc",
    {"username": "root", "password": "root"},
    pool_size=10
)

async with engine.session() as db:
    await db.use("myapp", "myapp")
    people = await Q(Person).execute(db)

๐Ÿ” Type Safety in Action

Tapestry provides full type inference for IDE autocomplete and static type checking:

# Type checker knows this returns Q[Person]
query = Q(Person).where(Person.email == "alice@example.com")

# Type checker knows this returns list[Person]
people: list[Person] = await query.execute(db)

# IDE provides autocomplete on person
for person in people:
    person.  # โ† Your IDE shows: first_name, last_name, email, date_of_birth, id

Works with mypy and pyright for catching errors at build time!


๐Ÿ“š Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Models    โ”‚  Define using Python classes (Node/Edge)
โ”‚ (Your Code) โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚
       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Tapestry  โ”‚  Handles validation, serialization, queries
โ”‚     ORM     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚
       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SurrealDB  โ”‚  Graph database with SQL-like queries
โ”‚   Database  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ› ๏ธ Development

Running Tests

# Run all tests
uv run pytest

# Run specific test
uv run pytest tests/test_complete.py::TestWorkflow::test_select_queries

Running CI Locally

To test the GitLab CI pipeline locally before pushing, use gitlab-ci-local:

# Run the test job locally (configuration is already set up)
gitlab-ci-local

The project includes two configuration files that make this work:

  • .gitlab-ci-local-env - Mounts the Docker socket from your host
  • .gitlab-ci-local-variables.yml - Configures testcontainers to use the mounted socket

This gives you an identical testing experience to the actual CI pipeline, ensuring your tests will pass when you push.

Type Checking

uv run basedpyright src/tapestry

Building Documentation

cd docs
make html
make serve  # View at http://localhost:8000

๐Ÿ“– Documentation


๐Ÿค Contributing

Contributions are welcome! Please feel free to submit issues and pull requests.


๐Ÿ“‹ Roadmap

Schema Definition

  • Add default values and constructors to fields
  • Custom validators (ASSERT clause)
  • Maximum size for arrays and sets

Queries

  • Aggregation queries (COUNT, SUM, AVG, etc.)
  • GROUP BY and LIMIT clauses
  • Subqueries and CTEs (imho should not be needed)

CRUD Operations

  • .update() method for instances
  • Bulk update operations
  • Bulk delete operations
  • Upsert with ON DUPLICATE KEY UPDATE

Advanced Features

  • Migration system
  • Query result caching
  • Lazy relationship loading
  • Transaction support

๐Ÿ™ Acknowledgments

Built with:

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

tapestry_orm-0.0.4.tar.gz (29.4 kB view details)

Uploaded Source

Built Distribution

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

tapestry_orm-0.0.4-py3-none-any.whl (35.1 kB view details)

Uploaded Python 3

File details

Details for the file tapestry_orm-0.0.4.tar.gz.

File metadata

  • Download URL: tapestry_orm-0.0.4.tar.gz
  • Upload date:
  • Size: 29.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for tapestry_orm-0.0.4.tar.gz
Algorithm Hash digest
SHA256 4738a41f88402c515965cafdb53b39e28c3a433558b99a47ca54596d44cd74fd
MD5 66af52a22bfb5e4e1e977093f96ab7d0
BLAKE2b-256 8a9e305719cf9295998461e10bf5c0f5d880b1820a04c36bb07900bb9732f9f6

See more details on using hashes here.

File details

Details for the file tapestry_orm-0.0.4-py3-none-any.whl.

File metadata

  • Download URL: tapestry_orm-0.0.4-py3-none-any.whl
  • Upload date:
  • Size: 35.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for tapestry_orm-0.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 9f3b85f51d93b32c6b7ad5d879b10d0b4eec82d88b1b1659160b0fd724acc72e
MD5 9059d26f3c1769fb748d38474dd9fdcf
BLAKE2b-256 1fa22ada92ec7078ae47b49a8e2e039aec256f0a3c2e43ecb0594bddcfebb787

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