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,265 @@
1
+ """
2
+ Live Query Streaming Implementation.
3
+
4
+ Provides real-time change notifications via WebSocket Live Queries.
5
+ """
6
+
7
+ from typing import Any, Callable, Awaitable
8
+ from dataclasses import dataclass
9
+ from enum import StrEnum
10
+
11
+ from ..connection.websocket import WebSocketConnection
12
+ from ..exceptions import LiveQueryError
13
+
14
+
15
+ class LiveAction(StrEnum):
16
+ """Live query action types."""
17
+
18
+ CREATE = "CREATE"
19
+ UPDATE = "UPDATE"
20
+ DELETE = "DELETE"
21
+
22
+
23
+ @dataclass
24
+ class LiveNotification:
25
+ """
26
+ Live query notification.
27
+
28
+ Attributes:
29
+ id: Live query UUID
30
+ action: CREATE, UPDATE, or DELETE
31
+ result: The affected record
32
+ """
33
+
34
+ id: str
35
+ action: LiveAction
36
+ result: dict[str, Any]
37
+
38
+ @classmethod
39
+ def from_dict(cls, data: dict[str, Any]) -> "LiveNotification":
40
+ """Parse from WebSocket message."""
41
+ return cls(
42
+ id=data.get("id", ""),
43
+ action=LiveAction(data.get("action", "UPDATE")),
44
+ result=data.get("result", {}),
45
+ )
46
+
47
+
48
+ # Type alias for callbacks
49
+ LiveCallback = Callable[[LiveNotification], Awaitable[None]]
50
+
51
+
52
+ class LiveQuery:
53
+ """
54
+ Manage a live query subscription.
55
+
56
+ Live queries provide real-time notifications when data changes.
57
+ They require a WebSocket connection.
58
+
59
+ Usage:
60
+ async with WebSocketConnection("ws://localhost:8000", "ns", "db") as conn:
61
+ await conn.signin("root", "root")
62
+
63
+ async def on_change(notification: LiveNotification):
64
+ print(f"{notification.action}: {notification.result}")
65
+
66
+ live = LiveQuery(conn, "orders")
67
+ await live.subscribe(on_change)
68
+
69
+ # Keep running...
70
+ await asyncio.sleep(3600)
71
+
72
+ await live.unsubscribe()
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ connection: WebSocketConnection,
78
+ table: str,
79
+ where: str | None = None,
80
+ diff: bool = False,
81
+ ):
82
+ """
83
+ Initialize live query.
84
+
85
+ Args:
86
+ connection: WebSocket connection to use
87
+ table: Table to watch
88
+ where: Optional WHERE clause filter
89
+ diff: If True, receive only changed fields
90
+ """
91
+ self.connection = connection
92
+ self.table = table
93
+ self.where = where
94
+ self.diff = diff
95
+ self._live_id: str | None = None
96
+ self._callback: LiveCallback | None = None
97
+ self._active = False
98
+
99
+ @property
100
+ def is_active(self) -> bool:
101
+ """Check if live query is active."""
102
+ return self._active and self._live_id is not None
103
+
104
+ @property
105
+ def live_id(self) -> str | None:
106
+ """Get the live query UUID."""
107
+ return self._live_id
108
+
109
+ async def subscribe(self, callback: LiveCallback) -> str:
110
+ """
111
+ Start the live query subscription.
112
+
113
+ Args:
114
+ callback: Async function to call on changes
115
+
116
+ Returns:
117
+ Live query UUID
118
+
119
+ Raises:
120
+ LiveQueryError: If subscription fails
121
+ """
122
+ if self._active:
123
+ raise LiveQueryError("Live query already active")
124
+
125
+ self._callback = callback
126
+
127
+ # Build query
128
+ sql = f"LIVE SELECT * FROM {self.table}"
129
+ if self.where:
130
+ sql += f" WHERE {self.where}"
131
+ if self.diff:
132
+ sql += " DIFF"
133
+
134
+ try:
135
+ response = await self.connection.query(sql)
136
+
137
+ # Extract live query UUID
138
+ if response.results:
139
+ first_result = response.results[0]
140
+ if first_result.is_ok:
141
+ result_data = first_result.result
142
+ if isinstance(result_data, str):
143
+ self._live_id = result_data
144
+ elif isinstance(result_data, dict) and "result" in result_data:
145
+ self._live_id = str(result_data["result"])
146
+ else:
147
+ raise LiveQueryError("Invalid live query response")
148
+
149
+ # Register callback with connection
150
+ self.connection._live_callbacks[self._live_id] = self._handle_notification
151
+ self._active = True
152
+ return self._live_id
153
+
154
+ raise LiveQueryError("No live query ID returned")
155
+
156
+ except Exception as e:
157
+ raise LiveQueryError(f"Failed to start live query: {e}")
158
+
159
+ async def unsubscribe(self) -> None:
160
+ """Stop the live query subscription."""
161
+ if self._live_id:
162
+ try:
163
+ await self.connection.kill(self._live_id)
164
+ except Exception:
165
+ pass
166
+ finally:
167
+ self.connection._live_callbacks.pop(self._live_id, None)
168
+ self._live_id = None
169
+ self._active = False
170
+
171
+ async def _handle_notification(self, data: dict[str, Any]) -> None:
172
+ """Handle incoming live query notification."""
173
+ if self._callback:
174
+ notification = LiveNotification.from_dict(data)
175
+ await self._callback(notification)
176
+
177
+ async def __aenter__(self) -> "LiveQuery":
178
+ """Context manager entry (requires subscribe call)."""
179
+ return self
180
+
181
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
182
+ """Context manager exit."""
183
+ await self.unsubscribe()
184
+
185
+
186
+ class LiveQueryManager:
187
+ """
188
+ Manage multiple live queries on a single connection.
189
+
190
+ Usage:
191
+ async with WebSocketConnection("ws://localhost:8000", "ns", "db") as conn:
192
+ await conn.signin("root", "root")
193
+
194
+ manager = LiveQueryManager(conn)
195
+
196
+ await manager.watch("users", on_user_change)
197
+ await manager.watch("orders", on_order_change)
198
+ await manager.watch("products", on_product_change)
199
+
200
+ # Keep running...
201
+ await asyncio.sleep(3600)
202
+
203
+ await manager.unwatch_all()
204
+ """
205
+
206
+ def __init__(self, connection: WebSocketConnection):
207
+ """
208
+ Initialize live query manager.
209
+
210
+ Args:
211
+ connection: WebSocket connection to use
212
+ """
213
+ self.connection = connection
214
+ self._queries: dict[str, LiveQuery] = {}
215
+
216
+ async def watch(
217
+ self,
218
+ table: str,
219
+ callback: LiveCallback,
220
+ where: str | None = None,
221
+ diff: bool = False,
222
+ ) -> str:
223
+ """
224
+ Start watching a table.
225
+
226
+ Args:
227
+ table: Table to watch
228
+ callback: Callback for changes
229
+ where: Optional filter
230
+ diff: If True, receive only changed fields
231
+
232
+ Returns:
233
+ Live query UUID
234
+ """
235
+ query = LiveQuery(self.connection, table, where, diff)
236
+ live_id = await query.subscribe(callback)
237
+ self._queries[live_id] = query
238
+ return live_id
239
+
240
+ async def unwatch(self, live_id: str) -> None:
241
+ """
242
+ Stop watching a specific live query.
243
+
244
+ Args:
245
+ live_id: Live query UUID to stop
246
+ """
247
+ if live_id in self._queries:
248
+ await self._queries[live_id].unsubscribe()
249
+ del self._queries[live_id]
250
+
251
+ async def unwatch_all(self) -> None:
252
+ """Stop all live queries."""
253
+ for query in list(self._queries.values()):
254
+ await query.unsubscribe()
255
+ self._queries.clear()
256
+
257
+ @property
258
+ def active_queries(self) -> list[str]:
259
+ """Get list of active live query IDs."""
260
+ return list(self._queries.keys())
261
+
262
+ @property
263
+ def count(self) -> int:
264
+ """Number of active live queries."""
265
+ return len(self._queries)
@@ -0,0 +1,369 @@
1
+ """
2
+ Live Select Stream Implementation.
3
+
4
+ Provides async iterator pattern for real-time change notifications via WebSocket Live Queries.
5
+ """
6
+
7
+ from typing import Any, Callable, Awaitable, Coroutine, Self
8
+ from dataclasses import dataclass, field
9
+ from enum import StrEnum
10
+ import asyncio
11
+
12
+ from ..connection.websocket import WebSocketConnection
13
+ from ..exceptions import LiveQueryError
14
+
15
+
16
+ class LiveAction(StrEnum):
17
+ """Live query action types."""
18
+
19
+ CREATE = "CREATE"
20
+ UPDATE = "UPDATE"
21
+ DELETE = "DELETE"
22
+
23
+
24
+ @dataclass
25
+ class LiveChange:
26
+ """
27
+ Enhanced live query change notification.
28
+
29
+ Attributes:
30
+ id: Live query UUID
31
+ action: CREATE, UPDATE, or DELETE
32
+ record_id: The affected record ID (e.g., "players:abc")
33
+ result: The full record after the change
34
+ before: The record before the change (if DIFF mode)
35
+ changed_fields: List of changed field names (if DIFF mode)
36
+ """
37
+
38
+ id: str
39
+ action: LiveAction
40
+ record_id: str
41
+ result: dict[str, Any]
42
+ before: dict[str, Any] | None = None
43
+ changed_fields: list[str] = field(default_factory=list)
44
+
45
+ @classmethod
46
+ def from_dict(cls, data: dict[str, Any]) -> "LiveChange":
47
+ """Parse from WebSocket message."""
48
+ result = data.get("result", {})
49
+ record_id = ""
50
+
51
+ # Extract record ID from result
52
+ if isinstance(result, dict) and "id" in result:
53
+ record_id = str(result["id"])
54
+
55
+ # Parse changed fields if present (DIFF mode)
56
+ changed_fields: list[str] = []
57
+ if isinstance(result, list):
58
+ # DIFF mode returns list of patches
59
+ for patch in result:
60
+ if isinstance(patch, dict) and "path" in patch:
61
+ # JSON Patch format: {"op": "replace", "path": "/field", "value": ...}
62
+ path = patch.get("path", "")
63
+ if path.startswith("/"):
64
+ changed_fields.append(path[1:].split("/")[0])
65
+
66
+ return cls(
67
+ id=data.get("id", ""),
68
+ action=LiveAction(data.get("action", "UPDATE")),
69
+ record_id=record_id,
70
+ result=result if isinstance(result, dict) else {},
71
+ before=None, # Could be populated from DIFF data
72
+ changed_fields=changed_fields,
73
+ )
74
+
75
+
76
+ # Type alias for callbacks
77
+ LiveCallback = Callable[[LiveChange], Awaitable[None]]
78
+ ReconnectCallback = Callable[[str, str], Coroutine[Any, Any, None]] # (old_id, new_id)
79
+
80
+
81
+ @dataclass
82
+ class LiveSubscriptionParams:
83
+ """Parameters for recreating a live subscription after reconnect."""
84
+
85
+ table: str
86
+ where: str | None
87
+ params: dict[str, Any]
88
+ diff: bool
89
+ callback: LiveCallback | None
90
+ on_reconnect: ReconnectCallback | None
91
+
92
+
93
+ class LiveSelectStream:
94
+ """
95
+ Async iterator for live query subscriptions.
96
+
97
+ Provides a pythonic async iterator interface for receiving real-time
98
+ database change notifications.
99
+
100
+ Usage:
101
+ async with conn.live_select("players", where="table_id = $id", params={"id": table_id}) as stream:
102
+ async for change in stream:
103
+ match change.action:
104
+ case LiveAction.CREATE:
105
+ print(f"New player: {change.result}")
106
+ case LiveAction.UPDATE:
107
+ print(f"Player updated: {change.record_id}")
108
+ case LiveAction.DELETE:
109
+ print(f"Player left: {change.record_id}")
110
+ """
111
+
112
+ def __init__(
113
+ self,
114
+ connection: WebSocketConnection,
115
+ table: str,
116
+ where: str | None = None,
117
+ params: dict[str, Any] | None = None,
118
+ diff: bool = False,
119
+ auto_resubscribe: bool = True,
120
+ on_reconnect: ReconnectCallback | None = None,
121
+ ):
122
+ """
123
+ Initialize live select stream.
124
+
125
+ Args:
126
+ connection: WebSocket connection to use
127
+ table: Table to watch
128
+ where: Optional WHERE clause filter (e.g., "table_id = $id")
129
+ params: Parameters for the WHERE clause
130
+ diff: If True, receive only changed fields
131
+ auto_resubscribe: If True, automatically resubscribe on reconnect
132
+ on_reconnect: Optional callback when resubscribed (old_id, new_id)
133
+ """
134
+ self.connection = connection
135
+ self.table = table
136
+ self.where = where
137
+ self.params = params or {}
138
+ self.diff = diff
139
+ self.auto_resubscribe = auto_resubscribe
140
+ self.on_reconnect = on_reconnect
141
+
142
+ self._live_id: str | None = None
143
+ self._queue: asyncio.Queue[LiveChange] = asyncio.Queue()
144
+ self._active = False
145
+ self._closed = False
146
+
147
+ @property
148
+ def is_active(self) -> bool:
149
+ """Check if stream is active."""
150
+ return self._active and self._live_id is not None
151
+
152
+ @property
153
+ def live_id(self) -> str | None:
154
+ """Get the live query UUID."""
155
+ return self._live_id
156
+
157
+ async def start(self) -> str:
158
+ """
159
+ Start the live query subscription.
160
+
161
+ Returns:
162
+ Live query UUID
163
+
164
+ Raises:
165
+ LiveQueryError: If subscription fails
166
+ """
167
+ if self._active:
168
+ raise LiveQueryError("Live select already active")
169
+
170
+ # Build query with variable substitution
171
+ sql = f"LIVE SELECT * FROM {self.table}"
172
+ if self.where:
173
+ sql += f" WHERE {self.where}"
174
+ if self.diff:
175
+ sql += " DIFF"
176
+
177
+ try:
178
+ # Set session variables for parameters
179
+ for key, value in self.params.items():
180
+ await self.connection.let(key, value)
181
+
182
+ response = await self.connection.query(sql)
183
+
184
+ # Extract live query UUID
185
+ if response.results:
186
+ first_result = response.results[0]
187
+ if first_result.is_ok:
188
+ result_data = first_result.result
189
+ if isinstance(result_data, str):
190
+ self._live_id = result_data
191
+ elif isinstance(result_data, dict) and "result" in result_data:
192
+ self._live_id = str(result_data["result"])
193
+ else:
194
+ raise LiveQueryError("Invalid live query response")
195
+
196
+ # Register callback with connection
197
+ self.connection._live_callbacks[self._live_id] = self._handle_notification
198
+
199
+ # Register for auto-resubscribe if enabled
200
+ if self.auto_resubscribe:
201
+ self.connection._register_live_subscription(
202
+ self._live_id,
203
+ LiveSubscriptionParams(
204
+ table=self.table,
205
+ where=self.where,
206
+ params=self.params,
207
+ diff=self.diff,
208
+ callback=None, # We use queue, not callback
209
+ on_reconnect=self.on_reconnect,
210
+ ),
211
+ )
212
+
213
+ self._active = True
214
+ return self._live_id
215
+
216
+ raise LiveQueryError("No live query ID returned")
217
+
218
+ except Exception as e:
219
+ raise LiveQueryError(f"Failed to start live select: {e}")
220
+
221
+ async def stop(self) -> None:
222
+ """Stop the live query subscription."""
223
+ if self._live_id:
224
+ try:
225
+ # Unregister from auto-resubscribe
226
+ self.connection._unregister_live_subscription(self._live_id)
227
+ await self.connection.kill(self._live_id)
228
+ except Exception:
229
+ pass
230
+ finally:
231
+ self.connection._live_callbacks.pop(self._live_id, None)
232
+ self._live_id = None
233
+ self._active = False
234
+
235
+ self._closed = True
236
+ # Signal end of stream
237
+ await self._queue.put(None) # type: ignore
238
+
239
+ async def _handle_notification(self, data: dict[str, Any]) -> None:
240
+ """Handle incoming live query notification."""
241
+ change = LiveChange.from_dict(data)
242
+ await self._queue.put(change)
243
+
244
+ def _update_live_id(self, new_id: str) -> None:
245
+ """Update live ID after reconnection (called by connection)."""
246
+ self._live_id = new_id
247
+ # Re-register callback with new ID
248
+ self.connection._live_callbacks[new_id] = self._handle_notification
249
+
250
+ # Async iterator protocol
251
+
252
+ def __aiter__(self) -> Self:
253
+ """Return async iterator."""
254
+ return self
255
+
256
+ async def __anext__(self) -> LiveChange:
257
+ """Get next change from stream."""
258
+ if self._closed and self._queue.empty():
259
+ raise StopAsyncIteration
260
+
261
+ change = await self._queue.get()
262
+ if change is None:
263
+ raise StopAsyncIteration
264
+ return change
265
+
266
+ # Context manager protocol
267
+
268
+ async def __aenter__(self) -> Self:
269
+ """Start stream on context entry."""
270
+ await self.start()
271
+ return self
272
+
273
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
274
+ """Stop stream on context exit."""
275
+ await self.stop()
276
+
277
+
278
+ class LiveSelectManager:
279
+ """
280
+ Manage multiple live select streams on a single connection.
281
+
282
+ Usage:
283
+ manager = LiveSelectManager(conn)
284
+
285
+ await manager.watch("game_tables", on_table_change, where="id = $id", params={"id": table_id})
286
+ await manager.watch("players", on_player_change, where="table_id = $id", params={"id": table_id})
287
+
288
+ # Keep running...
289
+ await asyncio.sleep(3600)
290
+
291
+ await manager.stop_all()
292
+ """
293
+
294
+ def __init__(self, connection: WebSocketConnection):
295
+ """Initialize manager."""
296
+ self.connection = connection
297
+ self._streams: dict[str, LiveSelectStream] = {}
298
+
299
+ async def watch(
300
+ self,
301
+ table: str,
302
+ callback: LiveCallback,
303
+ where: str | None = None,
304
+ params: dict[str, Any] | None = None,
305
+ diff: bool = False,
306
+ auto_resubscribe: bool = True,
307
+ on_reconnect: ReconnectCallback | None = None,
308
+ ) -> str:
309
+ """
310
+ Start watching a table with callback.
311
+
312
+ Args:
313
+ table: Table to watch
314
+ callback: Async callback for changes
315
+ where: Optional filter
316
+ params: Filter parameters
317
+ diff: If True, receive only changed fields
318
+ auto_resubscribe: Resubscribe on reconnect
319
+ on_reconnect: Callback when resubscribed
320
+
321
+ Returns:
322
+ Live query UUID
323
+ """
324
+ stream = LiveSelectStream(
325
+ self.connection,
326
+ table,
327
+ where=where,
328
+ params=params,
329
+ diff=diff,
330
+ auto_resubscribe=auto_resubscribe,
331
+ on_reconnect=on_reconnect,
332
+ )
333
+ live_id = await stream.start()
334
+ self._streams[live_id] = stream
335
+
336
+ # Start background task to forward to callback
337
+ asyncio.create_task(self._forward_to_callback(stream, callback))
338
+
339
+ return live_id
340
+
341
+ async def _forward_to_callback(self, stream: LiveSelectStream, callback: LiveCallback) -> None:
342
+ """Forward stream changes to callback."""
343
+ try:
344
+ async for change in stream:
345
+ await callback(change)
346
+ except Exception:
347
+ pass
348
+
349
+ async def stop(self, live_id: str) -> None:
350
+ """Stop a specific stream."""
351
+ if live_id in self._streams:
352
+ await self._streams[live_id].stop()
353
+ del self._streams[live_id]
354
+
355
+ async def stop_all(self) -> None:
356
+ """Stop all streams."""
357
+ for stream in list(self._streams.values()):
358
+ await stream.stop()
359
+ self._streams.clear()
360
+
361
+ @property
362
+ def active_streams(self) -> list[str]:
363
+ """Get list of active stream IDs."""
364
+ return list(self._streams.keys())
365
+
366
+ @property
367
+ def count(self) -> int:
368
+ """Number of active streams."""
369
+ return len(self._streams)