audex 1.0.7a3__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 (192) hide show
  1. audex/__init__.py +9 -0
  2. audex/__main__.py +7 -0
  3. audex/cli/__init__.py +189 -0
  4. audex/cli/apis/__init__.py +12 -0
  5. audex/cli/apis/init/__init__.py +34 -0
  6. audex/cli/apis/init/gencfg.py +130 -0
  7. audex/cli/apis/init/setup.py +330 -0
  8. audex/cli/apis/init/vprgroup.py +125 -0
  9. audex/cli/apis/serve.py +141 -0
  10. audex/cli/args.py +356 -0
  11. audex/cli/exceptions.py +44 -0
  12. audex/cli/helper/__init__.py +0 -0
  13. audex/cli/helper/ansi.py +193 -0
  14. audex/cli/helper/display.py +288 -0
  15. audex/config/__init__.py +64 -0
  16. audex/config/core/__init__.py +30 -0
  17. audex/config/core/app.py +29 -0
  18. audex/config/core/audio.py +45 -0
  19. audex/config/core/logging.py +163 -0
  20. audex/config/core/session.py +11 -0
  21. audex/config/helper/__init__.py +1 -0
  22. audex/config/helper/client/__init__.py +1 -0
  23. audex/config/helper/client/http.py +28 -0
  24. audex/config/helper/client/websocket.py +21 -0
  25. audex/config/helper/provider/__init__.py +1 -0
  26. audex/config/helper/provider/dashscope.py +13 -0
  27. audex/config/helper/provider/unisound.py +18 -0
  28. audex/config/helper/provider/xfyun.py +23 -0
  29. audex/config/infrastructure/__init__.py +31 -0
  30. audex/config/infrastructure/cache.py +51 -0
  31. audex/config/infrastructure/database.py +48 -0
  32. audex/config/infrastructure/recorder.py +32 -0
  33. audex/config/infrastructure/store.py +19 -0
  34. audex/config/provider/__init__.py +18 -0
  35. audex/config/provider/transcription.py +109 -0
  36. audex/config/provider/vpr.py +99 -0
  37. audex/container.py +40 -0
  38. audex/entity/__init__.py +468 -0
  39. audex/entity/doctor.py +109 -0
  40. audex/entity/doctor.pyi +51 -0
  41. audex/entity/fields.py +401 -0
  42. audex/entity/segment.py +115 -0
  43. audex/entity/segment.pyi +38 -0
  44. audex/entity/session.py +133 -0
  45. audex/entity/session.pyi +47 -0
  46. audex/entity/utterance.py +142 -0
  47. audex/entity/utterance.pyi +48 -0
  48. audex/entity/vp.py +68 -0
  49. audex/entity/vp.pyi +35 -0
  50. audex/exceptions.py +157 -0
  51. audex/filters/__init__.py +692 -0
  52. audex/filters/generated/__init__.py +21 -0
  53. audex/filters/generated/doctor.py +987 -0
  54. audex/filters/generated/segment.py +723 -0
  55. audex/filters/generated/session.py +978 -0
  56. audex/filters/generated/utterance.py +939 -0
  57. audex/filters/generated/vp.py +815 -0
  58. audex/helper/__init__.py +1 -0
  59. audex/helper/hash.py +33 -0
  60. audex/helper/mixin.py +65 -0
  61. audex/helper/net.py +19 -0
  62. audex/helper/settings/__init__.py +830 -0
  63. audex/helper/settings/fields.py +317 -0
  64. audex/helper/stream.py +153 -0
  65. audex/injectors/__init__.py +1 -0
  66. audex/injectors/config.py +12 -0
  67. audex/injectors/lifespan.py +7 -0
  68. audex/lib/__init__.py +1 -0
  69. audex/lib/cache/__init__.py +383 -0
  70. audex/lib/cache/inmemory.py +513 -0
  71. audex/lib/database/__init__.py +83 -0
  72. audex/lib/database/sqlite.py +406 -0
  73. audex/lib/exporter.py +189 -0
  74. audex/lib/injectors/__init__.py +1 -0
  75. audex/lib/injectors/cache.py +25 -0
  76. audex/lib/injectors/container.py +47 -0
  77. audex/lib/injectors/exporter.py +26 -0
  78. audex/lib/injectors/recorder.py +33 -0
  79. audex/lib/injectors/server.py +17 -0
  80. audex/lib/injectors/session.py +18 -0
  81. audex/lib/injectors/sqlite.py +24 -0
  82. audex/lib/injectors/store.py +13 -0
  83. audex/lib/injectors/transcription.py +42 -0
  84. audex/lib/injectors/usb.py +12 -0
  85. audex/lib/injectors/vpr.py +65 -0
  86. audex/lib/injectors/wifi.py +7 -0
  87. audex/lib/recorder.py +844 -0
  88. audex/lib/repos/__init__.py +149 -0
  89. audex/lib/repos/container.py +23 -0
  90. audex/lib/repos/database/__init__.py +1 -0
  91. audex/lib/repos/database/sqlite.py +672 -0
  92. audex/lib/repos/decorators.py +74 -0
  93. audex/lib/repos/doctor.py +286 -0
  94. audex/lib/repos/segment.py +302 -0
  95. audex/lib/repos/session.py +285 -0
  96. audex/lib/repos/tables/__init__.py +70 -0
  97. audex/lib/repos/tables/doctor.py +137 -0
  98. audex/lib/repos/tables/segment.py +113 -0
  99. audex/lib/repos/tables/session.py +140 -0
  100. audex/lib/repos/tables/utterance.py +131 -0
  101. audex/lib/repos/tables/vp.py +102 -0
  102. audex/lib/repos/utterance.py +288 -0
  103. audex/lib/repos/vp.py +286 -0
  104. audex/lib/restful.py +251 -0
  105. audex/lib/server/__init__.py +97 -0
  106. audex/lib/server/auth.py +98 -0
  107. audex/lib/server/handlers.py +248 -0
  108. audex/lib/server/templates/index.html.j2 +226 -0
  109. audex/lib/server/templates/login.html.j2 +111 -0
  110. audex/lib/server/templates/static/script.js +68 -0
  111. audex/lib/server/templates/static/style.css +579 -0
  112. audex/lib/server/types.py +123 -0
  113. audex/lib/session.py +503 -0
  114. audex/lib/store/__init__.py +238 -0
  115. audex/lib/store/localfile.py +411 -0
  116. audex/lib/transcription/__init__.py +33 -0
  117. audex/lib/transcription/dashscope.py +525 -0
  118. audex/lib/transcription/events.py +62 -0
  119. audex/lib/usb.py +554 -0
  120. audex/lib/vpr/__init__.py +38 -0
  121. audex/lib/vpr/unisound/__init__.py +185 -0
  122. audex/lib/vpr/unisound/types.py +469 -0
  123. audex/lib/vpr/xfyun/__init__.py +483 -0
  124. audex/lib/vpr/xfyun/types.py +679 -0
  125. audex/lib/websocket/__init__.py +8 -0
  126. audex/lib/websocket/connection.py +485 -0
  127. audex/lib/websocket/pool.py +991 -0
  128. audex/lib/wifi.py +1146 -0
  129. audex/lifespan.py +75 -0
  130. audex/service/__init__.py +27 -0
  131. audex/service/decorators.py +73 -0
  132. audex/service/doctor/__init__.py +652 -0
  133. audex/service/doctor/const.py +36 -0
  134. audex/service/doctor/exceptions.py +96 -0
  135. audex/service/doctor/types.py +54 -0
  136. audex/service/export/__init__.py +236 -0
  137. audex/service/export/const.py +17 -0
  138. audex/service/export/exceptions.py +34 -0
  139. audex/service/export/types.py +21 -0
  140. audex/service/injectors/__init__.py +1 -0
  141. audex/service/injectors/container.py +53 -0
  142. audex/service/injectors/doctor.py +34 -0
  143. audex/service/injectors/export.py +27 -0
  144. audex/service/injectors/session.py +49 -0
  145. audex/service/session/__init__.py +754 -0
  146. audex/service/session/const.py +34 -0
  147. audex/service/session/exceptions.py +67 -0
  148. audex/service/session/types.py +91 -0
  149. audex/types.py +39 -0
  150. audex/utils.py +287 -0
  151. audex/valueobj/__init__.py +81 -0
  152. audex/valueobj/common/__init__.py +1 -0
  153. audex/valueobj/common/auth.py +84 -0
  154. audex/valueobj/common/email.py +16 -0
  155. audex/valueobj/common/ops.py +22 -0
  156. audex/valueobj/common/phone.py +84 -0
  157. audex/valueobj/common/version.py +72 -0
  158. audex/valueobj/session.py +19 -0
  159. audex/valueobj/utterance.py +15 -0
  160. audex/view/__init__.py +51 -0
  161. audex/view/container.py +17 -0
  162. audex/view/decorators.py +303 -0
  163. audex/view/pages/__init__.py +1 -0
  164. audex/view/pages/dashboard/__init__.py +286 -0
  165. audex/view/pages/dashboard/wifi.py +407 -0
  166. audex/view/pages/login.py +110 -0
  167. audex/view/pages/recording.py +348 -0
  168. audex/view/pages/register.py +202 -0
  169. audex/view/pages/sessions/__init__.py +196 -0
  170. audex/view/pages/sessions/details.py +224 -0
  171. audex/view/pages/sessions/export.py +443 -0
  172. audex/view/pages/settings.py +374 -0
  173. audex/view/pages/voiceprint/__init__.py +1 -0
  174. audex/view/pages/voiceprint/enroll.py +195 -0
  175. audex/view/pages/voiceprint/update.py +195 -0
  176. audex/view/static/css/dashboard.css +452 -0
  177. audex/view/static/css/glass.css +22 -0
  178. audex/view/static/css/global.css +541 -0
  179. audex/view/static/css/login.css +386 -0
  180. audex/view/static/css/recording.css +439 -0
  181. audex/view/static/css/register.css +293 -0
  182. audex/view/static/css/sessions/styles.css +501 -0
  183. audex/view/static/css/settings.css +186 -0
  184. audex/view/static/css/voiceprint/enroll.css +43 -0
  185. audex/view/static/css/voiceprint/styles.css +209 -0
  186. audex/view/static/css/voiceprint/update.css +44 -0
  187. audex/view/static/images/logo.svg +95 -0
  188. audex/view/static/js/recording.js +42 -0
  189. audex-1.0.7a3.dist-info/METADATA +361 -0
  190. audex-1.0.7a3.dist-info/RECORD +192 -0
  191. audex-1.0.7a3.dist-info/WHEEL +4 -0
  192. audex-1.0.7a3.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,485 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio as aio
