dsmq 1.3.1__py3-none-any.whl → 1.4.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/.client.py.swp ADDED
Binary file
dsmq/.server.py.swp ADDED
Binary file
dsmq/client.py CHANGED
@@ -18,6 +18,7 @@ def connect(host=_default_host, port=_default_port, verbose=False):
18
18
  class DSMQClientSideConnection:
19
19
  def __init__(self, host, port, verbose=False):
20
20
  self.uri = f"ws://{host}:{port}"
21
+ self.port = port
21
22
  self.verbose = verbose
22
23
  if self.verbose:
23
24
  print(f"Connecting to dsmq server at {self.uri}")
@@ -40,7 +41,11 @@ class DSMQClientSideConnection:
40
41
 
41
42
  def get(self, topic):
42
43
  msg = {"action": "get", "topic": topic}
43
- self.websocket.send(json.dumps(msg))
44
+ try:
45
+ self.websocket.send(json.dumps(msg))
46
+ except ConnectionClosedError:
47
+ return ""
48
+
44
49
  try:
45
50
  msg_text = self.websocket.recv()
46
51
  except ConnectionClosedError:
@@ -50,6 +55,29 @@ class DSMQClientSideConnection:
50
55
  msg = json.loads(msg_text)
51
56
  return msg["message"]
52
57
 
58
+ def get_latest(self, topic):
59
+ """
60
+ A variant of `get()` that grabs the latest available message
61
+ (if there is one) rather than grabbing the oldest unread message.
62
+ It will not go back to read older ones on subsequent calls;
63
+ it will leave them unread.
64
+ """
65
+ msg = {"action": "get_latest", "topic": topic}
66
+ try:
67
+ self.websocket.send(json.dumps(msg))
68
+ except ConnectionClosedError:
69
+ return ""
70
+
71
+ try:
72
+ msg_text = self.websocket.recv()
73
+ except ConnectionClosedError:
74
+ self.close()
75
+ return ""
76
+
77
+ msg = json.loads(msg_text)
78
+
79
+ return msg["message"]
80
+
53
81
  def get_wait(self, topic):
54
82
  """
55
83
  A variant of `get()` that retries a few times until it gets
@@ -65,7 +93,10 @@ class DSMQClientSideConnection:
65
93
 
66
94
  def put(self, topic, msg_body):
67
95
  msg_dict = {"action": "put", "topic": topic, "message": msg_body}
68
- self.websocket.send(json.dumps(msg_dict))
96
+ try:
97
+ self.websocket.send(json.dumps(msg_dict))
98
+ except ConnectionClosedError:
99
+ return
69
100
 
70
101
  def shutdown_server(self):
71
102
  msg_dict = {"action": "shutdown", "topic": ""}
dsmq/demo.py CHANGED
@@ -1,7 +1,12 @@
1
1
  import multiprocessing as mp
2
- from dsmq.server import serve
3
- import dsmq.example_get_client
4
- import dsmq.example_put_client
2
+
3
+ # spawn is the default method on macOS,
4
+ # starting in Python 3.14 it will be the default in Linux too.
5
+ mp.set_start_method("spawn")
6
+
7
+ from dsmq.server import serve # noqa: E402
8
+ import dsmq.example_get_client # noqa: E402
9
+ import dsmq.example_put_client # noqa: E402
5
10
 
6
11
  HOST = "127.0.0.1"
7
12
  PORT = 25252
dsmq/server.py CHANGED
@@ -9,10 +9,10 @@ from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
9
9
 
10
10
  _default_host = "127.0.0.1"
11
11
  _default_port = 30008
12
- _n_retries = 20
13
- _first_retry = 0.005 # seconds
14
- _time_between_cleanup = 0.05 # seconds
15
12
  _max_queue_length = 10
13
+ _shutdown_pause = 1.0 # seconds
14
+ _time_between_cleanup = 3.0 # seconds
15
+ _time_to_keep = 0.3 # seconds
16
16
 
17
17
  # Make this global so it's easy to share
18
18
  dsmq_server = None
@@ -42,7 +42,11 @@ def serve(
42
42
  # and keep long-term latency more predictable.
43
43
  # These also make it more susceptible to corruption during shutdown,
44
44
  # but since dsmq is meant to be ephemeral, that's not a concern.
45
+ # See https://www.sqlite.org/pragma.html
46
+ #
45
47
  cursor.execute("PRAGMA journal_mode = OFF")
48
+ # cursor.execute("PRAGMA journal_mode = MEMORY")
49
+ # cursor.execute("PRAGMA synchronous = NORMAL")
46
50
  cursor.execute("PRAGMA synchronous = OFF")
47
51
  cursor.execute("PRAGMA secure_delete = OFF")
48
52
  cursor.execute("PRAGMA temp_store = MEMORY")
@@ -56,36 +60,33 @@ CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
56
60
  # and a method of last resort.
57
61
  global dsmq_server
58
62
 
59
- for i_retry in range(_n_retries):
60
- try:
63
+ try:
64
+ with ws_serve(request_handler, host, port) as dsmq_server:
65
+ dsmq_server.serve_forever()
66
+
67
+ except OSError:
68
+ # Catch the case where the address is already in use
69
+ if verbose:
70
+ print()
71
+ print(f"Found a dsmq server already running on {host} on port {port}.")
72
+ print(" Closing it down.")
73
+
74
+ def shutdown_gracefully(server_to_shutdown):
75
+ server_to_shutdown.shutdown()
76
+
77
+ Thread(target=shutdown_gracefully, args=(dsmq_server,)).start()
78
+ time.sleep(_shutdown_pause)
79
+
61
80
  with ws_serve(request_handler, host, port) as dsmq_server:
62
81
  dsmq_server.serve_forever()
63
82
 
64
- if verbose:
65
- print()
66
- print(f"Server started at {host} on port {port}.")
67
- print("Waiting for clients...")
68
-
69
- break
70
-
71
- except OSError:
72
- # Catch the case where the address is already in use
73
- if verbose:
74
- print()
75
- if i_retry < _n_retries - 1:
76
- print(f"Couldn't start dsmq server on {host} on port {port}.")
77
- print(f" Trying again ({i_retry}) ...")
78
- else:
79
- print()
80
- print(f"Failed to start dsmq server on {host} on port {port}.")
81
- print()
82
- raise
83
-
84
- wait_time = _first_retry * 2**i_retry
85
- time.sleep(wait_time)
83
+ if verbose:
84
+ print()
85
+ print(f"Server started at {host} on port {port}.")
86
+ print("Waiting for clients...")
86
87
 
87
88
  sqlite_conn.close()
88
- time.sleep(_time_between_cleanup)
89
+ time.sleep(_shutdown_pause)
89
90
  cleanup_temp_files()
90
91
 
91
92
 
@@ -121,9 +122,10 @@ def request_handler(websocket):
121
122
  for msg_text in websocket:
122
123
  msg = json.loads(msg_text)
123
124
  topic = msg["topic"]
125
+ action = msg["action"]
124
126
  timestamp = time.time()
125
127
 
126
- if msg["action"] == "put":
128
+ if action == "put":
127
129
  msg["timestamp"] = timestamp
128
130
 
129
131
  try:
@@ -138,7 +140,7 @@ def request_handler(websocket):
138
140
  except sqlite3.OperationalError:
139
141
  pass
140
142
 
141
- elif msg["action"] == "get":
143
+ elif action == "get":
142
144
  try:
143
145
  last_read_time = last_read_times[topic]
144
146
  except KeyError:
@@ -151,15 +153,46 @@ def request_handler(websocket):
151
153
  """
152
154
  SELECT message,
153
155
  timestamp
154
- FROM messages,
155
- (
156
- SELECT MIN(timestamp) AS min_time
157
156
  FROM messages
158
157
  WHERE topic = :topic
159
158
  AND timestamp > :last_read_time
160
- ) a
159
+ ORDER BY timestamp ASC
160
+ LIMIT 1
161
+ """,
162
+ msg,
163
+ )
164
+ except sqlite3.OperationalError:
165
+ pass
166
+
167
+ try:
168
+ result = cursor.fetchall()[0]
169
+ message = result[0]
170
+ timestamp = result[1]
171
+ last_read_times[topic] = timestamp
172
+ except IndexError:
173
+ # Handle the case where no results are returned
174
+ message = ""
175
+
176
+ websocket.send(json.dumps({"message": message}))
177
+
178
+ elif action == "get_latest":
179
+ try:
180
+ last_read_time = last_read_times[topic]
181
+ except KeyError:
182
+ last_read_times[topic] = client_creation_time
183
+ last_read_time = last_read_times[topic]
184
+ msg["last_read_time"] = last_read_time
185
+
186
+ try:
187
+ cursor.execute(
188
+ """
189
+ SELECT message,
190
+ timestamp
191
+ FROM messages
161
192
  WHERE topic = :topic
162
- AND timestamp = a.min_time
193
+ AND timestamp > :last_read_time
194
+ ORDER BY timestamp DESC
195
+ LIMIT 1;
163
196
  """,
164
197
  msg,
165
198
  )
@@ -177,25 +210,41 @@ def request_handler(websocket):
177
210
 
178
211
  websocket.send(json.dumps({"message": message}))
179
212
 
180
- elif msg["action"] == "shutdown":
213
+ elif action == "shutdown":
181
214
  # Run this from a separate thread to prevent deadlock
182
215
  global dsmq_server
183
216
 
184
217
  def shutdown_gracefully(server_to_shutdown):
185
218
  server_to_shutdown.shutdown()
186
- # cleanup_temp_files()
187
219
 
188
220
  Thread(target=shutdown_gracefully, args=(dsmq_server,)).start()
189
221
  break
190
222
  else:
191
223
  raise RuntimeWarning(
192
- "dsmq client action must either be 'put', 'get', or 'shutdown'"
224
+ "dsmq client action must either be\n"
225
+ + "'put', 'get', 'get_wait', 'get_latest', or 'shutdown'"
193
226
  )
194
227
 
195
228
  # Periodically clean out messages to keep individual queues at
196
229
  # a manageable length and the overall mq small.
197
230
  if time.time() - time_of_last_purge > _time_between_cleanup:
231
+ cutoff_time = time.time() - _time_to_keep
198
232
  try:
233
+ cursor.execute(
234
+ """
235
+ DELETE
236
+ FROM messages
237
+ WHERE topic = :topic
238
+ AND timestamp < :cutoff_time
239
+ """,
240
+ {
241
+ "cutoff_time": cutoff_time,
242
+ "topic": topic,
243
+ },
244
+ )
245
+ sqlite_conn.commit()
246
+ time_of_last_purge = time.time()
247
+
199
248
  cursor.execute(
200
249
  """
201
250
  DELETE
@@ -219,6 +268,7 @@ def request_handler(websocket):
219
268
  )
220
269
  sqlite_conn.commit()
221
270
  time_of_last_purge = time.time()
271
+
222
272
  except sqlite3.OperationalError:
223
273
  # Database may be locked. Try again next time.
224
274
  pass
@@ -1,10 +1,17 @@
1
1
  import multiprocessing as mp
2
+
3
+ try:
4
+ # spawn is the default method on macOS,
5
+ # starting in Python 3.14 it will be the default in Linux too.
6
+ mp.set_start_method("spawn")
7
+ except RuntimeError:
8
+ # Will throw an error if the start method has alraedy been set.
9
+ pass
10
+
2
11
  import time
3
12
  from dsmq.server import serve
4
13
  from dsmq.client import connect
5
14
 
6
- # spawn is the default method on macOS
7
- # mp.set_start_method('spawn')
8
15
 
9
16
  host = "127.0.0.1"
10
17
  port = 30303
@@ -53,6 +60,10 @@ def test_write_one_read_one():
53
60
  write_client = connect(host, port)
54
61
  read_client = connect(host, port)
55
62
 
63
+ msg = read_client.get("test")
64
+
65
+ assert msg == ""
66
+
56
67
  write_client.put("test", "test_msg")
57
68
 
58
69
  # It takes a moment for the write to complete
@@ -61,6 +72,10 @@ def test_write_one_read_one():
61
72
 
62
73
  assert msg == "test_msg"
63
74
 
75
+ msg = read_client.get("test")
76
+
77
+ assert msg == ""
78
+
64
79
  write_client.shutdown_server()
65
80
  write_client.close()
66
81
  read_client.close()
@@ -83,6 +98,29 @@ def test_get_wait():
83
98
  read_client.close()
84
99
 
85
100
 
101
+ def test_get_latest():
102
+ p_server = mp.Process(target=serve, args=(host, port))
103
+ p_server.start()
104
+ write_client = connect(host, port)
105
+ read_client = connect(host, port)
106
+
107
+ for i in range(5):
108
+ write_client.put("test", f"test_msg {i}")
109
+
110
+ time.sleep(_pause)
111
+ msg = read_client.get_latest("test")
112
+
113
+ assert msg == "test_msg 4"
114
+
115
+ msg = read_client.get_latest("test")
116
+
117
+ assert msg == ""
118
+
119
+ write_client.shutdown_server()
120
+ write_client.close()
121
+ read_client.close()
122
+
123
+
86
124
  def test_multitopics():
87
125
  p_server = mp.Process(target=serve, args=(host, port))
88
126
  p_server.start()
@@ -183,15 +221,8 @@ def test_speed_writing():
183
221
  p_speed_write.start()
184
222
  time.sleep(_pause)
185
223
 
186
- # time_a = time.time()
187
224
  write_client.put("test", "test_msg")
188
- # time_b = time.time()
189
225
  msg = read_client.get_wait("test")
190
- # time_c = time.time()
191
-
192
- # write_time = int((time_b - time_a) * 1e6)
193
- # read_time = int((time_c - time_b) * 1e6)
194
- # print(f"write time: {write_time} us, read time: {read_time} us")
195
226
 
196
227
  assert msg == "test_msg"
197
228
 
@@ -221,15 +252,8 @@ def test_speed_reading():
221
252
  p_speed_read = mp.Process(target=speed_read, args=(stop_flag,))
222
253
  p_speed_read.start()
223
254
 
224
- # time_a = time.time()
225
255
  write_client.put("test", "test_msg")
226
- # time_b = time.time()
227
256
  msg = read_client.get_wait("test")
228
- # time_c = time.time()
229
-
230
- # write_time = int((time_b - time_a) * 1e6)
231
- # read_time = int((time_c - time_b) * 1e6)
232
- # print(f"write time: {write_time} us, read time: {read_time} us")
233
257
 
234
258
  assert msg == "test_msg"
235
259
 
@@ -238,5 +262,8 @@ def test_speed_reading():
238
262
  p_speed_read.kill()
239
263
  read_client.close()
240
264
  write_client.shutdown_server()
241
- # time.sleep(_pause)
242
265
  write_client.close()
266
+
267
+
268
+ if __name__ == "__main__":
269
+ test_get_latest()
@@ -1,4 +1,13 @@
1
1
  import multiprocessing as mp
2
+
3
+ # spawn is the default method on macOS,
4
+ # starting in Python 3.14 it will be the default in Linux too.
5
+ try:
6
+ mp.set_start_method("spawn")
7
+ except RuntimeError as e:
8
+ print(e)
9
+ print(f"Using multiprocessing start method: {mp.get_start_method()}")
10
+
2
11
  import time
3
12
 
4
13
  from dsmq.server import serve
@@ -11,8 +20,8 @@ verbose = False
11
20
  _pause = 0.01
12
21
  _very_long_pause = 1.0
13
22
 
14
- _n_iter = int(1e3)
15
- _n_long_char = int(1e4)
23
+ _n_iter = int(1e4)
24
+ _n_long_char = int(1e5)
16
25
 
17
26
  _short_msg = "q"
18
27
  _long_msg = str(["q"] * _n_long_char)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsmq
3
- Version: 1.3.1
3
+ Version: 1.4.0
4
4
  Summary: A dead simple message queue
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -152,6 +152,16 @@ connected to the server.
152
152
  in the topic, or the topic doesn't yet exist,
