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.
@@ -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
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
20
+ [![Type Checked](https://img.shields.io/badge/type%20checked-mypy%2Bpyright-blue)](https://github.com/python/mypy)
21
+ [![SurrealDB](https://img.shields.io/badge/SurrealDB-compatible-purple)](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