motorcortex-python 1.2.0__tar.gz → 1.2.1__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.
Files changed (37) hide show
  1. {motorcortex_python-1.2.0/motorcortex_python.egg-info → motorcortex_python-1.2.1}/PKG-INFO +1 -1
  2. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/__init__.py +27 -2
  3. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/_request_utils.py +51 -7
  4. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/request.py +27 -6
  5. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/subscribe.py +35 -10
  6. motorcortex_python-1.2.1/motorcortex/version.py +1 -0
  7. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1/motorcortex_python.egg-info}/PKG-INFO +1 -1
  8. motorcortex_python-1.2.0/motorcortex/version.py +0 -1
  9. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/LICENSE +0 -0
  10. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/MANIFEST.in +0 -0
  11. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/README.md +0 -0
  12. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/_connection_state.py +0 -0
  13. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/_request_builders.py +0 -0
  14. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/_sp_stream.py +0 -0
  15. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/_subscribe_dispatch.py +0 -0
  16. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/exceptions.py +0 -0
  17. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/init_threads.py +0 -0
  18. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/message_types.py +0 -0
  19. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/motorcortex_hash.json +0 -0
  20. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/motorcortex_pb2.py +0 -0
  21. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/motorcortex_pb2.pyi +0 -0
  22. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/nng_url.py +0 -0
  23. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/parameter_tree.py +0 -0
  24. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/py.typed +0 -0
  25. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/reply.py +0 -0
  26. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/session.py +0 -0
  27. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/setup_logger.py +0 -0
  28. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/state_callback_handler.py +0 -0
  29. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/subscription.py +0 -0
  30. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex/timespec.py +0 -0
  31. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex_python.egg-info/SOURCES.txt +0 -0
  32. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex_python.egg-info/dependency_links.txt +0 -0
  33. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex_python.egg-info/requires.txt +0 -0
  34. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/motorcortex_python.egg-info/top_level.txt +0 -0
  35. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/pyproject.toml +0 -0
  36. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/setup.cfg +0 -0
  37. {motorcortex_python-1.2.0 → motorcortex_python-1.2.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: motorcortex-python
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Python bindings for Motorcortex Engine
5
5
  Home-page: https://www.motorcortex.io
6
6
  Author: Alexey Zakharov
@@ -41,6 +41,7 @@ from motorcortex.exceptions import (
41
41
  McxTimeout,
42
42
  )
43
43
  from motorcortex.motorcortex_pb2 import StatusCode as _PbStatusCode
44
+ from motorcortex import _request_utils
44
45
  from motorcortex.setup_logger import logger # not re-exported — ``__all__`` gates package surface
45
46
 
46
47
  # Build an ``IntEnum`` facade over the protobuf ``StatusCode`` enum so
@@ -312,7 +313,15 @@ def connect(
312
313
 
313
314
  if reconnect and not kwargs.get("state_update"):
314
315
 
316
+ # The initial CONNECTION_OK edge can land after connect() returns
317
+ # (the cookie probe + hello finish in the background), so it must
318
+ # not be mistaken for a reconnect that needs a re-login.
319
+ initial_ok_seen = [False]
320
+
315
321
  def stateUpdate(req, sub, state):
322
+ if state == ConnectionState.CONNECTION_OK and not initial_ok_seen[0]:
323
+ initial_ok_seen[0] = True
324
+ return
316
325
  _reconnect_state_update(
317
326
  req, sub, state,
318
327
  motorcortex_types=motorcortex_types,
@@ -342,7 +351,12 @@ def connect(
342
351
  )
343
352
  # Open subscribe connection
344
353
  sub = Subscribe(req, motorcortex_types, kwargs.get("sub_number_of_threads", 2))
345
- if not sub.connect(makeUrl(sub_address, sub_port), **kwargs).get():
354
+ sub_reply = sub.connect(makeUrl(sub_address, sub_port), **kwargs)
355
+ # Wait for the dial only: the cookie probe and hello carry on in
356
+ # the background (up to 2 s on an old server that never answers)
357
+ # while login and a cached tree load go ahead.
358
+ dialed = getattr(sub, "_dialed", None)
359
+ if not (dialed.result() if dialed is not None else sub_reply.get()):
346
360
  raise McxConnectionError(
347
361
  "Failed to establish subscribe connection: {}:{}".format(sub_address, sub_port)
348
362
  )
@@ -360,7 +374,18 @@ def connect(
360
374
  )
361
375
 
362
376
  # Requesting a parameter tree
363
- param_tree_reply = req.getParameterTree()
377
+ # On a cache miss, wait for the cookie-probe verdict so an old
378
+ # server (no compressed tree handler, silently drops the request)
379
+ # gets the plain tree straight away instead of waiting on a reply
380
+ # that never comes. A cache hit never calls this.
381
+ def compressed_hint():
382
+ try:
383
+ sub_reply.get(timeout_ms=_request_utils.CAPABILITY_PROBE_TIMEOUT_MS + 3000)
384
+ except Exception:
385
+ return None
386
+ return getattr(sub, "_server_has_routing", None)
387
+
388
+ param_tree_reply = req.getParameterTree(compressed=compressed_hint)
364
389
  tree = param_tree_reply.get()
365
390
  param_tree.load(tree)
366
391
  except Exception:
@@ -21,7 +21,10 @@ import os
21
21
  import tempfile
22
22
  import zlib
23
23
  from threading import Event
24
- from typing import Any, Callable, Optional, Tuple
24
+ from typing import Any, Callable, Optional, Tuple, Union
25
+
26
+ from pynng._nng import lib # type: ignore[import-untyped]
27
+ from pynng.exceptions import check_err # type: ignore[import-untyped]
25
28
 
26
29
  from motorcortex.exceptions import McxConnectionError
27
30
  from motorcortex.setup_logger import logger
@@ -29,6 +32,12 @@ from motorcortex.setup_logger import logger
29
32
 
30
33
  _CACHE_PREFIX = "mcx-python-pt"
31
34
 
35
+ #: Old servers silently drop unknown request hashes — cap how long a
36
+ #: capability probe (routing cookie, compressed tree) may wait so a
37
+ #: pre-2.36 server costs at most 2 s per probe. Mirrors
38
+ #: CAPABILITY_PROBE_TIMEOUT_MS in motorcortex-cpp and motorcortex-js.
39
+ CAPABILITY_PROBE_TIMEOUT_MS = 2000
40
+
32
41
 
33
42
  def parse_connect_kwargs(
34
43
  conn_timeout_ms: int = 0,
@@ -114,6 +123,7 @@ def fetch_parameter_tree(
114
123
  protobuf_types: Any,
115
124
  socket: Any,
116
125
  url: Optional[str] = None,
126
+ compressed: Union[Optional[bool], Callable[[], Optional[bool]]] = None,
117
127
  ) -> Any:
118
128
  """Fetch the parameter tree, hitting the local cache first.
119
129
 
@@ -132,6 +142,19 @@ def fetch_parameter_tree(
132
142
  :func:`send_and_recv`.
133
143
  url: Connection URL — hashed into the cache filename so two
134
144
  engines with the same ``tree_hash`` don't collide.
145
+ compressed: Server-capability hint from the routing-cookie probe
146
+ (same scheme as motorcortex-js). The cookie handler and the
147
+ compressed tree handler both landed in core 2.36, so:
148
+ ``False`` — the server did not hand out a cookie, so it has no
149
+ compressed handler: skip straight to the plain request.
150
+ ``True`` — the server is >= 2.36: request compressed with the
151
+ socket's own timeout, so a big reply on a slow link never
152
+ races a short cap.
153
+ ``None`` — unknown (no Subscribe, raw ``Request`` use): try
154
+ compressed, capped at ``CAPABILITY_PROBE_TIMEOUT_MS``.
155
+ May also be a callable returning one of those; it is only
156
+ called on a cache miss, so a still-running probe never delays
157
+ a cached tree load.
135
158
 
136
159
  Returns:
137
160
  A ``ParameterTreeMsg`` — either the cached one or the freshly
@@ -152,10 +175,10 @@ def fetch_parameter_tree(
152
175
  ``ParameterTreeMsg`` type, so callers (and the cache write below)
153
176
  can't tell the difference. Tried for every URL scheme (ipc, tcp,
154
177
  ws, tls+tcp, wss) — the request is just bytes over
155
- :func:`send_and_recv`. ``send_and_recv`` in this repo has no
156
- per-call timeout knob, so on an old server the compressed attempt
157
- costs at most one ``recv_timeout`` (the socket's own) before
158
- falling back.
178
+ :func:`send_and_recv`. Old servers never answer the unknown hash, and
179
+ the socket's ``recv_timeout`` is infinite by default, so an uncapped
180
+ attempt would hang the connect forever hence the ``compressed``
181
+ hint and the per-request probe cap.
159
182
  """
160
183
  path = parameter_tree_cache_path(url, tree_hash)
161
184
  cached = load_parameter_tree_file(path, protobuf_types)
@@ -164,11 +187,20 @@ def fetch_parameter_tree(
164
187
  return cached
165
188
  logger.debug("[REQUEST] Failed to find parameter tree in the cache")
166
189
 
190
+ if callable(compressed):
191
+ compressed = compressed()
192
+ if compressed is False:
193
+ logger.debug("[REQUEST] server predates the compressed tree — requesting plain")
194
+ request_msg = protobuf_types.createType("motorcortex.GetParameterTreeMsg")
195
+ handle = send_and_recv(socket, protobuf_types.encode(request_msg), protobuf_types)
196
+ return save_parameter_tree_file(path, handle)
197
+
167
198
  try:
168
199
  compressed_request = protobuf_types.createType(
169
200
  "motorcortex.GetParameterTreeCompressedMsg")
170
201
  compressed_reply = send_and_recv(
171
- socket, protobuf_types.encode(compressed_request), protobuf_types)
202
+ socket, protobuf_types.encode(compressed_request), protobuf_types,
203
+ timeout_ms=None if compressed else CAPABILITY_PROBE_TIMEOUT_MS)
172
204
  if compressed_reply is not None \
173
205
  and getattr(compressed_reply, "status", 1) == 0 \
174
206
  and getattr(compressed_reply, "compressed", b""):
@@ -209,6 +241,7 @@ def send_and_recv(
209
241
  socket: Any,
210
242
  encoded_msg: Any,
211
243
  protobuf_types: Optional[Any],
244
+ timeout_ms: Optional[int] = None,
212
245
  ) -> Any:
213
246
  """One-shot synchronous request/reply over an nng Req0 socket.
214
247
 
@@ -225,6 +258,12 @@ def send_and_recv(
225
258
  encoded_msg: Bytes to send.
226
259
  protobuf_types: If truthy, decoded wire reply is returned.
227
260
  Otherwise the raw buffer is returned.
261
+ timeout_ms: Receive timeout for this one request, set on the nng
262
+ context so the worker thread is released when it expires
263
+ (``pynng.Timeout`` is raised). ``None`` keeps the socket's
264
+ ``recv_timeout``. A ``Reply.get(timeout_ms=...)`` alone only
265
+ stops the caller waiting — the worker stays blocked in
266
+ ``recv()`` for as long as the socket timeout allows.
228
267
 
229
268
  Returns:
230
269
  Decoded reply (when ``protobuf_types`` supplied and buffer is
@@ -232,6 +271,8 @@ def send_and_recv(
232
271
  """
233
272
  ctx = socket.new_context()
234
273
  try:
274
+ if timeout_ms is not None:
275
+ check_err(lib.nng_ctx_set_ms(ctx.context, b"recv-timeout", timeout_ms))
235
276
  ctx.send(encoded_msg)
236
277
  buffer = ctx.recv()
237
278
  if buffer:
@@ -239,7 +280,10 @@ def send_and_recv(
239
280
  return protobuf_types.decode(buffer)
240
281
  return buffer
241
282
  except Exception as e:
242
- logger.error("[SEND] Error during send/recv: %s: %s", type(e).__name__, e)
283
+ # A capped request timing out is an expected probe outcome on old
284
+ # servers, not an error worth shouting about.
285
+ log = logger.debug if timeout_ms is not None else logger.error
286
+ log("[SEND] Error during send/recv: %s: %s", type(e).__name__, e)
243
287
  raise
244
288
  finally:
245
289
  ctx.close()
@@ -344,12 +344,16 @@ class Request:
344
344
  self._callback_handler.stop()
345
345
  self._pool.shutdown(wait=True)
346
346
 
347
- def send(self, encoded_msg: Any, do_not_decode_reply: bool = False) -> "Reply[Any]":
347
+ def send(self, encoded_msg: Any, do_not_decode_reply: bool = False,
348
+ timeout_ms: Optional[int] = None) -> "Reply[Any]":
348
349
  """Send an encoded message to the server.
349
350
 
350
351
  Args:
351
352
  encoded_msg: Encoded protobuf message.
352
353
  do_not_decode_reply: If True, do not decode the reply.
354
+ timeout_ms: Receive timeout for this request only; on expiry
355
+ the reply fails with ``pynng.Timeout``. Defaults to the
356
+ socket's ``recv_timeout_ms``.
353
357
 
354
358
  Returns:
355
359
  Reply: A promise for the server's reply.
@@ -368,7 +372,7 @@ class Request:
368
372
  )
369
373
  return Reply(self._pool.submit(
370
374
  _request_utils.send_and_recv, self._socket, encoded_msg,
371
- None if do_not_decode_reply else self._protobuf_types,
375
+ None if do_not_decode_reply else self._protobuf_types, timeout_ms,
372
376
  ))
373
377
 
374
378
  def login(self, login: str, password: str) -> "Reply[_pb.StatusMsg]":
@@ -412,7 +416,7 @@ class Request:
412
416
  # encoding and sending data
413
417
  return self.send(self._protobuf_types.encode(param_tree_hash_msg))
414
418
 
415
- def getSubCookie(self) -> "Reply[_pb.SubCookieMsg]":
419
+ def getSubCookie(self, timeout_ms: Optional[int] = None) -> "Reply[_pb.SubCookieMsg]":
416
420
  """Request a single-use pub/sub identity cookie (internal plumbing).
417
421
 
418
422
  Doubles as the routing capability probe: old servers (<2.36.1)
@@ -420,16 +424,31 @@ class Request:
420
424
  FAILED — both mean "no routing", not an error. Used by Subscribe
421
425
  for the data-channel hello; not needed in application code.
422
426
 
427
+ Args:
428
+ timeout_ms: Receive timeout for this request (see ``send``).
429
+
423
430
  Returns:
424
431
  Reply: A promise for the sub cookie.
425
432
  """
426
433
  msg = self._protobuf_types.createType('motorcortex.GetSubCookieMsg')
427
- return self.send(self._protobuf_types.encode(msg))
434
+ return self.send(self._protobuf_types.encode(msg), timeout_ms=timeout_ms)
428
435
 
429
- def getParameterTree(self) -> "Reply[_pb.ParameterTreeMsg]":
436
+ def getParameterTree(
437
+ self,
438
+ compressed: Union[Optional[bool], Callable[[], Optional[bool]]] = None,
439
+ ) -> "Reply[_pb.ParameterTreeMsg]":
430
440
  """
431
441
  Request a parameter tree from the server.
432
442
 
443
+ Args:
444
+ compressed: Server-capability hint for the compressed tree
445
+ transport: ``True`` / ``False`` if the routing-cookie
446
+ probe showed the server is / is not >= 2.36, ``None``
447
+ (default) to try it with a short probe timeout, or a
448
+ callable returning one of those, evaluated only on a
449
+ cache miss. See
450
+ :func:`motorcortex._request_utils.fetch_parameter_tree`.
451
+
433
452
  Returns:
434
453
  Reply: A promise for the parameter tree.
435
454
  """
@@ -437,6 +456,7 @@ class Request:
437
456
  return Reply(self._pool.submit(
438
457
  self._getParameterTree,
439
458
  self.getParameterTreeHash(), self._protobuf_types, self._socket, self._url,
459
+ compressed,
440
460
  ))
441
461
 
442
462
  def save(self, path: str, file_name: str) -> "Reply[_pb.StatusMsg]":
@@ -692,6 +712,7 @@ class Request:
692
712
  protobuf_types: MessageTypes,
693
713
  socket: Req0,
694
714
  url: Optional[str] = None,
715
+ compressed: Union[Optional[bool], Callable[[], Optional[bool]]] = None,
695
716
  ) -> Any:
696
717
  """Thunk that unwraps ``hash_reply`` and delegates to
697
718
  :func:`motorcortex._request_utils.fetch_parameter_tree`.
@@ -702,6 +723,6 @@ class Request:
702
723
  """
703
724
  tree_hash = hash_reply.get()
704
725
  return _request_utils.fetch_parameter_tree(
705
- tree_hash.hash, protobuf_types, socket, url,
726
+ tree_hash.hash, protobuf_types, socket, url, compressed,
706
727
  )
707
728
 
@@ -25,10 +25,10 @@ if TYPE_CHECKING: # pragma: no cover
25
25
  from motorcortex.message_types import MessageTypes
26
26
 
27
27
 
28
- #: Old servers silently drop unknown request hashes — cap how long a
29
- #: capability probe may wait so a pre-2.36.1 server costs at most 2 s
30
- #: per connect attempt. Mirrors CAPABILITY_PROBE_TIMEOUT_MS in cpp.
31
- CAPABILITY_PROBE_TIMEOUT_MS = 2000
28
+ #: Re-exported from ``_request_utils`` (shared with the compressed-tree
29
+ #: probe) old servers silently drop unknown request hashes, so a
30
+ #: pre-2.36.1 server costs at most 2 s per connect attempt.
31
+ CAPABILITY_PROBE_TIMEOUT_MS = _request_utils.CAPABILITY_PROBE_TIMEOUT_MS
32
32
 
33
33
  #: First upstream message on a routed data-channel stream: magic + the
34
34
  #: single-use cookie fetched via ``Request.getSubCookie``.
@@ -39,7 +39,12 @@ def _fetch_cookie(req: "Request") -> Optional[str]:
39
39
  """Best-effort single-use cookie fetch. None is not an error — it
40
40
  selects the anonymous (pre-routing) subscribe path."""
41
41
  try:
42
- msg = req.getSubCookie().get(timeout_ms=CAPABILITY_PROBE_TIMEOUT_MS)
42
+ # The cap goes on the nng context, not only on Reply.get: an old
43
+ # server never answers, and with the default infinite socket
44
+ # recv_timeout the request worker would stay blocked for good —
45
+ # one leaked worker per (re)connect until the pool is exhausted.
46
+ msg = req.getSubCookie(timeout_ms=CAPABILITY_PROBE_TIMEOUT_MS).get(
47
+ timeout_ms=CAPABILITY_PROBE_TIMEOUT_MS + 1000)
43
48
  except Exception as e:
44
49
  logger.debug("[SUBSCRIBE] cookie probe failed/timed out: %s", e)
45
50
  return None
@@ -117,6 +122,17 @@ class Subscribe:
117
122
  self._url: Optional[str] = None
118
123
  self._tls_config: Optional[TLSConfig] = None
119
124
  self._conn_timeout_ms: int = 1000
125
+ #: Outcome of the last conclusive cookie probe: True if the server
126
+ #: handed out a cookie (core >= 2.36, which also has the compressed
127
+ #: tree handler), False if it did not, None if never probed. Passed
128
+ #: to ``Request.getParameterTree`` as the compressed-tree hint.
129
+ self._server_has_routing: Optional[bool] = None
130
+ #: Resolves with the initial dial's outcome, before the cookie
131
+ #: probe and hello. ``motorcortex.connect`` waits on this instead
132
+ #: of the full ``connect()`` Reply so the ~2 s probe against an
133
+ #: old server overlaps login and a cached tree load (mirrors
134
+ #: motorcortex-js, which backgrounds the routing hello).
135
+ self._dialed: "Future[bool]" = Future()
120
136
  self._req: Request = req
121
137
  self._protobuf_types: Any = protobuf_types
122
138
  self._subscriptions: Dict[int, Subscription] = dict()
@@ -247,11 +263,19 @@ class Subscribe:
247
263
  return Reply(future)
248
264
 
249
265
  def _initial_connect(self) -> bool:
266
+ try:
267
+ return self._initial_connect_body()
268
+ finally:
269
+ # Every exit path must resolve it — connect() callers block on it.
270
+ if not self._dialed.done():
271
+ self._dialed.set_result(False)
272
+
273
+ def _initial_connect_body(self) -> bool:
250
274
  if self._closed:
251
275
  # close() beat this pool worker to it before it even started.
252
276
  return False
253
277
 
254
- ok = self._dial_and_hello(allow_cookie=True)
278
+ ok = self._dial_and_hello(allow_cookie=True, dialed=self._dialed)
255
279
 
256
280
  if not ok:
257
281
  self._connection_state = ConnectionState.CONNECTION_FAILED
@@ -276,7 +300,8 @@ class Subscribe:
276
300
  return False
277
301
  return True
278
302
 
279
- def _dial_and_hello(self, allow_cookie: bool) -> bool:
303
+ def _dial_and_hello(self, allow_cookie: bool,
304
+ dialed: "Optional[Future[bool]]" = None) -> bool:
280
305
  """Dial FIRST, then fetch the single-use cookie, then hello.
281
306
 
282
307
  Fetch-first would burn a cookie on every attempt against a dead
@@ -299,6 +324,8 @@ class Subscribe:
299
324
  except Exception as e:
300
325
  logger.debug("[SUBSCRIBE] dial failed: %s", e)
301
326
  return False
327
+ if dialed is not None:
328
+ dialed.set_result(True)
302
329
 
303
330
  cookie = None
304
331
  if allow_cookie:
@@ -320,6 +347,7 @@ class Subscribe:
320
347
  self._req.connectionState().name)
321
348
  self._stream.close()
322
349
  return False
350
+ self._server_has_routing = cookie is not None
323
351
  if cookie:
324
352
  try:
325
353
  # Fire-and-forget by design (no ack on the wire): a send
@@ -469,7 +497,6 @@ class Subscribe:
469
497
 
470
498
  self._connection_state = ConnectionState.DISCONNECTING
471
499
  for s in self._subscriptions.values():
472
- s.done()
473
500
  s.stop() # stop the ordered dispatch consumer thread
474
501
  self._subscriptions.clear()
475
502
 
@@ -525,8 +552,6 @@ class Subscribe:
525
552
  # find and remove subscription
526
553
  if sub_id in self._subscriptions:
527
554
  sub = self._subscriptions[sub_id]
528
- # stop sub update thread
529
- sub.done()
530
555
  sub.stop() # stop the ordered dispatch consumer thread
531
556
  del self._subscriptions[sub_id]
532
557
 
@@ -0,0 +1 @@
1
+ __version__ = '1.2.1'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: motorcortex-python
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Python bindings for Motorcortex Engine
5
5
  Home-page: https://www.motorcortex.io
6
6
  Author: Alexey Zakharov
@@ -1 +0,0 @@
1
- __version__ = '1.2.0'