motorcortex-python 1.0.0rc1__tar.gz → 1.0.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 (36) hide show
  1. {motorcortex_python-1.0.0rc1/motorcortex_python.egg-info → motorcortex_python-1.0.1}/PKG-INFO +1 -1
  2. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/__init__.py +106 -20
  3. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/_request_builders.py +1 -1
  4. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/_request_utils.py +2 -2
  5. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/message_types.py +9 -24
  6. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/request.py +1 -1
  7. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/session.py +1 -1
  8. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/subscribe.py +2 -2
  9. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/subscription.py +2 -2
  10. motorcortex_python-1.0.1/motorcortex/version.py +1 -0
  11. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1/motorcortex_python.egg-info}/PKG-INFO +1 -1
  12. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/pyproject.toml +1 -1
  13. motorcortex_python-1.0.0rc1/motorcortex/version.py +0 -1
  14. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/LICENSE +0 -0
  15. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/MANIFEST.in +0 -0
  16. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/README.md +0 -0
  17. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/_connection_state.py +0 -0
  18. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/_subscribe_dispatch.py +0 -0
  19. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/exceptions.py +0 -0
  20. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/init_threads.py +0 -0
  21. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/motorcortex_hash.json +0 -0
  22. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/motorcortex_pb2.py +0 -0
  23. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/motorcortex_pb2.pyi +0 -0
  24. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/nng_url.py +0 -0
  25. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/parameter_tree.py +0 -0
  26. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/py.typed +0 -0
  27. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/reply.py +0 -0
  28. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/setup_logger.py +0 -0
  29. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/state_callback_handler.py +0 -0
  30. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex/timespec.py +0 -0
  31. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex_python.egg-info/SOURCES.txt +0 -0
  32. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex_python.egg-info/dependency_links.txt +0 -0
  33. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex_python.egg-info/requires.txt +0 -0
  34. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/motorcortex_python.egg-info/top_level.txt +0 -0
  35. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/setup.cfg +0 -0
  36. {motorcortex_python-1.0.0rc1 → motorcortex_python-1.0.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: motorcortex-python
3
- Version: 1.0.0rc1
3
+ Version: 1.0.1
4
4
  Summary: Python bindings for Motorcortex Engine
5
5
  Home-page: https://www.motorcortex.io
6
6
  Author: Alexey Zakharov
@@ -14,6 +14,7 @@ Provides high-level APIs for communication, login, and data exchange using proto
14
14
  See documentation for usage examples.
15
15
  """
16
16
 
17
+ from enum import IntEnum as _IntEnum
17
18
  from typing import Any
18
19
 
19
20
  from motorcortex.version import __version__
@@ -39,6 +40,39 @@ from motorcortex.exceptions import (
39
40
  McxLoginError,
40
41
  McxTimeout,
41
42
  )
43
+ from motorcortex.motorcortex_pb2 import StatusCode as _PbStatusCode
44
+ from motorcortex.setup_logger import logger # not re-exported — ``__all__`` gates package surface
45
+
46
+ # Build an ``IntEnum`` facade over the protobuf ``StatusCode`` enum so
47
+ # callers can write ``motorcortex.StatusCode.OK`` without reaching into
48
+ # the generated protobuf module. Subclassing ``int`` means comparisons
49
+ # like ``reply.status == StatusCode.OK`` work against the raw ints that
50
+ # come back on the wire.
51
+ StatusCode = _IntEnum(
52
+ "StatusCode",
53
+ {v.name: v.number for v in _PbStatusCode.DESCRIPTOR.values},
54
+ )
55
+ StatusCode.__doc__ = (
56
+ "Motorcortex response status codes. Members mirror the ``StatusCode`` "
57
+ "enum in the wire protocol. Use ``motorcortex.StatusCode.OK`` (or the "
58
+ "flat re-exports such as ``motorcortex.OK``) to compare against "
59
+ "``reply.status``."
60
+ )
61
+
62
+ # Flat re-exports for the conventional ``motorcortex.OK`` idiom. Values
63
+ # are members of :class:`StatusCode` and therefore also ``int``.
64
+ OK = StatusCode.OK
65
+ READ_ONLY_MODE = StatusCode.READ_ONLY_MODE
66
+ FAILED = StatusCode.FAILED
67
+ FAILED_TO_DECODE = StatusCode.FAILED_TO_DECODE
68
+ SUB_LIST_IS_FULL = StatusCode.SUB_LIST_IS_FULL
69
+ WRONG_PARAMETER_PATH = StatusCode.WRONG_PARAMETER_PATH
70
+ FAILED_TO_SET_REQUESTED_FRQ = StatusCode.FAILED_TO_SET_REQUESTED_FRQ
71
+ FAILED_TO_OPEN_FILE = StatusCode.FAILED_TO_OPEN_FILE
72
+ GROUP_LIST_IS_FULL = StatusCode.GROUP_LIST_IS_FULL
73
+ WRONG_PASSWORD = StatusCode.WRONG_PASSWORD
74
+ USER_NOT_LOGGED_IN = StatusCode.USER_NOT_LOGGED_IN
75
+ PERMISSION_DENIED = StatusCode.PERMISSION_DENIED
42
76
 
43
77
  # ``__all__`` is the 1.0 public API surface. Anything not listed here —
44
78
  # including module-level helpers defined below (``parseUrl``,
@@ -72,6 +106,20 @@ __all__ = [
72
106
  "McxConnectionError",
73
107
  "McxLoginError",
74
108
  "McxTimeout",
109
+ # Status codes
110
+ "StatusCode",
111
+ "OK",
112
+ "READ_ONLY_MODE",
113
+ "FAILED",
114
+ "FAILED_TO_DECODE",
115
+ "SUB_LIST_IS_FULL",
116
+ "WRONG_PARAMETER_PATH",
117
+ "FAILED_TO_SET_REQUESTED_FRQ",
118
+ "FAILED_TO_OPEN_FILE",
119
+ "GROUP_LIST_IS_FULL",
120
+ "WRONG_PASSWORD",
121
+ "USER_NOT_LOGGED_IN",
122
+ "PERMISSION_DENIED",
75
123
  # Tuning
76
124
  "init_nng_threads",
77
125
  ]
@@ -133,6 +181,56 @@ def makeUrl(address: str, port: int | None) -> str:
133
181
  return address
134
182
 
135
183
 
184
+ def _reconnect_state_update(
185
+ req: Any, sub: Any, state: "ConnectionState", *,
186
+ motorcortex_types: "MessageTypes",
187
+ login: Any,
188
+ password: Any,
189
+ token_interval_sec: float,
190
+ initial_connect_done: list,
191
+ ) -> None:
192
+ """Default ``state_update`` body for :func:`connect` in reconnect mode.
193
+
194
+ Fires on every state transition from the ``StateCallbackHandler``
195
+ worker thread. Only acts on the ``CONNECTION_OK`` edge *after* the
196
+ initial connect has completed — i.e. on a successful **reconnect**:
197
+
198
+ 1. Try to restore the previous session via ``restoreSession(token)``.
199
+ Silent 5-second timeout + generic exception swallow — on any
200
+ failure we fall through to step 2 so a torn-down session never
201
+ leaves the reconnect half-authenticated.
202
+ 2. Otherwise re-login with the originally supplied credentials.
203
+ 3. Kick the token-refresh timer and resubscribe all existing groups
204
+ on the Subscribe side.
205
+
206
+ Extracted from a closure inside :func:`connect` so it can be
207
+ unit-tested without spinning up a real server. ``initial_connect_done``
208
+ is passed as a 1-element list so the caller (``connect``) can flip
209
+ the flag after the first successful connect without rebinding.
210
+ """
211
+ if state != ConnectionState.CONNECTION_OK or not initial_connect_done[0]:
212
+ return
213
+
214
+ restored = False
215
+ if req.token:
216
+ try:
217
+ restore_reply = req.restoreSession(req.token)
218
+ restore_msg = restore_reply.get(timeout_ms=5000)
219
+ motorcortex_msg = motorcortex_types.motorcortex()
220
+ if restore_msg and restore_msg.status == motorcortex_msg.OK:
221
+ restored = True
222
+ logger.debug("[SESSION] Session restored using token")
223
+ except Exception as e:
224
+ logger.debug(f"[SESSION] Token restore failed: {e}")
225
+
226
+ if not restored:
227
+ logger.debug("[SESSION] Falling back to login")
228
+ req.login(login, password).get()
229
+
230
+ req._startTokenRefresh(token_interval_sec)
231
+ sub.resubscribe()
232
+
233
+
136
234
  def connect(
137
235
  url: str,
138
236
  motorcortex_types: "MessageTypes",
@@ -185,26 +283,14 @@ def connect(
185
283
  if reconnect and not kwargs.get("state_update"):
186
284
 
187
285
  def stateUpdate(req, sub, state):
188
- if state == ConnectionState.CONNECTION_OK and initial_connect_done[0]:
189
- # Try to restore session using token, fall back to login
190
- restored = False
191
- if req.token:
192
- try:
193
- restore_reply = req.restoreSession(req.token)
194
- restore_msg = restore_reply.get(timeout_ms=5000)
195
- motorcortex_msg = motorcortex_types.motorcortex()
196
- if restore_msg and restore_msg.status == motorcortex_msg.OK:
197
- restored = True
198
- logger.debug("[SESSION] Session restored using token")
199
- except Exception as e:
200
- logger.debug(f"[SESSION] Token restore failed: {e}")
201
-
202
- if not restored:
203
- logger.debug("[SESSION] Falling back to login")
204
- req.login(kwargs.get("login"), kwargs.get("password")).get()
205
-
206
- req._startTokenRefresh(token_interval_sec)
207
- sub.resubscribe()
286
+ _reconnect_state_update(
287
+ req, sub, state,
288
+ motorcortex_types=motorcortex_types,
289
+ login=kwargs.get("login"),
290
+ password=kwargs.get("password"),
291
+ token_interval_sec=token_interval_sec,
292
+ initial_connect_done=initial_connect_done,
293
+ )
208
294
 
209
295
  kwargs.update(state_update=stateUpdate)
210
296
 
@@ -21,7 +21,7 @@ from typing import Any, Optional, TYPE_CHECKING
21
21
 
22
22
  from motorcortex.setup_logger import logger
23
23
 
24
- if TYPE_CHECKING:
24
+ if TYPE_CHECKING: # pragma: no cover
25
25
  from motorcortex.message_types import MessageTypes
26
26
  from motorcortex.parameter_tree import ParameterTree
27
27
 
@@ -242,7 +242,7 @@ def _purge_stale_cache_files(path: str) -> None:
242
242
  try:
243
243
  os.unlink(match)
244
244
  logger.debug("[REQUEST] Evicted stale cache file: %s", entry)
245
- except OSError:
245
+ except OSError: # pragma: no cover
246
246
  pass
247
247
 
248
248
 
@@ -276,7 +276,7 @@ def save_parameter_tree_file(path: str, parameter_tree: Any) -> Any:
276
276
  with os.fdopen(fd, "w") as outfile:
277
277
  outfile.write(json.dumps(envelope))
278
278
  os.replace(tmp_path, path)
279
- except Exception:
279
+ except Exception: # pragma: no cover
280
280
  # Clean up the temp file if the rename never ran.
281
281
  if os.path.exists(tmp_path):
282
282
  try:
@@ -5,35 +5,20 @@
5
5
  # All rights reserved. Copyright (c) 2016 VECTIONEER.
6
6
  #
7
7
 
8
- from __future__ import unicode_literals
9
8
  import motorcortex.motorcortex_pb2
10
9
  import logging
11
- import sys
12
10
  import os
11
+ from importlib.machinery import SourceFileLoader
12
+ from types import ModuleType
13
13
 
14
- if sys.version_info[0] >= 3:
15
- from importlib.machinery import SourceFileLoader
16
14
 
17
- if sys.version_info[1] < 4:
18
- def importLibrary(name, path):
19
- return SourceFileLoader(name, path).load_module()
20
- else:
21
- from types import ModuleType
15
+ def importLibrary(name, path):
16
+ loader = SourceFileLoader(name, path)
17
+ mod = ModuleType(loader.name)
18
+ loader.exec_module(mod)
19
+ return mod
22
20
 
23
21
 
24
- def importLibrary(name, path):
25
- loader = SourceFileLoader(name, path)
26
- mod = ModuleType(loader.name)
27
- loader.exec_module(mod)
28
- return mod
29
- else:
30
- from builtins import bytes
31
- from imp import load_source
32
-
33
-
34
- def importLibrary(name, path):
35
- return load_source(name, path)
36
-
37
22
  from json import load
38
23
  from inspect import ismodule
39
24
  from typing import Dict
@@ -270,7 +255,7 @@ class MessageTypes(object):
270
255
  self._hashes_by_name[name] = hash
271
256
  self._types_by_hash[hash] = PrimitiveTypes(name)
272
257
  logging.debug(f"Loaded types from {name}")
273
- except Exception:
258
+ except Exception: # pragma: no cover
274
259
  # Best-effort — not every namespace declares the enum we're
275
260
  # probing for. Swallowed by design, but scoped to Exception
276
261
  # so KeyboardInterrupt / SystemExit still propagate.
@@ -282,7 +267,7 @@ class MessageTypes(object):
282
267
  for key in enum.DESCRIPTOR.values:
283
268
  setattr(module, key.name, key.number)
284
269
  logging.debug(f"Loaded enumerator {enum_name}")
285
- except Exception:
270
+ except Exception: # pragma: no cover
286
271
  # Same best-effort pattern as _loadPrimitives.
287
272
  pass
288
273
 
@@ -59,7 +59,7 @@ def _register_shutdown(inst: "Request") -> None:
59
59
  try:
60
60
  import threading
61
61
  threading._register_atexit(_close_at_exit, inst) # type: ignore[attr-defined]
62
- except (AttributeError, ImportError):
62
+ except (AttributeError, ImportError): # pragma: no cover
63
63
  atexit.register(_close_at_exit, inst)
64
64
 
65
65
 
@@ -41,7 +41,7 @@ from typing import Any, Optional, Type, TYPE_CHECKING
41
41
 
42
42
  from motorcortex.exceptions import McxConnectionError
43
43
 
44
- if TYPE_CHECKING:
44
+ if TYPE_CHECKING: # pragma: no cover
45
45
  from motorcortex.request import Request, ConnectionState
46
46
  from motorcortex.subscribe import Subscribe
47
47
  from motorcortex.message_types import MessageTypes
@@ -20,7 +20,7 @@ from motorcortex.setup_logger import logger
20
20
  from motorcortex.nng_url import NngUrl
21
21
  from motorcortex import _connection_state, _request_utils, _subscribe_dispatch
22
22
 
23
- if TYPE_CHECKING:
23
+ if TYPE_CHECKING: # pragma: no cover
24
24
  from motorcortex.message_types import MessageTypes
25
25
 
26
26
 
@@ -64,7 +64,7 @@ def _register_shutdown(inst: "Subscribe") -> None:
64
64
  try:
65
65
  import threading
66
66
  threading._register_atexit(_close_at_exit, inst) # type: ignore[attr-defined]
67
- except (AttributeError, ImportError):
67
+ except (AttributeError, ImportError): # pragma: no cover
68
68
  atexit.register(_close_at_exit, inst)
69
69
 
70
70
 
@@ -272,8 +272,8 @@ class Subscription(object):
272
272
 
273
273
  Examples:
274
274
  >>> def update(parameters):
275
- >>> print(parameters) #list of Parameter tuples
276
- >>> ...
275
+ >>> # parameters is a list of Parameter tuples
276
+ >>> print(parameters)
277
277
  >>> data_sub.notify(update)
278
278
 
279
279
  """
@@ -0,0 +1 @@
1
+ __version__ = '1.0.1'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: motorcortex-python
3
- Version: 1.0.0rc1
3
+ Version: 1.0.1
4
4
  Summary: Python bindings for Motorcortex Engine
5
5
  Home-page: https://www.motorcortex.io
6
6
  Author: Alexey Zakharov
@@ -27,7 +27,7 @@ omit = [
27
27
  [tool.coverage.report]
28
28
  show_missing = true
29
29
  skip_covered = false
30
- fail_under = 60
30
+ fail_under = 90
31
31
  exclude_lines = [
32
32
  "pragma: no cover",
33
33
  "raise NotImplementedError",
@@ -1 +0,0 @@
1
- __version__ = '1.0.0rc1'