dsmq 1.0.0__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: dsmq
3
+ Version: 1.1.0
4
+ Summary: A dead simple message queue
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: pytest>=8.3.4
8
+ Requires-Dist: websockets>=14.1
9
+ Description-Content-Type: text/markdown
10
+
1
11
  # Dead Simple Message Queue
2
12
 
3
13
  ## What it does
@@ -77,7 +87,7 @@ p_mq.close()
77
87
  1. In the second, run `src/dsmq/example_put_client.py`.
78
88
  1. In the third, run `src/dsmq/example_get_client.py`.
79
89
 
80
- Alternatively, if you're on Linux just run `src/dsmq/demo_linux.py`.
90
+ Alternatively, you can run them all at once with `src/dsmq/demo.py`.
81
91
 
82
92
  ## How it works
83
93
 
@@ -141,3 +151,30 @@ connected to the server.
141
151
  - returns str, the content of the message. If there was no eligble message
142
152
  in the topic, or the topic doesn't yet exist,
143
153
  returns `""`.
154
+
155
+ ### `get_wait(topic)`
156
+
157
+ A variant of `get()` that retries a few times until it gets
158
+ a non-empty message. Adjust internal values `_n_tries` and `_initial_retry`
159
+ to change how persistent it will be.
160
+
161
+ - `topic` (str)
162
+ - returns str, the content of the message. If there was no eligble message
163
+ in the topic after the allotted number of tries,
164
+ or the topic doesn't yet exist,
165
+ returns `""`.
166
+
167
+ ### `shutdown_server()`
168
+
169
+ Gracefully shut down the server, through the client connection.
170
+
171
+ ### `close()`
172
+
173
+ Gracefully shut down the client connection.
174
+
175
+ # Testing
176
+
177
+ Run all the tests in `src/dsmq/tests/` with pytest, for example
178
+ ```
179
+ uv run pytest
180
+ ```
@@ -1,12 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: dsmq
3
- Version: 1.0.0
4
- Summary: A dead simple message queue
5
- License-File: LICENSE
6
- Requires-Python: >=3.10
7
- Requires-Dist: websockets>=14.1
8
- Description-Content-Type: text/markdown
9
-
10
1
  # Dead Simple Message Queue
11
2
 
12
3
  ## What it does
@@ -86,7 +77,7 @@ p_mq.close()
86
77
  1. In the second, run `src/dsmq/example_put_client.py`.
87
78
  1. In the third, run `src/dsmq/example_get_client.py`.
88
79
 
89
- Alternatively, if you're on Linux just run `src/dsmq/demo_linux.py`.
80
+ Alternatively, you can run them all at once with `src/dsmq/demo.py`.
90
81
 
91
82
  ## How it works
92
83
 
@@ -150,3 +141,30 @@ connected to the server.
150
141
  - returns str, the content of the message. If there was no eligble message
151
142
  in the topic, or the topic doesn't yet exist,
152
143
  returns `""`.
144
+
145
+ ### `get_wait(topic)`
146
+
147
+ A variant of `get()` that retries a few times until it gets
148
+ a non-empty message. Adjust internal values `_n_tries` and `_initial_retry`
149
+ to change how persistent it will be.
150
+
151
+ - `topic` (str)
152
+ - returns str, the content of the message. If there was no eligble message
153
+ in the topic after the allotted number of tries,
154
+ or the topic doesn't yet exist,
155
+ returns `""`.
156
+
157
+ ### `shutdown_server()`
158
+
159
+ Gracefully shut down the server, through the client connection.
160
+
161
+ ### `close()`
162
+
163
+ Gracefully shut down the client connection.
164
+
165
+ # Testing
166
+
167
+ Run all the tests in `src/dsmq/tests/` with pytest, for example
168
+ ```
169
+ uv run pytest
170
+ ```
@@ -1,10 +1,11 @@
1
1
  [project]
2
2
  name = "dsmq"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  description = "A dead simple message queue"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
+ "pytest>=8.3.4",
8
9
  "websockets>=14.1",
9
10
  ]
10
11
 
@@ -0,0 +1,65 @@
1
+ import json
2
+ import time
3
+ from websockets.sync.client import connect as ws_connect
4
+
5
+ _default_host = "127.0.0.1"
6
+ _default_port = 30008
7
+
8
+ _n_retries = 10
9
+ _initial_retry = 0.01 # seconds
10
+
11
+
12
+ def connect(host=_default_host, port=_default_port):
13
+ return DSMQClientSideConnection(host, port)
14
+
15
+
16
+ class DSMQClientSideConnection:
17
+ def __init__(self, host, port):
18
+ self.uri = f"ws://{host}:{port}"
19
+ print(f"Connecting to dsmq server at {self.uri}")
20
+ for i_retry in range(_n_retries):
21
+ try:
22
+ self.websocket = ws_connect(self.uri)
23
+ break
24
+ except ConnectionRefusedError:
25
+ self.websocket = None
26
+ # Exponential backoff
27
+ # Wait twice as long each time before trying again.
28
+ time.sleep(_initial_retry * 2**i_retry)
29
+ print(" ...trying again")
30
+
31
+ if self.websocket is None:
32
+ raise ConnectionRefusedError("Could not connect to dsmq server.")
33
+
34
+ self.time_of_last_request = time.time()
35
+
36
+ def get(self, topic):
37
+ msg = {"action": "get", "topic": topic}
38
+ self.websocket.send(json.dumps(msg))
39
+ msg_text = self.websocket.recv()
40
+ msg = json.loads(msg_text)
41
+ return msg["message"]
42
+
43
+ def get_wait(self, topic):
44
+ """
45
+ A variant of `get()` that retries a few times until it gets
46
+ a non-empty message. Adjust `_n_tries` and `_initial_retry`
47
+ to change how persistent it will be.
48
+ """
49
+ for i_retry in range(_n_retries):
50
+ message = self.get(topic)
51
+ if message != "":
52
+ return message
53
+ time.sleep(_initial_retry * 2**i_retry)
54
+ return message
55
+
56
+ def put(self, topic, msg_body):
57
+ msg_dict = {"action": "put", "topic": topic, "message": msg_body}
58
+ self.websocket.send(json.dumps(msg_dict))
59
+
60
+ def shutdown_server(self):
61
+ msg_dict = {"action": "shutdown", "topic": ""}
62
+ self.websocket.send(json.dumps(msg_dict))
63
+
64
+ def close(self):
65
+ self.websocket.close()
@@ -1,11 +1,8 @@
1
- # Heads up: This script only works on Linux because of use of "fork" method.
2
1
  import multiprocessing as mp
