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/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]: ...