Jeremiah Lowin commited on
Commit
92533b3
·
unverified ·
2 Parent(s): aec5743d138f33

Merge pull request #100 from jlowin/memory-client

Browse files

Add in-memory client for calling FastMCP servers (and tests)

pyproject.toml CHANGED
@@ -6,6 +6,8 @@ authors = [{ name = "Jeremiah Lowin" }]
6
  dependencies = [
7
  "mcp>=1.6.0,<2.0.0",
8
  "rich>=13.9.4",
 
 
9
  ]
10
  requires-python = ">=3.10"
11
  readme = "README.md"
 
6
  dependencies = [
7
  "mcp>=1.6.0,<2.0.0",
8
  "rich>=13.9.4",
9
+ "typer>=0.15.2",
10
+ "websockets>=15.0.1",
11
  ]
12
  requires-python = ">=3.10"
13
  readme = "README.md"
src/fastmcp/__init__.py CHANGED
@@ -2,9 +2,11 @@
2
 
3
  from importlib.metadata import version
4
  from fastmcp.server import FastMCP, Context
 
5
 
6
  __version__ = version("fastmcp")
7
  __all__ = [
8
  "FastMCP",
9
  "Context",
 
10
  ]
 
2
 
3
  from importlib.metadata import version
4
  from fastmcp.server import FastMCP, Context
5
+ from . import clients
6
 
7
  __version__ = version("fastmcp")
8
  __all__ = [
9
  "FastMCP",
10
  "Context",
11
+ "clients",
12
  ]
src/fastmcp/{client → clients}/__init__.py RENAMED
@@ -1,12 +1,12 @@
1
  from .websocket import WebSocketClient
2
  from .sse import SSEClient
3
  from .stdio import StdioClient, UvxClient
4
- from .memory import InMemoryClient
5
 
6
  __all__ = [
7
  "StdioClient",
8
  "SSEClient",
9
  "WebSocketClient",
10
  "UvxClient",
11
- "InMemoryClient",
12
  ]
 
1
  from .websocket import WebSocketClient
2
  from .sse import SSEClient
3
  from .stdio import StdioClient, UvxClient
4
+ from .fastmcp_client import FastMCPClient
5
 
6
  __all__ = [
7
  "StdioClient",
8
  "SSEClient",
9
  "WebSocketClient",
10
  "UvxClient",
11
+ "FastMCPClient",
12
  ]
src/fastmcp/{client → clients}/base.py RENAMED
File without changes
src/fastmcp/{client/memory.py → clients/fastmcp_client.py} RENAMED
@@ -1,17 +1,17 @@
1
  import contextlib
2
- from typing import Any, TypeVar
3
 
4
- from mcp.server import Server
5
  from mcp.shared.memory import create_connected_server_and_client_session
6
  from typing_extensions import Unpack
7
 
8
- from fastmcp.client.base import BaseClient, ClientKwargs
 
9
 
10
  T = TypeVar("T")
11
 
12
 
13
- class InMemoryClient(BaseClient):
14
- """Client that connects to an in-memory MCP server.
15
 
16
  This client creates and manages an in-memory connection to a server,
17
  without using any external processes or network connections.
@@ -19,33 +19,29 @@ class InMemoryClient(BaseClient):
19
 
20
  def __init__(
21
  self,
22
- server: Server[Any],
23
- raise_exceptions: bool = False,
24
  **kwargs: Unpack[ClientKwargs],
25
  ):
26
  """Initialize an InMemoryClient that connects to an in-memory MCP server.
27
 
28
  Args:
29
- server: The MCP server instance to connect to
30
- raise_exceptions: Whether to raise exceptions from the server
31
  **kwargs: Additional arguments for BaseClient
32
  """
33
  super().__init__(**kwargs)
34
  self.server = server
35
- self.raise_exceptions = raise_exceptions
36
  self._cm_session = None
37
 
38
  @contextlib.asynccontextmanager
39
  async def _connect(self):
40
  """Set up in-memory connection and session"""
41
  self._cm_session = create_connected_server_and_client_session(
42
- server=self.server,
43
  read_timeout_seconds=self._read_timeout_seconds,
44
  sampling_callback=self._sampling_callback,
45
  list_roots_callback=self._list_roots_callback,
46
  logging_callback=self._logging_callback,
47
  message_handler=self._message_handler,
48
- raise_exceptions=self.raise_exceptions,
49
  )
50
 
51
  async with self._cm_session as session:
 
1
  import contextlib
2
+ from typing import TypeVar
3
 
 
4
  from mcp.shared.memory import create_connected_server_and_client_session
5
  from typing_extensions import Unpack
6
 
7
+ from fastmcp.clients.base import BaseClient, ClientKwargs
8
+ from fastmcp.server.server import FastMCP
9
 
10
  T = TypeVar("T")
11
 
12
 
13
+ class FastMCPClient(BaseClient):
14
+ """Client that connects directly to an in-memory FastMCP server.
15
 