3
2
  from dsmq.server import serve
4
3
  import dsmq.example_get_client
5
4
  import dsmq.example_put_client
6
5
 
7
- mp.set_start_method("fork")
8
-
9
6
  HOST = "127.0.0.1"
10
7
  PORT = 25252
11
8
 
@@ -1,6 +1,8 @@
1
1
  import json
2
+ import os
2
3
  import sqlite3
3
4
  import sys
5
+ from threading import Thread
4
6
  import time
5
7
  from websockets.sync.server import serve as ws_serve
6
8
 
@@ -8,30 +10,57 @@ _default_host = "127.0.0.1"
8
10
  _default_port = 30008
9
11
  _n_retries = 5
10
12
  _first_retry = 0.01 # seconds
13
+ _pause = 0.01 # seconds
11
14
  _time_to_live = 600.0 # seconds
12
15
 
16
+ _db_name = "file::memory:?cache=shared"
17
+
18
+ # Make this global so it's easy to share
19
+ dsmq_server = None
20
+
13
21
 
14
22
  def serve(host=_default_host, port=_default_port):
15
23
  """
16
24
  For best results, start this running in its own process and walk away.
17
25
  """
18
- sqlite_conn = sqlite3.connect("file:mem1?mode=memory&cache=shared")
26
+ # Cleanup temp files.
27
+ # Under some condition
28
+ # (which I haven't yet been able to pin down)
29
+ # a file is generated with the db name.
30
+ # If it is not removed, it gets
31
+ # treated as a SQLite db on disk,
32
+ # which dramatically slows it down,
33
+ # especially the way it's used here for
34
+ # rapid-fire one-item reads and writes.
35
+ filenames = os.listdir()
36
+ for filename in filenames:
37
+ if filename[: len(_db_name)] == _db_name:
38
+ os.remove(filename)
39
+
40
+ sqlite_conn = sqlite3.connect(_db_name)
19
41
  cursor = sqlite_conn.cursor()
20
42
  cursor.execute("""
21
43
  CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
22
44
  """)
23
45
 
24
- with ws_serve(request_handler, host, port) as server:
25
- server.serve_forever()
26
- print()
27
- print(f"Server started at {host} on port {port}.")
28
- print("Waiting for clients...")
46
+ # Making this global in scope is a way to make it available
47
+ # to the shutdown operation. It's an awkward construction,
48
+ # and a method of last resort. (If you stumble across this and
49
+ # figure out something more elegant, please submit a PR!
50
+ # or send it to me at brohrer@gmail.com,
51
+ global dsmq_server
52
+
53
+ dsmq_server = ws_serve(request_handler, host, port)
54
+ dsmq_server.serve_forever()
55
+ print()
56
+ print(f"Server started at {host} on port {port}.")
57
+ print("Waiting for clients...")
29
58
 
30
59
  sqlite_conn.close()
31
60
 
32
61
 
33
62
  def request_handler(websocket):
34
- sqlite_conn = sqlite3.connect("file:mem1?mode=memory&cache=shared")
63
+ sqlite_conn = sqlite3.connect(_db_name)
35
64
  cursor = sqlite_conn.cursor()
36
65
 
37
66
  client_creation_time = time.time()
@@ -85,7 +114,7 @@ FROM messages,
85
114
  SELECT MIN(timestamp) AS min_time
86
115
  FROM messages
87
116
  WHERE topic = :topic
88
- AND timestamp > :last_read_time
117
+ AND timestamp > :last_read_time
89
118
  ) a
90
119
  WHERE topic = :topic
91
120
  AND timestamp = a.min_time
@@ -108,9 +137,27 @@ AND timestamp = a.min_time
108
137
  message = ""
109
138
 
110
139
  websocket.send(json.dumps({"message": message}))
140
+ elif msg["action"] == "shutdown":
141
+ # Run this from a separate thread to prevent deadlock
142
+ global dsmq_server
143
+ print("Shutting down the dsmq server.")
144
+
145
+ def shutdown_gracefully(server_to_shutdown):
146
+ server_to_shutdown.shutdown()
147
+ time.sleep(_pause)
148
+
149
+ filenames = os.listdir()
150
+ for filename in filenames:
151
+ if filename[: len(_db_name)] == _db_name:
152
+ try:
153
+ os.remove(filename)
154
+ except FileNotFoundError:
155
+ pass
156
+
157
+ Thread(target=shutdown_gracefully, args=(dsmq_server,)).start()
158
+ sqlite_conn.close()
111
159
  else:
112
- print("Action must either be 'put' or 'get'")
113
-
160
+ print("Action must either be 'put', 'get', or 'shudown'")
114
161
 
115
162
  # Periodically clean out messages from the queue that are
116
163
  # past their sell buy date.
@@ -121,7 +168,7 @@ AND timestamp = a.min_time
121
168
  DELETE FROM messages
122
169
  WHERE timestamp < :time_threshold
123
170
  """,
124
- {"time_threshold": time_of_last_purge}
171
+ {"time_threshold": time_of_last_purge},
125
172
  )
126
173
  sqlite_conn.commit()
127
174
  time_of_last_purge = time.time()
File without changes
@@ -0,0 +1,238 @@
1
+ import multiprocessing as mp
2
+ import time
3
+ from dsmq.server import serve
4
+ from dsmq.client import connect
5
+
6
+ # spawn is the default method on macOS
7
+ # mp.set_start_method('spawn')
8
+
9
+ host = "127.0.0.1"
10
+ port = 30303
11
+
12
+ _short_pause = 0.001
13
+ _pause = 0.01
14
+ _long_pause = 0.1
15
+ _very_long_pause = 0.1
16
+
17
+
18
+ def test_client_server():
19
+ p_server = mp.Process(target=serve, args=(host, port))
20
+ p_server.start()
21
+ mq = connect(host, port)
22
+ read_completes = False
23
+ write_completes = False
24
+
25
+ n_iter = 11
26
+ for i in range(n_iter):
27
+ mq.put("test", f"msg_{i}")
28
+ write_completes = True
29
+
30
+ for i in range(n_iter):
31
+ msg = mq.get("test")
32
+ read_completes = True
33
+
34
+ assert msg
35
+ assert write_completes
36
+ assert read_completes
37
+
38
+ mq.shutdown_server()
39
+ mq.close()
40
+
41
+ # It takes a sec to shut down the server
42
+ time.sleep(_long_pause)
43
+ assert not p_server.is_alive()
44
+
45
+
46
+ def test_write_one_read_one():
47
+ p_server = mp.Process(target=serve, args=(host, port))
48
+ p_server.start()
49
+ write_client = connect(host, port)
50
+ read_client = connect(host, port)
51
+
52
+ write_client.put("test", "test_msg")
53
+
54
+ # It takes a moment for the write to complete
55
+ time.sleep(_pause)
56
+ msg = read_client.get("test")
57
+
58
+ assert msg == "test_msg"
59
+
60
+ write_client.shutdown_server()
61
+ write_client.close()
62
+ read_client.close()
63
+
64
+
65
+ def test_get_wait():
66
+ p_server = mp.Process(target=serve, args=(host, port))
67
+ p_server.start()
68
+ write_client = connect(host, port)
69
+ read_client = connect(host, port)
70
+
71
+ write_client.put("test", "test_msg")
72
+
73
+ msg = read_client.get_wait("test")
74
+
75
+ assert msg == "test_msg"
76
+
77
+ write_client.shutdown_server()
78
+ write_client.close()
79
+ read_client.close()
80
+
81
+
82
+ def test_multitopics():
83
+ p_server = mp.Process(target=serve, args=(host, port))
84
+ p_server.start()
85
+ write_client = connect(host, port)
86
+ read_client = connect(host, port)
87
+
88
+ write_client.put("test_A", "test_msg_A")
89
+ write_client.put("test_B", "test_msg_B")
90
+
91
+ msg_A = read_client.get_wait("test_A")
92
+ msg_B = read_client.get_wait("test_B")
93
+
94
+ assert msg_A == "test_msg_A"
95
+ assert msg_B == "test_msg_B"
96
+
97
+ write_client.shutdown_server()
98
+ write_client.close()
99
+ read_client.close()
100
+
101
+
102
+ def test_client_history_cutoff():
103
+ p_server = mp.Process(target=serve, args=(host, port))
104
+ p_server.start()
105
+ write_client = connect(host, port)
106
+ write_client.put("test", "test_msg")
107
+ time.sleep(_pause)
108
+
109
+ read_client = connect(host, port)
110
+ msg = read_client.get("test")
111
+
112
+ assert msg == ""
113
+
114
+ write_client.shutdown_server()
115
+ write_client.close()
116
+ read_client.close()
117
+
118
+
119
+ def test_two_write_clients():
120
+ p_server = mp.Process(target=serve, args=(host, port))
121
+ p_server.start()
122
+ write_client_A = connect(host, port)
123
+ write_client_B = connect(host, port)
124
+ read_client = connect(host, port)
125
+
126
+ write_client_A.put("test", "test_msg_A")
127
+ # Wait briefly, to ensure the order of writes
128
+ time.sleep(_pause)
129
+ write_client_B.put("test", "test_msg_B")
130
+ msg_A = read_client.get_wait("test")
131
+ msg_B = read_client.get_wait("test")
132
+
133
+ assert msg_A == "test_msg_A"
134
+ assert msg_B == "test_msg_B"
135
+
136
+ write_client_A.shutdown_server()
137
+ write_client_A.close()
138
+ write_client_B.close()
139
+ read_client.close()
140
+
141
+
142
+ def test_two_read_clients():
143
+ p_server = mp.Process(target=serve, args=(host, port))
144
+ p_server.start()
145
+ write_client = connect(host, port)
146
+ read_client_A = connect(host, port)
147
+ read_client_B = connect(host, port)
148
+
149
+ write_client.put("test", "test_msg")
150
+ msg_A = read_client_A.get_wait("test")
151
+ msg_B = read_client_B.get_wait("test")
152
+
153
+ assert msg_A == "test_msg"
154
+ assert msg_B == "test_msg"
155
+
156
+ write_client.shutdown_server()
157
+ write_client.close()
158
+ read_client_A.close()
159
+ read_client_B.close()
160
+
161
+
162
+ def speed_write(stop_flag):
163
+ fast_write_client = connect(host, port)
164
+ while True:
165
+ fast_write_client.put("speed_test", "speed")
166
+ if stop_flag.is_set():
167
+ break
168
+ fast_write_client.close()
169
+
170
+
171
+ def test_speed_writing():
172
+ p_server = mp.Process(target=serve, args=(host, port))
173
+ p_server.start()
174
+ write_client = connect(host, port)
175
+ read_client = connect(host, port)
176
+
177
+ stop_flag = mp.Event()
178
+ p_speed_write = mp.Process(target=speed_write, args=(stop_flag,))
179
+ p_speed_write.start()
180
+ time.sleep(_pause)
181
+
182
+ # time_a = time.time()
183
+ write_client.put("test", "test_msg")
184
+ # time_b = time.time()
185
+ msg = read_client.get_wait("test")
186
+ # time_c = time.time()
187
+
188
+ # write_time = int((time_b - time_a) * 1e6)
189
+ # read_time = int((time_c - time_b) * 1e6)
190
+ # print(f"write time: {write_time} us, read time: {read_time} us")
191
+
192
+ assert msg == "test_msg"
193
+
194
+ stop_flag.set()
195
+ p_speed_write.join()
196
+ read_client.close()
197
+ write_client.shutdown_server()
198
+ write_client.close()
199
+
200
+
201
+ def speed_read(stop_flag):
202
+ fast_read_client = connect(host, port)
203
+ while True:
204
+ fast_read_client.get("speed_test")
205
+ if stop_flag.is_set():
206
+ break
207
+ fast_read_client.close()
208
+
209
+
210
+ def test_speed_reading():
211
+ p_server = mp.Process(target=serve, args=(host, port))
212
+ p_server.start()
213
+ write_client = connect(host, port)
214
+ read_client = connect(host, port)
215
+
216
+ stop_flag = mp.Event()
217
+ p_speed_read = mp.Process(target=speed_read, args=(stop_flag,))
218
+ p_speed_read.start()
219
+
220
+ # time_a = time.time()
221
+ write_client.put("test", "test_msg")
222
+ # time_b = time.time()
223
+ msg = read_client.get_wait("test")
224
+ # time_c = time.time()
225
+
226
+ # write_time = int((time_b - time_a) * 1e6)
227
+ # read_time = int((time_c - time_b) * 1e6)
228
+ # print(f"write time: {write_time} us, read time: {read_time} us")
229
+
230
+ assert msg == "test_msg"
231
+
232
+ stop_flag.set()
233
+ p_speed_read.join()
234
+ p_speed_read.kill()
235
+ read_client.close()
236
+ write_client.shutdown_server()
237
+ # time.sleep(_pause)
238
+ write_client.close()
dsmq-1.1.0/uv.lock ADDED
@@ -0,0 +1,177 @@
1
+ version = 1
2
+ requires-python = ">=3.10"
3
+
4
+ [[package]]
5
+ name = "colorama"
6
+ version = "0.4.6"
7
+ source = { registry = "https://pypi.org/simple" }
8
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
9
+ wheels = [
10
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
11
+ ]
12
+
13
+ [[package]]
14
+ name = "dsmq"
15
+ version = "1.1.0"
16
+ source = { editable = "." }
17
+ dependencies = [
18
+ { name = "pytest" },
19
+ { name = "websockets" },
20
+ ]
21
+
22
+ [package.metadata]
23
+ requires-dist = [
24
+ { name = "pytest", specifier = ">=8.3.4" },
25
+ { name = "websockets", specifier = ">=14.1" },
26
+ ]
27
+
28
+ [[package]]
29
+ name = "exceptiongroup"
30
+ version = "1.2.2"
31
+ source = { registry = "https://pypi.org/simple" }
32
+ sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
33
+ wheels = [
34
+ { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
35
+ ]
36
+
37
+ [[package]]
38
+ name = "iniconfig"
39
+ version = "2.0.0"
40
+ source = { registry = "https://pypi.org/simple" }
41
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
42
+ wheels = [
43
+ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
44
+ ]
45
+
46
+ [[package]]
47
+ name = "packaging"
48
+ version = "24.2"
49
+ source = { registry = "https://pypi.org/simple" }
50
+ sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
51
+ wheels = [
52
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
53
+ ]
54
+
55
+ [[package]]
56
+ name = "pluggy"
57
+ version = "1.5.0"
58
+ source = { registry = "https://pypi.org/simple" }
59
+ sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
60
+ wheels = [
61
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
62
+ ]
63
+
64
+ [[package]]
65
+ name = "pytest"
66
+ version = "8.3.4"
67
+ source = { registry = "https://pypi.org/simple" }
68
+ dependencies = [
69
+ { name = "colorama", marker = "sys_platform == 'win32'" },
70
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
71
+ { name = "iniconfig" },
72
+ { name = "packaging" },
73
+ { name = "pluggy" },
74
+ { name = "tomli", marker = "python_full_version < '3.11'" },
75
+ ]
76
+ sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
77
+ wheels = [
78
+ { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
79
+ ]
80
+
81
+ [[package]]
82
+ name = "tomli"
83
+ version = "2.2.1"
84
+ source = { registry = "https://pypi.org/simple" }
85
+ sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
86
+ wheels = [
87
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
88
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
89
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
90
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
91
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
92
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
93
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
94
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
95
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
96
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
97
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
98
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
99
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
100
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
101
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
102
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
103
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
104
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
105
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
106
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
107
+ { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
108
+ { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
109
+ { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
110
+ { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
111
+ { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
112
+ { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
113
+ { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
114
+ { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
115
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
116
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
117
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
118
+ ]
119
+
120
+ [[package]]
121
+ name = "websockets"
122
+ version = "14.2"
123
+ source = { registry = "https://pypi.org/simple" }
124
+ sdist = { url = "https://files.pythonhosted.org/packages/94/54/8359678c726243d19fae38ca14a334e740782336c9f19700858c4eb64a1e/websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5", size = 164394 }
125
+ wheels = [
126
+ { url = "https://files.pythonhosted.org/packages/28/fa/76607eb7dcec27b2d18d63f60a32e60e2b8629780f343bb83a4dbb9f4350/websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885", size = 163089 },
127
+ { url = "https://files.pythonhosted.org/packages/9e/00/ad2246b5030575b79e7af0721810fdaecaf94c4b2625842ef7a756fa06dd/websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397", size = 160741 },
128
+ { url = "https://files.pythonhosted.org/packages/72/f7/60f10924d333a28a1ff3fcdec85acf226281331bdabe9ad74947e1b7fc0a/websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610", size = 160996 },
129
+ { url = "https://files.pythonhosted.org/packages/63/7c/c655789cf78648c01ac6ecbe2d6c18f91b75bdc263ffee4d08ce628d12f0/websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3", size = 169974 },
130
+ { url = "https://files.pythonhosted.org/packages/fb/5b/013ed8b4611857ac92ac631079c08d9715b388bd1d88ec62e245f87a39df/websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980", size = 168985 },
131
+ { url = "https://files.pythonhosted.org/packages/cd/33/aa3e32fd0df213a5a442310754fe3f89dd87a0b8e5b4e11e0991dd3bcc50/websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8", size = 169297 },
132
+ { url = "https://files.pythonhosted.org/packages/93/17/dae0174883d6399f57853ac44abf5f228eaba86d98d160f390ffabc19b6e/websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7", size = 169677 },
133
+ { url = "https://files.pythonhosted.org/packages/42/e2/0375af7ac00169b98647c804651c515054b34977b6c1354f1458e4116c1e/websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f", size = 169089 },
134
+ { url = "https://files.pythonhosted.org/packages/73/8d/80f71d2a351a44b602859af65261d3dde3a0ce4e76cf9383738a949e0cc3/websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d", size = 169026 },
135
+ { url = "https://files.pythonhosted.org/packages/48/97/173b1fa6052223e52bb4054a141433ad74931d94c575e04b654200b98ca4/websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d", size = 163967 },
136
+ { url = "https://files.pythonhosted.org/packages/c0/5b/2fcf60f38252a4562b28b66077e0d2b48f91fef645d5f78874cd1dec807b/websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2", size = 164413 },
137
+ { url = "https://files.pythonhosted.org/packages/15/b6/504695fb9a33df0ca56d157f5985660b5fc5b4bf8c78f121578d2d653392/websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166", size = 163088 },
138
+ { url = "https://files.pythonhosted.org/packages/81/26/ebfb8f6abe963c795122439c6433c4ae1e061aaedfc7eff32d09394afbae/websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f", size = 160745 },
139
+ { url = "https://files.pythonhosted.org/packages/a1/c6/1435ad6f6dcbff80bb95e8986704c3174da8866ddb751184046f5c139ef6/websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910", size = 160995 },
140
+ { url = "https://files.pythonhosted.org/packages/96/63/900c27cfe8be1a1f2433fc77cd46771cf26ba57e6bdc7cf9e63644a61863/websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c", size = 170543 },
141
+ { url = "https://files.pythonhosted.org/packages/00/8b/bec2bdba92af0762d42d4410593c1d7d28e9bfd952c97a3729df603dc6ea/websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473", size = 169546 },
142
+ { url = "https://files.pythonhosted.org/packages/6b/a9/37531cb5b994f12a57dec3da2200ef7aadffef82d888a4c29a0d781568e4/websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473", size = 169911 },
143
+ { url = "https://files.pythonhosted.org/packages/60/d5/a6eadba2ed9f7e65d677fec539ab14a9b83de2b484ab5fe15d3d6d208c28/websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56", size = 170183 },
144
+ { url = "https://files.pythonhosted.org/packages/76/57/a338ccb00d1df881c1d1ee1f2a20c9c1b5b29b51e9e0191ee515d254fea6/websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142", size = 169623 },
145
+ { url = "https://files.pythonhosted.org/packages/64/22/e5f7c33db0cb2c1d03b79fd60d189a1da044e2661f5fd01d629451e1db89/websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d", size = 169583 },
146
+ { url = "https://files.pythonhosted.org/packages/aa/2e/2b4662237060063a22e5fc40d46300a07142afe30302b634b4eebd717c07/websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a", size = 163969 },
147
+ { url = "https://files.pythonhosted.org/packages/94/a5/0cda64e1851e73fc1ecdae6f42487babb06e55cb2f0dc8904b81d8ef6857/websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b", size = 164408 },
148
+ { url = "https://files.pythonhosted.org/packages/c1/81/04f7a397653dc8bec94ddc071f34833e8b99b13ef1a3804c149d59f92c18/websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c", size = 163096 },
149
+ { url = "https://files.pythonhosted.org/packages/ec/c5/de30e88557e4d70988ed4d2eabd73fd3e1e52456b9f3a4e9564d86353b6d/websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967", size = 160758 },
150
+ { url = "https://files.pythonhosted.org/packages/e5/8c/d130d668781f2c77d106c007b6c6c1d9db68239107c41ba109f09e6c218a/websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990", size = 160995 },
151
+ { url = "https://files.pythonhosted.org/packages/a6/bc/f6678a0ff17246df4f06765e22fc9d98d1b11a258cc50c5968b33d6742a1/websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda", size = 170815 },
152
+ { url = "https://files.pythonhosted.org/packages/d8/b2/8070cb970c2e4122a6ef38bc5b203415fd46460e025652e1ee3f2f43a9a3/websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95", size = 169759 },
153
+ { url = "https://files.pythonhosted.org/packages/81/da/72f7caabd94652e6eb7e92ed2d3da818626e70b4f2b15a854ef60bf501ec/websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3", size = 170178 },
154
+ { url = "https://files.pythonhosted.org/packages/31/e0/812725b6deca8afd3a08a2e81b3c4c120c17f68c9b84522a520b816cda58/websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9", size = 170453 },
155
+ { url = "https://files.pythonhosted.org/packages/66/d3/8275dbc231e5ba9bb0c4f93144394b4194402a7a0c8ffaca5307a58ab5e3/websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267", size = 169830 },
156
+ { url = "https://files.pythonhosted.org/packages/a3/ae/e7d1a56755ae15ad5a94e80dd490ad09e345365199600b2629b18ee37bc7/websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe", size = 169824 },
157
+ { url = "https://files.pythonhosted.org/packages/b6/32/88ccdd63cb261e77b882e706108d072e4f1c839ed723bf91a3e1f216bf60/websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205", size = 163981 },
158
+ { url = "https://files.pythonhosted.org/packages/b3/7d/32cdb77990b3bdc34a306e0a0f73a1275221e9a66d869f6ff833c95b56ef/websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce", size = 164421 },
159
+ { url = "https://files.pythonhosted.org/packages/82/94/4f9b55099a4603ac53c2912e1f043d6c49d23e94dd82a9ce1eb554a90215/websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e", size = 163102 },
160
+ { url = "https://files.pythonhosted.org/packages/8e/b7/7484905215627909d9a79ae07070057afe477433fdacb59bf608ce86365a/websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad", size = 160766 },
161
+ { url = "https://files.pythonhosted.org/packages/a3/a4/edb62efc84adb61883c7d2c6ad65181cb087c64252138e12d655989eec05/websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03", size = 160998 },
162
+ { url = "https://files.pythonhosted.org/packages/f5/79/036d320dc894b96af14eac2529967a6fc8b74f03b83c487e7a0e9043d842/websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f", size = 170780 },
163
+ { url = "https://files.pythonhosted.org/packages/63/75/5737d21ee4dd7e4b9d487ee044af24a935e36a9ff1e1419d684feedcba71/websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5", size = 169717 },
164
+ { url = "https://files.pythonhosted.org/packages/2c/3c/bf9b2c396ed86a0b4a92ff4cdaee09753d3ee389be738e92b9bbd0330b64/websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a", size = 170155 },
165
+ { url = "https://files.pythonhosted.org/packages/75/2d/83a5aca7247a655b1da5eb0ee73413abd5c3a57fc8b92915805e6033359d/websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20", size = 170495 },
166
+ { url = "https://files.pythonhosted.org/packages/79/dd/699238a92761e2f943885e091486378813ac8f43e3c84990bc394c2be93e/websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2", size = 169880 },
167
+ { url = "https://files.pythonhosted.org/packages/c8/c9/67a8f08923cf55ce61aadda72089e3ed4353a95a3a4bc8bf42082810e580/websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307", size = 169856 },
168
+ { url = "https://files.pythonhosted.org/packages/17/b1/1ffdb2680c64e9c3921d99db460546194c40d4acbef999a18c37aa4d58a3/websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc", size = 163974 },
169
+ { url = "https://files.pythonhosted.org/packages/14/13/8b7fc4cb551b9cfd9890f0fd66e53c18a06240319915533b033a56a3d520/websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f", size = 164420 },
170
+ { url = "https://files.pythonhosted.org/packages/10/3d/91d3d2bb1325cd83e8e2c02d0262c7d4426dc8fa0831ef1aa4d6bf2041af/websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29", size = 160773 },
171
+ { url = "https://files.pythonhosted.org/packages/33/7c/cdedadfef7381939577858b1b5718a4ab073adbb584e429dd9d9dc9bfe16/websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c", size = 161007 },
172
+ { url = "https://files.pythonhosted.org/packages/ca/35/7a20a3c450b27c04e50fbbfc3dfb161ed8e827b2a26ae31c4b59b018b8c6/websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2", size = 162264 },
173
+ { url = "https://files.pythonhosted.org/packages/e8/9c/e3f9600564b0c813f2448375cf28b47dc42c514344faed3a05d71fb527f9/websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c", size = 161873 },
174
+ { url = "https://files.pythonhosted.org/packages/3f/37/260f189b16b2b8290d6ae80c9f96d8b34692cf1bb3475df54c38d3deb57d/websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a", size = 161818 },
175
+ { url = "https://files.pythonhosted.org/packages/ff/1e/e47dedac8bf7140e59aa6a679e850c4df9610ae844d71b6015263ddea37b/websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3", size = 164465 },
176
+ { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 },
177
+ ]
dsmq-1.0.0/post.md DELETED
@@ -1,244 +0,0 @@
1
- # dsmq
2
-
3
- I'd like to introduce [dsmq](https://github.com/brohrer/dsmq),
4
- the Dead Simple Message Queue, to the world.
5
-
6
- Part mail room, part bulletin board, dsmq is a central location for sharing messages
7
- between processes, even when they are running on different computers.
8
-
9
- Its defining characteristic is bare-bones simplicity.
10
-
11
- ![
12
- A screenshot of the dsmq GitHub repository:
13
- src/dsmq,
14
- .python-version,
15
- LICENSE,
16
- README.md,
17
- pyproject.toml,
18
- README,
19
- MIT license,
20
- Dead Simple Message Queue
21
- What it does
22
- Part mail room, part bulletin board, dsmq is a central location
23
- for sharing messages between processes, even when they are running
24
- on computers scattered around the world.
25
- Its defining characteristic is bare-bones simplicity.
26
- ](https://brandonrohrer.com/images/dsmq/dsmq_00.png)
27
-
28
- ## What dsmq does
29
-
30
- A message queue lets different processes talk to each other, within or across machines.
31
- Message queues are a waystation, a place to publish messages and hold them until
32
- they get picked up by the process that needs them.
33
-
34
- In dsmq, a program running the message queue starts up first (the server).
35
- It handles all the
36
- receiving, delivering, sorting, and storing of messages.
37
-
38
- Other programs (the clients) connect to the server. They add messages to a queue
39
- or read messages from a queue. Each queue is a separate topic.
40
-
41
- ## Why message queues?
42
- Message queues are invaluable for distributed systems of all sorts,
43
- but my favorite application is robotics.
44
- Robots typically have several (or many) processes doing different things at different
45
- speeds. Communication between processes is a fundamental part of any moderately
46
- complex automated system.
47
- When [ROS](https://www.ros.org) (the Robot Operating System) was
48
- released, one of the big gifts it gave to robot builders was a reliable way to
49
- pass messages.
50
-
51
- ## Why another message queue?
52
-
53
- There are lots of [message queues](https://en.wikipedia.org/wiki/Message_queue)
54
- in the world, and some are quite well known--Amazon SQS,
55
- RabbitMQ, Apache Kafka to name a few. It's fair to ask why this one was created.
56
-
57
- The official reason for dsmq's existence is that all the other available options
58
- are pretty heavy. Take RabbitMQ for instance, a popular open source message queue.
59
- It has hundreds of associated repositories and it's core
60
- [rabbitmq-server](https://github.com/rabbitmq/rabbitmq-server) repo has many thousands
61
- of lines of code. It's a heavy lift to import this to support a small robotics project,
62
- and code running on small edge devices may struggle to run it at all.
63
-
64
- RabbitMQ is also mature and optimized and dockerized and multi-platform
65
- and fully featured and a lot of other things a robot doesn't need. It would
66
- be out of balance to use it for a small project.
67
-
68
- ![https://brandonrohrer.com/images/dsmq/dsmq_01.png]("""
69
- Screenshot of the RabbitMQ-server GitHub repo showing that it
70
- is many times larger than dsmq""")
71
-
72
- dsmq is only about 200 lines of Python, including client and server code. It's *tiny*.
73
- Good for reading and understanding front-to-back when you're integrating it with
74
- your project.
75
-
76
- But the real reason is that I wanted to understand how a message queue might work
77
- and the best way I know to learn this is to build one.
78
-
79
- ## How to use dsmq
80
-
81
- ### Install it
82
-
83
- ```
84
- pip install dsmq
85
- ```
86
- or
87
- ```
88
- uv add dsmq
89
- ```
90
-
91
- ### Spin up a dsmq server
92
-
93
- ```python
94
- from dsmq import dsmq
95
- dsmq.start_server(host="127.0.0.1", port=12345)
96
- ```
97
-
98
- ### Connect a client to a dsmq server
99
-
100
- ```python
101
- mq = dsmq.connect_to_server(host="127.0.0.1", port=12345)
102
- ```
103
-
104
- ### Add a message to a topic queue
105
-
106
- ```python
107
- topic = "greetings"
108
- msg = "hello world!"
109
- mq.put(topic, msg)
110
- ```
111
-
112
- ### Read a message from a topic queue
113
-
114
- ```python
115
- topic = "greetings"
116
- msg = mq.get(topic)
117
- ```
118
-
119
- ### Run a demo
120
-
121
- 0. Open 3 separate terminal windows.
122
- 1. In the first, run `src/dsmq/dsmq.py`.
123
- 2. In the second, run `src/dsmq/example_put_client.py`.
124
- 3. In the third, run `src/dsmq/example_get_client.py`.
125
-
126
- Alternatively, if you're on Linux just run `src/dsmq/demo_linux.py`.
127
- (Linux has some process forking capabilities that Windows doesn't and
128
- that macOS forbids. It makes for a nice self-contained multiprocess demo.)
129
-
130
- ## How it works
131
-
132
- - Many clients can read messages from the same topic queue. dsmq uses a one-to-many
133
- publication model.
134
-
135
- - A client will get the oldest message available on a requested topic.
136
- Queues are first-in-first-out.
137
-
138
- - Clients will only be able to get messages that were added to a queue after the
139
- time they connected to the server. Any messages older that that won't be visible.
140
-
141
- - dsmq is asyncronous. There are no guarantees about how soon a message will arrive
142
- at its intended client.
143
-
144
- - dsmq is backed by an in-memory SQLite database. If your message volumes
145
- get larger than your RAM, you will reach an out-of-memory condition.
146
- ```python
147
- sqlite_conn = sqlite3.connect("file:mem1?mode=memory&cache=shared")
148
- ```
149
- - `put()` and `get()` operations are fairly quick--less than 100 $`\mu`$s of processing
150
- time plus any network latency--so it can comfortably handle requests at rates of
151
- hundreds of times per second. But if you have several clients reading and writing
152
- at 1 kHz or more, you may overload the queue.
153
-
154
- - A client will not be able to read any of the messages that were put into
155
- a queue before it connected.
156
-
157
- - Messages older than 600 seconds will be deleted from the queue.
158
- - In case of contention for the lock on the database, failed queries will be
159
- retried several times, with exponential backoff. None of this is visible to
160
- the clients, but it helps keep the internal operations reliable.
161
-
162
- ```python
163
- for i_retry in range(_n_retries):
164
- try:
165
- cursor.execute("""...
166
- ```
167
-
168
- - To keep things bare bones, the connections are all implemented in the
169
- socket layer, rather than WebSockets or http in the application layer.
170
- ```python
171
- self.dsmq_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
172
- self.dsmq_conn.connect((host, port))
173
- ```
174
- - ...however, working on this lower level meant dsmq had to reinvent the wheel of
175
- message headers. Each dsmq message is sent with a header that tells it exactly
176
- how many bytes to expect in it. The header format is simplistic--a message
177
- with one million plus the number of byes in the following message. Doing it this
178
- way means that the header is
179
- exactly 23 bytes long every time. (As long as the message is less than nine million
180
- bytes long.)
181
-
182
- - Every time a new client connects to the server, a new thread is created.
183
- That thread listens for any `get()` or `put()` requests the client might make
184
- and handles them immediately.
185
- ```python
186
- Thread(target=_handle_client_connection, args=(socket_conn,)).start()
187
- ```
188
-
189
- - dsmq retrieves the oldest eligible message from the queue. If a client wants
190
- to ensure it is getting the most recent message from the queue, it will need
191
- to iteratively get messages until there are no more left to be gotten.
192
- ```python
193
- msg_str = "<no response>"
194
- response = None
195
- while response != "":
196
- msg_str = response
197
- response = self.mq.get("<topic>")
198
- ```
199
-
200
- - dsmq messages are text fields, but Python dictionaries are a very convenient and
201
- common format for passing structured messages. Use the `json` library to convert
202
- from dictionaries to strings and back.
203
- ```python
204
- topic = "update"
205
- msg_dict = {"timestep": 374, "value": 3.897}
206
- msg_str = json.dumps(msg_dict)
207
- dsmq.put(topic, msg_str)
208
-
209
- msg_str = dsmq.get(topic)
210
- msg_dict = json.loads(msg_str)
211
- ```
212
-
213
- - dsmq is opinionated. Parameters for controlling behavior are set to reasonable
214
- defaults and not exposed to the user. The additional complexity in the API is
215
- not worth the value of making them user-controlled.
216
- However they are also clearly labeled and very easy to find. If anyone cares enough to
217
- play with them, they are strongly encouraged to fork dsmq and make it their own.
218
-
219
- ```python
220
- _message_length_offset = 1_000_000
221
- _header_length = 23
222
- _n_retries = 5
223
- _first_retry = 0.01 # seconds
224
- _time_to_live = 600.0 # seconds
225
- ```
226
-
227
- - dsmq is deliberately built to have as few dependencies as it can get away with.
228
- As of this writing, it doesn't depend on any third party packages and just a handful
229
- of core packages: `json`, `socket`, `sqlite3`, `sys`, and `threading`.
230
-
231
- # Dead Simple
232
-
233
- Dead simple is an aesthetic.
234
- It says that the ideal is achieved not when nothing more can be added,
235
- but when nothing more can be taken away.
236
- It is the aims to follow the apocryphal advice of Albert Einstein, to make
237
- a thing as simple as possible, but no simpler.
238
-
239
- Dead simple is like keystroke golfing, but instead of minimizing the number of
240
- characters, it minimizes the number of concepts a developer or a user
241
- has to hold in their head.
242
-
243
- I've tried to embody it in dsmq.
244
- dsmq has fewer lines of code than RabbitMQ has *repositories*. And that tickles me.
@@ -1,27 +0,0 @@
1
- import json
2
- from websockets.sync.client import connect as ws_connect
3
-
4
- _default_host = "127.0.0.1"
5
- _default_port = 30008
6
-
7
-
8
- def connect(host=_default_host, port=_default_port):
9
- return DSMQClientSideConnection(host, port)
10
-
11
-
12
- class DSMQClientSideConnection:
13
- def __init__(self, host, port):
14
- self.uri = f"ws://{host}:{port}"
15
- self.websocket = ws_connect(self.uri)
16
-
17
- def get(self, topic):
18
- msg = {"action": "get", "topic": topic}
19
- self.websocket.send(json.dumps(msg))
20
-
21
- msg_text = self.websocket.recv()
22
- msg = json.loads(msg_text)
23
- return msg["message"]
24
-
25
- def put(self, topic, msg_body):
26
- msg_dict = {"action": "put", "topic": topic, "message": msg_body}
27
- self.websocket.send(json.dumps(msg_dict))
dsmq-1.0.0/uv.lock DELETED
@@ -1,72 +0,0 @@
1
- version = 1
2
- requires-python = ">=3.10"
3
-
4
- [[package]]
5
- name = "dsmq"
6
- version = "1.0.0"
7
- source = { editable = "." }
8
- dependencies = [
9
- { name = "websockets" },
10
- ]
11
-
12
- [package.metadata]
13
- requires-dist = [{ name = "websockets", specifier = ">=14.1" }]
14
-
15
- [[package]]
16
- name = "websockets"
17
- version = "14.1"
18
- source = { registry = "https://pypi.org/simple" }
19
- sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 }
20
- wheels = [
21
- { url = "https://files.pythonhosted.org/packages/af/91/b1b375dbd856fd5fff3f117de0e520542343ecaf4e8fc60f1ac1e9f5822c/websockets-14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29", size = 161950 },
22
- { url = "https://files.pythonhosted.org/packages/61/8f/4d52f272d3ebcd35e1325c646e98936099a348374d4a6b83b524bded8116/websockets-14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179", size = 159601 },
23
- { url = "https://files.pythonhosted.org/packages/c4/b1/29e87b53eb1937992cdee094a0988aadc94f25cf0b37e90c75eed7123d75/websockets-14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250", size = 159854 },
24
- { url = "https://files.pythonhosted.org/packages/3f/e6/752a2f5e8321ae2a613062676c08ff2fccfb37dc837a2ee919178a372e8a/websockets-14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0", size = 168835 },
25
- { url = "https://files.pythonhosted.org/packages/60/27/ca62de7877596926321b99071639275e94bb2401397130b7cf33dbf2106a/websockets-14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0", size = 167844 },
26
- { url = "https://files.pythonhosted.org/packages/7e/db/f556a1d06635c680ef376be626c632e3f2bbdb1a0189d1d1bffb061c3b70/websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199", size = 168157 },
27
- { url = "https://files.pythonhosted.org/packages/b3/bc/99e5f511838c365ac6ecae19674eb5e94201aa4235bd1af3e6fa92c12905/websockets-14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58", size = 168561 },
28
- { url = "https://files.pythonhosted.org/packages/c6/e7/251491585bad61c79e525ac60927d96e4e17b18447cc9c3cfab47b2eb1b8/websockets-14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078", size = 167979 },
29
- { url = "https://files.pythonhosted.org/packages/ac/98/7ac2e4eeada19bdbc7a3a66a58e3ebdf33648b9e1c5b3f08c3224df168cf/websockets-14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434", size = 167925 },
30
- { url = "https://files.pythonhosted.org/packages/ab/3d/09e65c47ee2396b7482968068f6e9b516221e1032b12dcf843b9412a5dfb/websockets-14.1-cp310-cp310-win32.whl", hash = "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10", size = 162831 },
31
- { url = "https://files.pythonhosted.org/packages/8a/67/59828a3d09740e6a485acccfbb66600632f2178b6ed1b61388ee96f17d5a/websockets-14.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e", size = 163266 },
32
- { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 },
33
- { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 },
34
- { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 },
35
- { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 },
36
- { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 },
37
- { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 },
38
- { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 },
39
- { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 },
40
- { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 },
41
- { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 },
42
- { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 },
43
- { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 },
44
- { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 },
45
- { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 },
46
- { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 },
47
- { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 },
48
- { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 },
49
- { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 },
50
- { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 },
51
- { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 },
52
- { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 },
53
- { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 },
54
- { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 },
55
- { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 },
56
- { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 },
57
- { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 },
58
- { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 },
59
- { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 },
60
- { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 },
61
- { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 },
62
- { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 },
63
- { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 },
64
- { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 },
65
- { url = "https://files.pythonhosted.org/packages/fb/cd/382a05a1ba2a93bd9fb807716a660751295df72e77204fb130a102fcdd36/websockets-14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8", size = 159633 },
66
- { url = "https://files.pythonhosted.org/packages/b7/a0/fa7c62e2952ef028b422fbf420f9353d9dd4dfaa425de3deae36e98c0784/websockets-14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e", size = 159867 },
67
- { url = "https://files.pythonhosted.org/packages/c1/94/954b4924f868db31d5f0935893c7a8446515ee4b36bb8ad75a929469e453/websockets-14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098", size = 161121 },
68
- { url = "https://files.pythonhosted.org/packages/7a/2e/f12bbb41a8f2abb76428ba4fdcd9e67b5b364a3e7fa97c88f4d6950aa2d4/websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb", size = 160731 },
69
- { url = "https://files.pythonhosted.org/packages/13/97/b76979401f2373af1fe3e08f960b265cecab112e7dac803446fb98351a52/websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7", size = 160681 },
70
- { url = "https://files.pythonhosted.org/packages/39/9c/16916d9a436c109a1d7ba78817e8fee357b78968be3f6e6f517f43afa43d/websockets-14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d", size = 163316 },
71
- { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 },
72
- ]
File without changes
File without changes
File without changes
File without changes