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

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.2.tar.gz (28.7 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.2-py3-none-any.whl (34.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: tapestry_orm-0.0.2.tar.gz
  • Upload date:
  • Size: 28.7 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.2.tar.gz
Algorithm Hash digest
SHA256 f4392c942ca692d85a32eae11cdd93a70c00a2d23e64a916d2fd6b5347db22bf
MD5 9cd6898f2c9fd0b30e221fa59000be64
BLAKE2b-256 1d9574f12d0836a9ce1121fc112d420b47f2cfbec05058b40df895cb6733daf4

See more details on using hashes here.

File details

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

File metadata

  • Download URL: tapestry_orm-0.0.2-py3-none-any.whl
  • Upload date:
  • Size: 34.4 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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a48adbf2e6c2fadbdde2a2b108cd3d1f90a1e78e51ae90cb86209afbb02cf126
MD5 1f31a5bd887945d13f40caef16649e44
BLAKE2b-256 24ab884b7b6805bf04c7ff6280ed2035458bd637e27cec2489825e00ce36e2fd

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