Voice et bot modif
This commit is contained in:
parent
189d56026b
commit
7333a22bcd
10774 changed files with 634644 additions and 933308 deletions
|
|
@ -4,17 +4,22 @@ import binascii
|
|||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from typing import Any, Final, Iterable, Optional, Tuple, Union, cast
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Final, Generic, Literal, cast, overload
|
||||
|
||||
import attr
|
||||
from multidict import CIMultiDict
|
||||
|
||||
from . import hdrs
|
||||
from ._websocket.reader import WebSocketDataQueue
|
||||
from ._websocket.writer import DEFAULT_LIMIT
|
||||
from .abc import AbstractStreamWriter
|
||||
from .client_exceptions import WSMessageTypeError
|
||||
from .helpers import calculate_timeout_when, set_exception, set_result
|
||||
from .helpers import (
|
||||
DEFAULT_CHUNK_SIZE,
|
||||
calculate_timeout_when,
|
||||
set_exception,
|
||||
set_result,
|
||||
)
|
||||
from .http import (
|
||||
WS_CLOSED_MESSAGE,
|
||||
WS_CLOSING_MESSAGE,
|
||||
|
|
@ -24,6 +29,8 @@ from .http import (
|
|||
WebSocketWriter,
|
||||
WSCloseCode,
|
||||
WSMessage,
|
||||
WSMessageDecodeText,
|
||||
WSMessageNoDecodeText,
|
||||
WSMsgType as WSMsgType,
|
||||
ws_ext_gen,
|
||||
ws_ext_parse,
|
||||
|
|
@ -31,15 +38,29 @@ from .http import (
|
|||
from .http_websocket import _INTERNAL_RECEIVE_TYPES
|
||||
from .log import ws_logger
|
||||
from .streams import EofStream
|
||||
from .typedefs import JSONDecoder, JSONEncoder
|
||||
from .typedefs import JSONBytesEncoder, JSONDecoder, JSONEncoder
|
||||
from .web_exceptions import HTTPBadRequest, HTTPException
|
||||
from .web_request import BaseRequest
|
||||
from .web_response import StreamResponse
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar
|
||||
else:
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from collections.abc import Buffer
|
||||
else:
|
||||
from typing import Union
|
||||
|
||||
Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import asyncio as async_timeout
|
||||
from typing import Self
|
||||
else:
|
||||
import async_timeout
|
||||
from typing_extensions import Self
|
||||
|
||||
__all__ = (
|
||||
"WebSocketResponse",
|
||||
|
|
@ -49,47 +70,53 @@ __all__ = (
|
|||
|
||||
THRESHOLD_CONNLOST_ACCESS: Final[int] = 5
|
||||
|
||||
# TypeVar for whether text messages are decoded to str (True) or kept as bytes (False)
|
||||
_DecodeText = TypeVar("_DecodeText", bound=bool, covariant=True, default=Literal[True])
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True, frozen=True, slots=True)
|
||||
class WebSocketReady:
|
||||
ok: bool
|
||||
protocol: Optional[str]
|
||||
protocol: str | None
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self.ok
|
||||
|
||||
|
||||
class WebSocketResponse(StreamResponse):
|
||||
class WebSocketResponse(StreamResponse, Generic[_DecodeText]):
|
||||
|
||||
_length_check: bool = False
|
||||
_ws_protocol: Optional[str] = None
|
||||
_writer: Optional[WebSocketWriter] = None
|
||||
_reader: Optional[WebSocketDataQueue] = None
|
||||
_ws_protocol: str | None = None
|
||||
_writer: WebSocketWriter | None = None
|
||||
_reader: WebSocketDataQueue | None = None
|
||||
_closed: bool = False
|
||||
_closing: bool = False
|
||||
_conn_lost: int = 0
|
||||
_close_code: Optional[int] = None
|
||||
_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
_close_code: int | None = None
|
||||
_loop: asyncio.AbstractEventLoop | None = None
|
||||
_waiting: bool = False
|
||||
_close_wait: Optional[asyncio.Future[None]] = None
|
||||
_exception: Optional[BaseException] = None
|
||||
_close_wait: asyncio.Future[None] | None = None
|
||||
_exception: BaseException | None = None
|
||||
_heartbeat_when: float = 0.0
|
||||
_heartbeat_cb: Optional[asyncio.TimerHandle] = None
|
||||
_pong_response_cb: Optional[asyncio.TimerHandle] = None
|
||||
_ping_task: Optional[asyncio.Task[None]] = None
|
||||
_heartbeat_cb: asyncio.TimerHandle | None = None
|
||||
_pong_response_cb: asyncio.TimerHandle | None = None
|
||||
_ping_task: asyncio.Task[None] | None = None
|
||||
_need_heartbeat_reset: bool = False
|
||||
_heartbeat_reset_handle: asyncio.Handle | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
timeout: float = 10.0,
|
||||
receive_timeout: Optional[float] = None,
|
||||
receive_timeout: float | None = None,
|
||||
autoclose: bool = True,
|
||||
autoping: bool = True,
|
||||
heartbeat: Optional[float] = None,
|
||||
heartbeat: float | None = None,
|
||||
protocols: Iterable[str] = (),
|
||||
compress: bool = True,
|
||||
max_msg_size: int = 4 * 1024 * 1024,
|
||||
writer_limit: int = DEFAULT_LIMIT,
|
||||
writer_limit: int = DEFAULT_CHUNK_SIZE,
|
||||
decode_text: bool = True,
|
||||
) -> None:
|
||||
super().__init__(status=101)
|
||||
self._protocols = protocols
|
||||
|
|
@ -100,12 +127,19 @@ class WebSocketResponse(StreamResponse):
|
|||
self._heartbeat = heartbeat
|
||||
if heartbeat is not None:
|
||||
self._pong_heartbeat = heartbeat / 2.0
|
||||
self._compress: Union[bool, int] = compress
|
||||
self._compress: bool | int = compress
|
||||
self._max_msg_size = max_msg_size
|
||||
self._writer_limit = writer_limit
|
||||
self._decode_text = decode_text
|
||||
self._need_heartbeat_reset = False
|
||||
self._heartbeat_reset_handle = None
|
||||
|
||||
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
|
||||
|
|
@ -118,6 +152,23 @@ class WebSocketResponse(StreamResponse):
|
|||
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
|
||||
|
|
@ -141,6 +192,12 @@ class WebSocketResponse(StreamResponse):
|
|||
|
||||
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
|
||||
assert loop is not None and self._writer is not None
|
||||
now = loop.time()
|
||||
|
|
@ -220,25 +277,23 @@ class WebSocketResponse(StreamResponse):
|
|||
|
||||
def _handshake(
|
||||
self, request: BaseRequest
|
||||
) -> Tuple["CIMultiDict[str]", Optional[str], int, bool]:
|
||||
) -> tuple["CIMultiDict[str]", str | None, int, bool]:
|
||||
headers = request.headers
|
||||
if "websocket" != headers.get(hdrs.UPGRADE, "").lower().strip():
|
||||
raise HTTPBadRequest(
|
||||
text=(
|
||||
"No WebSocket UPGRADE hdr: {}\n Can "
|
||||
f"No WebSocket UPGRADE hdr: {headers.get(hdrs.UPGRADE)}\n Can "
|
||||
'"Upgrade" only to "WebSocket".'
|
||||
).format(headers.get(hdrs.UPGRADE))
|
||||
)
|
||||
|
||||
if "upgrade" not in headers.get(hdrs.CONNECTION, "").lower():
|
||||
raise HTTPBadRequest(
|
||||
text="No CONNECTION upgrade hdr: {}".format(
|
||||
headers.get(hdrs.CONNECTION)
|
||||
)
|
||||
)
|
||||
|
||||
if not request._message.upgrade:
|
||||
raise HTTPBadRequest(
|
||||
text=f"No CONNECTION upgrade hdr: {headers.get(hdrs.CONNECTION)}"
|
||||
)
|
||||
|
||||
# find common sub-protocol between client and server
|
||||
protocol: Optional[str] = None
|
||||
protocol: str | None = None
|
||||
if hdrs.SEC_WEBSOCKET_PROTOCOL in headers:
|
||||
req_protocols = [
|
||||
str(proto.strip())
|
||||
|
|
@ -304,7 +359,7 @@ class WebSocketResponse(StreamResponse):
|
|||
notakeover,
|
||||
)
|
||||
|
||||
def _pre_start(self, request: BaseRequest) -> Tuple[Optional[str], WebSocketWriter]:
|
||||
def _pre_start(self, request: BaseRequest) -> tuple[str | None, WebSocketWriter]:
|
||||
self._loop = request._loop
|
||||
|
||||
headers, protocol, compress, notakeover = self._handshake(request)
|
||||
|
|
@ -314,7 +369,8 @@ class WebSocketResponse(StreamResponse):
|
|||
self.force_close()
|
||||
self._compress = compress
|
||||
transport = request._protocol.transport
|
||||
assert transport is not None
|
||||
if transport is None:
|
||||
raise ConnectionResetError("Connection lost")
|
||||
writer = WebSocketWriter(
|
||||
request._protocol,
|
||||
transport,
|
||||
|
|
@ -326,7 +382,7 @@ class WebSocketResponse(StreamResponse):
|
|||
return protocol, writer
|
||||
|
||||
def _post_start(
|
||||
self, request: BaseRequest, protocol: Optional[str], writer: WebSocketWriter
|
||||
self, request: BaseRequest, protocol: str | None, writer: WebSocketWriter
|
||||
) -> None:
|
||||
self._ws_protocol = protocol
|
||||
self._writer = writer
|
||||
|
|
@ -335,12 +391,17 @@ class WebSocketResponse(StreamResponse):
|
|||
|
||||
loop = self._loop
|
||||
assert loop is not None
|
||||
self._reader = WebSocketDataQueue(request._protocol, 2**16, loop=loop)
|
||||
request.protocol.set_parser(
|
||||
WebSocketReader(
|
||||
self._reader, self._max_msg_size, compress=bool(self._compress)
|
||||
)
|
||||
self._reader = WebSocketDataQueue(
|
||||
request._protocol, DEFAULT_CHUNK_SIZE, loop=loop
|
||||
)
|
||||
parser = WebSocketReader(
|
||||
self._reader,
|
||||
self._max_msg_size,
|
||||
compress=bool(self._compress),
|
||||
decode_text=self._decode_text,
|
||||
)
|
||||
cb = None if self._heartbeat is None else self._on_data_received
|
||||
request.protocol.set_parser(parser, data_received_cb=cb)
|
||||
# disable HTTP keepalive for WebSocket
|
||||
request.protocol.keep_alive(False)
|
||||
|
||||
|
|
@ -363,15 +424,15 @@ class WebSocketResponse(StreamResponse):
|
|||
return self._closed
|
||||
|
||||
@property
|
||||
def close_code(self) -> Optional[int]:
|
||||
def close_code(self) -> int | None:
|
||||
return self._close_code
|
||||
|
||||
@property
|
||||
def ws_protocol(self) -> Optional[str]:
|
||||
def ws_protocol(self) -> str | None:
|
||||
return self._ws_protocol
|
||||
|
||||
@property
|
||||
def compress(self) -> Union[int, bool]:
|
||||
def compress(self) -> int | bool:
|
||||
return self._compress
|
||||
|
||||
def get_extra_info(self, name: str, default: Any = None) -> Any:
|
||||
|
|
@ -387,7 +448,7 @@ class WebSocketResponse(StreamResponse):
|
|||
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:
|
||||
|
|
@ -402,14 +463,14 @@ class WebSocketResponse(StreamResponse):
|
|||
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."""
|
||||
if self._writer is None:
|
||||
raise RuntimeError("Call .prepare() first")
|
||||
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 self._writer is None:
|
||||
raise RuntimeError("Call .prepare() first")
|
||||
if not isinstance(data, str):
|
||||
|
|
@ -418,7 +479,7 @@ class WebSocketResponse(StreamResponse):
|
|||
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 self._writer is None:
|
||||
raise RuntimeError("Call .prepare() first")
|
||||
if not isinstance(data, (bytes, bytearray, memoryview)):
|
||||
|
|
@ -428,12 +489,26 @@ class WebSocketResponse(StreamResponse):
|
|||
async def send_json(
|
||||
self,
|
||||
data: Any,
|
||||
compress: Optional[int] = None,
|
||||
compress: int | None = None,
|
||||
*,
|
||||
dumps: JSONEncoder = json.dumps,
|
||||
) -> 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 write_eof(self) -> None: # type: ignore[override]
|
||||
if self._eof_sent:
|
||||
return
|
||||
|
|
@ -514,7 +589,24 @@ class WebSocketResponse(StreamResponse):
|
|||
if self._req is not None and self._req.transport is not None:
|
||||
self._req.transport.close()
|
||||
|
||||
async def receive(self, timeout: Optional[float] = None) -> WSMessage:
|
||||
@overload
|
||||
async def receive(
|
||||
self: "WebSocketResponse[Literal[True]]", timeout: float | None = None
|
||||
) -> WSMessageDecodeText: ...
|
||||
|
||||
@overload
|
||||
async def receive(
|
||||
self: "WebSocketResponse[Literal[False]]", timeout: float | None = None
|
||||
) -> WSMessageNoDecodeText: ...
|
||||
|
||||
@overload
|
||||
async def receive(
|
||||
self: "WebSocketResponse[_DecodeText]", timeout: float | None = None
|
||||
) -> WSMessageDecodeText | WSMessageNoDecodeText: ...
|
||||
|
||||
async def receive(
|
||||
self, timeout: float | None = None
|
||||
) -> WSMessageDecodeText | WSMessageNoDecodeText:
|
||||
if self._reader is None:
|
||||
raise RuntimeError("Call .prepare() first")
|
||||
|
||||
|
|
@ -543,7 +635,6 @@ class WebSocketResponse(StreamResponse):
|
|||
msg = await self._reader.read()
|
||||
else:
|
||||
msg = await self._reader.read()
|
||||
self._reset_heartbeat()
|
||||
finally:
|
||||
self._waiting = False
|
||||
if self._close_wait:
|
||||
|
|
@ -588,7 +679,26 @@ class WebSocketResponse(StreamResponse):
|
|||
|
||||
return msg
|
||||
|
||||
async def receive_str(self, *, timeout: Optional[float] = None) -> str:
|
||||
@overload
|
||||
async def receive_str(
|
||||
self: "WebSocketResponse[Literal[True]]", *, timeout: float | None = None
|
||||
) -> str: ...
|
||||
|
||||
@overload
|
||||
async def receive_str(
|
||||
self: "WebSocketResponse[Literal[False]]", *, timeout: float | None = None
|
||||
) -> bytes: ...
|
||||
|
||||
@overload
|
||||
async def receive_str(
|
||||
self: "WebSocketResponse[_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(
|
||||
|
|
@ -596,7 +706,7 @@ class WebSocketResponse(StreamResponse):
|
|||
)
|
||||
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(
|
||||
|
|
@ -604,19 +714,61 @@ class WebSocketResponse(StreamResponse):
|
|||
)
|
||||
return cast(bytes, msg.data)
|
||||
|
||||
@overload
|
||||
async def receive_json(
|
||||
self, *, loads: JSONDecoder = json.loads, timeout: Optional[float] = None
|
||||
self: "WebSocketResponse[Literal[True]]",
|
||||
*,
|
||||
loads: JSONDecoder = ...,
|
||||
timeout: float | None = None,
|
||||
) -> Any: ...
|
||||
|
||||
@overload
|
||||
async def receive_json(
|
||||
self: "WebSocketResponse[Literal[False]]",
|
||||
*,
|
||||
loads: Callable[[bytes], Any] = ...,
|
||||
timeout: float | None = None,
|
||||
) -> Any: ...
|
||||
|
||||
@overload
|
||||
async def receive_json(
|
||||
self: "WebSocketResponse[_DecodeText]",
|
||||
*,
|
||||
loads: JSONDecoder | Callable[[bytes], Any] = ...,
|
||||
timeout: float | None = None,
|
||||
) -> Any: ...
|
||||
|
||||
async def receive_json(
|
||||
self,
|
||||
*,
|
||||
loads: JSONDecoder | Callable[[bytes], Any] = json.loads,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
data = await self.receive_str(timeout=timeout)
|
||||
return loads(data)
|
||||
return loads(data) # type: ignore[arg-type]
|
||||
|
||||
async def write(self, data: bytes) -> None:
|
||||
async def write(self, data: Buffer) -> None:
|
||||
raise RuntimeError("Cannot call .write() for websocket")
|
||||
|
||||
def __aiter__(self) -> "WebSocketResponse":
|
||||
def __aiter__(self) -> Self:
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> WSMessage:
|
||||
@overload
|
||||
async def __anext__(
|
||||
self: "WebSocketResponse[Literal[True]]",
|
||||
) -> WSMessageDecodeText: ...
|
||||
|
||||
@overload
|
||||
async def __anext__(
|
||||
self: "WebSocketResponse[Literal[False]]",
|
||||
) -> WSMessageNoDecodeText: ...
|
||||
|
||||
@overload
|
||||
async def __anext__(
|
||||
self: "WebSocketResponse[_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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue