Загрузить файлы в «venv/Lib/site-packages/websockets»

This commit is contained in:
2026-07-02 21:02:55 +00:00
parent f00915d6b4
commit a2539099d1
5 changed files with 478 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
from __future__ import annotations
from collections.abc import Generator
class StreamReader:
"""
Generator-based stream reader.
This class doesn't support concurrent calls to :meth:`read_line`,
:meth:`read_exact`, or :meth:`read_to_eof`. Make sure calls are
serialized.
"""
def __init__(self) -> None:
self.buffer = bytearray()
self.eof = False
def read_line(self, m: int) -> Generator[None, None, bytearray]:
"""
Read a LF-terminated line from the stream.
This is a generator-based coroutine.
The return value includes the LF character.
Args:
m: Maximum number bytes to read; this is a security limit.
Raises:
EOFError: If the stream ends without a LF.
RuntimeError: If the stream ends in more than ``m`` bytes.
"""
n = 0 # number of bytes to read
p = 0 # number of bytes without a newline
while True:
n = self.buffer.find(b"\n", p) + 1
if n > 0:
break
p = len(self.buffer)
if p > m:
raise RuntimeError(f"read {p} bytes, expected no more than {m} bytes")
if self.eof:
raise EOFError(f"stream ends after {p} bytes, before end of line")
yield
if n > m:
raise RuntimeError(f"read {n} bytes, expected no more than {m} bytes")
r = self.buffer[:n]
del self.buffer[:n]
return r
def read_exact(self, n: int) -> Generator[None, None, bytearray]:
"""
Read a given number of bytes from the stream.
This is a generator-based coroutine.
Args:
n: How many bytes to read.
Raises:
EOFError: If the stream ends in less than ``n`` bytes.
"""
assert n >= 0
while len(self.buffer) < n:
if self.eof:
p = len(self.buffer)
raise EOFError(f"stream ends after {p} bytes, expected {n} bytes")
yield
r = self.buffer[:n]
del self.buffer[:n]
return r
def read_to_eof(self, m: int) -> Generator[None, None, bytearray]:
"""
Read all bytes from the stream.
This is a generator-based coroutine.
Args:
m: Maximum number bytes to read; this is a security limit.
Raises:
RuntimeError: If the stream ends in more than ``m`` bytes.
"""
while not self.eof:
p = len(self.buffer)
if p > m:
raise RuntimeError(f"read {p} bytes, expected no more than {m} bytes")
yield
r = self.buffer[:]
del self.buffer[:]
return r
def at_eof(self) -> Generator[None, None, bool]:
"""
Tell whether the stream has ended and all data was read.
This is a generator-based coroutine.
"""
while True:
if self.buffer:
return False
if self.eof:
return True
# When all data was read but the stream hasn't ended, we can't
# tell if until either feed_data() or feed_eof() is called.
yield
def feed_data(self, data: bytes | bytearray) -> None:
"""
Write data to the stream.
:meth:`feed_data` cannot be called after :meth:`feed_eof`.
Args:
data: Data to write.
Raises:
EOFError: If the stream has ended.
"""
if self.eof:
raise EOFError("stream ended")
self.buffer += data
def feed_eof(self) -> None:
"""
End the stream.
:meth:`feed_eof` cannot be called more than once.
Raises:
EOFError: If the stream has ended.
"""
if self.eof:
raise EOFError("stream ended")
self.eof = True
def discard(self) -> None:
"""
Discard all buffered data, but don't end the stream.
"""
del self.buffer[:]

View File