4
+ import contextlib
5
+ import time
6
+ import types
7
+ import typing as t
8
+ import uuid
9
+
10
+ from websockets import ClientConnection
11
+ from websockets import ConnectionClosed
12
+ from websockets import connect
13
+ from websockets import protocol
14
+
15
+ from audex.helper.mixin import LoggingMixin
16
+ from audex.lib.websocket import WebsocketError
17
+
18
+
19
+ class ConnectionBusyError(WebsocketError):
20
+ """Raised when attempting to use a connection that is already
21
+ busy."""
22
+
23
+ default_message = "Connection is already busy"
24
+
25
+
26
+ class ConnectionUnavailableError(WebsocketError):
27
+ """Raised when a connection is unavailable or cannot be
28
+ established."""
29
+
30
+ default_message = "Connection is unavailable"
31
+
32
+
33
+ class ConnectionClosedError(WebsocketError):
34
+ """Raised when attempting to use a closed connection."""
35
+
36
+ default_message = "Connection is closed"
37
+
38
+
39
+ class ConnectionDrainTimeoutError(WebsocketError):
40
+ """Raised when connection draining exceeds the timeout."""
41
+
42
+ default_message = "Connection draining timed out"
43
+
44
+
45
+ # Type alias for drain condition callback
46
+ DrainConditionCallback: t.TypeAlias = t.Callable[[str | bytes], bool]
47
+
48
+
49
+ class WebsocketConnection(LoggingMixin, t.Hashable):
50
+ """Manages a single WebSocket connection with lifecycle management.
51
+
52
+ This class provides automatic idle timeout monitoring, connection health
53
+ checks, and proper resource cleanup for WebSocket connections.
54
+
55
+ Attributes:
56
+ uri: The WebSocket URI to connect to.
57
+ headers: Optional HTTP headers for the connection.
58
+ idle_timeout: Maximum idle time before auto-close in seconds.
59
+ check_server_data_on_release: Whether to check for server data on release.
60
+ drain_timeout: Timeout for draining server data in seconds.
61
+ drain_condition: Callback to determine if data should be drained.
62
+ """
63
+
64
+ __logtag__ = "audex.lib.websocket.connection"
65
+
66
+ def __init__(
67
+ self,
68
+ *,
69
+ uri: str,
70
+ headers: dict[str, str] | None = None,
71
+ idle_timeout: float = 30.0,
72
+ check_server_data_on_release: bool = False,
73
+ drain_timeout: float = 5.0,
74
+ drain_condition: DrainConditionCallback | None = None,
75
+ **kwargs: t.Any,
76
+ ):
77
+ """Initialize a WebSocket connection.
78
+
79
+ Args:
80
+ uri: The WebSocket URI to connect to.
81
+ headers: Optional HTTP headers to include in connection requests.
82
+ idle_timeout: Maximum time in seconds before idle connection closes.
83
+ Defaults to 30.0.
84
+ check_server_data_on_release: Whether to check for server data
85
+ during release. Defaults to False.
86
+ drain_timeout: Maximum time in seconds to drain server data.
87
+ Defaults to 5.0.
88
+ drain_condition: Function to determine what constitutes server data
89
+ that should be drained. Defaults to None (uses default condition).
90
+ **kwargs: Additional parameters to pass to websockets.connect().
91
+ """
92
+ super().__init__()
93
+ self.uri = uri
94
+ self.headers = headers
95
+ self.idle_timeout = idle_timeout
96
+ self.check_server_data_on_release = check_server_data_on_release
97
+ self.drain_timeout = drain_timeout
98
+ self.drain_condition = drain_condition or self._default_drain_condition
99
+ self._params = kwargs
100
+
101
+ self.websocket: ClientConnection | None = None
102
+ self._is_busy = False
103
+ self._is_draining = False
104
+ self._last_activity = time.time()
105
+ self._monitor_task: aio.Task[None] | None = None
106
+ self._closed = False
107
+ self._lock = aio.Lock()
108
+ self._connection_id = uuid.uuid4().hex # For hashing
109
+
110
+ @staticmethod
111
+ def _default_drain_condition(message: str | bytes) -> bool:
112
+ """Default condition to determine if incoming data should be
113
+ drained.
114
+
115
+ Args:
116
+ message: The incoming message from the server.
117
+
118
+ Returns:
119
+ True if the message should be drained (considered as server data).
120
+ """
121
+ # By default, consider any non-empty message as server data
122
+ return len(message) > 0
123
+
124
+ @property
125
+ def is_busy(self) -> bool:
126
+ """Check if the connection is currently busy.
127
+
128
+ Returns:
129
+ True if the connection is busy, False otherwise.
130
+ """
131
+ return self._is_busy
132
+
133
+ @property
134
+ def is_draining(self) -> bool:
135
+ """Check if the connection is currently draining.
136
+
137
+ Returns:
138
+ True if the connection is draining, False otherwise.
139
+ """
140
+ return self._is_draining
141
+
142
+ @property
143
+ def is_connected(self) -> bool:
144
+ """Check if the connection is currently active.
145
+
146
+ Returns:
147
+ True if the connection is open and not closed, False otherwise.
148
+ """
149
+ return (
150
+ self.websocket is not None
151
+ and not self._closed
152
+ and self.websocket.state == protocol.OPEN
153
+ )
154
+
155
+ @property
156
+ def last_activity(self) -> float:
157
+ """Get the timestamp of the last activity.
158
+
159
+ Returns:
160
+ Unix timestamp of the last activity.
161
+ """
162
+ return self._last_activity
163
+
164
+ def _update_activity(self) -> None:
165
+ """Update the last activity timestamp to current time."""
166
+ self._last_activity = time.time()
167
+
168
+ async def _monitor_idle(self) -> None:
169
+ """Background task to monitor and close idle connections.
170
+
171
+ This task runs continuously and checks if the connection has
172
+ been idle for longer than idle_timeout. If so, it automatically
173
+ closes the connection.
174
+ """
175
+ try:
176
+ while not self._closed:
177
+ should_close = False
178
+ async with self._lock:
179
+ if (
180
+ self.is_connected
181
+ and not self.is_busy
182
+ and not self.is_draining
183
+ and (time.time() - self._last_activity) > self.idle_timeout
184
+ ):
185
+ should_close = True
186
+
187
+ if should_close:
188
+ self.logger.debug(f"Closing idle connection to {self.uri}")
189
+ await self.close()
190
+ break
191
+ await aio.sleep(1)
192
+ except aio.CancelledError:
193
+ pass
194
+
195
+ async def connect(self) -> None:
196
+ """Establish the WebSocket connection.
197
+
198
+ If already connected, this method does nothing. Otherwise, it attempts
199
+ to establish a new connection and starts the idle monitor task.
200
+
201
+ Raises:
202
+ ConnectionUnavailableError: If the connection has been closed or
203
+ if connection establishment fails.
204
+ """
205
+ async with self._lock:
206
+ if self.is_connected:
207
+ return
208
+
209
+ if self._closed:
210
+ raise ConnectionUnavailableError("Connection has been closed")
211
+
212
+ # Clean up old monitor task if done
213
+ if self._monitor_task is not None and self._monitor_task.done():
214
+ try:
215
+ await self._monitor_task
216
+ finally:
217
+ self._monitor_task = None
218
+
219
+ # Establish connection outside the lock to avoid blocking
220
+ try:
221
+ websocket = await connect(self.uri, additional_headers=self.headers, **self._params)
222
+
223
+ async with self._lock:
224
+ self.websocket = websocket
225
+ self._update_activity()
226
+
227
+ # Start monitor task if not already running
228
+ if self._monitor_task is None or self._monitor_task.done():
229
+ self._monitor_task = aio.create_task(self._monitor_idle())
230
+
231
+ self.logger.debug(f"Connected to {self.uri}")
232
+ except Exception as e:
233
+ self.logger.error(f"Failed to connect to {self.uri}: {e}")
234
+ raise ConnectionUnavailableError(f"Failed to connect: {e}") from e
235
+
236
+ async def close(self) -> None:
237
+ """Close the WebSocket connection and clean up resources.
238
+
239
+ This method cancels the idle monitor task, closes the WebSocket
240
+ connection, and resets all internal state flags.
241
+ """
242
+ async with self._lock:
243
+ if self._closed:
244
+ return
245
+
246
+ self._closed = True
247
+
248
+ # Cancel monitor task
249
+ if self._monitor_task is not None:
250
+ if not self._monitor_task.done():
251
+ self._monitor_task.cancel()
252
+
253
+ try:
254
+ await self._monitor_task
255
+ except aio.CancelledError:
256
+ self.logger.debug(f"Monitor task for {self.uri} cancelled")
257
+ finally:
258
+ self._monitor_task = None
259
+
260
+ # Close websocket
261
+ if self.websocket is not None:
262
+ try:
263
+ await self.websocket.close()
264
+ finally:
265
+ self.websocket = None
266
+
267
+ self._is_busy = False
268
+ self._is_draining = False
269
+ self.logger.debug(f"Closed connection to {self.uri}")
270
+
271
+ async def acquire(self) -> None:
272
+ """Acquire the connection for exclusive use.
273
+
274
+ This method marks the connection as busy and ensures it is connected.
275
+
276
+ Raises:
277
+ ConnectionUnavailableError: If the connection has been closed or
278
+ if connection establishment fails.
279
+ ConnectionBusyError: If the connection is already busy or draining.
280
+ """
281
+ async with self._lock:
282
+ if self._closed:
283
+ raise ConnectionUnavailableError("Connection has been closed")
284
+ if self._is_busy:
285
+ raise ConnectionBusyError("Connection is already busy")
286
+ if self._is_draining:
287
+ raise ConnectionBusyError("Connection is currently draining")
288
+
289
+ try:
290
+ await self.connect()
291
+ async with self._lock:
292
+ self._is_busy = True
293
+ self._update_activity()
294
+ except Exception as e:
295
+ self.logger.error(f"Failed to acquire connection to {self.uri}: {e}")
296
+ raise ConnectionUnavailableError(f"Failed to acquire connection: {e}") from e
297
+
298
+ async def release(self) -> None:
299
+ """Release the connection back to the pool.
300
+
301
+ This method marks the connection as no longer busy and updates
302
+ the activity timestamp.
303
+ """
304
+ async with self._lock:
305
+ if not self._is_busy:
306
+ return
307
+
308
+ self._is_busy = False
309
+ self._update_activity()
310
+
311
+ async def ping(self) -> None:
312
+ """Send a ping to the WebSocket server to check connection
313
+ health.
314
+
315
+ This method performs a health check by sending a ping frame to the
316
+ server. The connection must be open for this to succeed.
317
+
318
+ Raises:
319
+ ConnectionUnavailableError: If the websocket is not connected.
320
+ ConnectionClosedError: If the connection closes during the ping.
321
+ """
322
+ # Check connection state outside lock (quick check)
323
+ if not (
324
+ self.websocket is not None
325
+ and not self._closed
326
+ and self.websocket.state == protocol.OPEN
327
+ ):
328
+ raise ConnectionUnavailableError("Websocket is not connected")
329
+
330
+ # Get websocket reference under lock
331
+ async with self._lock:
332
+ if not self.is_connected:
333
+ raise ConnectionUnavailableError("Websocket is not connected")
334
+ websocket = self.websocket
335
+
336
+ # Perform ping outside lock to avoid blocking other operations
337
+ connection_closed = False
338
+ connection_closed_error = None
339
+ try:
340
+ await websocket.ping()
341
+ async with self._lock:
342
+ self._update_activity()
343
+ except ConnectionClosed as e:
344
+ self.logger.error(f"Connection closed during ping: {e}")
345
+ connection_closed = True
346
+ connection_closed_error = e
347
+
348
+ if connection_closed:
349
+ await self.close()
350
+ raise ConnectionClosedError(
351
+ f"Connection closed during ping: {connection_closed_error}"
352
+ ) from connection_closed_error
353
+
354
+ @contextlib.asynccontextmanager
355
+ async def session(self) -> t.AsyncGenerator[WebsocketConnection, None]:
356
+ """Create a context manager session for the connection.
357
+
358
+ The connection is automatically acquired on entry and released on exit.
359
+
360
+ Yields:
361
+ The WebsocketConnection instance.
362
+
363
+ Example:
364
+ async with connection.session():
365
+ await connection.send("Hello")
366
+ response = await connection.recv()
367
+ """
368
+ await self.acquire()
369
+ try:
370
+ yield self
371
+ finally:
372
+ await self.release()
373
+
374
+ async def send(self, message: str | bytes) -> None:
375
+ """Send a message through the WebSocket connection.
376
+
377
+ Args:
378
+ message: The message to send (string or bytes).
379
+
380
+ Raises:
381
+ ConnectionBusyError: If the connection has not been acquired.
382
+ ConnectionUnavailableError: If the websocket is not connected.
383
+ ConnectionClosedError: If the connection closes during sending.
384
+ """
385
+ if not self.is_busy:
386
+ raise ConnectionBusyError("Connection must be acquired before sending messages")
387
+
388
+ # Check connection state outside lock
389
+ if not (
390
+ self.websocket is not None
391
+ and not self._closed
392
+ and self.websocket.state == protocol.OPEN
393
+ ):
394
+ raise ConnectionUnavailableError("Websocket is not connected")
395
+
396
+ # Get websocket reference under lock
397
+ async with self._lock:
398
+ websocket = self.websocket
399
+ if websocket is None:
400
+ raise ConnectionUnavailableError("Websocket is not connected")
401
+
402
+ # Perform send outside lock
403
+ try:
404
+ await websocket.send(message)
405
+ async with self._lock:
406
+ self._update_activity()
407
+ except ConnectionClosed as e:
408
+ await self.close()
409
+ self.logger.error(f"Connection closed while sending message: {e}")
410
+ raise ConnectionClosedError(f"Connection closed while sending message: {e}") from e
411
+
412
+ async def recv(self) -> str | bytes:
413
+ """Receive a message from the WebSocket connection.
414
+
415
+ Returns:
416
+ The received message (string or bytes).
417
+
418
+ Raises:
419
+ ConnectionBusyError: If the connection has not been acquired.
420
+ ConnectionUnavailableError: If the websocket is not connected.
421
+ ConnectionClosedError: If the connection closes during receiving.
422
+ """
423
+ if not self._is_busy:
424
+ raise ConnectionBusyError("Connection must be acquired before receiving messages")
425
+
426
+ # Check connection state outside lock
427
+ if not (
428
+ self.websocket is not None
429
+ and not self._closed
430
+ and self.websocket.state == protocol.OPEN
431
+ ):
432
+ raise ConnectionUnavailableError("Websocket is not connected")
433
+
434
+ # Get websocket reference under lock
435
+ async with self._lock:
436
+ websocket = self.websocket
437
+ if websocket is None:
438
+ raise ConnectionUnavailableError("Websocket is not connected")
439
+
440
+ # Perform recv outside lock
441
+ try:
442
+ message = await websocket.recv()
443
+ async with self._lock:
444
+ self._update_activity()
445
+ return message
446
+ except ConnectionClosed as e:
447
+ await self.close()
448
+ self.logger.error(f"Connection closed while receiving message: {e}")
449
+ raise ConnectionClosedError(f"Connection closed while receiving message: {e}") from e
450
+
451
+ def __repr__(self) -> str:
452
+ return (
453
+ f"{self.__class__.__name__}("
454
+ f"connection_id={self._connection_id}, "
455
+ f"uri={self.uri}, "
456
+ f"busy={self._is_busy}, "
457
+ f"draining={self._is_draining}, "
458
+ f"connected={self.is_connected}, "
459
+ f"closed={self._closed})"
460
+ )
461
+
462
+ def __str__(self) -> str:
463
+ return f"WebsocketConnection({self._connection_id})"
464
+
465
+ def __hash__(self) -> int:
466
+ return hash(self._connection_id)
467
+
468
+ def __eq__(self, other: object) -> bool:
469
+ if not isinstance(other, WebsocketConnection):
470
+ return NotImplemented
471
+ return self._connection_id == other._connection_id
472
+
473
+ async def __aenter__(self) -> t.Self:
474
+ await self.connect()
475
+ await self.acquire()
476
+ return self
477
+
478
+ async def __aexit__(
479
+ self,
480
+ exc_type: type[BaseException] | None,
481
+ exc_val: BaseException | None,
482
+ exc_tb: types.TracebackType | None,
483
+ ) -> t.Literal[False]:
484
+ await self.release()
485
+ return False