import asyncio
from asyncio.exceptions import CancelledError
from contextlib import suppress
from urllib.parse import urljoin, urlparse
import websockets
from websockets.exceptions import ConnectionClosed
from .._exceptions import ISCPTransportClosedError
from ._negotiation_params import NegotiationParams
from ._transport import Connector, Transport, TransportName
__all__ = ["WebSocket", "WebSocketConnector"]
_KEEP_ALIVE_INTERVAL: float = 10
class WebSocket(Transport):
"""Websocketトランスポートです。"""
def __init__(
self,
url: str,
negotiation_params=NegotiationParams(),
):
self._url = url
self._keep_alive_interval = _KEEP_ALIVE_INTERVAL
self._called_close_event = asyncio.Event()
self._done_keep_alive_event = asyncio.Event()
self._negotiation_params = negotiation_params
async def open(self):
self._wsconn = await websockets.connect(f"{self._url}?{self._negotiation_params.encode_to_url_values()}")
self._keep_alive_task = asyncio.create_task(self._keep_alive(self._keep_alive_interval))
self._called_close_event.clear()
@property
def address(self) -> str:
url = urlparse(self._url)
return url.netloc
async def __aenter__(self):
await self.open()
return self
async def __aexit__(self, exc_type, exc, tb):
await self.close()
async def write(self, msg: bytes):
try:
await self._wsconn.send(msg)
except ConnectionClosed as e:
raise ISCPTransportClosedError from e
async def read(self) -> bytes:
try:
return await self._wsconn.recv()
except ConnectionClosed as e:
raise ISCPTransportClosedError from e
async def _keep_alive(self, interval: float):
while not self._called_close_event.is_set() and not self._wsconn.closed:
pong_waiter = await self._wsconn.ping()
await pong_waiter
await asyncio.sleep(interval)
async def close(self):
if self._called_close_event.is_set():
return None
self._called_close_event.set()
await self._wsconn.close()
await self._wsconn.wait_closed()
self._keep_alive_task.cancel()
with suppress(CancelledError):
await self._keep_alive_task
def negotiation_params(self):
return self._negotiation_params
@property
def name(self) -> TransportName:
return TransportName.WEBSOCKET
[ドキュメント]class WebSocketConnector(Connector):
"""
WebSocketのiSCPコネクターです。
Args:
enable_tls(bool): TLS有効化フラグ。Trueの場合は ``wss://${address}/${path}`` ,Falseの場合は ``ws://${address}/${path}`` でアクセスを試みます。
path(str): 接続先のaddressに続くパス
"""
def __init__(self, *, keep_alive_interval: float = 10, enable_tls=True, path="api/iscp/connect"):
self._keep_alive_interval = keep_alive_interval
self._scheme = "wss" if enable_tls else "ws"
self._path = path
async def connect(self, address: str, negotiation_params: NegotiationParams) -> WebSocket:
ws = WebSocket(
url=urljoin(f"{self._scheme}://{address}", self._path),
negotiation_params=negotiation_params,
)
await ws.open()
return ws