@@ -0,0 +1,75 @@
from __future__ import annotations
import http
import logging
from typing import TYPE_CHECKING, Any, NewType, Sequence
__all__ = [
"Data",
"LoggerLike",
"StatusLike",
"Origin",
"Subprotocol",
"ExtensionName",
"ExtensionParameter",
]
# Public types used in the signature of public APIs
Data = str | bytes
"""Types supported in a WebSocket message:
:class:`str` for a Text_ frame, :class:`bytes` for a Binary_ frame.
.. _Text: https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
.. _Binary : https://datatracker.ietf.org/doc/html/rfc6455#section-5.6
"""
BytesLike = bytes | bytearray | memoryview
"""Types accepted where :class:`bytes` is expected."""
DataLike = str | bytes | bytearray | memoryview
"""Types accepted where :class:`Data` is expected."""
if TYPE_CHECKING:
LoggerLike = logging.Logger | logging.LoggerAdapter[Any]
"""Types accepted where a :class:`~logging.Logger` is expected."""
else: # remove this branch when dropping support for Python < 3.11
LoggerLike = logging.Logger | logging.LoggerAdapter
"""Types accepted where a :class:`~logging.Logger` is expected."""
StatusLike = http.HTTPStatus | int
"""
Types accepted where an :class:`~http.HTTPStatus` is expected."""
Origin = NewType("Origin", str)
"""Value of a ``Origin`` header."""
Subprotocol = NewType("Subprotocol", str)
"""Subprotocol in a ``Sec-WebSocket-Protocol`` header."""
ExtensionName = NewType("ExtensionName", str)
"""Name of a WebSocket extension."""
ExtensionParameter = tuple[str, str | None]
"""Parameter of a WebSocket extension."""
# Private types
ExtensionHeader = tuple[ExtensionName, Sequence[ExtensionParameter]]
"""Extension in a ``Sec-WebSocket-Extensions`` header."""
ConnectionOption = NewType("ConnectionOption", str)
"""Connection option in a ``Connection`` header."""
UpgradeProtocol = NewType("UpgradeProtocol", str)
"""Upgrade protocol in an ``Upgrade`` header."""

View File

@@ -0,0 +1,107 @@
from __future__ import annotations
import dataclasses
import urllib.parse
from .exceptions import InvalidURI
__all__ = ["parse_uri", "WebSocketURI"]
# All characters from the gen-delims and sub-delims sets in RFC 3987.
DELIMS = ":/?#[]@!$&'()*+,;="
@dataclasses.dataclass
class WebSocketURI:
"""
WebSocket URI.
Attributes:
secure: :obj:`True` for a ``wss`` URI, :obj:`False` for a ``ws`` URI.
host: Normalized to lower case.
port: Always set even if it's the default.
path: May be empty.
query: May be empty if the URI doesn't include a query component.
username: Available when the URI contains `User Information`_.
password: Available when the URI contains `User Information`_.
.. _User Information: https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.1
"""
secure: bool
host: str
port: int
path: str
query: str
username: str | None = None
password: str | None = None
@property
def resource_name(self) -> str:
if self.path:
resource_name = self.path
else:
resource_name = "/"
if self.query:
resource_name += "?" + self.query
return resource_name
@property
def user_info(self) -> tuple[str, str] | None:
if self.username is None:
return None
assert self.password is not None
return (self.username, self.password)
def parse_uri(uri: str) -> WebSocketURI:
"""
Parse and validate a WebSocket URI.
Args:
uri: WebSocket URI.
Returns:
Parsed WebSocket URI.
Raises:
InvalidURI: If ``uri`` isn't a valid WebSocket URI.
"""
parsed = urllib.parse.urlparse(uri)
if parsed.scheme not in ["ws", "wss"]:
raise InvalidURI(uri, "scheme isn't ws or wss")
if parsed.hostname is None:
raise InvalidURI(uri, "hostname isn't provided")
if parsed.fragment != "":
raise InvalidURI(uri, "fragment identifier is meaningless")
secure = parsed.scheme == "wss"
host = parsed.hostname
port = parsed.port or (443 if secure else 80)
path = parsed.path
query = parsed.query
username = parsed.username
password = parsed.password
# urllib.parse.urlparse accepts URLs with a username but without a
# password. This doesn't make sense for HTTP Basic Auth credentials.
if username is not None and password is None:
raise InvalidURI(uri, "username provided without password")
try:
uri.encode("ascii")
except UnicodeEncodeError:
# Input contains non-ASCII characters.
# It must be an IRI. Convert it to a URI.
host = host.encode("idna").decode()
path = urllib.parse.quote(path, safe=DELIMS)
query = urllib.parse.quote(query, safe=DELIMS)
if username is not None:
assert password is not None
username = urllib.parse.quote(username, safe=DELIMS)
password = urllib.parse.quote(password, safe=DELIMS)
return WebSocketURI(secure, host, port, path, query, username, password)

