surrealdb-orm 0.1.3__py3-none-any.whl → 0.5.0__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.
Files changed (51) hide show
  1. surreal_orm/__init__.py +78 -3
  2. surreal_orm/aggregations.py +164 -0
  3. surreal_orm/auth/__init__.py +15 -0
  4. surreal_orm/auth/access.py +167 -0
  5. surreal_orm/auth/mixins.py +302 -0
  6. surreal_orm/cli/__init__.py +15 -0
  7. surreal_orm/cli/commands.py +369 -0
  8. surreal_orm/connection_manager.py +58 -18
  9. surreal_orm/fields/__init__.py +36 -0
  10. surreal_orm/fields/encrypted.py +166 -0
  11. surreal_orm/fields/relation.py +465 -0
  12. surreal_orm/migrations/__init__.py +51 -0
  13. surreal_orm/migrations/executor.py +380 -0
  14. surreal_orm/migrations/generator.py +272 -0
  15. surreal_orm/migrations/introspector.py +305 -0
  16. surreal_orm/migrations/migration.py +188 -0
  17. surreal_orm/migrations/operations.py +531 -0
  18. surreal_orm/migrations/state.py +406 -0
  19. surreal_orm/model_base.py +594 -135
  20. surreal_orm/py.typed +0 -0
  21. surreal_orm/query_set.py +609 -34
  22. surreal_orm/relations.py +645 -0
  23. surreal_orm/surreal_function.py +95 -0
  24. surreal_orm/surreal_ql.py +113 -0
  25. surreal_orm/types.py +86 -0
  26. surreal_sdk/README.md +79 -0
  27. surreal_sdk/__init__.py +151 -0
  28. surreal_sdk/connection/__init__.py +17 -0
  29. surreal_sdk/connection/base.py +516 -0
  30. surreal_sdk/connection/http.py +421 -0
  31. surreal_sdk/connection/pool.py +244 -0
  32. surreal_sdk/connection/websocket.py +519 -0
  33. surreal_sdk/exceptions.py +71 -0
  34. surreal_sdk/functions.py +607 -0
  35. surreal_sdk/protocol/__init__.py +13 -0
  36. surreal_sdk/protocol/rpc.py +218 -0
  37. surreal_sdk/py.typed +0 -0
  38. surreal_sdk/pyproject.toml +49 -0
  39. surreal_sdk/streaming/__init__.py +31 -0
  40. surreal_sdk/streaming/change_feed.py +278 -0
  41. surreal_sdk/streaming/live_query.py +265 -0
  42. surreal_sdk/streaming/live_select.py +369 -0
  43. surreal_sdk/transaction.py +386 -0
  44. surreal_sdk/types.py +346 -0
  45. surrealdb_orm-0.5.0.dist-info/METADATA +465 -0
  46. surrealdb_orm-0.5.0.dist-info/RECORD +52 -0
  47. {surrealdb_orm-0.1.3.dist-info → surrealdb_orm-0.5.0.dist-info}/WHEEL +1 -1
  48. surrealdb_orm-0.5.0.dist-info/entry_points.txt +2 -0
  49. {surrealdb_orm-0.1.3.dist-info → surrealdb_orm-0.5.0.dist-info}/licenses/LICENSE +1 -1
  50. surrealdb_orm-0.1.3.dist-info/METADATA +0 -184
  51. surrealdb_orm-0.1.3.dist-info/RECORD +0 -11
