muxit 0.1.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.
muxit/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ """Drive the instruments you set up in Muxit from Python.
2
+
3
+ import muxit
4
+
5
+ m = muxit.connect() # Muxit on this computer
6
+ psu = m.connector("psu")
7
+ psu.voltage = 3.0 # write
8
+ print(psu.voltage) # read (Muxit's latest polled value)
9
+ psu.reset() # action
10
+
11
+ Docs: https://docs.muxit.io/guides/python-client
12
+ """
13
+
14
+ from ._version import __version__
15
+ from .client import Muxit, Settings, connect
16
+ from .connector import Connector
17
+ from .errors import (
18
+ AuthenticationFailed,
19
+ ConnectionFailed,
20
+ LimitReached,
21
+ MuxitError,
22
+ NotFound,
23
+ ReadOnly,
24
+ SafetyBlocked,
25
+ SimulatedWarning,
26
+ Timeout,
27
+ UnknownName,
28
+ )
29
+
30
+ __all__ = [
31
+ "__version__",
32
+ "connect",
33
+ "Muxit",
34
+ "Connector",
35
+ "Settings",
36
+ "MuxitError",
37
+ "ConnectionFailed",
38
+ "AuthenticationFailed",
39
+ "NotFound",
40
+ "UnknownName",
41
+ "ReadOnly",
42
+ "SafetyBlocked",
43
+ "LimitReached",
44
+ "Timeout",
45
+ "SimulatedWarning",
46
+ ]
muxit/__main__.py ADDED
@@ -0,0 +1,286 @@
1
+ """Command line: check the connection and try things without writing Python.
2
+
3
+ python -m muxit doctor # is everything set up?
4
+ python -m muxit list [connector] # what is there
5
+ python -m muxit get psu voltage
6
+ python -m muxit set psu voltage 3
7
+ python -m muxit call psu reset
8
+ python -m muxit demo # read and write the built-in test device
9
+
10
+ Every command takes --host, --port, --password, --token and --timeout, or the
11
+ MUXIT_HOST / MUXIT_PORT / MUXIT_PASSWORD / MUXIT_TOKEN / MUXIT_TIMEOUT variables.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import platform
19
+ import socket
20
+ import sys
21
+ import time
22
+ import warnings
23
+ from typing import Any, List, Optional
24
+
25
+ from . import errors
26
+ from ._version import __version__
27
+ from .client import Muxit, Settings, login
28
+
29
+
30
+ def _out(text: str = "") -> None:
31
+ print(text, flush=True)
32
+
33
+
34
+ def _status(tag: str, text: str) -> None:
35
+ # ASCII tags: a Windows console may not print check marks.
36
+ _out(f" [{tag:^4}] {text}")
37
+
38
+
39
+ def _value(text: str) -> Any:
40
+ """Command-line value: JSON when it parses (3, true, [1,2], {"a":1}), else the plain string."""
41
+ try:
42
+ return json.loads(text)
43
+ except ValueError:
44
+ return text
45
+
46
+
47
+ def _show(value: Any) -> str:
48
+ return json.dumps(value, ensure_ascii=False) if not isinstance(value, str) else value
49
+
50
+
51
+ def _connect(args: argparse.Namespace) -> Muxit:
52
+ return Muxit(args.host, args.port, password=args.password, token=args.token, timeout=args.timeout)
53
+
54
+
55
+ # ── doctor ───────────────────────────────────────────────────────
56
+
57
+ def doctor(args: argparse.Namespace) -> int:
58
+ _out(f"muxit {__version__} (Python {platform.python_version()}, {platform.system()})")
59
+ _out()
60
+ try:
61
+ s = Settings(args.host, args.port, args.password, args.token, args.timeout)
62
+ except errors.MuxitError as e:
63
+ _status("FAIL", str(e))
64
+ return 1
65
+ _out("Settings")
66
+ _status("", f"host {s.host} (from {s.source['host']})")
67
+ _status("", f"port {s.port} (from {s.source['port']})")
68
+ _status("", f"password {'set' if s.password else 'not set'} (from {s.source['password']})")
69
+ if s.token:
70
+ _status("", f"token set (from {s.source['token']})")
71
+ _status("", f"address {s.ws_url}")
72
+ _out()
73
+
74
+ _out("Checks")
75
+ try:
76
+ with socket.create_connection((s.host, s.port), timeout=min(s.timeout, 5)):
77
+ pass
78
+ _status("OK", f"something is listening on {s.host}:{s.port}")
79
+ except OSError as e:
80
+ _status("FAIL", f"nothing answers on {s.host}:{s.port} ({e})")
81
+ if s.is_local:
82
+ _out(" Start Muxit on this computer, or check the port in its startup banner.")
83
+ else:
84
+ _out(" Is Muxit running on that computer, with remote access on (Settings > Remote")
85
+ _out(f" access), and does its firewall allow port {s.port}? Try the IP address too.")
86
+ return 1
87
+
88
+ if s.password and not s.token:
89
+ try:
90
+ s.token = login(s)
91
+ _status("OK", "logged in with the password")
92
+ except errors.MuxitError as e:
93
+ _status("FAIL", str(e))
94
+ return 1
95
+
96
+ try:
97
+ m = Muxit(s.host, s.port, token=s.token, timeout=s.timeout)
98
+ except errors.MuxitError as e:
99
+ _status("FAIL", str(e))
100
+ return 1
101
+
102
+ problems = 0
103
+ with m:
104
+ _status("OK", "connected to Muxit")
105
+ if m.token_source:
106
+ _status("OK", f"access token read from workspace {m.token_source}")
107
+ try:
108
+ lic = m.license()
109
+ used, cap = lic.get("currentConnectors"), lic.get("maxConnectors")
110
+ cap_text = "unlimited" if cap in (None, -1) else str(cap)
111
+ _status("OK", f"license tier {lic.get('tierName') or lic.get('tierId', '?')}, "
112
+ f"connectors {used if used is not None else '?'} of {cap_text}")
113
+ except errors.MuxitError as e:
114
+ _status("WARN", f"could not read the license: {e}")
115
+
116
+ try:
117
+ level = m.safety().get("level", "?")
118
+ if level == "Observe":
119
+ problems += 1
120
+ _status("WARN", "safety level is Observe: writes and actions are SIMULATED, nothing")
121
+ _out(" reaches a device. Pick another level with the safety chip in the Muxit")
122
+ _out(" window (Free can use Unrestricted).")
123
+ else:
124
+ _status("OK", f"safety level {level}")
125
+ except errors.MuxitError as e:
126
+ _status("WARN", f"could not read the safety level: {e}")
127
+
128
+ try:
129
+ names = m.connectors()
130
+ except errors.MuxitError as e:
131
+ _status("FAIL", f"could not list connectors: {e}")
132
+ return 1
133
+ if not names:
134
+ _status("WARN", "Muxit has no connectors loaded yet: add one in the Muxit window")
135
+ else:
136
+ _status("OK", f"{len(names)} connector(s): {', '.join(names)}")
137
+ _out()
138
+ _out("Ready. In Python:" if not problems else "Connected, with the warning above. In Python:")
139
+ parts = [] if s.source["host"] == "default" else [f'"{s.host}"']
140
+ if s.port != 8765:
141
+ parts.append(f"port={s.port}")
142
+ _out(f" import muxit; m = muxit.connect({', '.join(parts)})")
143
+ return 0
144
+
145
+
146
+ # ── list / get / set / call ──────────────────────────────────────
147
+
148
+ def list_cmd(args: argparse.Namespace) -> int:
149
+ with _connect(args) as m:
150
+ if args.connector:
151
+ _out(m.connector(args.connector).describe())
152
+ return 0
153
+ names = m.connectors()
154
+ if not names:
155
+ _out("No connectors loaded.")
156
+ for name in names:
157
+ _out(name)
158
+ return 0
159
+
160
+
161
+ def get_cmd(args: argparse.Namespace) -> int:
162
+ with _connect(args) as m:
163
+ _out(_show(m.connector(args.connector).read(args.property)))
164
+ return 0
165
+
166
+
167
+ def set_cmd(args: argparse.Namespace) -> int:
168
+ with _connect(args) as m:
169
+ c = m.connector(args.connector)
170
+ setattr(c, args.property, _value(args.value)) # checks read-only and spelling first
171
+ _out(f"{args.connector}.{args.property} = {args.value}")
172
+ return 0
173
+
174
+
175
+ def call_cmd(args: argparse.Namespace) -> int:
176
+ with _connect(args) as m:
177
+ result = m.connector(args.connector).call(args.action, *[_value(a) for a in args.args])
178
+ if result is not None:
179
+ _out(_show(result))
180
+ return 0
181
+
182
+
183
+ # ── demo ─────────────────────────────────────────────────────────
184
+
185
+ def demo(args: argparse.Namespace) -> int:
186
+ with _connect(args) as m:
187
+ names = m.connectors()
188
+ target: Optional[str] = args.connector
189
+ if target is None:
190
+ for name in names:
191
+ if str(m.schema(name).get("driver", "")).lower() == "testdevice":
192
+ target = name
193
+ break
194
+ if target is None:
195
+ _out("No TestDevice connector found. The default workspace has one called 'test-device';")
196
+ _out("or pass the name of another connector: python -m muxit demo <name>")
197
+ return 1
198
+ dev = m.connector(target)
199
+ _out(f"Using connector {target!r}")
200
+ _out()
201
+ _out(f">>> dev = m.connector({target!r})")
202
+ before = dev.temperature
203
+ _out(">>> dev.temperature")
204
+ _out(f"{before}")
205
+ new = 42.0 if before != 42.0 else 37.5
206
+ with warnings.catch_warnings(record=True) as caught:
207
+ warnings.simplefilter("always")
208
+ _out(f">>> dev.temperature = {new}")
209
+ dev.temperature = new
210
+ time.sleep(0.5) # driver properties are polled; give the poll a cycle
211
+ after = dev.temperature
212
+ _out(">>> dev.temperature")
213
+ _out(f"{after}")
214
+ _out(">>> dev.calculate(2, 3)")
215
+ _out(f"{dev.calculate(2, 3)}")
216
+ dev.temperature = before
217
+ if any(issubclass(w.category, errors.SimulatedWarning) for w in caught):
218
+ _out()
219
+ _out("NOTE: Muxit's safety level is Observe, so the write was simulated and the action")
220
+ _out("returned nothing. Pick another level with the safety chip in the Muxit window.")
221
+ return 0
222
+ _out()
223
+ _out(f"Wrote {new}, read back {after}; restored {before}. Everything works.")
224
+ return 0
225
+
226
+
227
+ # ── entry point ──────────────────────────────────────────────────
228
+
229
+ def _parser() -> argparse.ArgumentParser:
230
+ common = argparse.ArgumentParser(add_help=False)
231
+ common.add_argument("--host", help="IP address or host name of the Muxit computer (default 127.0.0.1)")
232
+ common.add_argument("--port", type=int, help="Muxit port (default 8765)")
233
+ common.add_argument("--password", help="remote-access password (only for another computer)")
234
+ common.add_argument("--token", help="remote-access token instead of a password")
235
+ common.add_argument("--timeout", type=float, help="seconds to wait for an answer (default 10)")
236
+
237
+ p = argparse.ArgumentParser(prog="python -m muxit", description="Muxit from the command line.")
238
+ p.add_argument("--version", action="version", version=f"muxit {__version__}")
239
+ sub = p.add_subparsers(dest="command", metavar="command")
240
+
241
+ sub.add_parser("doctor", parents=[common], help="check the connection and settings").set_defaults(fn=doctor)
242
+
243
+ q = sub.add_parser("list", parents=[common], help="list connectors, or one connector's properties and actions")
244
+ q.add_argument("connector", nargs="?")
245
+ q.set_defaults(fn=list_cmd)
246
+
247
+ q = sub.add_parser("get", parents=[common], help="read a property")
248
+ q.add_argument("connector")
249
+ q.add_argument("property")
250
+ q.set_defaults(fn=get_cmd)
251
+
252
+ q = sub.add_parser("set", parents=[common], help="write a property")
253
+ q.add_argument("connector")
254
+ q.add_argument("property")
255
+ q.add_argument("value", help="a number, true/false, JSON, or text")
256
+ q.set_defaults(fn=set_cmd)
257
+
258
+ q = sub.add_parser("call", parents=[common], help="call an action")
259
+ q.add_argument("connector")
260
+ q.add_argument("action")
261
+ q.add_argument("args", nargs="*", help="arguments (numbers, true/false, JSON, or text)")
262
+ q.set_defaults(fn=call_cmd)
263
+
264
+ q = sub.add_parser("demo", parents=[common], help="read and write the built-in test device")
265
+ q.add_argument("connector", nargs="?", help="connector to use (default: the first TestDevice)")
266
+ q.set_defaults(fn=demo)
267
+ return p
268
+
269
+
270
+ def main(argv: Optional[List[str]] = None) -> int:
271
+ parser = _parser()
272
+ args = parser.parse_args(argv)
273
+ if not getattr(args, "fn", None):
274
+ parser.print_help()
275
+ return 0
276
+ try:
277
+ return args.fn(args)
278
+ except errors.MuxitError as e:
279
+ print(f"Error: {e}", file=sys.stderr)
280
+ return 1
281
+ except KeyboardInterrupt:
282
+ return 130
283
+
284
+
285
+ if __name__ == "__main__":
286
+ sys.exit(main())
muxit/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"