Voice et bot modif
This commit is contained in:
parent
189d56026b
commit
7333a22bcd
10774 changed files with 634644 additions and 933308 deletions
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
import asyncio
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from types import TracebackType
|
||||
from typing import Any, Optional, Type, cast
|
||||
from typing import Any, Generic, Literal, Optional, cast, overload
|
||||
|
||||
import attr
|
||||
|
||||
|
|
@ -17,6 +18,8 @@ from .http import (
|
|||
WebSocketError,
|
||||
WSCloseCode,
|
||||
WSMessage,
|
||||
WSMessageDecodeText,
|
||||
WSMessageNoDecodeText,
|
||||
WSMsgType,
|
||||
)
|
||||
from .http_websocket import _INTERNAL_RECEIVE_TYPES, WebSocketWriter
|
||||
|
|
@ -24,14 +27,26 @@ from .streams import EofStream
|
|||
from .typedefs import (
|
||||
DEFAULT_JSON_DECODER,
|
||||
DEFAULT_JSON_ENCODER,
|
||||
JSONBytesEncoder,
|
||||
JSONDecoder,
|
||||
JSONEncoder,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar
|
||||
else:
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import asyncio as async_timeout
|
||||
from typing import Self
|
||||
else:
|
||||
import async_timeout
|
||||
from typing_extensions import Self
|
||||
|
||||
# TypeVar for whether text messages are decoded to str (True) or kept as bytes (False)
|
||||
# Covariant because it only affects return types, not input types
|
||||
_DecodeText = TypeVar("_DecodeText", bound=bool, covariant=True, default=Literal[True])
|
||||
|
||||
|
||||
@attr.s(frozen=True, slots=True)
|
||||
|
|
@ -43,19 +58,19 @@ class ClientWSTimeout:
|
|||
DEFAULT_WS_CLIENT_TIMEOUT = ClientWSTimeout(ws_receive=None, ws_close=10.0)
|
||||
|
||||
|
||||
class ClientWebSocketResponse:
|
||||
class ClientWebSocketResponse(Generic[_DecodeText]):
|
||||
def __init__(
|
||||
self,
|
||||
reader: WebSocketDataQueue,
|
||||
writer: WebSocketWriter,
|
||||
protocol: Optional[str],
|
||||
protocol: str | None,
|
||||
response: ClientResponse,
|
||||
timeout: ClientWSTimeout,
|
||||
autoclose: bool,
|
||||
autoping: bool,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
*,
|
||||
heartbeat: Optional[float] = None,
|
||||
heartbeat: float | None = None,
|
||||
compress: int = 0,
|
||||
client_notakeover: bool = False,
|
||||
) -> None:
|
||||
|
|
@ -67,28 +82,34 @@ class ClientWebSocketResponse:
|
|||
self._protocol = protocol
|
||||
self._closed = False
|
||||
self._closing = False
|
||||
self._close_code: Optional[int] = None
|
||||
self._close_code: int | None = None
|
||||
self._timeout = timeout
|
||||
self._autoclose = autoclose
|
||||
self._autoping = autoping
|
||||
self._heartbeat = heartbeat
|
||||
self._heartbeat_cb: Optional[asyncio.TimerHandle] = None
|
||||
self._heartbeat_cb: asyncio.TimerHandle | None = None
|
||||
self._heartbeat_when: float = 0.0
|
||||
if heartbeat is not None:
|
||||
self._pong_heartbeat = heartbeat / 2.0
|
||||
self._pong_response_cb: Optional[asyncio.TimerHandle] = None
|
||||
self._pong_response_cb: asyncio.TimerHandle | None = None
|
||||
self._loop = loop
|
||||
self._waiting: bool = False
|
||||
self._close_wait: Optional[asyncio.Future[None]] = None
|
||||
self._exception: Optional[BaseException] = None
|
||||
self._close_wait: asyncio.Future[None] | None = None
|
||||
self._exception: BaseException | None = None
|
||||
self._compress = compress
|
||||
self._client_notakeover = client_notakeover
|
||||
self._ping_task: Optional[asyncio.Task[None]] = None
|
||||
self._ping_task: asyncio.Task[None] | None = None
|
||||
self._need_heartbeat_reset = False
|
||||
self._heartbeat_reset_handle: asyncio.Handle | None = None
|
||||
|
||||
self._reset_heartbeat()
|
||||
|
||||
def _cancel_heartbeat(self) -> None:
|
||||
self._cancel_pong_response_cb()
|
||||
if self._heartbeat_reset_handle is not None:
|
||||
self._heartbeat_reset_handle.cancel()
|
||||
self._heartbeat_reset_handle = None
|
||||
self._need_heartbeat_reset = False
|
||||
if self._heartbeat_cb is not None:
|
||||
self._heartbeat_cb.cancel()
|
||||
self._heartbeat_cb = None
|
||||
|
|
@ -101,6 +122,23 @@ class ClientWebSocketResponse:
|
|||
self._pong_response_cb.cancel()
|
||||
self._pong_response_cb = None
|
||||
|
||||
def _on_data_received(self) -> None:
|
||||
if self._heartbeat is None or self._need_heartbeat_reset:
|
||||
return
|
||||
loop = self._loop
|
||||
assert loop is not None
|
||||
# Coalesce multiple chunks received in the same loop tick into a single
|
||||
# heartbeat reset. Resetting immediately per chunk increases timer churn.
|
||||
self._need_heartbeat_reset = True
|
||||
self._heartbeat_reset_handle = loop.call_soon(self._flush_heartbeat_reset)
|
||||
|
||||
def _flush_heartbeat_reset(self) -> None:
|
||||
self._heartbeat_reset_handle = None
|
||||
if not self._need_heartbeat_reset:
|
||||
return
|
||||
self._reset_heartbeat()
|
||||
self._need_heartbeat_reset = False
|
||||
|
||||
def _reset_heartbeat(self) -> None:
|
||||
if self._heartbeat is None:
|
||||
return
|
||||
|
|
@ -124,6 +162,12 @@ class ClientWebSocketResponse:
|
|||
|
||||
def _send_heartbeat(self) -> None:
|
||||
self._heartbeat_cb = None
|
||||
|
||||
# If heartbeat reset is pending (data is being received), skip sending
|
||||
# the ping and let the reset callback handle rescheduling the heartbeat.
|
||||
if self._need_heartbeat_reset:
|
||||
return
|
||||
|
||||
loop = self._loop
|
||||
now = loop.time()
|
||||
if now < self._heartbeat_when:
|
||||
|
|
@ -199,11 +243,11 @@ class ClientWebSocketResponse:
|
|||
return self._closed
|
||||
|
||||
@property
|
||||
def close_code(self) -> Optional[int]:
|
||||
def close_code(self) -> int | None:
|
||||
return self._close_code
|
||||
|
||||
@property
|
||||
def protocol(self) -> Optional[str]:
|
||||
def protocol(self) -> str | None:
|
||||
return self._protocol
|
||||
|
||||
@property
|
||||
|
|
@ -224,7 +268,7 @@ class ClientWebSocketResponse:
|
|||
return default
|
||||
return transport.get_extra_info(name, default)
|
||||
|
||||
def exception(self) -> Optional[BaseException]:
|
||||
def exception(self) -> BaseException | None:
|
||||
return self._exception
|
||||
|
||||
async def ping(self, message: bytes = b"") -> None:
|
||||
|
|
@ -234,19 +278,19 @@ class ClientWebSocketResponse:
|
|||
await self._writer.send_frame(message, WSMsgType.PONG)
|
||||
|
||||
async def send_frame(
|
||||
self, message: bytes, opcode: WSMsgType, compress: Optional[int] = None
|
||||
self, message: bytes, opcode: WSMsgType, compress: int | None = None
|
||||
) -> None:
|
||||
"""Send a frame over the websocket."""
|
||||
await self._writer.send_frame(message, opcode, compress)
|
||||
|
||||
async def send_str(self, data: str, compress: Optional[int] = None) -> None:
|
||||
async def send_str(self, data: str, compress: int | None = None) -> None:
|
||||
if not isinstance(data, str):
|
||||
raise TypeError("data argument must be str (%r)" % type(data))
|
||||
await self._writer.send_frame(
|
||||
data.encode("utf-8"), WSMsgType.TEXT, compress=compress
|
||||
)
|
||||
|
||||
async def send_bytes(self, data: bytes, compress: Optional[int] = None) -> None:
|
||||
async def send_bytes(self, data: bytes, compress: int | None = None) -> None:
|
||||
if not isinstance(data, (bytes, bytearray, memoryview)):
|
||||
raise TypeError("data argument must be byte-ish (%r)" % type(data))
|
||||
await self._writer.send_frame(data, WSMsgType.BINARY, compress=compress)
|
||||
|
|
@ -254,12 +298,26 @@ class ClientWebSocketResponse:
|
|||
async def send_json(
|
||||
self,
|
||||
data: Any,
|
||||
compress: Optional[int] = None,
|
||||
compress: int | None = None,
|
||||
*,
|
||||
dumps: JSONEncoder = DEFAULT_JSON_ENCODER,
|
||||
) -> None:
|
||||
await self.send_str(dumps(data), compress=compress)
|
||||
|
||||
async def send_json_bytes(
|
||||
self,
|
||||
data: Any,
|
||||
compress: int | None = None,
|
||||
*,
|
||||
dumps: JSONBytesEncoder,
|
||||
) -> None:
|
||||
"""Send JSON data using a bytes-returning encoder as a binary frame.
|
||||
|
||||
Use this when your JSON encoder (like orjson) returns bytes
|
||||
instead of str, avoiding the encode/decode overhead.
|
||||
"""
|
||||
await self.send_bytes(dumps(data), compress=compress)
|
||||
|
||||
async def close(self, *, code: int = WSCloseCode.OK, message: bytes = b"") -> bool:
|
||||
# we need to break `receive()` cycle first,
|
||||
# `close()` may be called from different task
|
||||
|
|
@ -309,7 +367,24 @@ class ClientWebSocketResponse:
|
|||
self._response.close()
|
||||
return True
|
||||
|
||||
async def receive(self, timeout: Optional[float] = None) -> WSMessage:
|
||||
@overload
|
||||
async def receive(
|
||||
self: "ClientWebSocketResponse[Literal[True]]", timeout: float | None = None
|
||||
) -> WSMessageDecodeText: ...
|
||||
|
||||
@overload
|
||||
async def receive(
|
||||
self: "ClientWebSocketResponse[Literal[False]]", timeout: float | None = None
|
||||
) -> WSMessageNoDecodeText: ...
|
||||
|
||||
@overload
|
||||
async def receive(
|
||||
self: "ClientWebSocketResponse[_DecodeText]", timeout: float | None = None
|
||||
) -> WSMessageDecodeText | WSMessageNoDecodeText: ...
|
||||
|
||||
async def receive(
|
||||
self, timeout: float | None = None
|
||||
) -> WSMessageDecodeText | WSMessageNoDecodeText:
|
||||
receive_timeout = timeout or self._timeout.ws_receive
|
||||
|
||||
while True:
|
||||
|
|
@ -334,7 +409,6 @@ class ClientWebSocketResponse:
|
|||
msg = await self._reader.read()
|
||||
else:
|
||||
msg = await self._reader.read()
|
||||
self._reset_heartbeat()
|
||||
finally:
|
||||
self._waiting = False
|
||||
if self._close_wait:
|
||||
|
|
@ -382,7 +456,26 @@ class ClientWebSocketResponse:
|
|||
|
||||
return msg
|
||||
|
||||
async def receive_str(self, *, timeout: Optional[float] = None) -> str:
|
||||
@overload
|
||||
async def receive_str(
|
||||
self: "ClientWebSocketResponse[Literal[True]]", *, timeout: float | None = None
|
||||
) -> str: ...
|
||||
|
||||
@overload
|
||||
async def receive_str(
|
||||
self: "ClientWebSocketResponse[Literal[False]]", *, timeout: float | None = None
|
||||
) -> bytes: ...
|
||||
|
||||
@overload
|
||||
async def receive_str(
|
||||
self: "ClientWebSocketResponse[_DecodeText]", *, timeout: float | None = None
|
||||
) -> str | bytes: ...
|
||||
|
||||
async def receive_str(self, *, timeout: float | None = None) -> str | bytes:
|
||||
"""Receive TEXT message.
|
||||
|
||||
Returns str when decode_text=True (default), bytes when decode_text=False.
|
||||
"""
|
||||
msg = await self.receive(timeout)
|
||||
if msg.type is not WSMsgType.TEXT:
|
||||
raise WSMessageTypeError(
|
||||
|
|
@ -390,7 +483,7 @@ class ClientWebSocketResponse:
|
|||
)
|
||||
return cast(str, msg.data)
|
||||
|
||||
async def receive_bytes(self, *, timeout: Optional[float] = None) -> bytes:
|
||||
async def receive_bytes(self, *, timeout: float | None = None) -> bytes:
|
||||
msg = await self.receive(timeout)
|
||||
if msg.type is not WSMsgType.BINARY:
|
||||
raise WSMessageTypeError(
|
||||
|
|
@ -398,31 +491,70 @@ class ClientWebSocketResponse:
|
|||
)
|
||||
return cast(bytes, msg.data)
|
||||
|
||||
@overload
|
||||
async def receive_json(
|
||||
self: "ClientWebSocketResponse[Literal[True]]",
|
||||
*,
|
||||
loads: JSONDecoder = ...,
|
||||
timeout: float | None = None,
|
||||
) -> Any: ...
|
||||
|
||||
@overload
|
||||
async def receive_json(
|
||||
self: "ClientWebSocketResponse[Literal[False]]",
|
||||
*,
|
||||
loads: Callable[[bytes], Any] = ...,
|
||||
timeout: float | None = None,
|
||||
) -> Any: ...
|
||||
|
||||
@overload
|
||||
async def receive_json(
|
||||
self: "ClientWebSocketResponse[_DecodeText]",
|
||||
*,
|
||||
loads: JSONDecoder | Callable[[bytes], Any] = ...,
|
||||
timeout: float | None = None,
|
||||
) -> Any: ...
|
||||
|
||||
async def receive_json(
|
||||
self,
|
||||
*,
|
||||
loads: JSONDecoder = DEFAULT_JSON_DECODER,
|
||||
timeout: Optional[float] = None,
|
||||
loads: JSONDecoder | Callable[[bytes], Any] = DEFAULT_JSON_DECODER,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
data = await self.receive_str(timeout=timeout)
|
||||
return loads(data)
|
||||
return loads(data) # type: ignore[arg-type]
|
||||
|
||||
def __aiter__(self) -> "ClientWebSocketResponse":
|
||||
def __aiter__(self) -> Self:
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> WSMessage:
|
||||
@overload
|
||||
async def __anext__(
|
||||
self: "ClientWebSocketResponse[Literal[True]]",
|
||||
) -> WSMessageDecodeText: ...
|
||||
|
||||
@overload
|
||||
async def __anext__(
|
||||
self: "ClientWebSocketResponse[Literal[False]]",
|
||||
) -> WSMessageNoDecodeText: ...
|
||||
|
||||
@overload
|
||||
async def __anext__(
|
||||
self: "ClientWebSocketResponse[_DecodeText]",
|
||||
) -> WSMessageDecodeText | WSMessageNoDecodeText: ...
|
||||
|
||||
async def __anext__(self) -> WSMessageDecodeText | WSMessageNoDecodeText:
|
||||
msg = await self.receive()
|
||||
if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED):
|
||||
raise StopAsyncIteration
|
||||
return msg
|
||||
|
||||
async def __aenter__(self) -> "ClientWebSocketResponse":
|
||||
async def __aenter__(self) -> Self:
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
await self.close()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue