dsmq 1.2.2__py3-none-any.whl → 1.2.4__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/client.py +6 -1
- dsmq/server.py +106 -96
- dsmq/tests/integration_test.py +0 -1
- {dsmq-1.2.2.dist-info → dsmq-1.2.4.dist-info}/METADATA +1 -1
- dsmq-1.2.4.dist-info/RECORD +12 -0
- dsmq-1.2.2.dist-info/RECORD +0 -12
- {dsmq-1.2.2.dist-info → dsmq-1.2.4.dist-info}/WHEEL +0 -0
- {dsmq-1.2.2.dist-info → dsmq-1.2.4.dist-info}/licenses/LICENSE +0 -0
dsmq/client.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import json
|
2
2
|
import time
|
3
3
|
from websockets.sync.client import connect as ws_connect
|
4
|
+
from websockets.exceptions import ConnectionClosedError
|
4
5
|
|
5
6
|
_default_host = "127.0.0.1"
|
6
7
|
_default_port = 30008
|
@@ -39,7 +40,11 @@ class DSMQClientSideConnection:
|
|
39
40
|
def get(self, topic):
|
40
41
|
msg = {"action": "get", "topic": topic}
|
41
42
|
self.websocket.send(json.dumps(msg))
|
42
|
-
|
43
|
+
try:
|
44
|
+
msg_text = self.websocket.recv()
|
45
|
+
except ConnectionClosedError:
|
46
|
+
self.close()
|
47
|
+
|
43
48
|
msg = json.loads(msg_text)
|
44
49
|
return msg["message"]
|
45
50
|
|
dsmq/server.py
CHANGED
@@ -5,6 +5,7 @@ import sys
|
|
5
5
|
from threading import Thread
|
6
6
|
import time
|
7
7
|
from websockets.sync.server import serve as ws_serve
|
8
|
+
from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
|
8
9
|
|
9
10
|
_default_host = "127.0.0.1"
|
10
11
|
_default_port = 30008
|
@@ -69,111 +70,120 @@ def request_handler(websocket):
|
|
69
70
|
last_read_times = {}
|
70
71
|
time_of_last_purge = time.time()
|
71
72
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
msg["
|
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
|
79
133
|
|
80
|
-
# This block allows for multiple retries if the database
|
81
|
-
# is busy.
|
82
|
-
for i_retry in range(_n_retries):
|
83
134
|
try:
|
84
|
-
cursor.
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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()
|
96
160
|
break
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
# This block allows for multiple retries if the database
|
107
|
-
# is busy.
|
108
|
-
for i_retry in range(_n_retries):
|
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:
|
109
170
|
try:
|
110
171
|
cursor.execute(
|
111
172
|
"""
|
112
|
-
|
113
|
-
timestamp
|
114
|
-
FROM messages,
|
115
|
-
(
|
116
|
-
SELECT MIN(timestamp) AS min_time
|
117
|
-
FROM messages
|
118
|
-
WHERE topic = :topic
|
119
|
-
AND timestamp > :last_read_time
|
120
|
-
) a
|
121
|
-
WHERE topic = :topic
|
122
|
-
AND timestamp = a.min_time
|
173
|
+
DELETE FROM messages
|
174
|
+
WHERE timestamp < :time_threshold
|
123
175
|
""",
|
124
|
-
|
176
|
+
{"time_threshold": time_of_last_purge},
|
125
177
|
)
|
178
|
+
sqlite_conn.commit()
|
179
|
+
time_of_last_purge = time.time()
|
126
180
|
except sqlite3.OperationalError:
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
result = cursor.fetchall()[0]
|
134
|
-
message = result[0]
|
135
|
-
timestamp = result[1]
|
136
|
-
last_read_times[topic] = timestamp
|
137
|
-
except IndexError:
|
138
|
-
# Handle the case where no results are returned
|
139
|
-
message = ""
|
140
|
-
|
141
|
-
websocket.send(json.dumps({"message": message}))
|
142
|
-
elif msg["action"] == "shutdown":
|
143
|
-
# Run this from a separate thread to prevent deadlock
|
144
|
-
global dsmq_server
|
145
|
-
|
146
|
-
def shutdown_gracefully(server_to_shutdown):
|
147
|
-
server_to_shutdown.shutdown()
|
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
|
-
break
|
159
|
-
else:
|
160
|
-
raise RuntimeWarning(
|
161
|
-
"dsmq client action must either be 'put', 'get', or 'shutdown'"
|
162
|
-
)
|
163
|
-
|
164
|
-
# Periodically clean out messages from the queue that are
|
165
|
-
# past their sell buy date.
|
166
|
-
# This operation is pretty fast. I clock it at 12 us on my machine.
|
167
|
-
if time.time() - time_of_last_purge > _time_to_live:
|
168
|
-
cursor.execute(
|
169
|
-
"""
|
170
|
-
DELETE FROM messages
|
171
|
-
WHERE timestamp < :time_threshold
|
172
|
-
""",
|
173
|
-
{"time_threshold": time_of_last_purge},
|
174
|
-
)
|
175
|
-
sqlite_conn.commit()
|
176
|
-
time_of_last_purge = time.time()
|
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
|
177
187
|
|
178
188
|
sqlite_conn.close()
|
179
189
|
|
dsmq/tests/integration_test.py
CHANGED
@@ -0,0 +1,12 @@
|
|
1
|
+
dsmq/__init__.py,sha256=YCgbnQAk8YbtHRyMcU0v2O7RdRhPhlT-vS_q40a7Q6g,50
|
2
|
+
dsmq/client.py,sha256=p6irQZOE4b2fpTUwSrEUraPmrJzvT8QSU01ak9qpGCQ,2351
|
3
|
+
dsmq/demo.py,sha256=K53cC5kN7K4kNJlPq7c5OTIMHRCKTo9hYX2aIos57rU,542
|
4
|
+
dsmq/example_get_client.py,sha256=PvAsDGEAH1kVBifLVg2rx8ZxnAZmvzVCvZq13VgpLds,301
|
5
|
+
dsmq/example_put_client.py,sha256=QxDc3i7KAjjhpwxRRpI0Ke5KTNSPuBf9kkcGyTvUEaw,353
|
6
|
+
dsmq/server.py,sha256=5c4mHWz6yslSxpOwrZ3vOCMcx_cm8zMF8iJNnQF8ft0,7249
|
7
|
+
dsmq/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
dsmq/tests/integration_test.py,sha256=dLsQGCmpXv4zRb93TriccH7TbUyD9MHcLckAQqfDOK4,5980
|
9
|
+
dsmq-1.2.4.dist-info/METADATA,sha256=CswX8bQph2pF-k-VMCLAPqaho3Mhny4xDLJ70xwWa9I,4730
|
10
|
+
dsmq-1.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
11
|
+
dsmq-1.2.4.dist-info/licenses/LICENSE,sha256=3Yu1mAp5VsKmnDtzkiOY7BdmrLeNwwZ3t6iWaLnlL0Y,1071
|
12
|
+
dsmq-1.2.4.dist-info/RECORD,,
|
dsmq-1.2.2.dist-info/RECORD
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
dsmq/__init__.py,sha256=YCgbnQAk8YbtHRyMcU0v2O7RdRhPhlT-vS_q40a7Q6g,50
|
2
|
-
dsmq/client.py,sha256=1fQLRExDkdLogu8M5aofkYp3-Qy5a2y3aMYy7qC3NkE,2214
|
3
|
-
dsmq/demo.py,sha256=K53cC5kN7K4kNJlPq7c5OTIMHRCKTo9hYX2aIos57rU,542
|
4
|
-
dsmq/example_get_client.py,sha256=PvAsDGEAH1kVBifLVg2rx8ZxnAZmvzVCvZq13VgpLds,301
|
5
|
-
dsmq/example_put_client.py,sha256=QxDc3i7KAjjhpwxRRpI0Ke5KTNSPuBf9kkcGyTvUEaw,353
|
6
|
-
dsmq/server.py,sha256=kCqBzgikvJlCgAfLHd18LS7fmkYW6e0RhmdwqdeXJ7Q,6079
|
7
|
-
dsmq/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
dsmq/tests/integration_test.py,sha256=lC97LAzdQixv75OwjqjKTvYnSZpsP0zuzFP8ocUnjl8,6031
|
9
|
-
dsmq-1.2.2.dist-info/METADATA,sha256=1n3_ZM8haJ5U99_fx6vZWpVYzfSR1RLUn_Ich87HaBo,4730
|
10
|
-
dsmq-1.2.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
11
|
-
dsmq-1.2.2.dist-info/licenses/LICENSE,sha256=3Yu1mAp5VsKmnDtzkiOY7BdmrLeNwwZ3t6iWaLnlL0Y,1071
|
12
|
-
dsmq-1.2.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|