16
  This client creates and manages an in-memory connection to a server,
17
  without using any external processes or network connections.
 
19
 
20
  def __init__(
21
  self,
22
+ server: FastMCP,
 
23
  **kwargs: Unpack[ClientKwargs],
24
  ):
25
  """Initialize an InMemoryClient that connects to an in-memory MCP server.
26
 
27
  Args:
28
+ server: The FastMCP instance to connect to
 
29
  **kwargs: Additional arguments for BaseClient
30
  """
31
  super().__init__(**kwargs)
32
  self.server = server
 
33
  self._cm_session = None
34
 
35
  @contextlib.asynccontextmanager
36
  async def _connect(self):
37
  """Set up in-memory connection and session"""
38
  self._cm_session = create_connected_server_and_client_session(
39
+ server=self.server._mcp_server,
40
  read_timeout_seconds=self._read_timeout_seconds,
41
  sampling_callback=self._sampling_callback,
42
  list_roots_callback=self._list_roots_callback,
43
  logging_callback=self._logging_callback,
44
  message_handler=self._message_handler,
 
45
  )
46
 
47
  async with self._cm_session as session:
src/fastmcp/{client → clients}/sse.py RENAMED
@@ -4,7 +4,7 @@ from mcp import ClientSession
4
  from mcp.client.sse import sse_client
5
  from typing_extensions import Unpack
6
 
7
- from fastmcp.client.base import BaseClient, ClientKwargs
8
 
9
 
10
  class SSEClient(BaseClient):
 
4
  from mcp.client.sse import sse_client
5
  from typing_extensions import Unpack
6
 
7
+ from fastmcp.clients.base import BaseClient, ClientKwargs
8
 
9
 
10
  class SSEClient(BaseClient):
src/fastmcp/{client → clients}/stdio.py RENAMED
@@ -6,7 +6,7 @@ from mcp import ClientSession, StdioServerParameters
6
  from mcp.client.stdio import stdio_client
7
  from typing_extensions import Unpack
8
 
9
- from fastmcp.client.base import BaseClient, ClientKwargs
10
 
11
 
12
  class StdioClient(BaseClient):
 
6
  from mcp.client.stdio import stdio_client
7
  from typing_extensions import Unpack
8
 
9
+ from fastmcp.clients.base import BaseClient, ClientKwargs
10
 
11
 
12
  class StdioClient(BaseClient):
src/fastmcp/{client → clients}/websocket.py RENAMED
@@ -4,7 +4,7 @@ from mcp import ClientSession
4
  from mcp.client.websocket import websocket_client
5
  from typing_extensions import Unpack
6
 
7
- from fastmcp.client.base import BaseClient, ClientKwargs
8
 
9
 
10
  class WebSocketClient(BaseClient):
 
4
  from mcp.client.websocket import websocket_client
5
  from typing_extensions import Unpack
6
 
7
+ from fastmcp.clients.base import BaseClient, ClientKwargs
8
 
9
 
10
  class WebSocketClient(BaseClient):
