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 with coverage
uv run pytest --cov=tapestry

# 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 test

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

# With mypy
uv run mypy src/tapestry

# With pyright
uv run pyright 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.1.tar.gz (29.1 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.1-py3-none-any.whl (36.5 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for tapestry_orm-0.0.1.tar.gz
Algorithm Hash digest
SHA256 ecb2030aefb7e991583ffb2c94b8e1d13ecc1cc9a4ad491fcb8874704a8f8e6a
MD5 5142f991bf150e4d65db520a1ae5b9a5
BLAKE2b-256 82ce73c7ae16169f5b4d51707f2f7fbcdc26f09318536214f70fa6f9683e1910

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for tapestry_orm-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 642927c197a04f9ee5a8a39d1c8ba6a23db0fb8e06455f5c5a2c7e524c208223
MD5 7fb8bd65832674e619df5199bff23af3
BLAKE2b-256 cdb8a06dd2e8906d2aaee8e5bb38380ce2c1f76f25934dfc9772e44fcdf37de2

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