153
153
  returns `""`.
154
154
 
155
+ ### `get_latest(topic)`
156
+
157
+ Get the *most recent* eligible message from the queue named `topic`.
158
+ All the messages older than that in the queue become ineligible and never
159
+ get seen by the client.
160
+ - `topic` (str)
161
+ - returns str, the content of the message. If there was no eligble message
162
+ in the topic, or the topic doesn't yet exist,
163
+ returns `""`.
164
+
155
165
  ### `get_wait(topic)`
156
166
 
157
167
  A variant of `get()` that retries a few times until it gets
@@ -0,0 +1,15 @@
1
+ dsmq/.client.py.swp,sha256=bm5ybp3ESthgsOMpHRygMeQWbSlK4xw8toIKiT5A0pM,12288
2
+ dsmq/.server.py.swp,sha256=R61qwjXxZUZAbNpEXSW_jvxkXvy4qqt7gA1FEzwE-jk,20480
3
+ dsmq/__init__.py,sha256=YCgbnQAk8YbtHRyMcU0v2O7RdRhPhlT-vS_q40a7Q6g,50
4
+ dsmq/client.py,sha256=a4_6jcF7RAC58And0OlR3vL42FgZ24q2BECS-VF4Ri4,3438
5
+ dsmq/demo.py,sha256=x2ueymZnSQNtQ-HjzRiSCa0rg4NOm0YUIaj5VPXfnD8,718
6
+ dsmq/example_get_client.py,sha256=PvAsDGEAH1kVBifLVg2rx8ZxnAZmvzVCvZq13VgpLds,301
7
+ dsmq/example_put_client.py,sha256=QxDc3i7KAjjhpwxRRpI0Ke5KTNSPuBf9kkcGyTvUEaw,353
8
+ dsmq/server.py,sha256=yANPboqqoAw7GnSyHs_SeYlZjKkQPDMIhRo7tGDw6zo,10041
9
+ dsmq/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ dsmq/tests/integration_test.py,sha256=zF6v_3UHt5KqveX22gEpCSLcoeOyEWLSy_gMLoytg2U,6303
11
+ dsmq/tests/performance_suite.py,sha256=LgNpqD0zumEOgrNDLkeirBmQAALhlxEH7bXe4z-EuIY,5474
12
+ dsmq-1.4.0.dist-info/METADATA,sha256=RTvtHRtUg4HSOvyMRTu7sFy3uu3Ddd7iOXD1IWK91QQ,5203
13
+ dsmq-1.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ dsmq-1.4.0.dist-info/licenses/LICENSE,sha256=3Yu1mAp5VsKmnDtzkiOY7BdmrLeNwwZ3t6iWaLnlL0Y,1071
15
+ dsmq-1.4.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- dsmq/__init__.py,sha256=YCgbnQAk8YbtHRyMcU0v2O7RdRhPhlT-vS_q40a7Q6g,50
2
- dsmq/client.py,sha256=ZsbuvsWX6_4-tkp7i3ixYAPEyvoebKX8qFtW38DHdXE,2569
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=HXPH9-nSzz2Iy2UzIWqgUVbL174jq1VBzs7im9vRLqk,8268
7
- dsmq/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- dsmq/tests/integration_test.py,sha256=dLsQGCmpXv4zRb93TriccH7TbUyD9MHcLckAQqfDOK4,5980
9
- dsmq/tests/performance_suite.py,sha256=E59zB2ZvM8V5f8RxaB7p-Kehqyhrgsl0sXuy7g74BaI,5218
10
- dsmq-1.3.1.dist-info/METADATA,sha256=K6fqrudTKsXR2xlHDlTNkrjepNcsKn5kX9I5yjpzxPk,4859
11
- dsmq-1.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
- dsmq-1.3.1.dist-info/licenses/LICENSE,sha256=3Yu1mAp5VsKmnDtzkiOY7BdmrLeNwwZ3t6iWaLnlL0Y,1071
13
- dsmq-1.3.1.dist-info/RECORD,,
File without changes