Remove the unsupported WebSocket transport (#2785)
This commit is contained in:
parent
ac96f88abd
commit
b478bff56d
8 changed files with 9 additions and 331 deletions
|
|
@ -145,6 +145,10 @@ group (spawned with `start_new_session=True`); the `getpgid()` lookup and the
|
||||||
per-process terminate/kill fallback are gone. The win32 utilities logger is now
|
per-process terminate/kill fallback are gone. The win32 utilities logger is now
|
||||||
named `mcp.os.win32.utilities` (was `client.stdio.win32`).
|
named `mcp.os.win32.utilities` (was `client.stdio.win32`).
|
||||||
|
|
||||||
|
### WebSocket transport removed
|
||||||
|
|
||||||
|
The WebSocket transport has been removed: `mcp.client.websocket.websocket_client`, `mcp.server.websocket.websocket_server`, and the `ws` optional dependency extra (`mcp[ws]`) no longer exist. WebSocket was never part of the MCP specification. Use the streamable HTTP transport instead (`mcp.client.streamable_http.streamable_http_client` on the client, `streamable_http_app()` on the server), which supports bidirectional communication with server-to-client streaming over standard HTTP.
|
||||||
|
|
||||||
### Removed type aliases and classes
|
### Removed type aliases and classes
|
||||||
|
|
||||||
The following deprecated type aliases and classes have been removed from `mcp.types`:
|
The following deprecated type aliases and classes have been removed from `mcp.types`:
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ dependencies = [
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
rich = ["rich>=13.9.4"]
|
rich = ["rich>=13.9.4"]
|
||||||
cli = ["typer>=0.16.0", "python-dotenv>=1.0.0"]
|
cli = ["typer>=0.16.0", "python-dotenv>=1.0.0"]
|
||||||
ws = ["websockets>=15.0.1"]
|
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
mcp = "mcp.cli:app [cli]"
|
mcp = "mcp.cli:app [cli]"
|
||||||
|
|
@ -75,8 +74,8 @@ build-constraint-dependencies = [
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
# We add mcp[cli,ws] so `uv sync` considers the extras.
|
# We add mcp[cli] so `uv sync` considers the extras.
|
||||||
"mcp[cli,ws]",
|
"mcp[cli]",
|
||||||
"pyright>=1.1.400",
|
"pyright>=1.1.400",
|
||||||
"pytest>=8.4.0",
|
"pytest>=8.4.0",
|
||||||
"ruff>=0.8.5",
|
"ruff>=0.8.5",
|
||||||
|
|
@ -204,9 +203,6 @@ addopts = """
|
||||||
"""
|
"""
|
||||||
filterwarnings = [
|
filterwarnings = [
|
||||||
"error",
|
"error",
|
||||||
# This should be fixed on Uvicorn's side.
|
|
||||||
"ignore::DeprecationWarning:websockets",
|
|
||||||
"ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning",
|
|
||||||
# pywin32 internal deprecation warning
|
# pywin32 internal deprecation warning
|
||||||
"ignore:getargs.*The 'u' format is deprecated:DeprecationWarning",
|
"ignore:getargs.*The 'u' format is deprecated:DeprecationWarning",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
import json
|
|
||||||
from collections.abc import AsyncGenerator
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
|
|
||||||
import anyio
|
|
||||||
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
|
||||||
from pydantic import ValidationError
|
|
||||||
from websockets.asyncio.client import connect as ws_connect
|
|
||||||
from websockets.typing import Subprotocol
|
|
||||||
|
|
||||||
from mcp import types
|
|
||||||
from mcp.shared.message import SessionMessage
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def websocket_client(
|
|
||||||
url: str,
|
|
||||||
) -> AsyncGenerator[
|
|
||||||
tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]],
|
|
||||||
None,
|
|
||||||
]:
|
|
||||||
"""WebSocket client transport for MCP, symmetrical to the server version.
|
|
||||||
|
|
||||||
Connects to 'url' using the 'mcp' subprotocol, then yields:
|
|
||||||
(read_stream, write_stream)
|
|
||||||
|
|
||||||
- read_stream: As you read from this stream, you'll receive either valid
|
|
||||||
SessionMessage objects or Exception objects (when validation fails).
|
|
||||||
- write_stream: Write SessionMessage objects to this stream to send them
|
|
||||||
over the WebSocket to the server.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Create two in-memory streams:
|
|
||||||
# - One for incoming messages (read_stream, written by ws_reader)
|
|
||||||
# - One for outgoing messages (write_stream, read by ws_writer)
|
|
||||||
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
|
|
||||||
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]
|
|
||||||
write_stream: MemoryObjectSendStream[SessionMessage]
|
|
||||||
write_stream_reader: MemoryObjectReceiveStream[SessionMessage]
|
|
||||||
|
|
||||||
# Connect using websockets, requesting the "mcp" subprotocol
|
|
||||||
async with ws_connect(url, subprotocols=[Subprotocol("mcp")]) as ws:
|
|
||||||
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
|
|
||||||
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
|
|
||||||
|
|
||||||
async def ws_reader():
|
|
||||||
"""Reads text messages from the WebSocket, parses them as JSON-RPC messages,
|
|
||||||
and sends them into read_stream_writer.
|
|
||||||
"""
|
|
||||||
async with read_stream_writer:
|
|
||||||
async for raw_text in ws:
|
|
||||||
try:
|
|
||||||
message = types.jsonrpc_message_adapter.validate_json(raw_text, by_name=False)
|
|
||||||
session_message = SessionMessage(message)
|
|
||||||
await read_stream_writer.send(session_message)
|
|
||||||
except ValidationError as exc: # pragma: no cover
|
|
||||||
# If JSON parse or model validation fails, send the exception
|
|
||||||
await read_stream_writer.send(exc)
|
|
||||||
|
|
||||||
async def ws_writer():
|
|
||||||
"""Reads JSON-RPC messages from write_stream_reader and
|
|
||||||
sends them to the server.
|
|
||||||
"""
|
|
||||||
async with write_stream_reader:
|
|
||||||
async for session_message in write_stream_reader:
|
|
||||||
# Convert to a dict, then to JSON
|
|
||||||
msg_dict = session_message.message.model_dump(by_alias=True, mode="json", exclude_unset=True)
|
|
||||||
await ws.send(json.dumps(msg_dict))
|
|
||||||
|
|
||||||
async with (
|
|
||||||
read_stream_writer,
|
|
||||||
read_stream,
|
|
||||||
write_stream,
|
|
||||||
write_stream_reader,
|
|
||||||
anyio.create_task_group() as tg,
|
|
||||||
):
|
|
||||||
# Start reader and writer tasks
|
|
||||||
tg.start_soon(ws_reader)
|
|
||||||
tg.start_soon(ws_writer)
|
|
||||||
|
|
||||||
# Yield the receive/send streams
|
|
||||||
yield (read_stream, write_stream)
|
|
||||||
|
|
||||||
# Once the caller's 'async with' block exits, we shut down
|
|
||||||
tg.cancel_scope.cancel()
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
|
|
||||||
import anyio
|
|
||||||
from pydantic_core import ValidationError
|
|
||||||
from starlette.types import Receive, Scope, Send
|
|
||||||
from starlette.websockets import WebSocket
|
|
||||||
|
|
||||||
from mcp import types
|
|
||||||
from mcp.shared._context_streams import create_context_streams
|
|
||||||
from mcp.shared.message import SessionMessage
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def websocket_server(scope: Scope, receive: Receive, send: Send):
|
|
||||||
"""WebSocket server transport for MCP. This is an ASGI application, suitable for use
|
|
||||||
with a framework like Starlette and a server like Hypercorn.
|
|
||||||
"""
|
|
||||||
|
|
||||||
websocket = WebSocket(scope, receive, send)
|
|
||||||
await websocket.accept(subprotocol="mcp")
|
|
||||||
|
|
||||||
read_stream_writer, read_stream = create_context_streams[SessionMessage | Exception](0)
|
|
||||||
write_stream, write_stream_reader = create_context_streams[SessionMessage](0)
|
|
||||||
|
|
||||||
async def ws_reader():
|
|
||||||
try:
|
|
||||||
async with read_stream_writer:
|
|
||||||
async for msg in websocket.iter_text():
|
|
||||||
try:
|
|
||||||
client_message = types.jsonrpc_message_adapter.validate_json(msg, by_name=False)
|
|
||||||
except ValidationError as exc: # pragma: no cover
|
|
||||||
await read_stream_writer.send(exc)
|
|
||||||
continue
|
|
||||||
|
|
||||||
session_message = SessionMessage(client_message)
|
|
||||||
await read_stream_writer.send(session_message)
|
|
||||||
except anyio.ClosedResourceError: # pragma: no cover
|
|
||||||
await websocket.close()
|
|
||||||
|
|
||||||
async def ws_writer():
|
|
||||||
try:
|
|
||||||
async with write_stream_reader:
|
|
||||||
async for session_message in write_stream_reader:
|
|
||||||
obj = session_message.message.model_dump_json(by_alias=True, exclude_unset=True)
|
|
||||||
await websocket.send_text(obj)
|
|
||||||
except anyio.ClosedResourceError: # pragma: no cover
|
|
||||||
await websocket.close()
|
|
||||||
|
|
||||||
async with anyio.create_task_group() as tg:
|
|
||||||
tg.start_soon(ws_reader)
|
|
||||||
tg.start_soon(ws_writer)
|
|
||||||
yield (read_stream, write_stream)
|
|
||||||
|
|
@ -21,7 +21,6 @@ import pytest
|
||||||
|
|
||||||
from mcp.client.sse import sse_client
|
from mcp.client.sse import sse_client
|
||||||
from mcp.client.streamable_http import streamable_http_client
|
from mcp.client.streamable_http import streamable_http_client
|
||||||
from mcp.client.websocket import websocket_client
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
|
@ -104,16 +103,3 @@ async def test_streamable_http_client_closes_all_streams_on_exit() -> None:
|
||||||
with _assert_no_memory_stream_leak():
|
with _assert_no_memory_stream_leak():
|
||||||
async with streamable_http_client("http://127.0.0.1:1/mcp"):
|
async with streamable_http_client("http://127.0.0.1:1/mcp"):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_websocket_client_closes_all_streams_on_connection_error(free_tcp_port: int) -> None:
|
|
||||||
"""websocket_client must close all 4 stream ends when ws_connect fails.
|
|
||||||
|
|
||||||
Before the fix, there was no try/finally at all — if ws_connect raised,
|
|
||||||
all 4 streams were leaked.
|
|
||||||
"""
|
|
||||||
with _assert_no_memory_stream_leak():
|
|
||||||
with pytest.raises(OSError):
|
|
||||||
async with websocket_client(f"ws://127.0.0.1:{free_tcp_port}/ws"):
|
|
||||||
pytest.fail("should not reach here") # pragma: no cover
|
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
"""Smoke test for the WebSocket transport.
|
|
||||||
|
|
||||||
Runs the full WS stack end-to-end over a real TCP connection, covering both
|
|
||||||
``src/mcp/client/websocket.py`` and ``src/mcp/server/websocket.py``. MCP
|
|
||||||
semantics (error propagation, timeouts, etc.) are transport-agnostic and are
|
|
||||||
covered in ``tests/client/test_client.py`` and ``tests/issues/test_88_random_error.py``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from collections.abc import Generator
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from starlette.applications import Starlette
|
|
||||||
from starlette.routing import WebSocketRoute
|
|
||||||
from starlette.websockets import WebSocket
|
|
||||||
|
|
||||||
from mcp.client.session import ClientSession
|
|
||||||
from mcp.client.websocket import websocket_client
|
|
||||||
from mcp.server import Server
|
|
||||||
from mcp.server.websocket import websocket_server
|
|
||||||
from mcp.types import EmptyResult, InitializeResult
|
|
||||||
from tests.test_helpers import run_uvicorn_in_thread
|
|
||||||
|
|
||||||
SERVER_NAME = "test_server_for_WS"
|
|
||||||
|
|
||||||
|
|
||||||
def make_server_app() -> Starlette:
|
|
||||||
srv = Server(SERVER_NAME)
|
|
||||||
|
|
||||||
async def handle_ws(websocket: WebSocket) -> None:
|
|
||||||
async with websocket_server(websocket.scope, websocket.receive, websocket.send) as streams:
|
|
||||||
await srv.run(streams[0], streams[1], srv.create_initialization_options())
|
|
||||||
|
|
||||||
return Starlette(routes=[WebSocketRoute("/ws", endpoint=handle_ws)])
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def ws_server_url() -> Generator[str, None, None]:
|
|
||||||
with run_uvicorn_in_thread(make_server_app()) as base_url:
|
|
||||||
yield base_url.replace("http://", "ws://") + "/ws"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_ws_client_basic_connection(ws_server_url: str) -> None:
|
|
||||||
async with websocket_client(ws_server_url) as streams:
|
|
||||||
async with ClientSession(*streams) as session:
|
|
||||||
result = await session.initialize()
|
|
||||||
assert isinstance(result, InitializeResult)
|
|
||||||
assert result.server_info.name == SERVER_NAME
|
|
||||||
|
|
||||||
ping_result = await session.send_ping()
|
|
||||||
assert isinstance(ping_result, EmptyResult)
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
"""Common test utilities for MCP server tests."""
|
|
||||||
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
from collections.abc import Generator
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import uvicorn
|
|
||||||
|
|
||||||
_SERVER_SHUTDOWN_TIMEOUT_S = 5.0
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def run_uvicorn_in_thread(app: Any, **config_kwargs: Any) -> Generator[str, None, None]:
|
|
||||||
"""Run a uvicorn server in a background thread on an ephemeral port.
|
|
||||||
|
|
||||||
The socket is bound and put into listening state *before* the thread
|
|
||||||
starts, so the port is known immediately with no wait. The kernel's
|
|
||||||
listen queue buffers any connections that arrive before uvicorn's event
|
|
||||||
loop reaches ``accept()``, so callers can connect as soon as this
|
|
||||||
function yields — no polling, no sleeps, no startup race.
|
|
||||||
|
|
||||||
This also avoids the TOCTOU race of the old pick-a-port-then-rebind
|
|
||||||
pattern: the socket passed here is the one uvicorn serves on, with no
|
|
||||||
gap where another pytest-xdist worker could claim it.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
app: ASGI application to serve.
|
|
||||||
**config_kwargs: Additional keyword arguments for :class:`uvicorn.Config`
|
|
||||||
(e.g. ``log_level``). ``host``/``port`` are ignored since the
|
|
||||||
socket is pre-bound.
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
The base URL of the running server, e.g. ``http://127.0.0.1:54321``.
|
|
||||||
"""
|
|
||||||
host = "127.0.0.1"
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
sock.bind((host, 0))
|
|
||||||
sock.listen()
|
|
||||||
port = sock.getsockname()[1]
|
|
||||||
|
|
||||||
config_kwargs.setdefault("log_level", "error")
|
|
||||||
# Uvicorn's interface autodetection calls asyncio.iscoroutinefunction,
|
|
||||||
# which Python 3.14 deprecates. Under filterwarnings=error this crashes
|
|
||||||
# the server thread silently. Starlette is asgi3; skip the autodetect.
|
|
||||||
config_kwargs.setdefault("interface", "asgi3")
|
|
||||||
server = uvicorn.Server(config=uvicorn.Config(app=app, **config_kwargs))
|
|
||||||
|
|
||||||
thread = threading.Thread(target=server.run, kwargs={"sockets": [sock]}, daemon=True)
|
|
||||||
thread.start()
|
|
||||||
try:
|
|
||||||
yield f"http://{host}:{port}"
|
|
||||||
finally:
|
|
||||||
server.should_exit = True
|
|
||||||
thread.join(timeout=_SERVER_SHUTDOWN_TIMEOUT_S)
|
|
||||||
69
uv.lock
generated
69
uv.lock
generated
|
|
@ -870,9 +870,6 @@ cli = [
|
||||||
rich = [
|
rich = [
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
]
|
]
|
||||||
ws = [
|
|
||||||
{ name = "websockets" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
|
@ -880,7 +877,7 @@ dev = [
|
||||||
{ name = "dirty-equals" },
|
{ name = "dirty-equals" },
|
||||||
{ name = "inline-snapshot" },
|
{ name = "inline-snapshot" },
|
||||||
{ name = "logfire" },
|
{ name = "logfire" },
|
||||||
{ name = "mcp", extra = ["cli", "ws"] },
|
{ name = "mcp", extra = ["cli"] },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "pyright" },
|
{ name = "pyright" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
|
|
@ -922,9 +919,8 @@ requires-dist = [
|
||||||
{ name = "typing-extensions", specifier = ">=4.13.0" },
|
{ name = "typing-extensions", specifier = ">=4.13.0" },
|
||||||
{ name = "typing-inspection", specifier = ">=0.4.1" },
|
{ name = "typing-inspection", specifier = ">=0.4.1" },
|
||||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'", specifier = ">=0.31.1" },
|
{ name = "uvicorn", marker = "sys_platform != 'emscripten'", specifier = ">=0.31.1" },
|
||||||
{ name = "websockets", marker = "extra == 'ws'", specifier = ">=15.0.1" },
|
|
||||||
]
|
]
|
||||||
provides-extras = ["cli", "rich", "ws"]
|
provides-extras = ["cli", "rich"]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
|
|
@ -932,7 +928,7 @@ dev = [
|
||||||
{ name = "dirty-equals", specifier = ">=0.9.0" },
|
{ name = "dirty-equals", specifier = ">=0.9.0" },
|
||||||
{ name = "inline-snapshot", specifier = ">=0.23.0" },
|
{ name = "inline-snapshot", specifier = ">=0.23.0" },
|
||||||
{ name = "logfire", specifier = ">=3.0.0" },
|
{ name = "logfire", specifier = ">=3.0.0" },
|
||||||
{ name = "mcp", extras = ["cli", "ws"], editable = "." },
|
{ name = "mcp", extras = ["cli"], editable = "." },
|
||||||
{ name = "pillow", specifier = ">=12.0" },
|
{ name = "pillow", specifier = ">=12.0" },
|
||||||
{ name = "pyright", specifier = ">=1.1.400" },
|
{ name = "pyright", specifier = ">=1.1.400" },
|
||||||
{ name = "pytest", specifier = ">=8.4.0" },
|
{ name = "pytest", specifier = ">=8.4.0" },
|
||||||
|
|
@ -2756,65 +2752,6 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "websockets"
|
|
||||||
version = "15.0.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wrapt"
|
name = "wrapt"
|
||||||
version = "1.17.3"
|
version = "1.17.3"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue