tapestry-orm 0.0.1__py3-none-any.whl
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/__init__.py +11 -0
- tapestry/base.py +401 -0
- tapestry/base.pyi +54 -0
- tapestry/edge.py +271 -0
- tapestry/edge.pyi +45 -0
- tapestry/engine.py +301 -0
- tapestry/field.py +315 -0
- tapestry/field.pyi +113 -0
- tapestry/node.py +169 -0
- tapestry/node.pyi +28 -0
- tapestry/py.typed +0 -0
- tapestry/query.py +404 -0
- tapestry/query.pyi +33 -0
- tapestry/table.py +464 -0
- tapestry/tokenizer.py +156 -0
- tapestry/utils.py +70 -0
- tapestry_orm-0.0.1.dist-info/METADATA +418 -0
- tapestry_orm-0.0.1.dist-info/RECORD +19 -0
- tapestry_orm-0.0.1.dist-info/WHEEL +4 -0
tapestry/query.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Query builder for SurrealQL-like syntax in Python.
|
|
3
|
+
|
|
4
|
+
This module provides a fluent interface for building SurrealQL queries using
|
|
5
|
+
Python operators and method chaining. The Q class enables type-safe query
|
|
6
|
+
construction with support for graph traversals, filtering, and projections.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from tapestry import Q, Node
|
|
10
|
+
>>>
|
|
11
|
+
>>> class Person(Node):
|
|
12
|
+
... name: str
|
|
13
|
+
... age: int
|
|
14
|
+
...
|
|
15
|
+
>>> # Simple query with filtering
|
|
16
|
+
>>> adults = Q(Person).where(Person.age >= 18)
|
|
17
|
+
>>> results = await adults.execute(db)
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Graph traversal query
|
|
20
|
+
>>> friends_of_john = (Q(Person)
|
|
21
|
+
... .where(Person.name == "John")
|
|
22
|
+
... >> FriendOf >> Person)
|
|
23
|
+
>>> results = await friends_of_john.execute(db)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from enum import Enum
|
|
29
|
+
from typing import Any, Optional, Union, Type, TYPE_CHECKING, TypeVar, Generic, get_type_hints, overload
|
|
30
|
+
from dataclasses import dataclass, field as dataclass_field
|
|
31
|
+
from datetime import date, datetime
|
|
32
|
+
from pydantic.fields import FieldInfo, ComputedFieldInfo
|
|
33
|
+
from surrealdb import AsyncWsSurrealConnection, AsyncHttpSurrealConnection, RecordID
|
|
34
|
+
|
|
35
|
+
# Import at module level to avoid circular imports
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from .node import Node
|
|
38
|
+
from .edge import Edge
|
|
39
|
+
|
|
40
|
+
from .field import Traversal, Condition, Direction
|
|
41
|
+
from .base import Base
|
|
42
|
+
|
|
43
|
+
# TypeVar for the model being queried
|
|
44
|
+
T = TypeVar('T', bound='Base')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Q(Generic[T]):
|
|
49
|
+
"""
|
|
50
|
+
Main query builder class for constructing SurrealQL queries.
|
|
51
|
+
|
|
52
|
+
The Q class provides a fluent interface for building complex database
|
|
53
|
+
queries with support for filtering, graph traversals, and projections.
|
|
54
|
+
It uses Python operators and method chaining to create readable and
|
|
55
|
+
type-safe queries.
|
|
56
|
+
|
|
57
|
+
Type Parameters:
|
|
58
|
+
T: The model type being queried (Node or Edge subclass)
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
table: The Node or Edge class to query from
|
|
62
|
+
_select_fields: List of specific fields to select
|
|
63
|
+
_where_clause: WHERE condition for filtering
|
|
64
|
+
_traversals: List of graph traversals to perform
|
|
65
|
+
_value_only: Whether to return only values without field names
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
>>> # Basic query - type is inferred as Q[Person]
|
|
69
|
+
>>> query = Q(Person)
|
|
70
|
+
>>>
|
|
71
|
+
>>> # With filtering
|
|
72
|
+
>>> adults = Q(Person).where(Person.age >= 18)
|
|
73
|
+
>>>
|
|
74
|
+
>>> # Select specific fields
|
|
75
|
+
>>> names = Q(Person).select("first_name", "last_name")
|
|
76
|
+
>>>
|
|
77
|
+
>>> # Graph traversal using >> operator
|
|
78
|
+
>>> managers = Q(Person) >> BelongsTo >> Role.where(Role.title == "Manager")
|
|
79
|
+
>>>
|
|
80
|
+
>>> # Execute the query - returns list[Person]
|
|
81
|
+
>>> results = await query.execute(db)
|
|
82
|
+
|
|
83
|
+
Notes:
|
|
84
|
+
- Uses >> for forward traversal and << for backward traversal
|
|
85
|
+
- Automatically deserializes results to model instances
|
|
86
|
+
- Supports chaining multiple operations
|
|
87
|
+
- Generates valid SurrealQL syntax
|
|
88
|
+
- Preserves type information for IDE autocomplete
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
table: Type[T]
|
|
92
|
+
_select_fields: list[str] = dataclass_field(default_factory=list)
|
|
93
|
+
_where_clause: Optional[str] = None
|
|
94
|
+
_traversals: list[Traversal] = dataclass_field(default_factory=list)
|
|
95
|
+
_value_only: bool = False
|
|
96
|
+
|
|
97
|
+
def __post_init__(self):
|
|
98
|
+
self.from_table = self.table.__name__.lower()
|
|
99
|
+
|
|
100
|
+
def select(self, *fields: str) -> Q[T]:
|
|
101
|
+
"""
|
|
102
|
+
Specify fields to include in the query results.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
*fields: Field names to select from the table
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Q[T]: The query instance for chaining
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> query = Q(Person).select("first_name", "last_name", "email")
|
|
112
|
+
>>> # Generates: SELECT first_name, last_name, email FROM person;
|
|
113
|
+
|
|
114
|
+
Notes:
|
|
115
|
+
- If not called, all fields are selected (SELECT *)
|
|
116
|
+
- Can be combined with value() for different output formats
|
|
117
|
+
"""
|
|
118
|
+
self._select_fields.extend(fields)
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def value(self) -> Q[T]:
|
|
122
|
+
"""
|
|
123
|
+
Return only values without field names.
|
|
124
|
+
|
|
125
|
+
Makes the query return raw values instead of objects with field names.
|
|
126
|
+
Useful for extracting single values or when field names are not needed.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Q[T]: The query instance for chaining
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
>>> # Get just the names as a list of strings
|
|
133
|
+
>>> names = Q(Person).select("name").value()
|
|
134
|
+
>>> # Generates: SELECT VALUE name FROM person;
|
|
135
|
+
>>>
|
|
136
|
+
>>> # Get IDs from a traversal
|
|
137
|
+
>>> ids = Q(Person).value() >> FriendOf >> Person
|
|
138
|
+
>>> # Generates: SELECT VALUE ->friend_of->person.* FROM person;
|
|
139
|
+
|
|
140
|
+
Notes:
|
|
141
|
+
- Changes SELECT to SELECT VALUE in the generated query
|
|
142
|
+
- Affects the structure of returned results
|
|
143
|
+
"""
|
|
144
|
+
self._value_only = True
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def where(self, condition: Condition) -> Q[T]:
|
|
148
|
+
"""
|
|
149
|
+
Add a WHERE clause to filter query results.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
condition: A Condition object created using field comparisons
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Q[T]: The query instance for chaining
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
>>> # Simple condition
|
|
159
|
+
>>> adults = Q(Person).where(Person.age >= 18)
|
|
160
|
+
>>>
|
|
161
|
+
>>> # Complex condition with AND
|
|
162
|
+
>>> query = Q(Person).where(
|
|
163
|
+
... (Person.age >= 18) & (Person.city == "Paris")
|
|
164
|
+
... )
|
|
165
|
+
>>>
|
|
166
|
+
>>> # Using OR
|
|
167
|
+
>>> query = Q(Person).where(
|
|
168
|
+
... (Person.role == "admin") | (Person.role == "moderator")
|
|
169
|
+
... )
|
|
170
|
+
>>>
|
|
171
|
+
>>> # Negation with ~
|
|
172
|
+
>>> active = Q(Person).where(~(Person.status == "deleted"))
|
|
173
|
+
|
|
174
|
+
Notes:
|
|
175
|
+
- Conditions are created using field comparisons (==, !=, <, >, <=, >=)
|
|
176
|
+
- Use & for AND, | for OR, ~ for NOT
|
|
177
|
+
- Supports full-text search with @ operator
|
|
178
|
+
"""
|
|
179
|
+
self._where_clause = condition.to_surreal()
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
def traverse(self, traversal: Traversal) -> Q[T]:
|
|
183
|
+
"""
|
|
184
|
+
Add a graph traversal to navigate relationships.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
traversal: A Traversal object defining the path to follow
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Q[T]: The query instance for chaining
|
|
191
|
+
|
|
192
|
+
Example:
|
|
193
|
+
>>> # Direct traversal object
|
|
194
|
+
>>> from tapestry.field import Traversal, Direction
|
|
195
|
+
>>> traversal = Traversal(Direction.FORWARD, FriendOf)
|
|
196
|
+
>>> query = Q(Person).traverse(traversal)
|
|
197
|
+
|
|
198
|
+
Notes:
|
|
199
|
+
- Prefer using >> and << operators for simpler syntax
|
|
200
|
+
- This method gives more control over traversal configuration
|
|
201
|
+
"""
|
|
202
|
+
self._traversals.append(traversal)
|
|
203
|
+
return self
|
|
204
|
+
|
|
205
|
+
def __rshift__(self, other: Union[Type['Edge'], Type['Node'], Traversal]) -> Q[T]:
|
|
206
|
+
"""
|
|
207
|
+
Forward traversal using >> operator for graph navigation.
|
|
208
|
+
|
|
209
|
+
Follows relationships in the forward direction (from in_ to out_).
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
other: An Edge class, Node class, or Traversal to follow
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Q[T]: The query instance for continued chaining
|
|
216
|
+
|
|
217
|
+
Example:
|
|
218
|
+
>>> # Navigate from Person through BelongsTo to Role
|
|
219
|
+
>>> managers = Q(Person) >> BelongsTo >> Role
|
|
220
|
+
>>> # Generates: SELECT ->belongs_to->role.* FROM person;
|
|
221
|
+
>>>
|
|
222
|
+
>>> # With conditions on edges
|
|
223
|
+
>>> recent = (Q(Person) >>
|
|
224
|
+
... BelongsTo.where(BelongsTo.begin_date > date(2020, 1, 1)) >>
|
|
225
|
+
... Role)
|
|
226
|
+
>>>
|
|
227
|
+
>>> # Chain multiple traversals
|
|
228
|
+
>>> network = Q(Person) >> FriendOf >> Person >> WorksAt >> Company
|
|
229
|
+
|
|
230
|
+
Notes:
|
|
231
|
+
- Forward means following from 'in_' to 'out_' of edges
|
|
232
|
+
- Can traverse through multiple relationships
|
|
233
|
+
- Supports conditional edges with .where()
|
|
234
|
+
"""
|
|
235
|
+
if isinstance(other, type):
|
|
236
|
+
traversal = Traversal(Direction.FORWARD, other)
|
|
237
|
+
self._traversals.append(traversal)
|
|
238
|
+
elif isinstance(other, Traversal):
|
|
239
|
+
self._traversals.append(other)
|
|
240
|
+
else:
|
|
241
|
+
# Handle EdgeWithCondition
|
|
242
|
+
if hasattr(other, 'to_traversal'):
|
|
243
|
+
traversal = other.to_traversal(Direction.FORWARD)
|
|
244
|
+
self._traversals.append(traversal)
|
|
245
|
+
else:
|
|
246
|
+
traversal = Traversal(Direction.FORWARD, other)
|
|
247
|
+
self._traversals.append(traversal)
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
def __lshift__(self, other: Union[Type['Edge'], Type['Node'], Traversal]) -> Q[T]:
|
|
251
|
+
"""
|
|
252
|
+
Backward traversal using << operator for reverse graph navigation.
|
|
253
|
+
|
|
254
|
+
Follows relationships in the backward direction (from out_ to in_).
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
other: An Edge class, Node class, or Traversal to follow
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Q[T]: The query instance for continued chaining
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
>>> # Find all people who belong to a specific role
|
|
264
|
+
>>> members = Q(Role).where(Role.title == "Manager") << BelongsTo << Person
|
|
265
|
+
>>> # Generates: SELECT <-belongs_to<-person.* FROM role WHERE title IS "Manager";
|
|
266
|
+
>>>
|
|
267
|
+
>>> # Reverse traversal to find who manages a department
|
|
268
|
+
>>> managers = Q(Department) << Manages << Person
|
|
269
|
+
>>>
|
|
270
|
+
>>> # Mix forward and backward
|
|
271
|
+
>>> related = Q(Person) >> FriendOf >> Person << WorksAt << Company
|
|
272
|
+
|
|
273
|
+
Notes:
|
|
274
|
+
- Backward means following from 'out_' to 'in_' of edges
|
|
275
|
+
- Useful for finding inverse relationships
|
|
276
|
+
- Can be combined with forward traversals
|
|
277
|
+
"""
|
|
278
|
+
if isinstance(other, type):
|
|
279
|
+
traversal = Traversal(Direction.BACKWARD, other)
|
|
280
|
+
self._traversals.append(traversal)
|
|
281
|
+
elif isinstance(other, Traversal):
|
|
282
|
+
# Keep the original direction
|
|
283
|
+
self._traversals.append(other)
|
|
284
|
+
else:
|
|
285
|
+
# Handle EdgeWithCondition
|
|
286
|
+
if hasattr(other, 'to_traversal'):
|
|
287
|
+
traversal = other.to_traversal(Direction.BACKWARD)
|
|
288
|
+
self._traversals.append(traversal)
|
|
289
|
+
else:
|
|
290
|
+
traversal = Traversal(Direction.BACKWARD, other)
|
|
291
|
+
self._traversals.append(traversal)
|
|
292
|
+
return self
|
|
293
|
+
|
|
294
|
+
def to_surreal(self) -> str:
|
|
295
|
+
"""
|
|
296
|
+
Convert the query to SurrealQL syntax.
|
|
297
|
+
|
|
298
|
+
Generates the complete SurrealQL query string that can be
|
|
299
|
+
executed against a SurrealDB database.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
str: The SurrealQL query string
|
|
303
|
+
|
|
304
|
+
Example:
|
|
305
|
+
>>> query = (Q(Person)
|
|
306
|
+
... .where(Person.age >= 18)
|
|
307
|
+
... .select("name", "email"))
|
|
308
|
+
>>> print(query.to_surreal())
|
|
309
|
+
'SELECT name, email FROM person WHERE age >= 18;'
|
|
310
|
+
>>>
|
|
311
|
+
>>> traversal = Q(Person) >> FriendOf >> Person
|
|
312
|
+
>>> print(traversal.to_surreal())
|
|
313
|
+
'SELECT ->friend_of->person.* FROM person;'
|
|
314
|
+
|
|
315
|
+
Notes:
|
|
316
|
+
- Always ends with a semicolon
|
|
317
|
+
- Generates valid SurrealQL syntax
|
|
318
|
+
- Handles all query components (SELECT, FROM, WHERE, traversals)
|
|
319
|
+
"""
|
|
320
|
+
parts = []
|
|
321
|
+
|
|
322
|
+
# SELECT clause
|
|
323
|
+
if self._value_only:
|
|
324
|
+
parts.append("SELECT VALUE")
|
|
325
|
+
elif self._select_fields:
|
|
326
|
+
parts.append(f"SELECT {', '.join(self._select_fields)}")
|
|
327
|
+
else:
|
|
328
|
+
parts.append("SELECT")
|
|
329
|
+
|
|
330
|
+
# Add traversals to the SELECT clause
|
|
331
|
+
if self._traversals:
|
|
332
|
+
traversal_str = self._build_traversal_string()
|
|
333
|
+
if self._value_only or self._select_fields:
|
|
334
|
+
parts[-1] += f" {traversal_str}"
|
|
335
|
+
else:
|
|
336
|
+
parts[-1] += f" {traversal_str}"
|
|
337
|
+
elif not self._select_fields and not self._value_only:
|
|
338
|
+
parts[-1] += " *"
|
|
339
|
+
|
|
340
|
+
# FROM clause
|
|
341
|
+
if self.from_table:
|
|
342
|
+
parts.append(f"FROM {self.from_table}")
|
|
343
|
+
|
|
344
|
+
# WHERE clause
|
|
345
|
+
if self._where_clause:
|
|
346
|
+
parts.append(f"WHERE {self._where_clause}")
|
|
347
|
+
|
|
348
|
+
return " ".join(parts) + ";"
|
|
349
|
+
|
|
350
|
+
def _build_traversal_string(self) -> str:
|
|
351
|
+
"""
|
|
352
|
+
Build the traversal part of the query.
|
|
353
|
+
|
|
354
|
+
Internal method that constructs the graph traversal syntax
|
|
355
|
+
from the list of traversals.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
str: The traversal string for the query
|
|
359
|
+
"""
|
|
360
|
+
return "".join(t.to_surreal() for t in self._traversals)
|
|
361
|
+
|
|
362
|
+
async def execute(self, con: AsyncWsSurrealConnection | AsyncHttpSurrealConnection) -> list[T]:
|
|
363
|
+
"""
|
|
364
|
+
Execute the query and return deserialized results.
|
|
365
|
+
|
|
366
|
+
Runs the generated SurrealQL query against the database and
|
|
367
|
+
automatically deserializes the results into the appropriate
|
|
368
|
+
model instances based on the Base registry.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
con: Active SurrealDB connection (WebSocket or HTTP)
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
list[T]: The query results, deserialized to model instances
|
|
375
|
+
|
|
376
|
+
Example:
|
|
377
|
+
>>> # Get all adults as Person instances
|
|
378
|
+
>>> query = Q(Person).where(Person.age >= 18)
|
|
379
|
+
>>> people = await query.execute(db)
|
|
380
|
+
>>> for person in people:
|
|
381
|
+
... print(f"{person.first_name} {person.last_name}")
|
|
382
|
+
>>>
|
|
383
|
+
>>> # Get names only
|
|
384
|
+
>>> names = await Q(Person).select("name").value().execute(db)
|
|
385
|
+
>>> print(names) # ['John', 'Jane', ...]
|
|
386
|
+
|
|
387
|
+
Notes:
|
|
388
|
+
- Automatically converts RecordIDs to model instances
|
|
389
|
+
- Handles both single records and lists
|
|
390
|
+
- Preserves non-record data types
|
|
391
|
+
- Uses the Base.deserialize_response for conversion
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
Exception: If the database query fails
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
result = await con.query(self.to_surreal())
|
|
398
|
+
|
|
399
|
+
# If no results or not a list, return as-is
|
|
400
|
+
if not result or not isinstance(result, list):
|
|
401
|
+
return result
|
|
402
|
+
|
|
403
|
+
# Let Base handle deserialization
|
|
404
|
+
return Base.deserialize_response(result)
|
tapestry/query.pyi
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TypeVar, Generic, Type, Union
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from surrealdb import AsyncWsSurrealConnection, AsyncHttpSurrealConnection
|
|
6
|
+
|
|
7
|
+
from .base import Base
|
|
8
|
+
from .node import Node
|
|
9
|
+
from .edge import Edge
|
|
10
|
+
from .field import Traversal, Condition
|
|
11
|
+
|
|
12
|
+
T = TypeVar('T', bound=Base)
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Q(Generic[T]):
|
|
16
|
+
table: Type[T]
|
|
17
|
+
_select_fields: list[str]
|
|
18
|
+
_where_clause: str | None
|
|
19
|
+
_traversals: list[Traversal]
|
|
20
|
+
_value_only: bool
|
|
21
|
+
from_table: str
|
|
22
|
+
|
|
23
|
+
def __init__(self, table: Type[T]) -> None: ...
|
|
24
|
+
def __post_init__(self) -> None: ...
|
|
25
|
+
def select(self, *fields: str) -> Q[T]: ...
|
|
26
|
+
def value(self) -> Q[T]: ...
|
|
27
|
+
def where(self, condition: Condition) -> Q[T]: ...
|
|
28
|
+
def traverse(self, traversal: Traversal) -> Q[T]: ...
|
|
29
|
+
def __rshift__(self, other: Union[Type[Edge], Type[Node], Traversal]) -> Q[T]: ...
|
|
30
|
+
def __lshift__(self, other: Union[Type[Edge], Type[Node], Traversal]) -> Q[T]: ...
|
|
31
|
+
def to_surreal(self) -> str: ...
|
|
32
|
+
def _build_traversal_string(self) -> str: ...
|
|
33
|
+
async def execute(self, con: AsyncWsSurrealConnection | AsyncHttpSurrealConnection) -> list[T]: ...
|