dsmq 1.2.3__tar.gz → 1.2.4__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-1.2.3 → dsmq-1.2.4}/PKG-INFO +1 -1
- {dsmq-1.2.3 → dsmq-1.2.4}/pyproject.toml +1 -1
- dsmq-1.2.4/src/dsmq/server.py +212 -0
- {dsmq-1.2.3 → dsmq-1.2.4}/src/dsmq/tests/integration_test.py +0 -1
- {dsmq-1.2.3 → dsmq-1.2.4}/uv.lock +1 -1
- dsmq-1.2.3/src/dsmq/server.py +0 -206
- {dsmq-1.2.3 → dsmq-1.2.4}/.gitignore +0 -0
- {dsmq-1.2.3 → dsmq-1.2.4}/.python-version +0 -0
- {dsmq-1.2.3 → dsmq-1.2.4}/LICENSE +0 -0
- {dsmq-1.2.3 → dsmq-1.2.4}/README.md +0 -0
- {dsmq-1.2.3 → dsmq-1.2.4}/src/dsmq/__init__.py +0 -0
- {dsmq-1.2.3 → dsmq-1.2.4}/src/dsmq/client.py +0 -0
- {dsmq-1.2.3 → dsmq-1.2.4}/src/dsmq/demo.py +0 -0
- {dsmq-1.2.3 → dsmq-1.2.4}/src/dsmq/example_get_client.py +0 -0
- {dsmq-1.2.3 → dsmq-1.2.4}/src/dsmq/example_put_client.py +0 -0
- {dsmq-1.2.3 → dsmq-1.2.4}/src/dsmq/tests/__init__.py +0 -0
@@ -0,0 +1,212 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
import sqlite3
|
4
|
+
import sys
|
5
|
+
from threading import Thread
|
6
|
+
import time
|
7
|
+
from websockets.sync.server import serve as ws_serve
|
8
|
+
from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
|
9
|
+
|
10
|
+
_default_host = "127.0.0.1"
|
11
|
+
_default_port = 30008
|
12
|
+
_n_retries = 5
|
13
|
+
_first_retry = 0.01 # seconds
|
14
|
+
_pause = 0.01 # seconds
|
15
|
+
_time_to_live = 600.0 # seconds
|
16
|
+
|
17
|
+
_db_name = "file::memory:?cache=shared"
|
18
|
+
|
19
|
+
# Make this global so it's easy to share
|
20
|
+
dsmq_server = None
|
21
|
+
|
22
|
+
|
23
|
+
def serve(host=_default_host, port=_default_port, verbose=False):
|
24
|
+
"""
|
25
|
+
For best results, start this running in its own process and walk away.
|
26
|
+
"""
|
27
|
+
# Cleanup temp files.
|
28
|
+
# Under some condition
|
29
|
+
# (which I haven't yet been able to pin down)
|
30
|
+
# a file is generated with the db name.
|
31
|
+
# If it is not removed, it gets
|
32
|
+
# treated as a SQLite db on disk,
|
33
|
+
# which dramatically slows it down,
|
34
|
+
# especially the way it's used here for
|
35
|
+
# rapid-fire one-item reads and writes.
|
36
|
+
filenames = os.listdir()
|
37
|
+
for filename in filenames:
|
38
|
+
if filename[: len(_db_name)] == _db_name:
|
39
|
+
os.remove(filename)
|
40
|
+
|
41
|
+
sqlite_conn = sqlite3.connect(_db_name)
|
42
|
+
cursor = sqlite_conn.cursor()
|
43
|
+
cursor.execute("""
|
44
|
+
CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
|
45
|
+
""")
|
46
|
+
|
47
|
+
# Making this global in scope is a way to make it available
|
48
|
+
# to the shutdown operation. It's an awkward construction,
|
49
|
+
# and a method of last resort. (If you stumble across this and
|
50
|
+
# figure out something more elegant, please submit a PR!
|
51
|
+
# or send it to me at brohrer@gmail.com,
|
52
|
+
global dsmq_server
|
53
|
+
|
54
|
+
# dsmq_server = ws_serve(request_handler, host, port)
|
55
|
+
with ws_serve(request_handler, host, port) as dsmq_server:
|
56
|
+
dsmq_server.serve_forever()
|
57
|
+
if verbose:
|
58
|
+
print()
|
59
|
+
print(f"Server started at {host} on port {port}.")
|
60
|
+
print("Waiting for clients...")
|
61
|
+
|
62
|
+
sqlite_conn.close()
|
63
|
+
|
64
|
+
|
65
|
+
def request_handler(websocket):
|
66
|
+
sqlite_conn = sqlite3.connect(_db_name)
|
67
|
+
cursor = sqlite_conn.cursor()
|
68
|
+
|
69
|
+
client_creation_time = time.time()
|
70
|
+
last_read_times = {}
|
71
|
+
time_of_last_purge = time.time()
|
72
|
+
|
73
|
+
try:
|
74
|
+
for msg_text in websocket:
|
75
|
+
msg = json.loads(msg_text)
|
76
|
+
topic = msg["topic"]
|
77
|
+
timestamp = time.time()
|
78
|
+
|
79
|
+
if msg["action"] == "put":
|
80
|
+
msg["timestamp"] = timestamp
|
81
|
+
|
82
|
+
# This block allows for multiple retries if the database
|
83
|
+
# is busy.
|
84
|
+
for i_retry in range(_n_retries):
|
85
|
+
try:
|
86
|
+
cursor.execute(
|
87
|
+
"""
|
88
|
+
INSERT INTO messages (timestamp, topic, message)
|
89
|
+
VALUES (:timestamp, :topic, :message)
|
90
|
+
""",
|
91
|
+
(msg),
|
92
|
+
)
|
93
|
+
sqlite_conn.commit()
|
94
|
+
except sqlite3.OperationalError:
|
95
|
+
wait_time = _first_retry * 2**i_retry
|
96
|
+
time.sleep(wait_time)
|
97
|
+
continue
|
98
|
+
break
|
99
|
+
|
100
|
+
elif msg["action"] == "get":
|
101
|
+
try:
|
102
|
+
last_read_time = last_read_times[topic]
|
103
|
+
except KeyError:
|
104
|
+
last_read_times[topic] = client_creation_time
|
105
|
+
last_read_time = last_read_times[topic]
|
106
|
+
msg["last_read_time"] = last_read_time
|
107
|
+
|
108
|
+
# This block allows for multiple retries if the database
|
109
|
+
# is busy.
|
110
|
+
for i_retry in range(_n_retries):
|
111
|
+
try:
|
112
|
+
cursor.execute(
|
113
|
+
"""
|
114
|
+
SELECT message,
|
115
|
+
timestamp
|
116
|
+
FROM messages,
|
117
|
+
(
|
118
|
+
SELECT MIN(timestamp) AS min_time
|
119
|
+
FROM messages
|
120
|
+
WHERE topic = :topic
|
121
|
+
AND timestamp > :last_read_time
|
122
|
+
) a
|
123
|
+
WHERE topic = :topic
|
124
|
+
AND timestamp = a.min_time
|
125
|
+
""",
|
126
|
+
msg,
|
127
|
+
)
|
128
|
+
except sqlite3.OperationalError:
|
129
|
+
wait_time = _first_retry * 2**i_retry
|
130
|
+
time.sleep(wait_time)
|
131
|
+
continue
|
132
|
+
break
|
133
|
+
|
134
|
+
try:
|
135
|
+
result = cursor.fetchall()[0]
|
136
|
+
message = result[0]
|
137
|
+
timestamp = result[1]
|
138
|
+
last_read_times[topic] = timestamp
|
139
|
+
except IndexError:
|
140
|
+
# Handle the case where no results are returned
|
141
|
+
message = ""
|
142
|
+
|
143
|
+
websocket.send(json.dumps({"message": message}))
|
144
|
+
elif msg["action"] == "shutdown":
|
145
|
+
# Run this from a separate thread to prevent deadlock
|
146
|
+
global dsmq_server
|
147
|
+
|
148
|
+
def shutdown_gracefully(server_to_shutdown):
|
149
|
+
server_to_shutdown.shutdown()
|
150
|
+
|
151
|
+
filenames = os.listdir()
|
152
|
+
for filename in filenames:
|
153
|
+
if filename[: len(_db_name)] == _db_name:
|
154
|
+
try:
|
155
|
+
os.remove(filename)
|
156
|
+
except FileNotFoundError:
|
157
|
+
pass
|
158
|
+
|
159
|
+
Thread(target=shutdown_gracefully, args=(dsmq_server,)).start()
|
160
|
+
break
|
161
|
+
else:
|
162
|
+
raise RuntimeWarning(
|
163
|
+
"dsmq client action must either be 'put', 'get', or 'shutdown'"
|
164
|
+
)
|
165
|
+
|
166
|
+
# Periodically clean out messages from the queue that are
|
167
|
+
# past their sell buy date.
|
168
|
+
# This operation is pretty fast. I clock it at 12 us on my machine.
|
169
|
+
if time.time() - time_of_last_purge > _time_to_live:
|
170
|
+
try:
|
171
|
+
cursor.execute(
|
172
|
+
"""
|
173
|
+
DELETE FROM messages
|
174
|
+
WHERE timestamp < :time_threshold
|
175
|
+
""",
|
176
|
+
{"time_threshold": time_of_last_purge},
|
177
|
+
)
|
178
|
+
sqlite_conn.commit()
|
179
|
+
time_of_last_purge = time.time()
|
180
|
+
except sqlite3.OperationalError:
|
181
|
+
# Database may be locked. Try again next time.
|
182
|
+
pass
|
183
|
+
except (ConnectionClosedError, ConnectionClosedOK):
|
184
|
+
# Something happened on the other end and this handler
|
185
|
+
# is no longer needed.
|
186
|
+
pass
|
187
|
+
|
188
|
+
sqlite_conn.close()
|
189
|
+
|
190
|
+
|
191
|
+
if __name__ == "__main__":
|
192
|
+
if len(sys.argv) == 3:
|
193
|
+
host = sys.argv[1]
|
194
|
+
port = int(sys.argv[2])
|
195
|
+
serve(host=host, port=port)
|
196
|
+
elif len(sys.argv) == 2:
|
197
|
+
host = sys.argv[1]
|
198
|
+
serve(host=host)
|
199
|
+
elif len(sys.argv) == 1:
|
200
|
+
serve()
|
201
|
+
else:
|
202
|
+
print(
|
203
|
+
"""
|
204
|
+
Try one of these:
|
205
|
+
$ python3 server.py
|
206
|
+
|
207
|
+
$ python3 server.py 127.0.0.1
|
208
|
+
|
209
|
+
$ python3 server.py 127.0.0.1 25853
|
210
|
+
|
211
|
+
"""
|
212
|
+
)
|
dsmq-1.2.3/src/dsmq/server.py
DELETED
@@ -1,206 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import os
|
3
|
-
import sqlite3
|
4
|
-
import sys
|
5
|
-
from threading import Thread
|
6
|
-
import time
|
7
|
-
from websockets.sync.server import serve as ws_serve
|
8
|
-
from websockets.exceptions import ConnectionClosedError
|
9
|
-
|
10
|
-
_default_host = "127.0.0.1"
|
11
|
-
_default_port = 30008
|
12
|
-
_n_retries = 5
|
13
|
-
_first_retry = 0.01 # seconds
|
14
|
-
_pause = 0.01 # seconds
|
15
|
-
_time_to_live = 600.0 # seconds
|
16
|
-
|
17
|
-
_db_name = "file::memory:?cache=shared"
|
18
|
-
|
19
|
-
# Make this global so it's easy to share
|
20
|
-
dsmq_server = None
|
21
|
-
|
22
|
-
|
23
|
-
def serve(host=_default_host, port=_default_port, verbose=False):
|
24
|
-
"""
|
25
|
-
For best results, start this running in its own process and walk away.
|
26
|
-
"""
|
27
|
-
# Cleanup temp files.
|
28
|
-
# Under some condition
|
29
|
-
# (which I haven't yet been able to pin down)
|
30
|
-
# a file is generated with the db name.
|
31
|
-
# If it is not removed, it gets
|
32
|
-
# treated as a SQLite db on disk,
|
33
|
-
# which dramatically slows it down,
|
34
|
-
# especially the way it's used here for
|
35
|
-
# rapid-fire one-item reads and writes.
|
36
|
-
filenames = os.listdir()
|
37
|
-
for filename in filenames:
|
38
|
-
if filename[: len(_db_name)] == _db_name:
|
39
|
-
os.remove(filename)
|
40
|
-
|
41
|
-
sqlite_conn = sqlite3.connect(_db_name)
|
42
|
-
cursor = sqlite_conn.cursor()
|
43
|
-
cursor.execute("""
|
44
|
-
CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
|
45
|
-
""")
|
46
|
-
|
47
|
-
# Making this global in scope is a way to make it available
|
48
|
-
# to the shutdown operation. It's an awkward construction,
|
49
|
-
# and a method of last resort. (If you stumble across this and
|
50
|
-
# figure out something more elegant, please submit a PR!
|
51
|
-
# or send it to me at brohrer@gmail.com,
|
52
|
-
global dsmq_server
|
53
|
-
|
54
|
-
# dsmq_server = ws_serve(request_handler, host, port)
|
55
|
-
with ws_serve(request_handler, host, port) as dsmq_server:
|
56
|
-
dsmq_server.serve_forever()
|
57
|
-
if verbose:
|
58
|
-
print()
|
59
|
-
print(f"Server started at {host} on port {port}.")
|
60
|
-
print("Waiting for clients...")
|
61
|
-
|
62
|
-
sqlite_conn.close()
|
63
|
-
|
64
|
-
|
65
|
-
def request_handler(websocket):
|
66
|
-
sqlite_conn = sqlite3.connect(_db_name)
|
67
|
-
cursor = sqlite_conn.cursor()
|
68
|
-
|
69
|
-
client_creation_time = time.time()
|
70
|
-
last_read_times = {}
|
71
|
-
time_of_last_purge = time.time()
|
72
|
-
|
73
|
-
for msg_text in websocket:
|
74
|
-
msg = json.loads(msg_text)
|
75
|
-
topic = msg["topic"]
|
76
|
-
timestamp = time.time()
|
77
|
-
|
78
|
-
if msg["action"] == "put":
|
79
|
-
msg["timestamp"] = timestamp
|
80
|
-
|
81
|
-
# This block allows for multiple retries if the database
|
82
|
-
# is busy.
|
83
|
-
for i_retry in range(_n_retries):
|
84
|
-
try:
|
85
|
-
cursor.execute(
|
86
|
-
"""
|
87
|
-
INSERT INTO messages (timestamp, topic, message)
|
88
|
-
VALUES (:timestamp, :topic, :message)
|
89
|
-
""",
|
90
|
-
(msg),
|
91
|
-
)
|
92
|
-
sqlite_conn.commit()
|
93
|
-
except sqlite3.OperationalError:
|
94
|
-
wait_time = _first_retry * 2**i_retry
|
95
|
-
time.sleep(wait_time)
|
96
|
-
continue
|
97
|
-
break
|
98
|
-
|
99
|
-
elif msg["action"] == "get":
|
100
|
-
try:
|
101
|
-
last_read_time = last_read_times[topic]
|
102
|
-
except KeyError:
|
103
|
-
last_read_times[topic] = client_creation_time
|
104
|
-
last_read_time = last_read_times[topic]
|
105
|
-
msg["last_read_time"] = last_read_time
|
106
|
-
|
107
|
-
# This block allows for multiple retries if the database
|
108
|
-
# is busy.
|
109
|
-
for i_retry in range(_n_retries):
|
110
|
-
try:
|
111
|
-
cursor.execute(
|
112
|
-
"""
|
113
|
-
SELECT message,
|
114
|
-
timestamp
|
115
|
-
FROM messages,
|
116
|
-
(
|
117
|
-
SELECT MIN(timestamp) AS min_time
|
118
|
-
FROM messages
|
119
|
-
WHERE topic = :topic
|
120
|
-
AND timestamp > :last_read_time
|
121
|
-
) a
|
122
|
-
WHERE topic = :topic
|
123
|
-
AND timestamp = a.min_time
|
124
|
-
""",
|
125
|
-
msg,
|
126
|
-
)
|
127
|
-
except sqlite3.OperationalError:
|
128
|
-
wait_time = _first_retry * 2**i_retry
|
129
|
-
time.sleep(wait_time)
|
130
|
-
continue
|
131
|
-
break
|
132
|
-
|
133
|
-
try:
|
134
|
-
result = cursor.fetchall()[0]
|
135
|
-
message = result[0]
|
136
|
-
timestamp = result[1]
|
137
|
-
last_read_times[topic] = timestamp
|
138
|
-
except IndexError:
|
139
|
-
# Handle the case where no results are returned
|
140
|
-
message = ""
|
141
|
-
|
142
|
-
try:
|
143
|
-
websocket.send(json.dumps({"message": message}))
|
144
|
-
except ConnectionClosedError:
|
145
|
-
pass
|
146
|
-
elif msg["action"] == "shutdown":
|
147
|
-
# Run this from a separate thread to prevent deadlock
|
148
|
-
global dsmq_server
|
149
|
-
|
150
|
-
def shutdown_gracefully(server_to_shutdown):
|
151
|
-
server_to_shutdown.shutdown()
|
152
|
-
|
153
|
-
filenames = os.listdir()
|
154
|
-
for filename in filenames:
|
155
|
-
if filename[: len(_db_name)] == _db_name:
|
156
|
-
try:
|
157
|
-
os.remove(filename)
|
158
|
-
except FileNotFoundError:
|
159
|
-
pass
|
160
|
-
|
161
|
-
Thread(target=shutdown_gracefully, args=(dsmq_server,)).start()
|
162
|
-
break
|
163
|
-
else:
|
164
|
-
raise RuntimeWarning(
|
165
|
-
"dsmq client action must either be 'put', 'get', or 'shutdown'"
|
166
|
-
)
|
167
|
-
|
168
|
-
# Periodically clean out messages from the queue that are
|
169
|
-
# past their sell buy date.
|
170
|
-
# This operation is pretty fast. I clock it at 12 us on my machine.
|
171
|
-
if time.time() - time_of_last_purge > _time_to_live:
|
172
|
-
cursor.execute(
|
173
|
-
"""
|
174
|
-
DELETE FROM messages
|
175
|
-
WHERE timestamp < :time_threshold
|
176
|
-
""",
|
177
|
-
{"time_threshold": time_of_last_purge},
|
178
|
-
)
|
179
|
-
sqlite_conn.commit()
|
180
|
-
time_of_last_purge = time.time()
|
181
|
-
|
182
|
-
sqlite_conn.close()
|
183
|
-
|
184
|
-
|
185
|
-
if __name__ == "__main__":
|
186
|
-
if len(sys.argv) == 3:
|
187
|
-
host = sys.argv[1]
|
188
|
-
port = int(sys.argv[2])
|
189
|
-
serve(host=host, port=port)
|
190
|
-
elif len(sys.argv) == 2:
|
191
|
-
host = sys.argv[1]
|
192
|
-
serve(host=host)
|
193
|
-
elif len(sys.argv) == 1:
|
194
|
-
serve()
|
195
|
-
else:
|
196
|
-
print(
|
197
|
-
"""
|
198
|
-
Try one of these:
|
199
|
-
$ python3 server.py
|
200
|
-
|
201
|
-
$ python3 server.py 127.0.0.1
|
202
|
-
|
203
|
-
$ python3 server.py 127.0.0.1 25853
|
204
|
-
|
205
|
-
"""
|
206
|
-
)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|