surrealdb-orm 0.1.4__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.
- surreal_orm/__init__.py +72 -3
- surreal_orm/aggregations.py +164 -0
- surreal_orm/auth/__init__.py +15 -0
- surreal_orm/auth/access.py +167 -0
- surreal_orm/auth/mixins.py +302 -0
- surreal_orm/cli/__init__.py +15 -0
- surreal_orm/cli/commands.py +369 -0
- surreal_orm/connection_manager.py +58 -18
- surreal_orm/fields/__init__.py +36 -0
- surreal_orm/fields/encrypted.py +166 -0
- surreal_orm/fields/relation.py +465 -0
- surreal_orm/migrations/__init__.py +51 -0
- surreal_orm/migrations/executor.py +380 -0
- surreal_orm/migrations/generator.py +272 -0
- surreal_orm/migrations/introspector.py +305 -0
- surreal_orm/migrations/migration.py +188 -0
- surreal_orm/migrations/operations.py +531 -0
- surreal_orm/migrations/state.py +406 -0
- surreal_orm/model_base.py +530 -44
- surreal_orm/query_set.py +609 -33
- surreal_orm/relations.py +645 -0
- surreal_orm/surreal_function.py +95 -0
- surreal_orm/surreal_ql.py +113 -0
- surreal_orm/types.py +86 -0
- surreal_sdk/README.md +79 -0
- surreal_sdk/__init__.py +151 -0
- surreal_sdk/connection/__init__.py +17 -0
- surreal_sdk/connection/base.py +516 -0
- surreal_sdk/connection/http.py +421 -0
- surreal_sdk/connection/pool.py +244 -0
- surreal_sdk/connection/websocket.py +519 -0
- surreal_sdk/exceptions.py +71 -0
- surreal_sdk/functions.py +607 -0
- surreal_sdk/protocol/__init__.py +13 -0
- surreal_sdk/protocol/rpc.py +218 -0
- surreal_sdk/py.typed +0 -0
- surreal_sdk/pyproject.toml +49 -0
- surreal_sdk/streaming/__init__.py +31 -0
- surreal_sdk/streaming/change_feed.py +278 -0
- surreal_sdk/streaming/live_query.py +265 -0
- surreal_sdk/streaming/live_select.py +369 -0
- surreal_sdk/transaction.py +386 -0
- surreal_sdk/types.py +346 -0
- surrealdb_orm-0.5.0.dist-info/METADATA +465 -0
- surrealdb_orm-0.5.0.dist-info/RECORD +52 -0
- {surrealdb_orm-0.1.4.dist-info → surrealdb_orm-0.5.0.dist-info}/WHEEL +1 -1
- surrealdb_orm-0.5.0.dist-info/entry_points.txt +2 -0
- {surrealdb_orm-0.1.4.dist-info → surrealdb_orm-0.5.0.dist-info}/licenses/LICENSE +1 -1
- surrealdb_orm-0.1.4.dist-info/METADATA +0 -184
- surrealdb_orm-0.1.4.dist-info/RECORD +0 -12
|
@@ -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)
|