tests/clients/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Client tests package."""
tests/clients/test_fastmcp_client.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import cast
2
+
3
+ import pytest
4
+ from pydantic import AnyUrl
5
+
6
+ from fastmcp.clients import FastMCPClient
7
+ from fastmcp.server.server import FastMCP
8
+
9
+
10
+ class _TestException(Exception):
11
+ """Test exception for testing raise_exceptions behavior."""
12
+
13
+ pass
14
+
15
+
16
+ @pytest.fixture
17
+ def fastmcp_server():
18
+ """Fixture that creates a FastMCP server with tools, resources, and prompts."""
19
+ server = FastMCP("TestServer")
20
+
21
+ # Add a tool
22
+ @server.tool()
23
+ def greet(name: str) -> str:
24
+ """Greet someone by name."""
25
+ return f"Hello, {name}!"
26
+
27
+ # Add a tool that raises an exception
28
+ @server.tool()
29
+ def error_tool() -> str:
30
+ """A tool that always raises an exception."""
31
+ raise _TestException("Deliberate test exception")
32
+
33
+ # Add a resource
34
+ @server.resource(uri="data://users")
35
+ async def get_users():
36
+ return ["Alice", "Bob", "Charlie"]
37
+
38
+ # Add a resource template
39
+ @server.resource(uri="data://user/{user_id}")
40
+ async def get_user(user_id: str):
41
+ return {"id": user_id, "name": f"User {user_id}", "active": True}
42
+
43
+ # Add a prompt
44
+ @server.prompt()
45
+ def welcome(name: str) -> str:
46
+ return f"Welcome to FastMCP, {name}!"
47
+
48
+ return server
49
+
50
+
51
+ async def test_list_tools(fastmcp_server):
52
+ """Test listing tools with InMemoryClient."""
53
+ client = FastMCPClient(server=fastmcp_server)
54
+
55
+ async with client:
56
+ result = await client.list_tools()
57
+
58
+ # Check that our tools are available
59
+ assert len(result.tools) == 2
60
+ tool_names = [tool.name for tool in result.tools]
61
+ assert "greet" in tool_names
62
+ assert "error_tool" in tool_names
63
+
64
+
65
+ async def test_call_tool(fastmcp_server):
66
+ """Test calling a tool with InMemoryClient."""
67
+ client = FastMCPClient(server=fastmcp_server)
68
+
69
+ async with client:
70
+ result = await client.call_tool("greet", {"name": "World"})
71
+
72
+ # The result content should contain our greeting
73
+ content_str = str(result.content[0])
74
+ assert "Hello, World!" in content_str
75
+
76
+
77
+ async def test_list_resources(fastmcp_server):
78
+ """Test listing resources with InMemoryClient."""
79
+ client = FastMCPClient(server=fastmcp_server)
80
+
81
+ async with client:
82
+ result = await client.list_resources()
83
+
84
+ # Check that our resource is available
85
+ assert len(result.resources) == 1
86
+ assert str(result.resources[0].uri) == "data://users"
87
+
88
+
89
+ async def test_list_prompts(fastmcp_server):
90
+ """Test listing prompts with InMemoryClient."""
91
+ client = FastMCPClient(server=fastmcp_server)
92
+
93
+ async with client:
94
+ result = await client.list_prompts()
95
+
96
+ # Check that our prompt is available
97
+ assert len(result.prompts) == 1
98
+ assert result.prompts[0].name == "welcome"
99
+
100
+
101
+ async def test_get_prompt(fastmcp_server):
102
+ """Test getting a prompt with InMemoryClient."""
103
+ client = FastMCPClient(server=fastmcp_server)
104
+
105
+ async with client:
106
+ result = await client.get_prompt("welcome", {"name": "Developer"})
107
+
108
+ # The result should contain our welcome message
109
+ result_str = str(result)
110
+ assert "Welcome to FastMCP, Developer!" in result_str
111
+
112
+
113
+ async def test_read_resource(fastmcp_server):
114
+ """Test reading a resource with InMemoryClient."""
115
+ client = FastMCPClient(server=fastmcp_server)
116
+
117
+ async with client:
118
+ # Use the URI from the resource we know exists in our server
119
+ uri = cast(
120
+ AnyUrl, "data://users"
121
+ ) # Use cast for type hint only, the URI is valid
122
+ result = await client.read_resource(uri)
123
+
124
+ # The contents should include our user list
125
+ contents_str = str(result.contents[0])
126
+ assert "Alice" in contents_str
127
+ assert "Bob" in contents_str
128
+ assert "Charlie" in contents_str
129
+
130
+
131
+ async def test_client_connection(fastmcp_server):
132
+ """Test that the client connects and disconnects properly."""
133
+ client = FastMCPClient(server=fastmcp_server)
134
+
135
+ # Before connection
136
+ assert not client.is_connected()
137
+
138
+ # During connection
139
+ async with client:
140
+ assert client.is_connected()
141
+
142
+ # After connection
143
+ assert not client.is_connected()
144
+
145
+
146
+ async def test_resource_template(fastmcp_server):
147
+ """Test using a resource template with InMemoryClient."""
148
+ client = FastMCPClient(server=fastmcp_server)
149
+
150
+ async with client:
151
+ # First, list templates
152
+ result = await client.list_resource_templates()
153
+
154
+ # Check that our template is available
155
+ assert len(result.resourceTemplates) == 1
156
+ assert "data://user/{user_id}" in result.resourceTemplates[0].uriTemplate
157
+
158
+ # Now use the template with a specific user_id
159
+ uri = cast(AnyUrl, "data://user/123")
160
+ result = await client.read_resource(uri)
161
+
162
+ # Check the content matches what we expect for the provided user_id
163
+ content_str = str(result.contents[0])
164
+ assert '"id": "123"' in content_str
165
+ assert '"name": "User 123"' in content_str
166
+ assert '"active": true' in content_str
uv.lock CHANGED
@@ -220,11 +220,13 @@ wheels = [
220
 
221
  [[package]]
222
  name = "fastmcp"
223
- version = "0.4.2.dev17+gfc3fc4e.d20250407"
224
  source = { editable = "." }
225
  dependencies = [
226
  { name = "mcp" },
227
  { name = "rich" },
 
 
228
  ]
229
 
230
  [package.dev-dependencies]
@@ -245,6 +247,8 @@ dev = [
245
  requires-dist = [
246
  { name = "mcp", specifier = ">=1.6.0,<2.0.0" },
247
  { name = "rich", specifier = ">=13.9.4" },
 
 
248
  ]
249
 
250
  [package.metadata.requires-dev]
@@ -1196,6 +1200,65 @@ wheels = [
1196
  { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
1197
  ]
1198
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1199
  [[package]]
1200
  name = "wmctrl"
1201
  version = "0.5"
 
220
 
221
  [[package]]
222
  name = "fastmcp"
223
+ version = "0.4.2.dev21+g0b6e56c.d20250407"
224
  source = { editable = "." }
225
  dependencies = [
226
  { name = "mcp" },
227
  { name = "rich" },
228
+ { name = "typer" },
229
+ { name = "websockets" },
230
  ]
231
 
232
  [package.dev-dependencies]
 
247
  requires-dist = [
248
  { name = "mcp", specifier = ">=1.6.0,<2.0.0" },
249
  { name = "rich", specifier = ">=13.9.4" },
250
+ { name = "typer", specifier = ">=0.15.2" },
251
+ { name = "websockets", specifier = ">=15.0.1" },
252
  ]
253
 
254
  [package.metadata.requires-dev]
 
1200
  { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
1201
  ]
1202
 
1203
+ [[package]]
1204
+ name = "websockets"
1205
+ version = "15.0.1"
1206
+ source = { registry = "https://pypi.org/simple" }
1207
+ sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 }
1208
+ wheels = [
1209
+ { 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 },
1210
+ { 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 },
1211
+ { 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 },
1212
+ { 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 },
1213
+ { 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 },
1214
+ { 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 },
1215
+ { 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 },
1216
+ { 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 },
1217
+ { 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 },
1218
+ { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388 },
1219
+ { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830 },
1220
+ { 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 },
1221
+ { 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 },
1222
+ { 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 },
1223
+ { 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 },
1224
+ { 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 },
1225
+ { 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 },
1226
+ { 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 },
1227
+ { 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 },
1228
+ { 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 },
1229
+ { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 },
1230
+ { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 },
1231
+ { 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 },
1232
+ { 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 },
1233
+ { 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 },
1234
+ { 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 },
1235
+ { 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 },
1236
+ { 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 },
1237
+ { 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 },
1238
+ { 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 },
1239
+ { 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 },
1240
+ { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 },
1241
+ { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 },
1242
+ { 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 },
1243
+ { 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 },
1244
+ { 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 },
1245
+ { 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 },
1246
+ { 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 },
1247
+ { 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 },
1248
+ { 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 },
1249
+ { 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 },
1250
+ { 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 },
1251
+ { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
1252
+ { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
1253
+ { 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 },
1254
+ { 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 },
1255
+ { 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 },
1256
+ { 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 },
1257
+ { 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 },
1258
+ { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 },
1259
+ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
1260
+ ]
1261
+
1262
  [[package]]
1263
  name = "wmctrl"
1264
  version = "0.5"