dsmq 0.6.0__tar.gz → 1.0.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.
- {dsmq-0.6.0 → dsmq-1.0.0}/PKG-INFO +15 -11
- {dsmq-0.6.0 → dsmq-1.0.0}/README.md +11 -9
- dsmq-1.0.0/post.md +244 -0
- {dsmq-0.6.0 → dsmq-1.0.0}/pyproject.toml +2 -1
- dsmq-1.0.0/src/dsmq/client.py +27 -0
- {dsmq-0.6.0 → dsmq-1.0.0}/src/dsmq/demo_linux.py +6 -6
- {dsmq-0.6.0 → dsmq-1.0.0}/src/dsmq/example_get_client.py +2 -2
- {dsmq-0.6.0 → dsmq-1.0.0}/src/dsmq/example_put_client.py +2 -2
- dsmq-0.6.0/src/dsmq/dsmq.py → dsmq-1.0.0/src/dsmq/server.py +27 -83
- dsmq-1.0.0/uv.lock +72 -0
- dsmq-0.6.0/src/dsmq/example_server.py +0 -3
- dsmq-0.6.0/uv.lock +0 -7
- {dsmq-0.6.0 → dsmq-1.0.0}/.gitignore +0 -0
- {dsmq-0.6.0 → dsmq-1.0.0}/.python-version +0 -0
- {dsmq-0.6.0 → dsmq-1.0.0}/LICENSE +0 -0
- {dsmq-0.6.0 → dsmq-1.0.0}/src/dsmq/__init__.py +0 -0
@@ -1,8 +1,10 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: dsmq
|
3
|
-
Version: 0.
|
3
|
+
Version: 1.0.0
|
4
4
|
Summary: A dead simple message queue
|
5
|
+
License-File: LICENSE
|
5
6
|
Requires-Python: >=3.10
|
7
|
+
Requires-Dist: websockets>=14.1
|
6
8
|
Description-Content-Type: text/markdown
|
7
9
|
|
8
10
|
# Dead Simple Message Queue
|
@@ -26,9 +28,9 @@ pip install dsmq
|
|
26
28
|
As in `src/dsmq/example_server.py`
|
27
29
|
|
28
30
|
```python
|
29
|
-
from dsmq import
|
31
|
+
from dsmq.server import serve
|
30
32
|
|
31
|
-
|
33
|
+
serve(host="127.0.0.1", port=30008)
|
32
34
|
```
|
33
35
|
|
34
36
|
### Connect a client to a dsmq server
|
@@ -36,7 +38,9 @@ dsmq.start_server(host="127.0.0.1", port=30008)
|
|
36
38
|
As in `src/dsmq/example_put_client.py`
|
37
39
|
|
38
40
|
```python
|
39
|
-
|
41
|
+
from dsmq.client import connect
|
42
|
+
|
43
|
+
mq = connect(host="127.0.0.1", port=12345)
|
40
44
|
```
|
41
45
|
### Add a message to a queue
|
42
46
|
|
@@ -66,7 +70,7 @@ managed
|
|
66
70
|
```python
|
67
71
|
import multiprocessing as mp
|
68
72
|
|
69
|
-
p_mq = mp.Process(target=
|
73
|
+
p_mq = mp.Process(target=serve, args=(config.MQ_HOST, config.MQ_PORT))
|
70
74
|
p_mq.start()
|
71
75
|
|
72
76
|
p_mq.join()
|
@@ -78,7 +82,7 @@ p_mq.close()
|
|
78
82
|
### Demo
|
79
83
|
|
80
84
|
1. Open 3 separate terminal windows.
|
81
|
-
1. In the first, run `src/dsmq/
|
85
|
+
1. In the first, run `src/dsmq/server.py` as a script.
|
82
86
|
1. In the second, run `src/dsmq/example_put_client.py`.
|
83
87
|
1. In the third, run `src/dsmq/example_get_client.py`.
|
84
88
|
|
@@ -109,9 +113,9 @@ get larger than your RAM, you will reach an out-of-memory condition.
|
|
109
113
|
|
110
114
|
|
111
115
|
# API Reference
|
112
|
-
[[source](https://github.com/brohrer/dsmq/blob/main/src/dsmq/
|
116
|
+
[[source](https://github.com/brohrer/dsmq/blob/main/src/dsmq/serve.py)]
|
113
117
|
|
114
|
-
### `
|
118
|
+
### `serve(host="127.0.0.1", port=30008)`
|
115
119
|
|
116
120
|
Kicks off the mesage queue server. This process will be the central exchange
|
117
121
|
for all incoming and outgoing messages.
|
@@ -119,9 +123,9 @@ for all incoming and outgoing messages.
|
|
119
123
|
- `port` (int), port. These will be used by all clients.
|
120
124
|
Non-privileged ports are numbered 1024 and higher.
|
121
125
|
|
122
|
-
### `
|
126
|
+
### `connect(host="127.0.0.1", port=30008)`
|
123
127
|
|
124
|
-
Connects to an existing message queue server.
|
128
|
+
Connects a client to an existing message queue server.
|
125
129
|
- `host` (str), IP address of the *server*.
|
126
130
|
- `port` (int), port on which the server is listening.
|
127
131
|
- returns a `DSMQClientSideConnection` object.
|
@@ -19,9 +19,9 @@ pip install dsmq
|
|
19
19
|
As in `src/dsmq/example_server.py`
|
20
20
|
|
21
21
|
```python
|
22
|
-
from dsmq import
|
22
|
+
from dsmq.server import serve
|
23
23
|
|
24
|
-
|
24
|
+
serve(host="127.0.0.1", port=30008)
|
25
25
|
```
|
26
26
|
|
27
27
|
### Connect a client to a dsmq server
|
@@ -29,7 +29,9 @@ dsmq.start_server(host="127.0.0.1", port=30008)
|
|
29
29
|
As in `src/dsmq/example_put_client.py`
|
30
30
|
|
31
31
|
```python
|
32
|
-
|
32
|
+
from dsmq.client import connect
|
33
|
+
|
34
|
+
mq = connect(host="127.0.0.1", port=12345)
|
33
35
|
```
|
34
36
|
### Add a message to a queue
|
35
37
|
|
@@ -59,7 +61,7 @@ managed
|
|
59
61
|
```python
|
60
62
|
import multiprocessing as mp
|
61
63
|
|
62
|
-
p_mq = mp.Process(target=
|
64
|
+
p_mq = mp.Process(target=serve, args=(config.MQ_HOST, config.MQ_PORT))
|
63
65
|
p_mq.start()
|
64
66
|
|
65
67
|
p_mq.join()
|
@@ -71,7 +73,7 @@ p_mq.close()
|
|
71
73
|
### Demo
|
72
74
|
|
73
75
|
1. Open 3 separate terminal windows.
|
74
|
-
1. In the first, run `src/dsmq/
|
76
|
+
1. In the first, run `src/dsmq/server.py` as a script.
|
75
77
|
1. In the second, run `src/dsmq/example_put_client.py`.
|
76
78
|
1. In the third, run `src/dsmq/example_get_client.py`.
|
77
79
|
|
@@ -102,9 +104,9 @@ get larger than your RAM, you will reach an out-of-memory condition.
|
|
102
104
|
|
103
105
|
|
104
106
|
# API Reference
|
105
|
-
[[source](https://github.com/brohrer/dsmq/blob/main/src/dsmq/
|
107
|
+
[[source](https://github.com/brohrer/dsmq/blob/main/src/dsmq/serve.py)]
|
106
108
|
|
107
|
-
### `
|
109
|
+
### `serve(host="127.0.0.1", port=30008)`
|
108
110
|
|
109
111
|
Kicks off the mesage queue server. This process will be the central exchange
|
110
112
|
for all incoming and outgoing messages.
|
@@ -112,9 +114,9 @@ for all incoming and outgoing messages.
|
|
112
114
|
- `port` (int), port. These will be used by all clients.
|
113
115
|
Non-privileged ports are numbered 1024 and higher.
|
114
116
|
|
115
|
-
### `
|
117
|
+
### `connect(host="127.0.0.1", port=30008)`
|
116
118
|
|
117
|
-
Connects to an existing message queue server.
|
119
|
+
Connects a client to an existing message queue server.
|
118
120
|
- `host` (str), IP address of the *server*.
|
119
121
|
- `port` (int), port on which the server is listening.
|
120
122
|
- returns a `DSMQClientSideConnection` object.
|
dsmq-1.0.0/post.md
ADDED
@@ -0,0 +1,244 @@
|
|
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
|
+

|
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
|
+

|
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.
|
@@ -0,0 +1,27 @@
|
|
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))
|
@@ -1,8 +1,8 @@
|
|
1
1
|
# Heads up: This script only works on Linux because of use of "fork" method.
|
2
2
|
import multiprocessing as mp
|
3
|
-
import
|
4
|
-
import example_get_client
|
5
|
-
import example_put_client
|
3
|
+
from dsmq.server import serve
|
4
|
+
import dsmq.example_get_client
|
5
|
+
import dsmq.example_put_client
|
6
6
|
|
7
7
|
mp.set_start_method("fork")
|
8
8
|
|
@@ -11,11 +11,11 @@ PORT = 25252
|
|
11
11
|
|
12
12
|
|
13
13
|
def test_server_with_clients():
|
14
|
-
p_server = mp.Process(target=
|
14
|
+
p_server = mp.Process(target=serve, args=(HOST, PORT))
|
15
15
|
p_server.start()
|
16
16
|
|
17
|
-
p_putter = mp.Process(target=example_put_client.run, args=(HOST, PORT, 20))
|
18
|
-
p_getter = mp.Process(target=example_get_client.run, args=(HOST, PORT, 20))
|
17
|
+
p_putter = mp.Process(target=dsmq.example_put_client.run, args=(HOST, PORT, 20))
|
18
|
+
p_getter = mp.Process(target=dsmq.example_get_client.run, args=(HOST, PORT, 20))
|
19
19
|
|
20
20
|
p_putter.start()
|
21
21
|
p_getter.start()
|
@@ -1,80 +1,36 @@
|
|
1
1
|
import json
|
2
|
-
import socket
|
3
2
|
import sqlite3
|
4
3
|
import sys
|
5
|
-
from threading import Thread
|
6
4
|
import time
|
5
|
+
from websockets.sync.server import serve as ws_serve
|
7
6
|
|
8
|
-
|
9
|
-
|
7
|
+
_default_host = "127.0.0.1"
|
8
|
+
_default_port = 30008
|
9
|
+
_n_retries = 5
|
10
|
+
_first_retry = 0.01 # seconds
|
11
|
+
_time_to_live = 600.0 # seconds
|
10
12
|
|
11
|
-
N_RETRIES = 5
|
12
|
-
FIRST_RETRY = 0.01 # seconds
|
13
|
-
TIME_TO_LIVE = 600.0 # seconds
|
14
13
|
|
15
|
-
|
16
|
-
def start_server(host=DEFAULT_HOST, port=DEFAULT_PORT):
|
14
|
+
def serve(host=_default_host, port=_default_port):
|
17
15
|
"""
|
18
16
|
For best results, start this running in its own process and walk away.
|
19
17
|
"""
|
20
18
|
sqlite_conn = sqlite3.connect("file:mem1?mode=memory&cache=shared")
|
21
19
|
cursor = sqlite_conn.cursor()
|
22
|
-
|
23
20
|
cursor.execute("""
|
24
|
-
|
21
|
+
CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
|
25
22
|
""")
|
26
23
|
|
27
|
-
with
|
28
|
-
|
29
|
-
# even if it's already in use.
|
30
|
-
# This is helpful in recovering from crashes where things didn't
|
31
|
-
# shut down properly.
|
32
|
-
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
33
|
-
|
34
|
-
s.bind((host, port))
|
35
|
-
s.listen()
|
36
|
-
|
24
|
+
with ws_serve(request_handler, host, port) as server:
|
25
|
+
server.serve_forever()
|
37
26
|
print()
|
38
27
|
print(f"Server started at {host} on port {port}.")
|
39
28
|
print("Waiting for clients...")
|
40
29
|
|
41
|
-
while True:
|
42
|
-
socket_conn, addr = s.accept()
|
43
|
-
print(f"Connected by {addr}")
|
44
|
-
Thread(target=_handle_client_connection, args=(socket_conn,)).start()
|
45
|
-
|
46
30
|
sqlite_conn.close()
|
47
31
|
|
48
32
|
|
49
|
-
def
|
50
|
-
return DSMQClientSideConnection(host, port)
|
51
|
-
|
52
|
-
|
53
|
-
class DSMQClientSideConnection:
|
54
|
-
def __init__(self, host, port):
|
55
|
-
self.dsmq_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
56
|
-
self.dsmq_conn.connect((host, port))
|
57
|
-
|
58
|
-
def get(self, topic):
|
59
|
-
msg = json.dumps({"action": "get", "topic": topic})
|
60
|
-
self.dsmq_conn.sendall(bytes(msg, "utf-8"))
|
61
|
-
|
62
|
-
data = self.dsmq_conn.recv(1024)
|
63
|
-
if not data:
|
64
|
-
raise RuntimeError("Connection terminated by server")
|
65
|
-
msg_str = data.decode("utf-8")
|
66
|
-
msg = json.loads(msg_str)
|
67
|
-
try:
|
68
|
-
return msg["message"]
|
69
|
-
except KeyError:
|
70
|
-
return ""
|
71
|
-
|
72
|
-
def put(self, topic, msg):
|
73
|
-
msg = json.dumps({"action": "put", "topic": topic, "message": msg})
|
74
|
-
self.dsmq_conn.sendall(bytes(msg, "utf-8"))
|
75
|
-
|
76
|
-
|
77
|
-
def _handle_client_connection(socket_conn):
|
33
|
+
def request_handler(websocket):
|
78
34
|
sqlite_conn = sqlite3.connect("file:mem1?mode=memory&cache=shared")
|
79
35
|
cursor = sqlite_conn.cursor()
|
80
36
|
|
@@ -82,20 +38,8 @@ def _handle_client_connection(socket_conn):
|
|
82
38
|
last_read_times = {}
|
83
39
|
time_of_last_purge = time.time()
|
84
40
|
|
85
|
-
|
86
|
-
|
87
|
-
# Check whether the connection has been terminated
|
88
|
-
if not data:
|
89
|
-
break
|
90
|
-
|
91
|
-
msg_str = data.decode("utf-8")
|
92
|
-
try:
|
93
|
-
msg = json.loads(msg_str)
|
94
|
-
except json.decoder.JSONDecodeError:
|
95
|
-
print("Message must be json-friendly")
|
96
|
-
print(f" Received: {msg}")
|
97
|
-
continue
|
98
|
-
|
41
|
+
for msg_text in websocket:
|
42
|
+
msg = json.loads(msg_text)
|
99
43
|
topic = msg["topic"]
|
100
44
|
timestamp = time.time()
|
101
45
|
|
@@ -104,7 +48,7 @@ def _handle_client_connection(socket_conn):
|
|
104
48
|
|
105
49
|
# This block allows for multiple retries if the database
|
106
50
|
# is busy.
|
107
|
-
for i_retry in range(
|
51
|
+
for i_retry in range(_n_retries):
|
108
52
|
try:
|
109
53
|
cursor.execute(
|
110
54
|
"""
|
@@ -115,7 +59,7 @@ VALUES (:timestamp, :topic, :message)
|
|
115
59
|
)
|
116
60
|
sqlite_conn.commit()
|
117
61
|
except sqlite3.OperationalError:
|
118
|
-
wait_time =
|
62
|
+
wait_time = _first_retry * 2**i_retry
|
119
63
|
time.sleep(wait_time)
|
120
64
|
continue
|
121
65
|
break
|
@@ -130,7 +74,7 @@ VALUES (:timestamp, :topic, :message)
|
|
130
74
|
|
131
75
|
# This block allows for multiple retries if the database
|
132
76
|
# is busy.
|
133
|
-
for i_retry in range(
|
77
|
+
for i_retry in range(_n_retries):
|
134
78
|
try:
|
135
79
|
cursor.execute(
|
136
80
|
"""
|
@@ -149,7 +93,7 @@ AND timestamp = a.min_time
|
|
149
93
|
msg,
|
150
94
|
)
|
151
95
|
except sqlite3.OperationalError:
|
152
|
-
wait_time =
|
96
|
+
wait_time = _first_retry * 2**i_retry
|
153
97
|
time.sleep(wait_time)
|
154
98
|
continue
|
155
99
|
break
|
@@ -163,15 +107,15 @@ AND timestamp = a.min_time
|
|
163
107
|
# Handle the case where no results are returned
|
164
108
|
message = ""
|
165
109
|
|
166
|
-
|
167
|
-
socket_conn.sendall(bytes(msg, "utf-8"))
|
110
|
+
websocket.send(json.dumps({"message": message}))
|
168
111
|
else:
|
169
112
|
print("Action must either be 'put' or 'get'")
|
170
113
|
|
114
|
+
|
171
115
|
# Periodically clean out messages from the queue that are
|
172
116
|
# past their sell buy date.
|
173
117
|
# This operation is pretty fast. I clock it at 12 us on my machine.
|
174
|
-
if time.time() - time_of_last_purge >
|
118
|
+
if time.time() - time_of_last_purge > _time_to_live:
|
175
119
|
cursor.execute(
|
176
120
|
"""
|
177
121
|
DELETE FROM messages
|
@@ -189,21 +133,21 @@ if __name__ == "__main__":
|
|
189
133
|
if len(sys.argv) == 3:
|
190
134
|
host = sys.argv[1]
|
191
135
|
port = int(sys.argv[2])
|
192
|
-
|
136
|
+
serve(host=host, port=port)
|
193
137
|
elif len(sys.argv) == 2:
|
194
138
|
host = sys.argv[1]
|
195
|
-
|
139
|
+
serve(host=host)
|
196
140
|
elif len(sys.argv) == 1:
|
197
|
-
|
141
|
+
serve()
|
198
142
|
else:
|
199
143
|
print(
|
200
144
|
"""
|
201
145
|
Try one of these:
|
202
|
-
$ python3
|
146
|
+
$ python3 server.py
|
203
147
|
|
204
|
-
$ python3
|
148
|
+
$ python3 server.py 127.0.0.1
|
205
149
|
|
206
|
-
$ python3
|
150
|
+
$ python3 server.py 127.0.0.1 25853
|
207
151
|
|
208
|
-
|
152
|
+
"""
|
209
153
|
)
|
dsmq-1.0.0/uv.lock
ADDED
@@ -0,0 +1,72 @@
|
|
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
|
+
]
|
dsmq-0.6.0/uv.lock
DELETED
File without changes
|
File without changes
|
File without changes
|
File without changes
|