tapestry-orm 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tapestry_orm-0.0.1/PKG-INFO +418 -0
- tapestry_orm-0.0.1/README.md +406 -0
- tapestry_orm-0.0.1/pyproject.toml +56 -0
- tapestry_orm-0.0.1/src/tapestry/__init__.py +11 -0
- tapestry_orm-0.0.1/src/tapestry/base.py +401 -0
- tapestry_orm-0.0.1/src/tapestry/base.pyi +54 -0
- tapestry_orm-0.0.1/src/tapestry/edge.py +271 -0
- tapestry_orm-0.0.1/src/tapestry/edge.pyi +45 -0
- tapestry_orm-0.0.1/src/tapestry/engine.py +301 -0
- tapestry_orm-0.0.1/src/tapestry/field.py +315 -0
- tapestry_orm-0.0.1/src/tapestry/field.pyi +113 -0
- tapestry_orm-0.0.1/src/tapestry/node.py +169 -0
- tapestry_orm-0.0.1/src/tapestry/node.pyi +28 -0
- tapestry_orm-0.0.1/src/tapestry/py.typed +0 -0
- tapestry_orm-0.0.1/src/tapestry/query.py +404 -0
- tapestry_orm-0.0.1/src/tapestry/query.pyi +33 -0
- tapestry_orm-0.0.1/src/tapestry/table.py +464 -0
- tapestry_orm-0.0.1/src/tapestry/tokenizer.py +156 -0
- tapestry_orm-0.0.1/src/tapestry/utils.py +70 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: tapestry-orm
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A prototype ORM for SurrealDB
|
|
5
|
+
Author-email: timothee@obrecht.xyz
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Dist: more-itertools>=10.7.0
|
|
8
|
+
Requires-Dist: pydantic>=2.11.7
|
|
9
|
+
Requires-Dist: surrealdb>=1.0.6
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# 🚀 Tapestry
|
|
14
|
+
|
|
15
|
+
**A modern, type-safe Python ORM for SurrealDB**
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
[](https://www.python.org/downloads/)
|
|
20
|
+
[](https://github.com/python/mypy)
|
|
21
|
+
[](https://surrealdb.com/)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## ✨ Features
|
|
26
|
+
|
|
27
|
+
- 🎯 **Type-Safe Queries** - Full IDE autocomplete and static type checking (to the extent of what Python allows)
|
|
28
|
+
- 📊 **Graph-First Design** - Native support for relationships and graph traversals
|
|
29
|
+
- 🔍 **Full-Text Search** - Built-in tokenizers for multilingual search (as long as you speak French)
|
|
30
|
+
- 🔄 **Auto Schema Generation** - Define models in Python, generate SurrealQL DDL (actually works quiet well)
|
|
31
|
+
- ✅ **Pydantic Integration** - Automatic validation and serialization
|
|
32
|
+
- ⚡ **Async/Await** - Built for modern async Python applications
|
|
33
|
+
- 🎨 **Pythonic API** - Clean, intuitive (...really ?) query building
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 📦 Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv add tapestry-orm
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 🎯 Quick Start
|
|
46
|
+
|
|
47
|
+
### Define Your Models
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from tapestry import Node, Edge
|
|
51
|
+
from datetime import date
|
|
52
|
+
|
|
53
|
+
class Person(Node):
|
|
54
|
+
"""A person in our database"""
|
|
55
|
+
first_name: str
|
|
56
|
+
last_name: str
|
|
57
|
+
email: str
|
|
58
|
+
date_of_birth: date
|
|
59
|
+
|
|
60
|
+
class Company(Node):
|
|
61
|
+
"""A company"""
|
|
62
|
+
name: str
|
|
63
|
+
founded: date
|
|
64
|
+
industry: str
|
|
65
|
+
|
|
66
|
+
class WorksAt(Edge):
|
|
67
|
+
"""Relationship: Person works at Company"""
|
|
68
|
+
in_: Person # Source: the person
|
|
69
|
+
out_: Company # Target: the company
|
|
70
|
+
position: str
|
|
71
|
+
since: date
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Connect and Setup
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from surrealdb import AsyncSurreal
|
|
78
|
+
from tapestry import Base
|
|
79
|
+
|
|
80
|
+
async with AsyncSurreal("ws://localhost:8000/rpc") as db:
|
|
81
|
+
await db.signin({"username": "root", "password": "root"})
|
|
82
|
+
await db.use("myapp", "myapp")
|
|
83
|
+
|
|
84
|
+
# Generate and apply schema automatically
|
|
85
|
+
schema = Base.generate_schema()
|
|
86
|
+
await db.query(schema)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Create Records
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# Create a person
|
|
93
|
+
alice = Person(
|
|
94
|
+
first_name="Alice",
|
|
95
|
+
last_name="Johnson",
|
|
96
|
+
email="alice@example.com",
|
|
97
|
+
date_of_birth=date(1990, 5, 15)
|
|
98
|
+
)
|
|
99
|
+
await alice.create(db)
|
|
100
|
+
|
|
101
|
+
# Batch insert multiple records
|
|
102
|
+
people = [
|
|
103
|
+
Person(first_name="Bob", last_name="Smith", email="bob@example.com", date_of_birth=date(1985, 3, 20)),
|
|
104
|
+
Person(first_name="Carol", last_name="Williams", email="carol@example.com", date_of_birth=date(1992, 7, 8))
|
|
105
|
+
]
|
|
106
|
+
await Person.insert(db, people)
|
|
107
|
+
|
|
108
|
+
# Create a company
|
|
109
|
+
acme = Company(
|
|
110
|
+
name="Acme Corp",
|
|
111
|
+
founded=date(2010, 1, 1),
|
|
112
|
+
industry="Technology"
|
|
113
|
+
)
|
|
114
|
+
await acme.create(db)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Build Relationships
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# Connect Alice to Acme Corp
|
|
121
|
+
employment = WorksAt(
|
|
122
|
+
in_=alice,
|
|
123
|
+
out_=acme,
|
|
124
|
+
position="Software Engineer",
|
|
125
|
+
since=date(2020, 6, 1)
|
|
126
|
+
)
|
|
127
|
+
await employment.relate(db)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Query with Type Safety
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from tapestry import Q
|
|
134
|
+
|
|
135
|
+
# Simple query - returns list[Person]
|
|
136
|
+
adults = await Q(Person).where(Person.date_of_birth < date(2000, 1, 1)).execute(db)
|
|
137
|
+
|
|
138
|
+
# IDE autocomplete works on results!
|
|
139
|
+
for person in adults:
|
|
140
|
+
print(f"{person.first_name} {person.last_name}") # ✓ Full autocomplete
|
|
141
|
+
print(f"Email: {person.email}") # ✓ Type-safe access
|
|
142
|
+
|
|
143
|
+
# Query with field selection
|
|
144
|
+
emails = await Q(Person).select("email", "first_name").execute(db)
|
|
145
|
+
|
|
146
|
+
# Get just the values
|
|
147
|
+
names = await Q(Person).select("first_name").value().execute(db)
|
|
148
|
+
|
|
149
|
+
# Complex conditions
|
|
150
|
+
tech_workers = await (Q(Person)
|
|
151
|
+
.where(
|
|
152
|
+
(Person.first_name == "Alice") |
|
|
153
|
+
(Person.last_name == "Smith")
|
|
154
|
+
)
|
|
155
|
+
.execute(db)
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Graph Traversals
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
# Forward traversal: Find where Alice works
|
|
163
|
+
companies = await (
|
|
164
|
+
Q(Person)
|
|
165
|
+
.where(Person.email == "alice@example.com")
|
|
166
|
+
>> WorksAt
|
|
167
|
+
>> Company
|
|
168
|
+
).execute(db)
|
|
169
|
+
|
|
170
|
+
# Backward traversal: Find who works at Acme
|
|
171
|
+
employees = await (
|
|
172
|
+
Q(Company)
|
|
173
|
+
.where(Company.name == "Acme Corp")
|
|
174
|
+
<< WorksAt
|
|
175
|
+
<< Person
|
|
176
|
+
).execute(db)
|
|
177
|
+
|
|
178
|
+
# Conditional edges: Senior positions only
|
|
179
|
+
seniors = await (
|
|
180
|
+
Q(Company)
|
|
181
|
+
<< WorksAt.where(WorksAt.position == "Senior Engineer")
|
|
182
|
+
<< Person
|
|
183
|
+
).execute(db)
|
|
184
|
+
|
|
185
|
+
# Multi-hop traversals
|
|
186
|
+
complex_query = (
|
|
187
|
+
Q(Person)
|
|
188
|
+
>> WorksAt
|
|
189
|
+
>> Company
|
|
190
|
+
.where(Company.industry == "Technology")
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Full-Text Search
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from tapestry import Text
|
|
198
|
+
from tapestry.tokenizer import EnglishTokenizer
|
|
199
|
+
|
|
200
|
+
class Article(Node):
|
|
201
|
+
title: str
|
|
202
|
+
content: Text[EnglishTokenizer]
|
|
203
|
+
author: str
|
|
204
|
+
|
|
205
|
+
# Search articles
|
|
206
|
+
results = await Q(Article).where(Article.content @ "machine learning").execute(db)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Update and Delete
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
# Update an existing record
|
|
213
|
+
alice.email = "alice.johnson@newcompany.com"
|
|
214
|
+
await alice.save(db)
|
|
215
|
+
|
|
216
|
+
# Query, modify, and save
|
|
217
|
+
person = (await Q(Person).where(Person.email == "bob@example.com").execute(db))[0]
|
|
218
|
+
person.last_name = "Johnson-Smith"
|
|
219
|
+
await person.save(db)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 🎨 Advanced Features
|
|
225
|
+
|
|
226
|
+
### Computed Fields
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from pydantic import computed_field
|
|
230
|
+
|
|
231
|
+
class Person(Node):
|
|
232
|
+
first_name: str
|
|
233
|
+
last_name: str
|
|
234
|
+
|
|
235
|
+
@computed_field
|
|
236
|
+
@property
|
|
237
|
+
def full_name(self) -> str:
|
|
238
|
+
return f"{self.first_name} {self.last_name}"
|
|
239
|
+
|
|
240
|
+
# Use in queries
|
|
241
|
+
people = await Q(Person).execute(db)
|
|
242
|
+
for person in people:
|
|
243
|
+
print(person.full_name)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Nested Field Queries
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
class Role(Node):
|
|
250
|
+
title: str
|
|
251
|
+
department: str
|
|
252
|
+
|
|
253
|
+
class Person(Node):
|
|
254
|
+
name: str
|
|
255
|
+
role: Role
|
|
256
|
+
|
|
257
|
+
# Query nested fields
|
|
258
|
+
managers = await Q(Person).where(Person.role.title == "Manager").execute(db)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Connection Pooling
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from tapestry import create_engine
|
|
265
|
+
|
|
266
|
+
# Create a connection pool
|
|
267
|
+
engine = create_engine(
|
|
268
|
+
"ws://localhost:8000/rpc",
|
|
269
|
+
{"username": "root", "password": "root"},
|
|
270
|
+
pool_size=10
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
async with engine.session() as db:
|
|
274
|
+
await db.use("myapp", "myapp")
|
|
275
|
+
people = await Q(Person).execute(db)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 🔍 Type Safety in Action
|
|
281
|
+
|
|
282
|
+
Tapestry provides **full type inference** for IDE autocomplete and static type checking:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
# Type checker knows this returns Q[Person]
|
|
286
|
+
query = Q(Person).where(Person.email == "alice@example.com")
|
|
287
|
+
|
|
288
|
+
# Type checker knows this returns list[Person]
|
|
289
|
+
people: list[Person] = await query.execute(db)
|
|
290
|
+
|
|
291
|
+
# IDE provides autocomplete on person
|
|
292
|
+
for person in people:
|
|
293
|
+
person. # ← Your IDE shows: first_name, last_name, email, date_of_birth, id
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Works with **mypy** and **pyright** for catching errors at build time!
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## 📚 Architecture
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
┌─────────────┐
|
|
304
|
+
│ Models │ Define using Python classes (Node/Edge)
|
|
305
|
+
│ (Your Code) │
|
|
306
|
+
└──────┬──────┘
|
|
307
|
+
│
|
|
308
|
+
▼
|
|
309
|
+
┌─────────────┐
|
|
310
|
+
│ Tapestry │ Handles validation, serialization, queries
|
|
311
|
+
│ ORM │
|
|
312
|
+
└──────┬──────┘
|
|
313
|
+
│
|
|
314
|
+
▼
|
|
315
|
+
┌─────────────┐
|
|
316
|
+
│ SurrealDB │ Graph database with SQL-like queries
|
|
317
|
+
│ Database │
|
|
318
|
+
└─────────────┘
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## 🛠️ Development
|
|
324
|
+
|
|
325
|
+
### Running Tests
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
# Run all tests
|
|
329
|
+
uv run pytest
|
|
330
|
+
|
|
331
|
+
# Run with coverage
|
|
332
|
+
uv run pytest --cov=tapestry
|
|
333
|
+
|
|
334
|
+
# Run specific test
|
|
335
|
+
uv run pytest tests/test_complete.py::TestWorkflow::test_select_queries
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Running CI Locally
|
|
339
|
+
|
|
340
|
+
To test the GitLab CI pipeline locally before pushing, use [`gitlab-ci-local`](https://github.com/firecow/gitlab-ci-local?tab=readme-ov-file#installation):
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
# Run the test job locally (configuration is already set up)
|
|
344
|
+
gitlab-ci-local test
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The project includes two configuration files that make this work:
|
|
348
|
+
- `.gitlab-ci-local-env` - Mounts the Docker socket from your host
|
|
349
|
+
- `.gitlab-ci-local-variables.yml` - Configures testcontainers to use the mounted socket
|
|
350
|
+
|
|
351
|
+
This gives you an identical testing experience to the actual CI pipeline, ensuring your tests will pass when you push.
|
|
352
|
+
|
|
353
|
+
### Type Checking
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
# With mypy
|
|
357
|
+
uv run mypy src/tapestry
|
|
358
|
+
|
|
359
|
+
# With pyright
|
|
360
|
+
uv run pyright src/tapestry
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Building Documentation
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
cd docs
|
|
367
|
+
make html
|
|
368
|
+
make serve # View at http://localhost:8000
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 📖 Documentation
|
|
374
|
+
|
|
375
|
+
- **[API Reference](docs/source/api/)** - Complete API documentation
|
|
376
|
+
- **[Tutorials](docs/source/tutorials/)** - Step-by-step guides
|
|
377
|
+
- **[Examples](tests/test_complete.py)** - Real-world usage examples
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## 🤝 Contributing
|
|
382
|
+
|
|
383
|
+
Contributions are welcome! Please feel free to submit issues and pull requests.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 📋 Roadmap
|
|
388
|
+
|
|
389
|
+
### Schema Definition
|
|
390
|
+
- [ ] Add default values and constructors to fields
|
|
391
|
+
- [ ] Custom validators (ASSERT clause)
|
|
392
|
+
- [ ] Maximum size for arrays and sets
|
|
393
|
+
|
|
394
|
+
### Queries
|
|
395
|
+
- [ ] Aggregation queries (COUNT, SUM, AVG, etc.)
|
|
396
|
+
- [ ] GROUP BY and LIMIT clauses
|
|
397
|
+
- [ ] Subqueries and CTEs (imho should not be needed)
|
|
398
|
+
|
|
399
|
+
### CRUD Operations
|
|
400
|
+
- [ ] `.update()` method for instances
|
|
401
|
+
- [ ] Bulk update operations
|
|
402
|
+
- [ ] Bulk delete operations
|
|
403
|
+
- [ ] Upsert with ON DUPLICATE KEY UPDATE
|
|
404
|
+
|
|
405
|
+
### Advanced Features
|
|
406
|
+
- [ ] Migration system
|
|
407
|
+
- [ ] Query result caching
|
|
408
|
+
- [ ] Lazy relationship loading
|
|
409
|
+
- [ ] Transaction support
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
## 🙏 Acknowledgments
|
|
415
|
+
|
|
416
|
+
Built with:
|
|
417
|
+
- [Pydantic](https://docs.pydantic.dev/) - Data validation and settings management
|
|
418
|
+
- [SurrealDB Python SDK](https://github.com/surrealdb/surrealdb.py) - Official Python driver
|