@@ -0,0 +1,386 @@
1
+ """
2
+ Transaction support for SurrealDB SDK.
3
+
4
+ Provides atomic transaction handling for both HTTP and WebSocket connections.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass, field
9
+ from typing import TYPE_CHECKING, Any, Self
10
+
11
+ from .exceptions import TransactionError
12
+
13
+ if TYPE_CHECKING:
14
+ from .connection.base import BaseSurrealConnection
15
+ from .types import (
16
+ DeleteResponse,
17
+ QueryResponse,
18
+ RecordResponse,
19
+ RecordsResponse,
20
+ )
21
+
22
+
23
+ @dataclass
24
+ class TransactionStatement:
25
+ """A single statement queued in a transaction."""
26
+
27
+ sql: str
28
+ vars: dict[str, Any] = field(default_factory=dict)
29
+
30
+
31
+ class BaseTransaction(ABC):
32
+ """
33
+ Abstract base class for transaction handling.
34
+
35
+ Provides a unified interface for atomic operations that works
36
+ across both HTTP and WebSocket connections.
37
+
38
+ Usage:
39
+ async with conn.transaction() as tx:
40
+ await tx.update("players:abc", {"is_ready": True})
41
+ await tx.update("game_tables:xyz", {"ready_count": 1})
42
+ # Auto-commit on success, auto-rollback on exception
43
+ """
44
+
45
+ def __init__(self, connection: "BaseSurrealConnection"):
46
+ self._connection = connection
47
+ self._statements: list[TransactionStatement] = []
48
+ self._committed = False
49
+ self._rolled_back = False
50
+ self._active = False
51
+
52
+ @property
53
+ def is_active(self) -> bool:
54
+ """Check if transaction is active."""
55
+ return self._active and not self._committed and not self._rolled_back
56
+
57
+ @property
58
+ def is_committed(self) -> bool:
59
+ """Check if transaction was committed."""
60
+ return self._committed
61
+
62
+ @property
63
+ def is_rolled_back(self) -> bool:
64
+ """Check if transaction was rolled back."""
65
+ return self._rolled_back
66
+
67
+ async def __aenter__(self) -> Self:
68
+ """Begin transaction on context entry."""
69
+ await self._begin()
70
+ return self
71
+
72
+ async def __aexit__(
73
+ self,
74
+ exc_type: type[BaseException] | None,
75
+ exc_val: BaseException | None,
76
+ exc_tb: Any,
77
+ ) -> bool:
78
+ """Commit on success, rollback on exception."""
79
+ if exc_type is not None:
80
+ await self.rollback()
81
+ return False # Re-raise exception
82
+ await self.commit()
83
+ return False
84
+
85
+ @abstractmethod
86
+ async def _begin(self) -> None:
87
+ """Begin the transaction."""
88
+ ...
89
+
90
+ @abstractmethod
91
+ async def commit(self) -> "QueryResponse":
92
+ """Commit the transaction."""
93
+ ...
94
+
95
+ @abstractmethod
96
+ async def rollback(self) -> None:
97
+ """Rollback the transaction."""
98
+ ...
99
+
100
+ # Transaction operations (to be implemented by subclasses)
101
+
102
+ @abstractmethod
103
+ async def query(self, sql: str, vars: dict[str, Any] | None = None) -> "QueryResponse":
104
+ """Execute a query within the transaction."""
105
+ ...
106
+
107
+ @abstractmethod
108
+ async def create(self, thing: str, data: dict[str, Any] | None = None) -> "RecordResponse":
109
+ """Create a record within the transaction."""
110
+ ...
111
+
112
+ @abstractmethod
113
+ async def insert(self, table: str, data: list[dict[str, Any]] | dict[str, Any]) -> "RecordsResponse":
114
+ """Insert records within the transaction."""
115
+ ...
116
+
117
+ @abstractmethod
118
+ async def update(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
119
+ """Update records within the transaction."""
120
+ ...
121
+
122
+ @abstractmethod
123
+ async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
124
+ """Merge data into records within the transaction."""
125
+ ...
126
+
127
+ @abstractmethod
128
+ async def delete(self, thing: str) -> "DeleteResponse":
129
+ """Delete records within the transaction."""
130
+ ...
131
+
132
+ @abstractmethod
133
+ async def relate(
134
+ self,
135
+ from_thing: str,
136
+ relation: str,
137
+ to_thing: str,
138
+ data: dict[str, Any] | None = None,
139
+ ) -> "RecordResponse":
140
+ """Create a relation within the transaction."""
141
+ ...
142
+
143
+
144
+ class HTTPTransaction(BaseTransaction):
145
+ """
146
+ HTTP-based transaction that batches statements.
147
+
148
+ Since HTTP is stateless, all statements are collected and
149
+ executed as a single atomic query on commit.
150
+
151
+ The statements are wrapped in BEGIN TRANSACTION / COMMIT TRANSACTION
152
+ and sent as a single request.
153
+ """
154
+
155
+ async def _begin(self) -> None:
156
+ """Mark transaction as active (no server call needed for HTTP)."""
157
+ if self._active:
158
+ raise TransactionError("Transaction already active")
159
+ self._active = True
160
+ self._statements = []
161
+
162
+ async def commit(self) -> "QueryResponse":
163
+ """Execute all queued statements atomically."""
164
+ from .types import QueryResponse
165
+
166
+ if not self.is_active:
167
+ raise TransactionError("Transaction not active")
168
+
169
+ if not self._statements:
170
+ # Empty transaction, just mark as committed
171
+ self._committed = True
172
+ self._active = False
173
+ return QueryResponse(results=[], raw=[])
174
+
175
+ # Build batched query with BEGIN/COMMIT wrapper
176
+ sql_parts = ["BEGIN TRANSACTION;"]
177
+ all_vars: dict[str, Any] = {}
178
+
179
+ for i, stmt in enumerate(self._statements):
180
+ sql_parts.append(stmt.sql)
181
+ # Namespace variables to avoid conflicts between statements
182
+ for key, val in stmt.vars.items():
183
+ namespaced_key = f"tx_{i}_{key}"
184
+ all_vars[namespaced_key] = val
185
+ # Replace variable reference in SQL
186
+ stmt.sql = stmt.sql.replace(f"${key}", f"${namespaced_key}")
187
+ sql_parts[i + 1] = stmt.sql # Update with namespaced vars
188
+
189
+ sql_parts.append("COMMIT TRANSACTION;")
190
+ full_sql = "\n".join(sql_parts)
191
+
192
+ try:
193
+ result = await self._connection.query(full_sql, all_vars)
194
+ self._committed = True
195
+ self._active = False
196
+ return result
197
+ except Exception as e:
198
+ self._active = False
199
+ raise TransactionError(f"Transaction commit failed: {e}")
200
+
201
+ async def rollback(self) -> None:
202
+ """Discard queued statements (no server call needed for HTTP)."""
203
+ self._statements = []
204
+ self._rolled_back = True
205
+ self._active = False
206
+
207
+ def _queue_statement(self, sql: str, vars: dict[str, Any] | None = None) -> None:
208
+ """Queue a statement for later execution."""
209
+ if not self.is_active:
210
+ raise TransactionError("Transaction not active")
211
+ self._statements.append(TransactionStatement(sql=sql, vars=vars or {}))
212
+
213
+ async def query(self, sql: str, vars: dict[str, Any] | None = None) -> "QueryResponse":
214
+ """Queue a query for execution on commit."""
215
+ from .types import QueryResponse
216
+
217
+ self._queue_statement(sql, vars)
218
+ # Return empty response since actual execution happens on commit
219
+ return QueryResponse(results=[], raw=[])
220
+
221
+ async def create(self, thing: str, data: dict[str, Any] | None = None) -> "RecordResponse":
222
+ """Queue a create operation."""
223
+ from .types import RecordResponse
224
+
225
+ if data:
226
+ # Build CREATE statement with data
227
+ fields = ", ".join(f"{k} = ${k}" for k in data.keys())
228
+ sql = f"CREATE {thing} SET {fields};"
229
+ self._queue_statement(sql, data)
230
+ else:
231
+ sql = f"CREATE {thing};"
232
+ self._queue_statement(sql)
233
+ return RecordResponse(record=None, raw=None)
234
+
235
+ async def insert(self, table: str, data: list[dict[str, Any]] | dict[str, Any]) -> "RecordsResponse":
236
+ """Queue an insert operation."""
237
+ from .types import RecordsResponse
238
+
239
+ if isinstance(data, dict):
240
+ data = [data]
241
+
242
+ for i, record in enumerate(data):
243
+ fields = ", ".join(f"{k} = $r{i}_{k}" for k in record.keys())
244
+ sql = f"CREATE {table} SET {fields};"
245
+ vars_with_prefix = {f"r{i}_{k}": v for k, v in record.items()}
246
+ self._queue_statement(sql, vars_with_prefix)
247
+
248
+ return RecordsResponse(records=[], raw=[])
249
+
250
+ async def update(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
251
+ """Queue an update operation."""
252
+ from .types import RecordsResponse
253
+
254
+ fields = ", ".join(f"{k} = ${k}" for k in data.keys())
255
+ sql = f"UPDATE {thing} SET {fields};"
256
+ self._queue_statement(sql, data)
257
+ return RecordsResponse(records=[], raw=[])
258
+
259
+ async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
260
+ """Queue a merge operation."""
261
+ from .types import RecordsResponse
262
+
263
+ fields = ", ".join(f"{k} = ${k}" for k in data.keys())
264
+ sql = f"UPDATE {thing} MERGE {{ {fields} }};"
265
+ self._queue_statement(sql, data)
266
+ return RecordsResponse(records=[], raw=[])
267
+
268
+ async def delete(self, thing: str) -> "DeleteResponse":
269
+ """Queue a delete operation."""
270
+ from .types import DeleteResponse
271
+
272
+ sql = f"DELETE {thing};"
273
+ self._queue_statement(sql)
274
+ return DeleteResponse(deleted=[], raw=[])
275
+
276
+ async def relate(
277
+ self,
278
+ from_thing: str,
279
+ relation: str,
280
+ to_thing: str,
281
+ data: dict[str, Any] | None = None,
282
+ ) -> "RecordResponse":
283
+ """Queue a relate operation."""
284
+ from .types import RecordResponse
285
+
286
+ if data:
287
+ fields = ", ".join(f"{k} = ${k}" for k in data.keys())
288
+ sql = f"RELATE {from_thing}->{relation}->{to_thing} SET {fields};"
289
+ self._queue_statement(sql, data)
290
+ else:
291
+ sql = f"RELATE {from_thing}->{relation}->{to_thing};"
292
+ self._queue_statement(sql)
293
+ return RecordResponse(record=None, raw=None)
294
+
295
+
296
+ class WebSocketTransaction(BaseTransaction):
297
+ """
298
+ WebSocket-based transaction with server-side state.
299
+
300
+ Uses actual BEGIN/COMMIT/ROLLBACK commands since
301
+ WebSocket maintains session state across requests.
302
+
303
+ Operations are executed immediately within the transaction context.
304
+ """
305
+
306
+ async def _begin(self) -> None:
307
+ """Send BEGIN TRANSACTION to server."""
308
+ if self._active:
309
+ raise TransactionError("Transaction already active")
310
+ await self._connection.query("BEGIN TRANSACTION;")
311
+ self._active = True
312
+
313
+ async def commit(self) -> "QueryResponse":
314
+ """Send COMMIT to server."""
315
+ if not self.is_active:
316
+ raise TransactionError("Transaction not active")
317
+
318
+ try:
319
+ result = await self._connection.query("COMMIT TRANSACTION;")
320
+ self._committed = True
321
+ self._active = False
322
+ return result
323
+ except Exception as e:
324
+ self._active = False
325
+ raise TransactionError(f"Commit failed: {e}")
326
+
327
+ async def rollback(self) -> None:
328
+ """Send ROLLBACK to server."""
329
+ if not self.is_active:
330
+ return
331
+
332
+ try:
333
+ await self._connection.query("CANCEL TRANSACTION;")
334
+ except Exception:
335
+ pass # Best effort rollback
336
+ finally:
337
+ self._rolled_back = True
338
+ self._active = False
339
+
340
+ async def query(self, sql: str, vars: dict[str, Any] | None = None) -> "QueryResponse":
341
+ """Execute query immediately within transaction."""
342
+ if not self.is_active:
343
+ raise TransactionError("Transaction not active")
344
+ return await self._connection.query(sql, vars)
345
+
346
+ async def create(self, thing: str, data: dict[str, Any] | None = None) -> "RecordResponse":
347
+ """Execute create immediately within transaction."""
348
+ if not self.is_active:
349
+ raise TransactionError("Transaction not active")
350
+ return await self._connection.create(thing, data)
351
+
352
+ async def insert(self, table: str, data: list[dict[str, Any]] | dict[str, Any]) -> "RecordsResponse":
353
+ """Execute insert immediately within transaction."""
354
+ if not self.is_active:
355
+ raise TransactionError("Transaction not active")
356
+ return await self._connection.insert(table, data)
357
+
358
+ async def update(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
359
+ """Execute update immediately within transaction."""
360
+ if not self.is_active:
361
+ raise TransactionError("Transaction not active")
362
+ return await self._connection.update(thing, data)
363
+
364
+ async def merge(self, thing: str, data: dict[str, Any]) -> "RecordsResponse":
365
+ """Execute merge immediately within transaction."""
366
+ if not self.is_active:
367
+ raise TransactionError("Transaction not active")
368
+ return await self._connection.merge(thing, data)
369
+
370
+ async def delete(self, thing: str) -> "DeleteResponse":
371
+ """Execute delete immediately within transaction."""
372
+ if not self.is_active:
373
+ raise TransactionError("Transaction not active")
374
+ return await self._connection.delete(thing)
375
+
376
+ async def relate(
377
+ self,
378
+ from_thing: str,
379
+ relation: str,
380
+ to_thing: str,
381
+ data: dict[str, Any] | None = None,
382
+ ) -> "RecordResponse":
383
+ """Execute relate immediately within transaction."""
384
+ if not self.is_active:
385
+ raise TransactionError("Transaction not active")
386
+ return await self._connection.relate(from_thing, relation, to_thing, data)