qem 0.2.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.
qem/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ # SPDX-FileCopyrightText: 2026 René de Hesselle <dehesselle@web.de>
2
+ #
3
+ # SPDX-License-Identifier: 0BSD
4
+
5
+ """Python library and CLI for NoMac cloud Macs (https://nomac.app).
6
+
7
+ from qem import Nomac
8
+
9
+ with Nomac() as nm, nm.start(max_duration=1800, max_cost=1) as mac:
10
+ print(mac.exec(["sw_vers"]).stdout)
11
+ """
12
+
13
+ from .errors import (
14
+ ApiError,
15
+ LoginFailed,
16
+ NetworkError,
17
+ NomacError,
18
+ NotFound,
19
+ NotLoggedIn,
20
+ OutputExpired,
21
+ PaymentRequired,
22
+ PreconditionFailed,
23
+ TransferError,
24
+ TunnelError,
25
+ Unauthorized,
26
+ WaitTimeout,
27
+ )
28
+ from .mac_session import MacSession
29
+ from .models import Access, Job, Login, OutputChunk, SessionInfo
30
+ from .nomac import Nomac
31
+ from .transfer import PullResult
32
+ from .upload import SyncResult
33
+ from .version_info import version
34
+ from .vnc import DesktopTunnel
35
+
36
+ __version__ = version()
37
+
38
+ __all__ = [
39
+ "Access",
40
+ "ApiError",
41
+ "DesktopTunnel",
42
+ "Job",
43
+ "Login",
44
+ "LoginFailed",
45
+ "MacSession",
46
+ "NetworkError",
47
+ "Nomac",
48
+ "NomacError",
49
+ "NotFound",
50
+ "NotLoggedIn",
51
+ "OutputChunk",
52
+ "OutputExpired",
53
+ "PaymentRequired",
54
+ "PreconditionFailed",
55
+ "PullResult",
56
+ "SessionInfo",
57
+ "SyncResult",
58
+ "TransferError",
59
+ "TunnelError",
60
+ "Unauthorized",
61
+ "WaitTimeout",
62
+ "__version__",
63
+ ]
qem/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2026 René de Hesselle <dehesselle@web.de>
2
+ #
3
+ # SPDX-License-Identifier: 0BSD
4
+
5
+ from qem.cli import main
6
+
7
+ main()
qem/cli/__init__.py ADDED
@@ -0,0 +1,202 @@
1
+ # SPDX-FileCopyrightText: 2026 René de Hesselle <dehesselle@web.de>
2
+ #
3
+ # SPDX-License-Identifier: 0BSD
4
+
5
+ """The `qem` command: argparse, the per-project session store and output.
6
+
7
+ Everything else is a thin call into the library.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import sys
14
+
15
+ from ..version_info import version
16
+
17
+ EXIT_STILL_RUNNING = 75 # EX_TEMPFAIL: the command keeps running on the Mac
18
+
19
+
20
+ def build_parser() -> argparse.ArgumentParser:
21
+ # prog is fixed because argparse would otherwise name the program after
22
+ # sys.argv[0], which is "__main__.py" under `python -m qem`.
23
+ parser = argparse.ArgumentParser(
24
+ prog="qem", description="Use NoMac cloud Macs from the terminal."
25
+ )
26
+ parser.add_argument("--version", action="version", version=f"%(prog)s {version()}")
27
+ sub = parser.add_subparsers(dest="command", metavar="COMMAND", required=True)
28
+
29
+ def cmd(name: str, help: str | None, **kwargs) -> argparse.ArgumentParser:
30
+ return sub.add_parser(name, help=help, description=help, **kwargs)
31
+
32
+ p = cmd("login", "link this computer to your NoMac account")
33
+ p.add_argument(
34
+ "--with-token", action="store_true", help="read an API key from stdin instead"
35
+ )
36
+ cmd("logout", "forget the stored API key")
37
+ cmd("whoami", "show the account the API key belongs to")
38
+ cmd("credits", "show remaining credit")
39
+
40
+ p = cmd("start", "start a Mac for this project folder and wait until it is ready")
41
+ p.add_argument(
42
+ "--max-duration",
43
+ default="2h",
44
+ metavar="TIME",
45
+ help="hard limit, up to 8h (default: 2h)",
46
+ )
47
+ p.add_argument(
48
+ "--idle-timeout",
49
+ default="10m",
50
+ metavar="TIME",
51
+ help="stop after this much idle time (default: 10m)",
52
+ )
53
+ p.add_argument(
54
+ "--max-cost",
55
+ type=float,
56
+ default=5.0,
57
+ metavar="USD",
58
+ help="spend cap in dollars (default: 5)",
59
+ )
60
+ p.add_argument(
61
+ "--parallel", action="store_true", help="start another Mac even if one runs"
62
+ )
63
+ p.add_argument(
64
+ "--request-key", help="reuse after a lost response so no second Mac starts"
65
+ )
66
+ p.add_argument(
67
+ "--wait",
68
+ type=float,
69
+ default=240,
70
+ metavar="SECONDS",
71
+ help="how long to wait for ready (default: 240)",
72
+ )
73
+
74
+ p = cmd("status", "show this project's Mac")
75
+ p.add_argument("--json", action="store_true")
76
+ p = cmd("sessions", "list the account's Macs")
77
+ p.add_argument("--json", action="store_true")
78
+ p = cmd("attach", "use a running Mac for this project folder")
79
+ p.add_argument("session_id", nargs="?", help="default: the account's running Mac")
80
+ p = cmd("extend", "add time to this project's Mac")
81
+ p.add_argument("minutes", type=int, nargs="?", default=60)
82
+ p = cmd("stop", "stop this project's Mac")
83
+ p.add_argument(
84
+ "--wait",
85
+ type=float,
86
+ default=90,
87
+ metavar="SECONDS",
88
+ help="how long to wait for stopped (default: 90)",
89
+ )
90
+
91
+ p = cmd(
92
+ "exec",
93
+ "run a command on the Mac",
94
+ usage="%(prog)s [options] -- COMMAND [ARG ...]",
95
+ )
96
+ p.add_argument(
97
+ "--wait",
98
+ type=float,
99
+ default=30,
100
+ metavar="SECONDS",
101
+ help="wait this long, then exit 75 if still running (default: 30)",
102
+ )
103
+ p.add_argument(
104
+ "--timeout",
105
+ type=int,
106
+ metavar="SECONDS",
107
+ help="kill after this long, up to 3600 (default: 300)",
108
+ )
109
+ p.add_argument(
110
+ "--cwd",
111
+ help="directory on the Mac (default: the synced project folder, if any)",
112
+ )
113
+ p.add_argument(
114
+ "--request-key",
115
+ help="reuse after a lost response so the command does not run twice",
116
+ )
117
+ p.add_argument("--json", action="store_true", help="print the job as JSON")
118
+ p.add_argument("argv", nargs=argparse.REMAINDER)
119
+ p = cmd("job", "wait for a command and print its output")
120
+ p.add_argument("job_id")
121
+ p.add_argument("--wait", type=float, default=0, metavar="SECONDS")
122
+ p.add_argument("--json", action="store_true")
123
+ p = cmd("output", "read part of a command's output")
124
+ p.add_argument("job_id")
125
+ p.add_argument("--stream", choices=("stdout", "stderr"), default="stdout")
126
+ p.add_argument("--cursor", type=int, default=0)
127
+ p = cmd("cancel", "stop a running command")
128
+ p.add_argument("job_id")
129
+
130
+ p = cmd(
131
+ "ssh",
132
+ "open a shell on the Mac, run a command, or forward a port",
133
+ usage="%(prog)s [--forward LOCAL:REMOTE] [-- COMMAND [ARG ...]]",
134
+ )
135
+ p.add_argument(
136
+ "--forward",
137
+ metavar="LOCAL:REMOTE",
138
+ help="forward a local port to a port on the Mac",
139
+ )
140
+ p.add_argument("argv", nargs=argparse.REMAINDER)
141
+ p = cmd("ssh-config", "let plain `ssh NAME` reach this project's Mac")
142
+ p.add_argument(
143
+ "name",
144
+ nargs="?",
145
+ default="nomac",
146
+ metavar="NAME",
147
+ help="ssh host name (default: nomac)",
148
+ )
149
+ p.add_argument(
150
+ "--yes",
151
+ action="store_true",
152
+ help="add the Include line to ~/.ssh/config without asking",
153
+ )
154
+
155
+ p = cmd("desktop", "show the Mac's desktop in a window, through ssh")
156
+ p.add_argument(
157
+ "--port",
158
+ type=int,
159
+ metavar="PORT",
160
+ help="local port for the tunnel (default: a free one)",
161
+ )
162
+ p.add_argument(
163
+ "--no-viewer",
164
+ action="store_true",
165
+ help="only open the tunnel and print its address, for another viewer",
166
+ )
167
+ p = cmd("password", "print the Mac's login password for this session")
168
+ p.add_argument(
169
+ "--copy", action="store_true", help="put it on the clipboard instead"
170
+ )
171
+
172
+ p = cmd("sync", "upload a project folder to ~/drive/src/<folder> on the Mac")
173
+ p.add_argument("path", nargs="?", default=".")
174
+ p.add_argument(
175
+ "--delete", action="store_true", help="delete Mac files that are gone locally"
176
+ )
177
+ p.add_argument("--watch", action="store_true", help="keep syncing on every change")
178
+ p = cmd("pull", "download a path from the Mac's project folder")
179
+ p.add_argument("local", nargs="?", default=".", help="local folder (default: .)")
180
+ p.add_argument(
181
+ "--from",
182
+ dest="remote",
183
+ required=True,
184
+ metavar="PATH",
185
+ help="path on the Mac, relative to the project folder unless absolute",
186
+ )
187
+ p.add_argument(
188
+ "--overwrite", action="store_true", help="replace files that exist locally"
189
+ )
190
+
191
+ # The ssh ProxyCommand; not for people.
192
+ p = sub.add_parser("proxy")
193
+ p.add_argument("session_id")
194
+ return parser
195
+
196
+
197
+ def main(argv: list[str] | None = None) -> None:
198
+ args = build_parser().parse_args(argv)
199
+ # Imported after parse_args so `--version` and `--help` stay fast.
200
+ from .commands import run
201
+
202
+ sys.exit(run(args))