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.
โจ 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
- API Reference - Complete API documentation
- Tutorials - Step-by-step guides
- Examples - Real-world usage examples
๐ค 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:
- Pydantic - Data validation and settings management
- SurrealDB Python SDK - Official Python driver
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tapestry_orm-0.0.3.tar.gz.
File metadata
- Download URL: tapestry_orm-0.0.3.tar.gz
- Upload date:
- Size: 28.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46bf2521864b869d788698d7a2c6efc404ce5ba62f50aee95a95d38d28dbc53b
|
|
| MD5 |
3955fd2df3e2d4ef1928b4ca7a274888
|
|
| BLAKE2b-256 |
b62bbcac9db598a7f43bc6448f1846d277999c566efa828d2edc413cc7e94a53
|
File details
Details for the file tapestry_orm-0.0.3-py3-none-any.whl.
File metadata
- Download URL: tapestry_orm-0.0.3-py3-none-any.whl
- Upload date:
- Size: 34.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
21a0244302ae9fb1a205b4ed82992511f83cf3ddc9eda4653f37fa42b9a94d3e
|
|
| MD5 |
4b37260c077dc426a3062ac04f1805ac
|
|
| BLAKE2b-256 |
7cff30f2a928ae3403265cc363a1f642bfe82863e3271abd81a01a8eca2e7181
|