from __future__ import annotations
import base64
import concurrent.futures
import contextlib
import json
import queue
import secrets
import sys
import threading
import typing
from types import TracebackType
if sys.version_info >= (3, 13):
from typing import TypeVar else:
from typing_extensions import TypeVar
import anyio
import wsproto
import wsproto.utilities
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from wsproto.frame_protocol import CloseReason
from eggfetch.compat.httpx2._client import USE_CLIENT_DEFAULT
from eggfetch.compat.httpx2._config import (
DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
DEFAULT_MAX_MESSAGE_SIZE_BYTES,
DEFAULT_QUEUE_SIZE,
)
from eggfetch.compat.httpx2._headers import Headers
from ._exceptions import (
HTTPXWSException,
WebSocketDisconnect,
WebSocketInvalidTypeReceived,
WebSocketNetworkError,
WebSocketUpgradeError,
)
from ._ping import AsyncPingManager, PingManager
from ._transport import ASGIWebSocketAsyncNetworkStream
if typing.TYPE_CHECKING:
from typing import Any as AsyncNetworkStream from typing import Any as NetworkStream
from eggfetch.compat.httpx2._client import AsyncClient, Client, UseClientDefault
from eggfetch.compat.httpx._response import Response
from typing import Any as AuthTypes from typing import Any as CookieTypes from typing import Any as HeaderTypes from typing import Any as QueryParamTypes from typing import Any as RequestExtensions from typing import Any as TimeoutTypes
JSONMode = typing.Literal["text", "binary"]
TaskFunction = typing.TypeVar("TaskFunction")
TaskResult = typing.TypeVar("TaskResult")
SyncSession = TypeVar("SyncSession", bound="WebSocketSession", default="WebSocketSession")
AsyncSession = TypeVar("AsyncSession", bound="AsyncWebSocketSession", default="AsyncWebSocketSession")
class ShouldClose(Exception):
pass
class EndOfStream(Exception):
pass
class WebSocketSession:
subprotocol: str | None
response: Response | None
def __init__(
self,
stream: NetworkStream,
*,
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
queue_size: int = DEFAULT_QUEUE_SIZE,
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
response: Response | None = None,
) -> None:
self.stream = stream
self.connection = wsproto.connection.Connection(wsproto.ConnectionType.CLIENT)
self.response = response
if self.response is not None:
self.subprotocol = self.response.headers.get("sec-websocket-protocol")
else:
self.subprotocol = None
self._events: queue.Queue[wsproto.events.Event | HTTPXWSException] = queue.Queue(queue_size)
self._ping_manager = PingManager()
self._should_close = threading.Event()
self._write_lock = threading.Lock()
self._should_close_task: concurrent.futures.Future[bool] | None = None
self._executor: concurrent.futures.ThreadPoolExecutor | None = None
self._max_message_size_bytes = max_message_size_bytes
self._queue_size = queue_size
self._keepalive_ping_interval_seconds = keepalive_ping_interval_seconds
self._keepalive_ping_timeout_seconds = keepalive_ping_timeout_seconds
def _get_executor_should_close_task(
self,
) -> tuple[concurrent.futures.ThreadPoolExecutor, concurrent.futures.Future[bool]]:
if self._should_close_task is None:
self._executor = concurrent.futures.ThreadPoolExecutor()
self._should_close_task = self._executor.submit(self._should_close.wait)
assert self._executor is not None
return self._executor, self._should_close_task
def __enter__(self) -> WebSocketSession:
self._background_receive_task = threading.Thread(
target=self._background_receive, args=(self._max_message_size_bytes,)
)
self._background_receive_task.start()
self._background_keepalive_ping_task: threading.Thread | None = None
if self._keepalive_ping_interval_seconds is not None:
self._background_keepalive_ping_task = threading.Thread(
target=self._background_keepalive_ping,
args=(
self._keepalive_ping_interval_seconds,
self._keepalive_ping_timeout_seconds,
),
)
self._background_keepalive_ping_task.start()
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
self.close()
self._background_receive_task.join()
if self._background_keepalive_ping_task is not None:
self._background_keepalive_ping_task.join()
def ping(self, payload: bytes = b"") -> threading.Event:
ping_id, callback = self._ping_manager.create(payload)
event = wsproto.events.Ping(ping_id)
self.send(event)
return callback
def send(self, event: wsproto.events.Event) -> None:
import httpcore2
try:
data = self.connection.send(event)
with self._write_lock:
self.stream.write(data)
except httpcore2.WriteError as e:
self.close(CloseReason.INTERNAL_ERROR, "Stream write error")
raise WebSocketNetworkError() from e
def send_text(self, data: str) -> None:
event = wsproto.events.TextMessage(data=data)
self.send(event)
def send_bytes(self, data: bytes) -> None:
event = wsproto.events.BytesMessage(data=data)
self.send(event)
def send_json(self, data: typing.Any, mode: JSONMode = "text") -> None:
assert mode in ["text", "binary"]
serialized_data = json.dumps(data)
if mode == "text":
self.send_text(serialized_data)
else:
self.send_bytes(serialized_data.encode("utf-8"))
def receive(self, timeout: float | None = None) -> wsproto.events.Event:
try:
event = self._events.get(block=True, timeout=timeout)
except queue.Empty as e:
raise TimeoutError from e
if isinstance(event, HTTPXWSException):
raise event
if isinstance(event, wsproto.events.CloseConnection):
raise WebSocketDisconnect(event.code, event.reason)
return event
def receive_text(self, timeout: float | None = None) -> str:
event = self.receive(timeout)
if isinstance(event, wsproto.events.TextMessage):
return event.data
raise WebSocketInvalidTypeReceived(event)
def receive_bytes(self, timeout: float | None = None) -> bytes:
event = self.receive(timeout)
if isinstance(event, wsproto.events.BytesMessage):
return bytes(event.data)
raise WebSocketInvalidTypeReceived(event)
def receive_json(self, timeout: float | None = None, mode: JSONMode = "text") -> typing.Any:
assert mode in ["text", "binary"]
data: str | bytes
if mode == "text":
data = self.receive_text(timeout)
elif mode == "binary":
data = self.receive_bytes(timeout)
return json.loads(data)
def close(self, code: int = 1000, reason: str | None = None) -> None:
import httpcore2
self._should_close.set()
if self._executor is not None:
self._executor.shutdown(False)
if self.connection.state not in {
wsproto.connection.ConnectionState.LOCAL_CLOSING,
wsproto.connection.ConnectionState.CLOSED,
}:
event = wsproto.events.CloseConnection(code, reason)
data = self.connection.send(event)
try:
with self._write_lock:
self.stream.write(data)
except httpcore2.WriteError:
pass
self.stream.close()
def _background_receive(self, max_bytes: int) -> None:
import httpcore2
partial_message_buffer: str | bytes | None = None
partial_message_size = 0
try:
while not self._should_close.is_set():
data = self._wait_until_closed(self._read_stream, max_bytes)
self.connection.receive_data(data)
for event in self.connection.events():
if isinstance(event, wsproto.events.Ping):
data = self.connection.send(event.response())
with self._write_lock:
self.stream.write(data)
continue
if isinstance(event, wsproto.events.Pong):
self._ping_manager.ack(event.payload)
continue
if isinstance(event, wsproto.events.CloseConnection):
self._should_close.set()
if isinstance(event, wsproto.events.Message):
partial_message_size += len(event.data.encode() if isinstance(event.data, str) else event.data)
if partial_message_size > max_bytes:
self.close(CloseReason.MESSAGE_TOO_BIG, "Message too big")
self._events.put(WebSocketDisconnect(CloseReason.MESSAGE_TOO_BIG, "Message too big"))
break
if not event.message_finished:
if partial_message_buffer is None:
partial_message_buffer = event.data
else:
partial_message_buffer += event.data
elif partial_message_buffer is None:
partial_message_size = 0
self._events.put(event)
else:
event_type = type(event)
full_message_event = event_type(partial_message_buffer + event.data)
partial_message_buffer = None
partial_message_size = 0
self._events.put(full_message_event)
continue
self._events.put(event)
except (httpcore2.ReadError, httpcore2.WriteError, EndOfStream):
self.close(CloseReason.INTERNAL_ERROR, "Stream error")
self._events.put(WebSocketNetworkError())
except ShouldClose:
pass
def _background_keepalive_ping(self, interval_seconds: float, timeout_seconds: float | None = None) -> None:
try:
while not self._should_close.is_set():
should_close = self._wait_until_closed(self._should_close.wait, interval_seconds)
if should_close: raise ShouldClose()
pong_callback = self.ping()
if timeout_seconds is not None:
acknowledged = self._wait_until_closed(pong_callback.wait, timeout_seconds)
if not acknowledged:
self.close(CloseReason.INTERNAL_ERROR, "Keepalive ping timeout")
self._events.put(WebSocketNetworkError())
except ShouldClose:
pass
def _wait_until_closed(
self, callable: typing.Callable[..., TaskResult], *args: typing.Any, **kwargs: typing.Any
) -> TaskResult:
try:
executor, should_close_task = self._get_executor_should_close_task()
todo_task = executor.submit(callable, *args, **kwargs)
except RuntimeError as e:
raise ShouldClose() from e
else:
done, _ = concurrent.futures.wait(
(todo_task, should_close_task), return_when=concurrent.futures.FIRST_COMPLETED,
)
if should_close_task in done:
raise ShouldClose()
assert todo_task in done
result = todo_task.result()
return result
def _read_stream(self, max_bytes: int) -> bytes:
data = self.stream.read(max_bytes)
if data == b"":
raise EndOfStream()
return data
class AsyncWebSocketSession(anyio.AsyncContextManagerMixin):
subprotocol: str | None
response: Response | None
_send_event: MemoryObjectSendStream[wsproto.events.Event | HTTPXWSException]
_receive_event: MemoryObjectReceiveStream[wsproto.events.Event | HTTPXWSException]
def __init__(
self,
stream: AsyncNetworkStream,
*,
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
queue_size: int = DEFAULT_QUEUE_SIZE,
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
response: Response | None = None,
) -> None:
self.stream = stream
self.connection = wsproto.connection.Connection(wsproto.ConnectionType.CLIENT)
self.response = response
if self.response is not None:
self.subprotocol = self.response.headers.get("sec-websocket-protocol")
else:
self.subprotocol = None
self._ping_manager = AsyncPingManager()
self._should_close = anyio.Event()
self._write_lock = anyio.Lock()
self._max_message_size_bytes = max_message_size_bytes
self._queue_size = queue_size
if isinstance(stream, ASGIWebSocketAsyncNetworkStream):
self._keepalive_ping_interval_seconds = None
self._keepalive_ping_timeout_seconds = None
else:
self._keepalive_ping_interval_seconds = keepalive_ping_interval_seconds
self._keepalive_ping_timeout_seconds = keepalive_ping_timeout_seconds
@contextlib.asynccontextmanager
async def __asynccontextmanager__(self) -> typing.AsyncGenerator[AsyncWebSocketSession, None]:
self._send_event, self._receive_event = anyio.create_memory_object_stream[
wsproto.events.Event | HTTPXWSException
]()
self._background_task_group = anyio.create_task_group()
async with self._send_event, self._receive_event, self._background_task_group:
self._background_task_group.start_soon(self._background_receive, self._max_message_size_bytes)
if self._keepalive_ping_interval_seconds is not None:
self._background_task_group.start_soon(
self._background_keepalive_ping,
self._keepalive_ping_interval_seconds,
self._keepalive_ping_timeout_seconds,
)
try:
yield self
finally:
self._background_task_group.cancel_scope.cancel()
with anyio.CancelScope(shield=True):
await self.close()
async def ping(self, payload: bytes = b"") -> anyio.Event:
ping_id, callback = self._ping_manager.create(payload)
event = wsproto.events.Ping(ping_id)
await self.send(event)
return callback
async def send(self, event: wsproto.events.Event) -> None:
import httpcore2
try:
data = self.connection.send(event)
async with self._write_lock:
await self.stream.write(data)
except httpcore2.WriteError as e:
await self.close(CloseReason.INTERNAL_ERROR, "Stream write error")
raise WebSocketNetworkError() from e
async def send_text(self, data: str) -> None:
event = wsproto.events.TextMessage(data=data)
await self.send(event)
async def send_bytes(self, data: bytes) -> None:
event = wsproto.events.BytesMessage(data=data)
await self.send(event)
async def send_json(self, data: typing.Any, mode: JSONMode = "text") -> None:
assert mode in ["text", "binary"]
serialized_data = json.dumps(data)
if mode == "text":
await self.send_text(serialized_data)
else:
await self.send_bytes(serialized_data.encode("utf-8"))
async def receive(self, timeout: float | None = None) -> wsproto.events.Event:
with anyio.fail_after(timeout):
event = await self._receive_event.receive()
if isinstance(event, HTTPXWSException):
raise event
if isinstance(event, wsproto.events.CloseConnection):
raise WebSocketDisconnect(event.code, event.reason)
return event
async def receive_text(self, timeout: float | None = None) -> str:
event = await self.receive(timeout)
if isinstance(event, wsproto.events.TextMessage):
return event.data
raise WebSocketInvalidTypeReceived(event)
async def receive_bytes(self, timeout: float | None = None) -> bytes:
event = await self.receive(timeout)
if isinstance(event, wsproto.events.BytesMessage):
return bytes(event.data)
raise WebSocketInvalidTypeReceived(event)
async def receive_json(self, timeout: float | None = None, mode: JSONMode = "text") -> typing.Any:
assert mode in ["text", "binary"]
data: str | bytes
if mode == "text":
data = await self.receive_text(timeout)
elif mode == "binary":
data = await self.receive_bytes(timeout)
return json.loads(data)
async def close(self, code: int = 1000, reason: str | None = None) -> None:
import httpcore2
self._should_close.set()
if self.connection.state not in {
wsproto.connection.ConnectionState.LOCAL_CLOSING,
wsproto.connection.ConnectionState.CLOSED,
}:
event = wsproto.events.CloseConnection(code, reason)
data = self.connection.send(event)
try:
async with self._write_lock:
await self.stream.write(data)
except httpcore2.WriteError:
pass
await self.stream.aclose()
async def _background_receive(self, max_bytes: int) -> None:
import httpcore2
partial_message_buffer: str | bytes | None = None
partial_message_size = 0
try:
while not self._should_close.is_set():
data = await self._read_stream(max_bytes)
self.connection.receive_data(data)
for event in self.connection.events():
if isinstance(event, wsproto.events.Ping):
data = self.connection.send(event.response())
async with self._write_lock:
await self.stream.write(data)
continue
if isinstance(event, wsproto.events.Pong):
self._ping_manager.ack(event.payload)
continue
if isinstance(event, wsproto.events.CloseConnection):
self._should_close.set()
if isinstance(event, wsproto.events.Message):
partial_message_size += len(event.data.encode() if isinstance(event.data, str) else event.data)
if partial_message_size > max_bytes:
await self.close(CloseReason.MESSAGE_TOO_BIG, "Message too big")
await self._send_event.send(
WebSocketDisconnect(CloseReason.MESSAGE_TOO_BIG, "Message too big")
)
break
if not event.message_finished:
if partial_message_buffer is None:
partial_message_buffer = event.data
else:
partial_message_buffer += event.data
elif partial_message_buffer is None:
partial_message_size = 0
await self._send_event.send(event)
else:
event_type = type(event)
full_message_event = event_type(partial_message_buffer + event.data)
partial_message_buffer = None
partial_message_size = 0
await self._send_event.send(full_message_event)
continue
await self._send_event.send(event)
except (httpcore2.ReadError, httpcore2.WriteError, EndOfStream):
await self.close(CloseReason.INTERNAL_ERROR, "Stream error")
await self._send_event.send(WebSocketNetworkError())
async def _background_keepalive_ping(self, interval_seconds: float, timeout_seconds: float | None = None) -> None:
while not self._should_close.is_set():
await anyio.sleep(interval_seconds)
try:
pong_callback = await self.ping()
except wsproto.utilities.LocalProtocolError:
return
if timeout_seconds is not None:
try:
with anyio.fail_after(timeout_seconds):
await pong_callback.wait()
except TimeoutError:
await self.close(CloseReason.INTERNAL_ERROR, "Keepalive ping timeout")
await self._send_event.send(WebSocketNetworkError())
async def _read_stream(self, max_bytes: int) -> bytes:
data = await self.stream.read(max_bytes)
if data == b"":
raise EndOfStream()
return data
def _get_headers(
subprotocols: list[str] | None,
) -> dict[str, typing.Any]:
headers = {
"connection": "upgrade",
"upgrade": "websocket",
"sec-websocket-key": base64.b64encode(secrets.token_bytes(16)).decode("utf-8"),
"sec-websocket-version": "13",
}
if subprotocols is not None:
headers["sec-websocket-protocol"] = ", ".join(subprotocols)
return headers
class WebSocketClient(typing.Generic[SyncSession]):
def __init__(
self,
client: Client,
*,
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
queue_size: int = DEFAULT_QUEUE_SIZE,
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
session_class: type[SyncSession] = WebSocketSession, ) -> None:
self.client = client
self.max_message_size_bytes = max_message_size_bytes
self.queue_size = queue_size
self.keepalive_ping_interval_seconds = keepalive_ping_interval_seconds
self.keepalive_ping_timeout_seconds = keepalive_ping_timeout_seconds
self.session_class = session_class
@contextlib.contextmanager
def connect(
self,
url: str,
*,
subprotocols: list[str] | None = None,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
extensions: RequestExtensions | None = None,
) -> typing.Generator[SyncSession, None, None]:
with self.client.stream(
"GET",
url,
params=params,
headers=Headers(headers) | _get_headers(subprotocols),
cookies=cookies,
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
) as response:
if response.status_code != 101:
raise WebSocketUpgradeError(response)
session = self.session_class(
response.extensions["network_stream"],
max_message_size_bytes=self.max_message_size_bytes,
queue_size=self.queue_size,
keepalive_ping_interval_seconds=self.keepalive_ping_interval_seconds,
keepalive_ping_timeout_seconds=self.keepalive_ping_timeout_seconds,
response=response,
)
with session:
yield session
@contextlib.contextmanager
def connect_ws(
url: str,
client: Client | None = None,
*,
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
queue_size: int = DEFAULT_QUEUE_SIZE,
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
subprotocols: list[str] | None = None,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
extensions: RequestExtensions | None = None,
) -> typing.Generator[WebSocketSession, None, None]:
if client is None:
from .._client import Client
owned_client: contextlib.AbstractContextManager[Client] = Client()
else:
owned_client = contextlib.nullcontext(client)
with owned_client as client:
ws_client = WebSocketClient(
client=client,
max_message_size_bytes=max_message_size_bytes,
queue_size=queue_size,
keepalive_ping_interval_seconds=keepalive_ping_interval_seconds,
keepalive_ping_timeout_seconds=keepalive_ping_timeout_seconds,
)
with ws_client.connect(
url,
subprotocols=subprotocols,
params=params,
headers=headers,
cookies=cookies,
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
) as websocket:
yield websocket
class AsyncWebSocketClient(typing.Generic[AsyncSession]):
def __init__(
self,
client: AsyncClient,
*,
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
queue_size: int = DEFAULT_QUEUE_SIZE,
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
session_class: type[AsyncSession] = AsyncWebSocketSession, ) -> None:
self.client = client
self.max_message_size_bytes = max_message_size_bytes
self.queue_size = queue_size
self.keepalive_ping_interval_seconds = keepalive_ping_interval_seconds
self.keepalive_ping_timeout_seconds = keepalive_ping_timeout_seconds
self.session_class = session_class
@contextlib.asynccontextmanager
async def connect(
self,
url: str,
*,
subprotocols: list[str] | None = None,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
extensions: RequestExtensions | None = None,
) -> typing.AsyncGenerator[AsyncSession, None]:
async with self.client.stream(
"GET",
url,
params=params,
headers=Headers(headers) | _get_headers(subprotocols),
cookies=cookies,
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
) as response:
if response.status_code != 101:
raise WebSocketUpgradeError(response)
session = self.session_class(
response.extensions["network_stream"],
max_message_size_bytes=self.max_message_size_bytes,
queue_size=self.queue_size,
keepalive_ping_interval_seconds=self.keepalive_ping_interval_seconds,
keepalive_ping_timeout_seconds=self.keepalive_ping_timeout_seconds,
response=response,
)
async with session:
yield session
@contextlib.asynccontextmanager
async def aconnect_ws(
url: str,
client: AsyncClient | None = None,
*,
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
queue_size: int = DEFAULT_QUEUE_SIZE,
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
subprotocols: list[str] | None = None,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
extensions: RequestExtensions | None = None,
) -> typing.AsyncGenerator[AsyncWebSocketSession, None]:
if client is None:
from .._client import AsyncClient
owned_client: contextlib.AbstractAsyncContextManager[AsyncClient] = AsyncClient()
else:
owned_client = contextlib.nullcontext(client)
async with owned_client as client:
ws_client = AsyncWebSocketClient(
client=client,
max_message_size_bytes=max_message_size_bytes,
queue_size=queue_size,
keepalive_ping_interval_seconds=keepalive_ping_interval_seconds,
keepalive_ping_timeout_seconds=keepalive_ping_timeout_seconds,
)
async with ws_client.connect(
url,
subprotocols=subprotocols,
params=params,
headers=headers,
cookies=cookies,
auth=auth,
follow_redirects=follow_redirects,
timeout=timeout,
extensions=extensions,
) as websocket:
yield websocket