ringcentral.websocket.web_socket_client
1#!/usr/bin/env python 2# encoding: utf-8 3from observable import Observable 4import websockets 5from .web_socket_subscription import WebSocketSubscription 6from .events import WebSocketEvents 7import json 8import asyncio 9import uuid 10 11class WebSocketClient(Observable): 12 def __init__(self, platform): 13 Observable.__init__(self) 14 self._platform = platform 15 self._web_socket = None 16 self._done = False 17 self._is_ready = False 18 self._heartbeat_task = None 19 self._send_attempt_counter = 0 20 self._subscription = None 21 22 async def create_new_connection(self): 23 """ 24 Creates a new WebSocket connection. 25 26 Returns: 27 Any: Response object containing the result of the connection creation. 28 29 Raises: 30 Exception: If any error occurs during the process. 31 32 Note: 33 - Retrieves the WebSocket token using `get_web_socket_token`. 34 - Attempts to open a WebSocket connection using the retrieved token's URI and access token. 35 - Triggers the createConnectionError event if an error occurs and raises the exception. 36 """ 37 try: 38 web_socket_token = self.get_web_socket_token() 39 open_connection_response = await self.open_connection( 40 web_socket_token["uri"], web_socket_token["ws_access_token"] 41 ) 42 return open_connection_response 43 except Exception as e: 44 self.trigger(WebSocketEvents.createConnectionError, e) 45 raise 46 47 def get_web_socket_token(self): 48 """ 49 Retrieves a WebSocket token. 50 51 Returns: 52 dict: WebSocket token containing URI and access token. 53 54 Raises: 55 Exception: If any error occurs during the process. 56 57 Note: 58 - Sends a POST request to the '/restapi/oauth/wstoken' endpoint to obtain the WebSocket token. 59 - Returns the WebSocket token as a dictionary containing the URI and access token. 60 - Triggers the getTokenError event if an error occurs and raises the exception. 61 """ 62 try: 63 response = self._platform.post("/restapi/oauth/wstoken", body={}) 64 return response.json_dict() 65 except Exception as e: 66 self.trigger(WebSocketEvents.getTokenError, e) 67 raise 68 69 async def open_connection(self, ws_uri, ws_access_token): 70 """ 71 Opens a WebSocket connection. 72 73 Args: 74 ws_uri (str): The WebSocket URI. 75 ws_access_token (str): The access token for WebSocket authentication. 76 77 Raises: 78 Exception: If any error occurs during the process. 79 80 Note: 81 - Attempts to establish a WebSocket connection to the provided URI with the given access token. 82 - Upon successful connection, sets up a heartbeat mechanism to maintain the connection. 83 - Triggers the connectionCreated event upon successful connection establishment. 84 - Listens for incoming messages and triggers the receiveMessage event for each received message. 85 - Isolates receive-message handler failures so a handler that raises does not stop delivery to the remaining handlers or reception of future messages; each failure triggers the receiveMessageError event with the original exception. 86 - Triggers the createConnectionError event if an error occurs while establishing the connection or during the initial handshake, and raises the exception. 87 - A failure from receiving messages after the initial handshake triggers the receiveMessageError event once; it is not reported as a connection-creation error and does not propagate to the connection-setup or recovery wrappers. 88 - When the receive loop terminates (receive failure, cancellation, or intentional closure), the client is marked not ready and its heartbeat task is cancelled. 89 - An intentional closure does not trigger the receiveMessageError event. 90 """ 91 self._done = False 92 try: 93 websocket = await websockets.connect( 94 f"{ws_uri}?access_token={ws_access_token}" 95 ) 96 connectionMessage = await websocket.recv() 97 except Exception as e: 98 self.trigger(WebSocketEvents.createConnectionError, e) 99 raise 100 101 connection_info = {} 102 connection_info["connection"] = websocket 103 connection_info["connection_details"] = connectionMessage 104 self._web_socket = connection_info 105 self._is_ready = True 106 self.trigger(WebSocketEvents.connectionCreated, self) 107 108 # heartbeat every 10 minutes 109 async def timer_function(): 110 while True: 111 if self._done: 112 timer.cancel() 113 break 114 await asyncio.sleep(600) 115 await self.send_message([{"type": "Heartbeat", "messageId": str(uuid.uuid4())}]) 116 timer = asyncio.create_task(timer_function()) 117 self._heartbeat_task = timer 118 119 try: 120 await asyncio.sleep(0) 121 while True: 122 message = await websocket.recv() 123 self.trigger(WebSocketEvents.receiveMessage, message) 124 await asyncio.sleep(0) 125 except Exception as e: 126 if not self._done: 127 self.trigger(WebSocketEvents.receiveMessageError, e) 128 finally: 129 self._is_ready = False 130 timer.cancel() 131 132 def trigger(self, event, *args, **kw): 133 """ 134 Triggers the handlers registered for an event. 135 136 Args: 137 event (str): The event to trigger. 138 args: The event arguments. 139 kw: The event keyword arguments. 140 141 Returns: 142 bool: True if any handler was triggered, False if the event has no handlers. 143 144 Note: 145 - receiveMessage handlers are invoked independently: if one raises, the remaining handlers still receive the same raw message and one receiveMessageError event is triggered with the original exception. 146 - receiveMessageError handlers are also invoked independently: a failing error handler is contained so error reporting and reception continue. 147 - Every other event keeps the default dispatch behavior. 148 """ 149 if event == WebSocketEvents.receiveMessage: 150 return self._trigger_receive_message(*args, **kw) 151 if event == WebSocketEvents.receiveMessageError: 152 return self._trigger_receive_message_error(*args, **kw) 153 return Observable.trigger(self, event, *args, **kw) 154 155 def _trigger_receive_message(self, *args, **kw): 156 handlers = list(self.events.get(WebSocketEvents.receiveMessage) or []) 157 if not handlers: 158 return False 159 for handler in handlers: 160 try: 161 handler(*args, **kw) 162 except Exception as e: 163 self.trigger(WebSocketEvents.receiveMessageError, e) 164 return True 165 166 def _trigger_receive_message_error(self, *args, **kw): 167 handlers = list(self.events.get(WebSocketEvents.receiveMessageError) or []) 168 if not handlers: 169 return False 170 for handler in handlers: 171 try: 172 handler(*args, **kw) 173 except Exception: 174 pass 175 return True 176 177 def get_connection_info(self): 178 return self._web_socket 179 180 def get_connection(self): 181 return self._web_socket["connection"] 182 183 async def close_connection(self): 184 """ 185 Closes the WebSocket connection. 186 187 Raises: 188 Exception: If any error occurs during the process. 189 190 Note: 191 - Sets the `_done` flag to True to signal the termination of the heartbeat mechanism. 192 - Sets the `_is_ready` flag to False to indicate that the connection is no longer ready. 193 - Retrieves the WebSocket connection using `get_connection`. 194 - Closes the WebSocket connection. 195 - Triggers the closeConnectionError event if an error occurs during the closing process and raises the exception. 196 """ 197 try: 198 self._done = True 199 self._is_ready = False 200 ws_connection = self.get_connection() 201 await ws_connection.close() 202 except Exception as e: 203 self.trigger(WebSocketEvents.closeConnectionError, e) 204 raise 205 206 async def recover_connection(self): 207 try: 208 ws_connection_info = self.get_web_socket_token() 209 recovered_connection_info = await self.open_connection( 210 ws_connection_info["uri"], ws_connection_info["ws_access_token"] 211 ) 212 # if recovered_connection_info['recoveryState'] === "Successful", then subscription is restored 213 # otherwise, need to create a new subscription 214 # IMPORTANT: WebSocket creation is successful if it doesn't raise any exception 215 return recovered_connection_info 216 except Exception as e: 217 self.trigger(WebSocketEvents.recoverConnectionError, e) 218 raise 219 220 async def send_message(self, message): 221 """ 222 Sends a message over the WebSocket connection. 223 224 Args: 225 message (Any): The message to be sent. 226 227 Raises: 228 Exception: If any error occurs during the process or if the connection is not ready after multiple attempts. 229 230 Note: 231 - Checks if the WebSocket connection is ready (`_is_ready` flag). 232 - If the connection is ready, resets the send attempt counter and sends the message. 233 - If the connection is not ready, retries after a delay and increments the send attempt counter. 234 - If the send attempt counter exceeds a threshold, triggers the connectionNotReady event and raises an exception. 235 """ 236 try: 237 if self._is_ready: 238 self._send_attempt_counter = 0 239 requestBodyJson = json.dumps(message) 240 await self.get_connection().send(requestBodyJson) 241 else: 242 await asyncio.sleep(1) 243 await self.send_message(message) 244 self._send_attempt_counter += 1 245 if(self._send_attempt_counter > 10): 246 self.trigger(WebSocketEvents.connectionNotReady) 247 self._send_attempt_counter = 0 248 raise 249 except Exception as e: 250 self.trigger(WebSocketEvents.sendMessageError, e) 251 raise 252 253 async def create_subscription(self, events): 254 """ 255 Creates a subscription to WebSocket events. 256 257 Args: 258 events (list): A required, non-empty list of events to subscribe to. 259 260 Raises: 261 Exception: If any error occurs during the process, if events are omitted, None, or empty, if a subscription creation is already in progress, or if a subscription already exists. 262 263 Note: 264 - Events are required: an omitted argument is rejected by ordinary Python argument checking, and None or empty events raise "Events are undefined" before any request is sent or any receive listener is attached. 265 - The client retains and reuses a single subscription object; a retry after a failed creation and a creation after removal reuse the retained object's existing registration flow with the newly supplied events. 266 - Rejects a call while a subscription creation is already in progress with "WebSocket subscription creation is already in progress; wait for subscriptionCreated or createSubscriptionError before retrying". 267 - Rejects a call after a subscription has been created with "A WebSocket subscription already exists; use update_subscription() to change its events or remove_subscription() before creating another". 268 - Rejections are delivered through the createSubscriptionError event and raised to the caller. 269 - If the WebSocket connection is ready (`_is_ready` flag), resets the send attempt counter and registers the retained subscription with the specified events. 270 - If the connection is not ready, retries after a delay and increments the send attempt counter. 271 - If the send attempt counter exceeds a threshold, triggers the connectionNotReady event and raises an exception. 272 """ 273 try: 274 if not events or len(events) == 0: 275 raise Exception("Events are undefined") 276 277 if self._subscription is None: 278 self._subscription = WebSocketSubscription(self) 279 subscription = self._subscription 280 281 if subscription._pending_creation_message_id is not None: 282 raise Exception("WebSocket subscription creation is already in progress; wait for subscriptionCreated or createSubscriptionError before retrying") 283 if subscription.get_subscription_info() is not None: 284 raise Exception("A WebSocket subscription already exists; use update_subscription() to change its events or remove_subscription() before creating another") 285 286 if self._is_ready: 287 self._send_attempt_counter = 0 288 await subscription.register(events) 289 else: 290 await asyncio.sleep(1) 291 await self.create_subscription(events) 292 self._send_attempt_counter += 1 293 if(self._send_attempt_counter > 10): 294 self.trigger(WebSocketEvents.connectionNotReady) 295 self._send_attempt_counter = 0 296 raise 297 298 except Exception as e: 299 self.trigger(WebSocketEvents.createSubscriptionError, e) 300 raise 301 302 async def update_subscription(self, subscription, events=None): 303 """ 304 Updates an existing WebSocket subscription with new events. 305 306 Args: 307 subscription : The WebSocket subscription to update. 308 events (list, optional): A list of events to update the subscription with. Default is None. 309 310 Returns: 311 WebSocketSubscription: The updated WebSocket subscription. 312 313 Raises: 314 Exception: If any error occurs during the process. 315 316 Note: 317 - Updates the specified WebSocket subscription with the new events provided. 318 - If the update is successful, returns the updated WebSocket subscription. 319 - If an error occurs during the update process, triggers the updateSubscriptionError event and raises an exception. 320 """ 321 try: 322 await subscription.update(events) 323 return subscription 324 except Exception as e: 325 self.trigger(WebSocketEvents.updateSubscriptionError, e) 326 raise 327 328 async def remove_subscription(self, subscription): 329 """ 330 Removes an existing WebSocket subscription. 331 332 Args: 333 subscription : The WebSocket subscription to remove. 334 335 Raises: 336 Exception: If any error occurs during the removal process. 337 338 Note: 339 - Removes the specified WebSocket subscription. 340 - If the removal is successful, the subscription is effectively unsubscribed from the events it was subscribed to. 341 - If an error occurs during the removal process, triggers the removeSubscriptionError event and raises an exception. 342 """ 343 try: 344 await subscription.remove() 345 except Exception as e: 346 self.trigger(WebSocketEvents.removeSubscriptionError, e) 347 raise 348 349 350if __name__ == "__main__": 351 pass
12class WebSocketClient(Observable): 13 def __init__(self, platform): 14 Observable.__init__(self) 15 self._platform = platform 16 self._web_socket = None 17 self._done = False 18 self._is_ready = False 19 self._heartbeat_task = None 20 self._send_attempt_counter = 0 21 self._subscription = None 22 23 async def create_new_connection(self): 24 """ 25 Creates a new WebSocket connection. 26 27 Returns: 28 Any: Response object containing the result of the connection creation. 29 30 Raises: 31 Exception: If any error occurs during the process. 32 33 Note: 34 - Retrieves the WebSocket token using `get_web_socket_token`. 35 - Attempts to open a WebSocket connection using the retrieved token's URI and access token. 36 - Triggers the createConnectionError event if an error occurs and raises the exception. 37 """ 38 try: 39 web_socket_token = self.get_web_socket_token() 40 open_connection_response = await self.open_connection( 41 web_socket_token["uri"], web_socket_token["ws_access_token"] 42 ) 43 return open_connection_response 44 except Exception as e: 45 self.trigger(WebSocketEvents.createConnectionError, e) 46 raise 47 48 def get_web_socket_token(self): 49 """ 50 Retrieves a WebSocket token. 51 52 Returns: 53 dict: WebSocket token containing URI and access token. 54 55 Raises: 56 Exception: If any error occurs during the process. 57 58 Note: 59 - Sends a POST request to the '/restapi/oauth/wstoken' endpoint to obtain the WebSocket token. 60 - Returns the WebSocket token as a dictionary containing the URI and access token. 61 - Triggers the getTokenError event if an error occurs and raises the exception. 62 """ 63 try: 64 response = self._platform.post("/restapi/oauth/wstoken", body={}) 65 return response.json_dict() 66 except Exception as e: 67 self.trigger(WebSocketEvents.getTokenError, e) 68 raise 69 70 async def open_connection(self, ws_uri, ws_access_token): 71 """ 72 Opens a WebSocket connection. 73 74 Args: 75 ws_uri (str): The WebSocket URI. 76 ws_access_token (str): The access token for WebSocket authentication. 77 78 Raises: 79 Exception: If any error occurs during the process. 80 81 Note: 82 - Attempts to establish a WebSocket connection to the provided URI with the given access token. 83 - Upon successful connection, sets up a heartbeat mechanism to maintain the connection. 84 - Triggers the connectionCreated event upon successful connection establishment. 85 - Listens for incoming messages and triggers the receiveMessage event for each received message. 86 - Isolates receive-message handler failures so a handler that raises does not stop delivery to the remaining handlers or reception of future messages; each failure triggers the receiveMessageError event with the original exception. 87 - Triggers the createConnectionError event if an error occurs while establishing the connection or during the initial handshake, and raises the exception. 88 - A failure from receiving messages after the initial handshake triggers the receiveMessageError event once; it is not reported as a connection-creation error and does not propagate to the connection-setup or recovery wrappers. 89 - When the receive loop terminates (receive failure, cancellation, or intentional closure), the client is marked not ready and its heartbeat task is cancelled. 90 - An intentional closure does not trigger the receiveMessageError event. 91 """ 92 self._done = False 93 try: 94 websocket = await websockets.connect( 95 f"{ws_uri}?access_token={ws_access_token}" 96 ) 97 connectionMessage = await websocket.recv() 98 except Exception as e: 99 self.trigger(WebSocketEvents.createConnectionError, e) 100 raise 101 102 connection_info = {} 103 connection_info["connection"] = websocket 104 connection_info["connection_details"] = connectionMessage 105 self._web_socket = connection_info 106 self._is_ready = True 107 self.trigger(WebSocketEvents.connectionCreated, self) 108 109 # heartbeat every 10 minutes 110 async def timer_function(): 111 while True: 112 if self._done: 113 timer.cancel() 114 break 115 await asyncio.sleep(600) 116 await self.send_message([{"type": "Heartbeat", "messageId": str(uuid.uuid4())}]) 117 timer = asyncio.create_task(timer_function()) 118 self._heartbeat_task = timer 119 120 try: 121 await asyncio.sleep(0) 122 while True: 123 message = await websocket.recv() 124 self.trigger(WebSocketEvents.receiveMessage, message) 125 await asyncio.sleep(0) 126 except Exception as e: 127 if not self._done: 128 self.trigger(WebSocketEvents.receiveMessageError, e) 129 finally: 130 self._is_ready = False 131 timer.cancel() 132 133 def trigger(self, event, *args, **kw): 134 """ 135 Triggers the handlers registered for an event. 136 137 Args: 138 event (str): The event to trigger. 139 args: The event arguments. 140 kw: The event keyword arguments. 141 142 Returns: 143 bool: True if any handler was triggered, False if the event has no handlers. 144 145 Note: 146 - receiveMessage handlers are invoked independently: if one raises, the remaining handlers still receive the same raw message and one receiveMessageError event is triggered with the original exception. 147 - receiveMessageError handlers are also invoked independently: a failing error handler is contained so error reporting and reception continue. 148 - Every other event keeps the default dispatch behavior. 149 """ 150 if event == WebSocketEvents.receiveMessage: 151 return self._trigger_receive_message(*args, **kw) 152 if event == WebSocketEvents.receiveMessageError: 153 return self._trigger_receive_message_error(*args, **kw) 154 return Observable.trigger(self, event, *args, **kw) 155 156 def _trigger_receive_message(self, *args, **kw): 157 handlers = list(self.events.get(WebSocketEvents.receiveMessage) or []) 158 if not handlers: 159 return False 160 for handler in handlers: 161 try: 162 handler(*args, **kw) 163 except Exception as e: 164 self.trigger(WebSocketEvents.receiveMessageError, e) 165 return True 166 167 def _trigger_receive_message_error(self, *args, **kw): 168 handlers = list(self.events.get(WebSocketEvents.receiveMessageError) or []) 169 if not handlers: 170 return False 171 for handler in handlers: 172 try: 173 handler(*args, **kw) 174 except Exception: 175 pass 176 return True 177 178 def get_connection_info(self): 179 return self._web_socket 180 181 def get_connection(self): 182 return self._web_socket["connection"] 183 184 async def close_connection(self): 185 """ 186 Closes the WebSocket connection. 187 188 Raises: 189 Exception: If any error occurs during the process. 190 191 Note: 192 - Sets the `_done` flag to True to signal the termination of the heartbeat mechanism. 193 - Sets the `_is_ready` flag to False to indicate that the connection is no longer ready. 194 - Retrieves the WebSocket connection using `get_connection`. 195 - Closes the WebSocket connection. 196 - Triggers the closeConnectionError event if an error occurs during the closing process and raises the exception. 197 """ 198 try: 199 self._done = True 200 self._is_ready = False 201 ws_connection = self.get_connection() 202 await ws_connection.close() 203 except Exception as e: 204 self.trigger(WebSocketEvents.closeConnectionError, e) 205 raise 206 207 async def recover_connection(self): 208 try: 209 ws_connection_info = self.get_web_socket_token() 210 recovered_connection_info = await self.open_connection( 211 ws_connection_info["uri"], ws_connection_info["ws_access_token"] 212 ) 213 # if recovered_connection_info['recoveryState'] === "Successful", then subscription is restored 214 # otherwise, need to create a new subscription 215 # IMPORTANT: WebSocket creation is successful if it doesn't raise any exception 216 return recovered_connection_info 217 except Exception as e: 218 self.trigger(WebSocketEvents.recoverConnectionError, e) 219 raise 220 221 async def send_message(self, message): 222 """ 223 Sends a message over the WebSocket connection. 224 225 Args: 226 message (Any): The message to be sent. 227 228 Raises: 229 Exception: If any error occurs during the process or if the connection is not ready after multiple attempts. 230 231 Note: 232 - Checks if the WebSocket connection is ready (`_is_ready` flag). 233 - If the connection is ready, resets the send attempt counter and sends the message. 234 - If the connection is not ready, retries after a delay and increments the send attempt counter. 235 - If the send attempt counter exceeds a threshold, triggers the connectionNotReady event and raises an exception. 236 """ 237 try: 238 if self._is_ready: 239 self._send_attempt_counter = 0 240 requestBodyJson = json.dumps(message) 241 await self.get_connection().send(requestBodyJson) 242 else: 243 await asyncio.sleep(1) 244 await self.send_message(message) 245 self._send_attempt_counter += 1 246 if(self._send_attempt_counter > 10): 247 self.trigger(WebSocketEvents.connectionNotReady) 248 self._send_attempt_counter = 0 249 raise 250 except Exception as e: 251 self.trigger(WebSocketEvents.sendMessageError, e) 252 raise 253 254 async def create_subscription(self, events): 255 """ 256 Creates a subscription to WebSocket events. 257 258 Args: 259 events (list): A required, non-empty list of events to subscribe to. 260 261 Raises: 262 Exception: If any error occurs during the process, if events are omitted, None, or empty, if a subscription creation is already in progress, or if a subscription already exists. 263 264 Note: 265 - Events are required: an omitted argument is rejected by ordinary Python argument checking, and None or empty events raise "Events are undefined" before any request is sent or any receive listener is attached. 266 - The client retains and reuses a single subscription object; a retry after a failed creation and a creation after removal reuse the retained object's existing registration flow with the newly supplied events. 267 - Rejects a call while a subscription creation is already in progress with "WebSocket subscription creation is already in progress; wait for subscriptionCreated or createSubscriptionError before retrying". 268 - Rejects a call after a subscription has been created with "A WebSocket subscription already exists; use update_subscription() to change its events or remove_subscription() before creating another". 269 - Rejections are delivered through the createSubscriptionError event and raised to the caller. 270 - If the WebSocket connection is ready (`_is_ready` flag), resets the send attempt counter and registers the retained subscription with the specified events. 271 - If the connection is not ready, retries after a delay and increments the send attempt counter. 272 - If the send attempt counter exceeds a threshold, triggers the connectionNotReady event and raises an exception. 273 """ 274 try: 275 if not events or len(events) == 0: 276 raise Exception("Events are undefined") 277 278 if self._subscription is None: 279 self._subscription = WebSocketSubscription(self) 280 subscription = self._subscription 281 282 if subscription._pending_creation_message_id is not None: 283 raise Exception("WebSocket subscription creation is already in progress; wait for subscriptionCreated or createSubscriptionError before retrying") 284 if subscription.get_subscription_info() is not None: 285 raise Exception("A WebSocket subscription already exists; use update_subscription() to change its events or remove_subscription() before creating another") 286 287 if self._is_ready: 288 self._send_attempt_counter = 0 289 await subscription.register(events) 290 else: 291 await asyncio.sleep(1) 292 await self.create_subscription(events) 293 self._send_attempt_counter += 1 294 if(self._send_attempt_counter > 10): 295 self.trigger(WebSocketEvents.connectionNotReady) 296 self._send_attempt_counter = 0 297 raise 298 299 except Exception as e: 300 self.trigger(WebSocketEvents.createSubscriptionError, e) 301 raise 302 303 async def update_subscription(self, subscription, events=None): 304 """ 305 Updates an existing WebSocket subscription with new events. 306 307 Args: 308 subscription : The WebSocket subscription to update. 309 events (list, optional): A list of events to update the subscription with. Default is None. 310 311 Returns: 312 WebSocketSubscription: The updated WebSocket subscription. 313 314 Raises: 315 Exception: If any error occurs during the process. 316 317 Note: 318 - Updates the specified WebSocket subscription with the new events provided. 319 - If the update is successful, returns the updated WebSocket subscription. 320 - If an error occurs during the update process, triggers the updateSubscriptionError event and raises an exception. 321 """ 322 try: 323 await subscription.update(events) 324 return subscription 325 except Exception as e: 326 self.trigger(WebSocketEvents.updateSubscriptionError, e) 327 raise 328 329 async def remove_subscription(self, subscription): 330 """ 331 Removes an existing WebSocket subscription. 332 333 Args: 334 subscription : The WebSocket subscription to remove. 335 336 Raises: 337 Exception: If any error occurs during the removal process. 338 339 Note: 340 - Removes the specified WebSocket subscription. 341 - If the removal is successful, the subscription is effectively unsubscribed from the events it was subscribed to. 342 - If an error occurs during the removal process, triggers the removeSubscriptionError event and raises an exception. 343 """ 344 try: 345 await subscription.remove() 346 except Exception as e: 347 self.trigger(WebSocketEvents.removeSubscriptionError, e) 348 raise
Event system for python
23 async def create_new_connection(self): 24 """ 25 Creates a new WebSocket connection. 26 27 Returns: 28 Any: Response object containing the result of the connection creation. 29 30 Raises: 31 Exception: If any error occurs during the process. 32 33 Note: 34 - Retrieves the WebSocket token using `get_web_socket_token`. 35 - Attempts to open a WebSocket connection using the retrieved token's URI and access token. 36 - Triggers the createConnectionError event if an error occurs and raises the exception. 37 """ 38 try: 39 web_socket_token = self.get_web_socket_token() 40 open_connection_response = await self.open_connection( 41 web_socket_token["uri"], web_socket_token["ws_access_token"] 42 ) 43 return open_connection_response 44 except Exception as e: 45 self.trigger(WebSocketEvents.createConnectionError, e) 46 raise
Creates a new WebSocket connection.
Returns: Any: Response object containing the result of the connection creation.
Raises: Exception: If any error occurs during the process.
Note:
- Retrieves the WebSocket token using get_web_socket_token.
- Attempts to open a WebSocket connection using the retrieved token's URI and access token.
- Triggers the createConnectionError event if an error occurs and raises the exception.
48 def get_web_socket_token(self): 49 """ 50 Retrieves a WebSocket token. 51 52 Returns: 53 dict: WebSocket token containing URI and access token. 54 55 Raises: 56 Exception: If any error occurs during the process. 57 58 Note: 59 - Sends a POST request to the '/restapi/oauth/wstoken' endpoint to obtain the WebSocket token. 60 - Returns the WebSocket token as a dictionary containing the URI and access token. 61 - Triggers the getTokenError event if an error occurs and raises the exception. 62 """ 63 try: 64 response = self._platform.post("/restapi/oauth/wstoken", body={}) 65 return response.json_dict() 66 except Exception as e: 67 self.trigger(WebSocketEvents.getTokenError, e) 68 raise
Retrieves a WebSocket token.
Returns: dict: WebSocket token containing URI and access token.
Raises: Exception: If any error occurs during the process.
Note: - Sends a POST request to the '/restapi/oauth/wstoken' endpoint to obtain the WebSocket token. - Returns the WebSocket token as a dictionary containing the URI and access token. - Triggers the getTokenError event if an error occurs and raises the exception.
70 async def open_connection(self, ws_uri, ws_access_token): 71 """ 72 Opens a WebSocket connection. 73 74 Args: 75 ws_uri (str): The WebSocket URI. 76 ws_access_token (str): The access token for WebSocket authentication. 77 78 Raises: 79 Exception: If any error occurs during the process. 80 81 Note: 82 - Attempts to establish a WebSocket connection to the provided URI with the given access token. 83 - Upon successful connection, sets up a heartbeat mechanism to maintain the connection. 84 - Triggers the connectionCreated event upon successful connection establishment. 85 - Listens for incoming messages and triggers the receiveMessage event for each received message. 86 - Isolates receive-message handler failures so a handler that raises does not stop delivery to the remaining handlers or reception of future messages; each failure triggers the receiveMessageError event with the original exception. 87 - Triggers the createConnectionError event if an error occurs while establishing the connection or during the initial handshake, and raises the exception. 88 - A failure from receiving messages after the initial handshake triggers the receiveMessageError event once; it is not reported as a connection-creation error and does not propagate to the connection-setup or recovery wrappers. 89 - When the receive loop terminates (receive failure, cancellation, or intentional closure), the client is marked not ready and its heartbeat task is cancelled. 90 - An intentional closure does not trigger the receiveMessageError event. 91 """ 92 self._done = False 93 try: 94 websocket = await websockets.connect( 95 f"{ws_uri}?access_token={ws_access_token}" 96 ) 97 connectionMessage = await websocket.recv() 98 except Exception as e: 99 self.trigger(WebSocketEvents.createConnectionError, e) 100 raise 101 102 connection_info = {} 103 connection_info["connection"] = websocket 104 connection_info["connection_details"] = connectionMessage 105 self._web_socket = connection_info 106 self._is_ready = True 107 self.trigger(WebSocketEvents.connectionCreated, self) 108 109 # heartbeat every 10 minutes 110 async def timer_function(): 111 while True: 112 if self._done: 113 timer.cancel() 114 break 115 await asyncio.sleep(600) 116 await self.send_message([{"type": "Heartbeat", "messageId": str(uuid.uuid4())}]) 117 timer = asyncio.create_task(timer_function()) 118 self._heartbeat_task = timer 119 120 try: 121 await asyncio.sleep(0) 122 while True: 123 message = await websocket.recv() 124 self.trigger(WebSocketEvents.receiveMessage, message) 125 await asyncio.sleep(0) 126 except Exception as e: 127 if not self._done: 128 self.trigger(WebSocketEvents.receiveMessageError, e) 129 finally: 130 self._is_ready = False 131 timer.cancel()
Opens a WebSocket connection.
Args: ws_uri (str): The WebSocket URI. ws_access_token (str): The access token for WebSocket authentication.
Raises: Exception: If any error occurs during the process.
Note: - Attempts to establish a WebSocket connection to the provided URI with the given access token. - Upon successful connection, sets up a heartbeat mechanism to maintain the connection. - Triggers the connectionCreated event upon successful connection establishment. - Listens for incoming messages and triggers the receiveMessage event for each received message. - Isolates receive-message handler failures so a handler that raises does not stop delivery to the remaining handlers or reception of future messages; each failure triggers the receiveMessageError event with the original exception. - Triggers the createConnectionError event if an error occurs while establishing the connection or during the initial handshake, and raises the exception. - A failure from receiving messages after the initial handshake triggers the receiveMessageError event once; it is not reported as a connection-creation error and does not propagate to the connection-setup or recovery wrappers. - When the receive loop terminates (receive failure, cancellation, or intentional closure), the client is marked not ready and its heartbeat task is cancelled. - An intentional closure does not trigger the receiveMessageError event.
133 def trigger(self, event, *args, **kw): 134 """ 135 Triggers the handlers registered for an event. 136 137 Args: 138 event (str): The event to trigger. 139 args: The event arguments. 140 kw: The event keyword arguments. 141 142 Returns: 143 bool: True if any handler was triggered, False if the event has no handlers. 144 145 Note: 146 - receiveMessage handlers are invoked independently: if one raises, the remaining handlers still receive the same raw message and one receiveMessageError event is triggered with the original exception. 147 - receiveMessageError handlers are also invoked independently: a failing error handler is contained so error reporting and reception continue. 148 - Every other event keeps the default dispatch behavior. 149 """ 150 if event == WebSocketEvents.receiveMessage: 151 return self._trigger_receive_message(*args, **kw) 152 if event == WebSocketEvents.receiveMessageError: 153 return self._trigger_receive_message_error(*args, **kw) 154 return Observable.trigger(self, event, *args, **kw)
Triggers the handlers registered for an event.
Args: event (str): The event to trigger. args: The event arguments. kw: The event keyword arguments.
Returns: bool: True if any handler was triggered, False if the event has no handlers.
Note: - receiveMessage handlers are invoked independently: if one raises, the remaining handlers still receive the same raw message and one receiveMessageError event is triggered with the original exception. - receiveMessageError handlers are also invoked independently: a failing error handler is contained so error reporting and reception continue. - Every other event keeps the default dispatch behavior.
184 async def close_connection(self): 185 """ 186 Closes the WebSocket connection. 187 188 Raises: 189 Exception: If any error occurs during the process. 190 191 Note: 192 - Sets the `_done` flag to True to signal the termination of the heartbeat mechanism. 193 - Sets the `_is_ready` flag to False to indicate that the connection is no longer ready. 194 - Retrieves the WebSocket connection using `get_connection`. 195 - Closes the WebSocket connection. 196 - Triggers the closeConnectionError event if an error occurs during the closing process and raises the exception. 197 """ 198 try: 199 self._done = True 200 self._is_ready = False 201 ws_connection = self.get_connection() 202 await ws_connection.close() 203 except Exception as e: 204 self.trigger(WebSocketEvents.closeConnectionError, e) 205 raise
Closes the WebSocket connection.
Raises: Exception: If any error occurs during the process.
Note:
- Sets the _done flag to True to signal the termination of the heartbeat mechanism.
- Sets the _is_ready flag to False to indicate that the connection is no longer ready.
- Retrieves the WebSocket connection using get_connection.
- Closes the WebSocket connection.
- Triggers the closeConnectionError event if an error occurs during the closing process and raises the exception.
207 async def recover_connection(self): 208 try: 209 ws_connection_info = self.get_web_socket_token() 210 recovered_connection_info = await self.open_connection( 211 ws_connection_info["uri"], ws_connection_info["ws_access_token"] 212 ) 213 # if recovered_connection_info['recoveryState'] === "Successful", then subscription is restored 214 # otherwise, need to create a new subscription 215 # IMPORTANT: WebSocket creation is successful if it doesn't raise any exception 216 return recovered_connection_info 217 except Exception as e: 218 self.trigger(WebSocketEvents.recoverConnectionError, e) 219 raise
221 async def send_message(self, message): 222 """ 223 Sends a message over the WebSocket connection. 224 225 Args: 226 message (Any): The message to be sent. 227 228 Raises: 229 Exception: If any error occurs during the process or if the connection is not ready after multiple attempts. 230 231 Note: 232 - Checks if the WebSocket connection is ready (`_is_ready` flag). 233 - If the connection is ready, resets the send attempt counter and sends the message. 234 - If the connection is not ready, retries after a delay and increments the send attempt counter. 235 - If the send attempt counter exceeds a threshold, triggers the connectionNotReady event and raises an exception. 236 """ 237 try: 238 if self._is_ready: 239 self._send_attempt_counter = 0 240 requestBodyJson = json.dumps(message) 241 await self.get_connection().send(requestBodyJson) 242 else: 243 await asyncio.sleep(1) 244 await self.send_message(message) 245 self._send_attempt_counter += 1 246 if(self._send_attempt_counter > 10): 247 self.trigger(WebSocketEvents.connectionNotReady) 248 self._send_attempt_counter = 0 249 raise 250 except Exception as e: 251 self.trigger(WebSocketEvents.sendMessageError, e) 252 raise
Sends a message over the WebSocket connection.
Args: message (Any): The message to be sent.
Raises: Exception: If any error occurs during the process or if the connection is not ready after multiple attempts.
Note:
- Checks if the WebSocket connection is ready (_is_ready flag).
- If the connection is ready, resets the send attempt counter and sends the message.
- If the connection is not ready, retries after a delay and increments the send attempt counter.
- If the send attempt counter exceeds a threshold, triggers the connectionNotReady event and raises an exception.
254 async def create_subscription(self, events): 255 """ 256 Creates a subscription to WebSocket events. 257 258 Args: 259 events (list): A required, non-empty list of events to subscribe to. 260 261 Raises: 262 Exception: If any error occurs during the process, if events are omitted, None, or empty, if a subscription creation is already in progress, or if a subscription already exists. 263 264 Note: 265 - Events are required: an omitted argument is rejected by ordinary Python argument checking, and None or empty events raise "Events are undefined" before any request is sent or any receive listener is attached. 266 - The client retains and reuses a single subscription object; a retry after a failed creation and a creation after removal reuse the retained object's existing registration flow with the newly supplied events. 267 - Rejects a call while a subscription creation is already in progress with "WebSocket subscription creation is already in progress; wait for subscriptionCreated or createSubscriptionError before retrying". 268 - Rejects a call after a subscription has been created with "A WebSocket subscription already exists; use update_subscription() to change its events or remove_subscription() before creating another". 269 - Rejections are delivered through the createSubscriptionError event and raised to the caller. 270 - If the WebSocket connection is ready (`_is_ready` flag), resets the send attempt counter and registers the retained subscription with the specified events. 271 - If the connection is not ready, retries after a delay and increments the send attempt counter. 272 - If the send attempt counter exceeds a threshold, triggers the connectionNotReady event and raises an exception. 273 """ 274 try: 275 if not events or len(events) == 0: 276 raise Exception("Events are undefined") 277 278 if self._subscription is None: 279 self._subscription = WebSocketSubscription(self) 280 subscription = self._subscription 281 282 if subscription._pending_creation_message_id is not None: 283 raise Exception("WebSocket subscription creation is already in progress; wait for subscriptionCreated or createSubscriptionError before retrying") 284 if subscription.get_subscription_info() is not None: 285 raise Exception("A WebSocket subscription already exists; use update_subscription() to change its events or remove_subscription() before creating another") 286 287 if self._is_ready: 288 self._send_attempt_counter = 0 289 await subscription.register(events) 290 else: 291 await asyncio.sleep(1) 292 await self.create_subscription(events) 293 self._send_attempt_counter += 1 294 if(self._send_attempt_counter > 10): 295 self.trigger(WebSocketEvents.connectionNotReady) 296 self._send_attempt_counter = 0 297 raise 298 299 except Exception as e: 300 self.trigger(WebSocketEvents.createSubscriptionError, e) 301 raise
Creates a subscription to WebSocket events.
Args: events (list): A required, non-empty list of events to subscribe to.
Raises: Exception: If any error occurs during the process, if events are omitted, None, or empty, if a subscription creation is already in progress, or if a subscription already exists.
Note:
- Events are required: an omitted argument is rejected by ordinary Python argument checking, and None or empty events raise "Events are undefined" before any request is sent or any receive listener is attached.
- The client retains and reuses a single subscription object; a retry after a failed creation and a creation after removal reuse the retained object's existing registration flow with the newly supplied events.
- Rejects a call while a subscription creation is already in progress with "WebSocket subscription creation is already in progress; wait for subscriptionCreated or createSubscriptionError before retrying".
- Rejects a call after a subscription has been created with "A WebSocket subscription already exists; use update_subscription() to change its events or remove_subscription() before creating another".
- Rejections are delivered through the createSubscriptionError event and raised to the caller.
- If the WebSocket connection is ready (_is_ready flag), resets the send attempt counter and registers the retained subscription with the specified events.
- If the connection is not ready, retries after a delay and increments the send attempt counter.
- If the send attempt counter exceeds a threshold, triggers the connectionNotReady event and raises an exception.
303 async def update_subscription(self, subscription, events=None): 304 """ 305 Updates an existing WebSocket subscription with new events. 306 307 Args: 308 subscription : The WebSocket subscription to update. 309 events (list, optional): A list of events to update the subscription with. Default is None. 310 311 Returns: 312 WebSocketSubscription: The updated WebSocket subscription. 313 314 Raises: 315 Exception: If any error occurs during the process. 316 317 Note: 318 - Updates the specified WebSocket subscription with the new events provided. 319 - If the update is successful, returns the updated WebSocket subscription. 320 - If an error occurs during the update process, triggers the updateSubscriptionError event and raises an exception. 321 """ 322 try: 323 await subscription.update(events) 324 return subscription 325 except Exception as e: 326 self.trigger(WebSocketEvents.updateSubscriptionError, e) 327 raise
Updates an existing WebSocket subscription with new events.
Args: subscription : The WebSocket subscription to update. events (list, optional): A list of events to update the subscription with. Default is None.
Returns: WebSocketSubscription: The updated WebSocket subscription.
Raises: Exception: If any error occurs during the process.
Note: - Updates the specified WebSocket subscription with the new events provided. - If the update is successful, returns the updated WebSocket subscription. - If an error occurs during the update process, triggers the updateSubscriptionError event and raises an exception.
329 async def remove_subscription(self, subscription): 330 """ 331 Removes an existing WebSocket subscription. 332 333 Args: 334 subscription : The WebSocket subscription to remove. 335 336 Raises: 337 Exception: If any error occurs during the removal process. 338 339 Note: 340 - Removes the specified WebSocket subscription. 341 - If the removal is successful, the subscription is effectively unsubscribed from the events it was subscribed to. 342 - If an error occurs during the removal process, triggers the removeSubscriptionError event and raises an exception. 343 """ 344 try: 345 await subscription.remove() 346 except Exception as e: 347 self.trigger(WebSocketEvents.removeSubscriptionError, e) 348 raise
Removes an existing WebSocket subscription.
Args: subscription : The WebSocket subscription to remove.
Raises: Exception: If any error occurs during the removal process.
Note: - Removes the specified WebSocket subscription. - If the removal is successful, the subscription is effectively unsubscribed from the events it was subscribed to. - If an error occurs during the removal process, triggers the removeSubscriptionError event and raises an exception.
Inherited Members
- observable.core.Observable
- events
- on
- off
- once