View File

@@ -0,0 +1,53 @@
from __future__ import annotations
import base64
import hashlib
import secrets
import sys
from .typing import BytesLike
__all__ = ["accept_key", "apply_mask"]
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
def generate_key() -> str:
"""
Generate a random key for the Sec-WebSocket-Key header.
"""
key = secrets.token_bytes(16)
return base64.b64encode(key).decode()
def accept_key(key: str) -> str:
"""
Compute the value of the Sec-WebSocket-Accept header.
Args:
key: Value of the Sec-WebSocket-Key header.
"""
sha1 = hashlib.sha1((key + GUID).encode()).digest()
return base64.b64encode(sha1).decode()
def apply_mask(data: BytesLike, mask: bytes | bytearray) -> bytes:
"""
Apply masking to the data of a WebSocket message.
Args:
data: Data to mask.
mask: 4-bytes mask.
"""
if len(mask) != 4:
raise ValueError("mask must contain 4 bytes")
data_int = int.from_bytes(data, sys.byteorder)
mask_repeated = mask * (len(data) // 4) + mask[: len(data) % 4]
mask_int = int.from_bytes(mask_repeated, sys.byteorder)
return (data_int ^ mask_int).to_bytes(len(data), sys.byteorder)

View File

@@ -0,0 +1,92 @@
from __future__ import annotations
import importlib.metadata
__all__ = ["tag", "version", "commit"]
# ========= =========== ===================
# release development
# ========= =========== ===================
# tag X.Y X.Y (upcoming)
# version X.Y X.Y.dev1+g5678cde
# commit X.Y 5678cde
# ========= =========== ===================
# When tagging a release, set `released = True`.
# After tagging a release, set `released = False` and increment `tag`.
released = True
tag = version = commit = "16.0"
if not released: # pragma: no cover
import pathlib
import re
import subprocess
def get_version(tag: str) -> str:
# Since setup.py executes the contents of src/websockets/version.py,
# __file__ can point to either of these two files.
file_path = pathlib.Path(__file__)
root_dir = file_path.parents[0 if file_path.name == "setup.py" else 2]
# Read version from package metadata if it is installed.
try:
version = importlib.metadata.version("websockets")
except ImportError:
pass
else:
# Check that this file belongs to the installed package.
files = importlib.metadata.files("websockets")
if files:
version_files = [f for f in files if f.name == file_path.name]
if version_files:
version_file = version_files[0]
if version_file.locate() == file_path:
return version
# Read version from git if available.
try:
description = subprocess.run(
["git", "describe", "--dirty", "--tags", "--long"],
capture_output=True,
cwd=root_dir,
timeout=1,
check=True,
text=True,
).stdout.strip()
# subprocess.run raises FileNotFoundError if git isn't on $PATH.
except (
FileNotFoundError,
subprocess.CalledProcessError,
subprocess.TimeoutExpired,
):
pass
else:
description_re = r"[0-9.]+-([0-9]+)-(g[0-9a-f]{7,}(?:-dirty)?)"
match = re.fullmatch(description_re, description)
if match is None:
raise ValueError(f"Unexpected git description: {description}")
distance, remainder = match.groups()
remainder = remainder.replace("-", ".") # required by PEP 440
return f"{tag}.dev{distance}+{remainder}"
# Avoid crashing if the development version cannot be determined.
return f"{tag}.dev0+gunknown"
version = get_version(tag)
def get_commit(tag: str, version: str) -> str:
# Extract commit from version, falling back to tag if not available.
version_re = r"[0-9.]+\.dev[0-9]+\+g([0-9a-f]{7,}|unknown)(?:\.dirty)?"
match = re.fullmatch(version_re, version)
if match is None:
raise ValueError(f"Unexpected version: {version}")
(commit,) = match.groups()
return tag if commit == "unknown" else commit
commit = get_commit(tag, version)