dsmq 0.2.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from dsmq!"
dsmq/demo_linux.py ADDED
@@ -0,0 +1,25 @@
1
+ # Heads up: This script only works on Linux because of use of "fork" method.
2
+ import multiprocessing as mp
3
+ import dsmq
4
+ import example_get_client
5
+ import example_put_client
6
+
7
+ mp.set_start_method("fork")
8
+
9
+ HOST = "127.0.0.1"
10
+ PORT = 25252
11
+
12
+
13
+ def test_server_with_clients():
14
+ p_server = mp.Process(target=dsmq.run, args=(HOST, PORT))
15
+ p_server.start()
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))
19
+
20
+ p_putter.start()
21
+ p_getter.start()
22
+
23
+
24
+ if __name__ == "__main__":
25
+ test_server_with_clients()
dsmq/dsmq.py ADDED
@@ -0,0 +1,160 @@
1
+ import json
2
+ import socket
3
+ import sqlite3
4
+ import sys
5
+ from threading import Thread
6
+ import time
7
+
8
+ N_RETRIES = 5
9
+ FIRST_RETRY = 0.01 # seconds
10
+
11
+
12
+ def run(host="127.0.0.1", port=30008):
13
+ sqlite_conn = sqlite3.connect("file:mem1?mode=memory&cache=shared")
14
+ cursor = sqlite_conn.cursor()
15
+
16
+ cursor.execute("""
17
+ CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
18
+ """)
19
+
20
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
21
+ # Setting this socket option to re-use the address,
22
+ # even if it's already in use.
23
+ # This is helpful in recovering from crashes where things didn't
24
+ # shut down properly.
25
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
26
+
27
+ s.bind((host, port))
28
+ s.listen()
29
+
30
+ print()
31
+ print(f"Server started at {host} on port {port}.")
32
+ print("Waiting for clients...")
33
+
34
+ while True:
35
+ socket_conn, addr = s.accept()
36
+ print(f"Connected by {addr}")
37
+ Thread(target=handle_socket, args=(socket_conn,)).start()
38
+
39
+ sqlite_conn.close()
40
+
41
+
42
+ def handle_socket(socket_conn):
43
+ sqlite_conn = sqlite3.connect("file:mem1?mode=memory&cache=shared")
44
+ cursor = sqlite_conn.cursor()
45
+
46
+ creation_time = time.time()
47
+ last_read = {}
48
+
49
+ while True:
50
+ data = socket_conn.recv(1024)
51
+ # Check whether the connection has been terminated
52
+ if not data:
53
+ break
54
+
55
+ msg_str = data.decode("utf-8")
56
+ try:
57
+ # print("dsmq received ", msg_str)
58
+ msg = json.loads(msg_str)
59
+ except json.decoder.JSONDecodeError:
60
+ print("Message must be json-friendly")
61
+ print(f" Received: {msg}")
62
+ continue
63
+
64
+ topic = msg["topic"]
65
+ timestamp = time.time()
66
+
67
+ if msg["action"] == "put":
68
+ msg["timestamp"] = timestamp
69
+
70
+ # This block allows for multiple retries if the database
71
+ # is busy.
72
+ for i_retry in range(N_RETRIES):
73
+ try:
74
+ cursor.execute(
75
+ """
76
+ INSERT INTO messages (timestamp, topic, message)
77
+ VALUES (:timestamp, :topic, :message)
78
+ """,
79
+ (msg),
80
+ )
81
+ sqlite_conn.commit()
82
+ except sqlite3.OperationalError:
83
+ wait_time = FIRST_RETRY * 2**i_retry
84
+ time.sleep(wait_time)
85
+ continue
86
+ break
87
+
88
+ elif msg["action"] == "get":
89
+ try:
90
+ last_read_time = last_read[topic]
91
+ except KeyError:
92
+ last_read[topic] = creation_time
93
+ last_read_time = last_read[topic]
94
+ msg["last_read_time"] = last_read_time
95
+
96
+ # This block allows for multiple retries if the database
97
+ # is busy.
98
+ for i_retry in range(N_RETRIES):
99
+ try:
100
+ cursor.execute(
101
+ """
102
+ SELECT message,
103
+ timestamp
104
+ FROM messages,
105
+ (
106
+ SELECT MIN(timestamp) AS min_time
107
+ FROM messages
108
+ WHERE topic = :topic
109
+ AND timestamp > :last_read_time
110
+ ) a
111
+ WHERE topic = :topic
112
+ AND timestamp = a.min_time
113
+ """,
114
+ msg,
115
+ )
116
+ except sqlite3.OperationalError:
117
+ wait_time = FIRST_RETRY * 2**i_retry
118
+ time.sleep(wait_time)
119
+ continue
120
+ break
121
+
122
+ try:
123
+ result = cursor.fetchall()[0]
124
+ message = result[0]
125
+ timestamp = result[1]
126
+ last_read[topic] = timestamp
127
+ except IndexError:
128
+ # Handle the case where no results are returned
129
+ message = ""
130
+
131
+ msg = json.dumps({"message": message})
132
+ socket_conn.sendall(bytes(msg, "utf-8"))
133
+ else:
134
+ print("Action must either be 'put' or 'get'")
135
+
136
+ sqlite_conn.close()
137
+
138
+
139
+ if __name__ == "__main__":
140
+ if len(sys.argv) == 3:
141
+ host = sys.argv[1]
142
+ port = int(sys.argv[2])
143
+ run(host=host, port=port)
144
+ elif len(sys.argv) == 2:
145
+ host = sys.argv[1]
146
+ run(host=host)
147
+ elif len(sys.argv) == 1:
148
+ run()
149
+ else:
150
+ print(
151
+ """
152
+ Try one of these:
153
+ $ python3 dsmq.py
154
+
155
+ $ python3 dsmq.py 127.0.0.1
156
+
157
+ $ python3 dsmq.py 127.0.0.1 25853
158
+
159
+ """
160
+ )
@@ -0,0 +1,22 @@
1
+ import json
2
+ import socket
3
+ import time
4
+
5
+
6
+ def run(host="127.0.0.1", port=30008, n_iter=1000):
7
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
8
+ s.connect((host, port))
9
+
10
+ for i in range(n_iter):
11
+ time.sleep(1)
12
+ msg = json.dumps({"action": "get", "topic": "greetings"})
13
+ s.sendall(bytes(msg, "utf-8"))
14
+ data = s.recv(1024)
15
+ if not data:
16
+ raise RuntimeError("Connection terminated by server")
17
+ msg_str = data.decode("utf-8")
18
+ print(f"client received {json.loads(msg_str)}")
19
+
20
+
21
+ if __name__ == "__main__":
22
+ run()
@@ -0,0 +1,19 @@
1
+ import json
2
+ import socket
3
+ import time
4
+
5
+
6
+ def run(host="127.0.0.1", port=30008, n_iter=1000):
7
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
8
+ s.connect((host, port))
9
+
10
+ for i in range(n_iter):
11
+ time.sleep(1)
12
+ note = f"{i}. Hello, world"
13
+ msg = json.dumps({"action": "put", "topic": "greetings", "message": note})
14
+ s.sendall(bytes(msg, "utf-8"))
15
+ print(f"client sent {msg}")
16
+
17
+
18
+ if __name__ == "__main__":
19
+ run()
dsmq/example_server.py ADDED
@@ -0,0 +1,3 @@
1
+ from dsmq import dsmq
2
+
3
+ dsmq.run(host="127.0.0.1", port=12345)
dsmq/py.typed ADDED
File without changes
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.3
2
+ Name: dsmq
3
+ Version: 0.2.0
4
+ Summary: A dead simple message queue
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+
9
+ # Dead Simple Message Queue
10
+
11
+ ## What it does
12
+
13
+ Part mail room, part bulletin board, dsmq is a central location for sharing messages
14
+ between processes, even when they are running on computers scattered around the world.
15
+
16
+ Its defining characteristic is its bare-bones simplicity.
17
+
18
+ ## How to use it
19
+
20
+ ### Install
21
+
22
+ ```bash
23
+ pip install dsmq
24
+ ```
25
+ ### Create a dsmq server
26
+
27
+ As in `src/dsmq/example_server.py`
28
+
29
+ ```python
30
+ from dsmq import dsmq
31
+
32
+ dsmq.run(host="127.0.0.1", port=12345)
33
+ ```
34
+
35
+ ### Add a message to a queue
36
+
37
+ As in `src/dsmq/example_put_client.py`
38
+
39
+ ```python
40
+ import json
41
+ import socket
42
+
43
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
44
+ s.connect(("127.0.0.1", 12345))
45
+ message_content = {"action": "put", "topic": "greetings", "message": "Hello!"}
46
+ msg = json.dumps(message_content)
47
+ s.sendall(bytes(msg, "utf-8"))
48
+ ```
49
+
50
+ ### Read a message from a queue
51
+
52
+ As in `src/dsmq/example_get_client.py`
53
+
54
+ ```python
55
+ import json
56
+ import socket
57
+
58
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
59
+ s.connect(("127.0.0.1", 12345))
60
+
61
+ for i in range(n_iter):
62
+ request_message_content = {"action": "get", "topic": "greetings"}
63
+ request_msg = json.dumps(request_message_content)
64
+ s.sendall(bytes(reequest_msg, "utf-8"))
65
+
66
+ reply_msg = s.recv(1024)
67
+ if not reply_msg:
68
+ raise RuntimeError("Connection terminated by server")
69
+ reply_msg_content = reply_msg.decode("utf-8")
70
+ ```
71
+
72
+ ### Demo
73
+
74
+ 1. Open 3 separate terminal windows.
75
+ 1. In the first, run `src/dsmq/dsmq.py`.
76
+ 1. In the second, run `src/dsmq/example_put_client.py`.
77
+ 1. In the third, run `src/dsmq/example_get_client.py`.
78
+
79
+
80
+ ## How it works
81
+
82
+ ### Expected behavior and limitations
83
+
84
+ - Many clients can read messages of the same topic. It is a one-to-many
85
+ pulication model.
86
+
87
+ - A client will not be able to read any of the messages that were put into
88
+ a queue before it connected.
89
+
90
+ - A client will get the oldest message available on a requested topic.
91
+ Queues are first-in-first-out.
92
+
93
+ - Put and get operations are fairly quick--less than 100 $`\mu`$s of processing
94
+ time plus any network latency--so it can comfortably handle operations at
95
+ hundreds of Hz. But if you try to have several clients reading and writing
96
+ at 1 kHz or more, you may overload the queue.
97
+
98
+ - The queue is backed by an in-memory SQLite database. If your message volumes
99
+ get larger than your RAM, you may reach an out-of-memory condition.
100
+
101
+
102
+ # API Reference and Cookbook
103
+ [[source](https://github.com/brohrer/dsmq/blob/main/src/dsmq/dsmq.py)]
104
+
105
+ ### Start a server
106
+
107
+ ```python
108
+ run(host="127.0.0.1", port=30008)
109
+ ```
110
+
111
+ Kicks off the mesage queue server. This process will be the central exchange
112
+ for all incoming and outgoing messages.
113
+ - `host` (str), IP address on which the server will be visible and
114
+ - `port` (int), port. These will be used by all clients.
115
+ Non-privileged ports are numbered 1024 and higher.
116
+
117
+ ### Open a connection from a client
118
+
119
+ ```python
120
+ import socket
121
+
122
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
123
+ s.connect((host, port ))
124
+ ```
125
+
126
+ ### Add a message to a queue
127
+
128
+ ```python
129
+ import json
130
+ msg = json.dumps({
131
+ "action": "put",
132
+ "topic": <queue-name>,
133
+ "message": <message-content>
134
+ })
135
+ s.sendall(bytes(msg, "utf-8"))
136
+ ```
137
+
138
+ - `s`, the socket connection to the server
139
+ - `<queue-name>` (str), a name for the queue where the message will be added
140
+ - `<message-content>` (str), whatever message content you want
141
+
142
+ Place `message-content` into the queue named `queue-name`.
143
+ If the queue doesn't exist yet, create it.
144
+
145
+ ### Get a message from a queue
146
+
147
+ ```python
148
+ request_msg = json.dumps({"action": "get", "topic": <queue-name>})
149
+ s.sendall(bytes(request_msg, "utf-8"))
150
+ data = s.recv(1024)
151
+ msg_str = data.decode("utf-8")
152
+ ```
153
+
154
+ Get the oldest eligible message from the queue named `<queue-name>`.
155
+ The client is only elgibile to receive messages added after it
156
+ connected to the server.
@@ -0,0 +1,11 @@
1
+ dsmq/__init__.py,sha256=YCgbnQAk8YbtHRyMcU0v2O7RdRhPhlT-vS_q40a7Q6g,50
2
+ dsmq/demo_linux.py,sha256=D5NjEvPG-fTGRRbYzMJXgtwfrigOt_eaR6D0AozBNbA,613
3
+ dsmq/dsmq.py,sha256=EJW6tMeG57QYBneqBEM4R_5CrtG-Lx2qz0D1BM0YtjI,4452
4
+ dsmq/example_get_client.py,sha256=ZWZrAu7sIEn8VwCK09cTwsm1XnF3Jkor3r7Gl4ljUMw,630
5
+ dsmq/example_put_client.py,sha256=bSjSJZsFZB6inVGUlTnJn0jgrpVNXvrN46STTvgwCzs,497
6
+ dsmq/example_server.py,sha256=UM69GN7uORDetXQM8LlW68nrA-1i7Jgp2dZREVEqxaU,62
7
+ dsmq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ dsmq-0.2.0.dist-info/METADATA,sha256=TW6tM_oF2LuyRrmOBSBTHFCkJvNDA8O3QfuEtgD1-S8,4055
9
+ dsmq-0.2.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
10
+ dsmq-0.2.0.dist-info/licenses/LICENSE,sha256=3Yu1mAp5VsKmnDtzkiOY7BdmrLeNwwZ3t6iWaLnlL0Y,1071
11
+ dsmq-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Brandon Rohrer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.