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.
- audex/__init__.py +9 -0
- audex/__main__.py +7 -0
- audex/cli/__init__.py +189 -0
- audex/cli/apis/__init__.py +12 -0
- audex/cli/apis/init/__init__.py +34 -0
- audex/cli/apis/init/gencfg.py +130 -0
- audex/cli/apis/init/setup.py +330 -0
- audex/cli/apis/init/vprgroup.py +125 -0
- audex/cli/apis/serve.py +141 -0
- audex/cli/args.py +356 -0
- audex/cli/exceptions.py +44 -0
- audex/cli/helper/__init__.py +0 -0
- audex/cli/helper/ansi.py +193 -0
- audex/cli/helper/display.py +288 -0
- audex/config/__init__.py +64 -0
- audex/config/core/__init__.py +30 -0
- audex/config/core/app.py +29 -0
- audex/config/core/audio.py +45 -0
- audex/config/core/logging.py +163 -0
- audex/config/core/session.py +11 -0
- audex/config/helper/__init__.py +1 -0
- audex/config/helper/client/__init__.py +1 -0
- audex/config/helper/client/http.py +28 -0
- audex/config/helper/client/websocket.py +21 -0
- audex/config/helper/provider/__init__.py +1 -0
- audex/config/helper/provider/dashscope.py +13 -0
- audex/config/helper/provider/unisound.py +18 -0
- audex/config/helper/provider/xfyun.py +23 -0
- audex/config/infrastructure/__init__.py +31 -0
- audex/config/infrastructure/cache.py +51 -0
- audex/config/infrastructure/database.py +48 -0
- audex/config/infrastructure/recorder.py +32 -0
- audex/config/infrastructure/store.py +19 -0
- audex/config/provider/__init__.py +18 -0
- audex/config/provider/transcription.py +109 -0
- audex/config/provider/vpr.py +99 -0
- audex/container.py +40 -0
- audex/entity/__init__.py +468 -0
- audex/entity/doctor.py +109 -0
- audex/entity/doctor.pyi +51 -0
- audex/entity/fields.py +401 -0
- audex/entity/segment.py +115 -0
- audex/entity/segment.pyi +38 -0
- audex/entity/session.py +133 -0
- audex/entity/session.pyi +47 -0
- audex/entity/utterance.py +142 -0
- audex/entity/utterance.pyi +48 -0
- audex/entity/vp.py +68 -0
- audex/entity/vp.pyi +35 -0
- audex/exceptions.py +157 -0
- audex/filters/__init__.py +692 -0
- audex/filters/generated/__init__.py +21 -0
- audex/filters/generated/doctor.py +987 -0
- audex/filters/generated/segment.py +723 -0
- audex/filters/generated/session.py +978 -0
- audex/filters/generated/utterance.py +939 -0
- audex/filters/generated/vp.py +815 -0
- audex/helper/__init__.py +1 -0
- audex/helper/hash.py +33 -0
- audex/helper/mixin.py +65 -0
- audex/helper/net.py +19 -0
- audex/helper/settings/__init__.py +830 -0
- audex/helper/settings/fields.py +317 -0
- audex/helper/stream.py +153 -0
- audex/injectors/__init__.py +1 -0
- audex/injectors/config.py +12 -0
- audex/injectors/lifespan.py +7 -0
- audex/lib/__init__.py +1 -0
- audex/lib/cache/__init__.py +383 -0
- audex/lib/cache/inmemory.py +513 -0
- audex/lib/database/__init__.py +83 -0
- audex/lib/database/sqlite.py +406 -0
- audex/lib/exporter.py +189 -0
- audex/lib/injectors/__init__.py +1 -0
- audex/lib/injectors/cache.py +25 -0
- audex/lib/injectors/container.py +47 -0
- audex/lib/injectors/exporter.py +26 -0
- audex/lib/injectors/recorder.py +33 -0
- audex/lib/injectors/server.py +17 -0
- audex/lib/injectors/session.py +18 -0
- audex/lib/injectors/sqlite.py +24 -0
- audex/lib/injectors/store.py +13 -0
- audex/lib/injectors/transcription.py +42 -0
- audex/lib/injectors/usb.py +12 -0
- audex/lib/injectors/vpr.py +65 -0
- audex/lib/injectors/wifi.py +7 -0
- audex/lib/recorder.py +844 -0
- audex/lib/repos/__init__.py +149 -0
- audex/lib/repos/container.py +23 -0
- audex/lib/repos/database/__init__.py +1 -0
- audex/lib/repos/database/sqlite.py +672 -0
- audex/lib/repos/decorators.py +74 -0
- audex/lib/repos/doctor.py +286 -0
- audex/lib/repos/segment.py +302 -0
- audex/lib/repos/session.py +285 -0
- audex/lib/repos/tables/__init__.py +70 -0
- audex/lib/repos/tables/doctor.py +137 -0
- audex/lib/repos/tables/segment.py +113 -0
- audex/lib/repos/tables/session.py +140 -0
- audex/lib/repos/tables/utterance.py +131 -0
- audex/lib/repos/tables/vp.py +102 -0
- audex/lib/repos/utterance.py +288 -0
- audex/lib/repos/vp.py +286 -0
- audex/lib/restful.py +251 -0
- audex/lib/server/__init__.py +97 -0
- audex/lib/server/auth.py +98 -0
- audex/lib/server/handlers.py +248 -0
- audex/lib/server/templates/index.html.j2 +226 -0
- audex/lib/server/templates/login.html.j2 +111 -0
- audex/lib/server/templates/static/script.js +68 -0
- audex/lib/server/templates/static/style.css +579 -0
- audex/lib/server/types.py +123 -0
- audex/lib/session.py +503 -0
- audex/lib/store/__init__.py +238 -0
- audex/lib/store/localfile.py +411 -0
- audex/lib/transcription/__init__.py +33 -0
- audex/lib/transcription/dashscope.py +525 -0
- audex/lib/transcription/events.py +62 -0
- audex/lib/usb.py +554 -0
- audex/lib/vpr/__init__.py +38 -0
- audex/lib/vpr/unisound/__init__.py +185 -0
- audex/lib/vpr/unisound/types.py +469 -0
- audex/lib/vpr/xfyun/__init__.py +483 -0
- audex/lib/vpr/xfyun/types.py +679 -0
- audex/lib/websocket/__init__.py +8 -0
- audex/lib/websocket/connection.py +485 -0
- audex/lib/websocket/pool.py +991 -0
- audex/lib/wifi.py +1146 -0
- audex/lifespan.py +75 -0
- audex/service/__init__.py +27 -0
- audex/service/decorators.py +73 -0
- audex/service/doctor/__init__.py +652 -0
- audex/service/doctor/const.py +36 -0
- audex/service/doctor/exceptions.py +96 -0
- audex/service/doctor/types.py +54 -0
- audex/service/export/__init__.py +236 -0
- audex/service/export/const.py +17 -0
- audex/service/export/exceptions.py +34 -0
- audex/service/export/types.py +21 -0
- audex/service/injectors/__init__.py +1 -0
- audex/service/injectors/container.py +53 -0
- audex/service/injectors/doctor.py +34 -0
- audex/service/injectors/export.py +27 -0
- audex/service/injectors/session.py +49 -0
- audex/service/session/__init__.py +754 -0
- audex/service/session/const.py +34 -0
- audex/service/session/exceptions.py +67 -0
- audex/service/session/types.py +91 -0
- audex/types.py +39 -0
- audex/utils.py +287 -0
- audex/valueobj/__init__.py +81 -0
- audex/valueobj/common/__init__.py +1 -0
- audex/valueobj/common/auth.py +84 -0
- audex/valueobj/common/email.py +16 -0
- audex/valueobj/common/ops.py +22 -0
- audex/valueobj/common/phone.py +84 -0
- audex/valueobj/common/version.py +72 -0
- audex/valueobj/session.py +19 -0
- audex/valueobj/utterance.py +15 -0
- audex/view/__init__.py +51 -0
- audex/view/container.py +17 -0
- audex/view/decorators.py +303 -0
- audex/view/pages/__init__.py +1 -0
- audex/view/pages/dashboard/__init__.py +286 -0
- audex/view/pages/dashboard/wifi.py +407 -0
- audex/view/pages/login.py +110 -0
- audex/view/pages/recording.py +348 -0
- audex/view/pages/register.py +202 -0
- audex/view/pages/sessions/__init__.py +196 -0
- audex/view/pages/sessions/details.py +224 -0
- audex/view/pages/sessions/export.py +443 -0
- audex/view/pages/settings.py +374 -0
- audex/view/pages/voiceprint/__init__.py +1 -0
- audex/view/pages/voiceprint/enroll.py +195 -0
- audex/view/pages/voiceprint/update.py +195 -0
- audex/view/static/css/dashboard.css +452 -0
- audex/view/static/css/glass.css +22 -0
- audex/view/static/css/global.css +541 -0
- audex/view/static/css/login.css +386 -0
- audex/view/static/css/recording.css +439 -0
- audex/view/static/css/register.css +293 -0
- audex/view/static/css/sessions/styles.css +501 -0
- audex/view/static/css/settings.css +186 -0
- audex/view/static/css/voiceprint/enroll.css +43 -0
- audex/view/static/css/voiceprint/styles.css +209 -0
- audex/view/static/css/voiceprint/update.css +44 -0
- audex/view/static/images/logo.svg +95 -0
- audex/view/static/js/recording.js +42 -0
- audex-1.0.7a3.dist-info/METADATA +361 -0
- audex-1.0.7a3.dist-info/RECORD +192 -0
- audex-1.0.7a3.dist-info/WHEEL +4 -0
- 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
|