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 +63 -0
- qem/__main__.py +7 -0
- qem/cli/__init__.py +202 -0
- qem/cli/commands.py +629 -0
- qem/cli/project_store.py +99 -0
- qem/config.py +75 -0
- qem/device_login.py +143 -0
- qem/errors.py +95 -0
- qem/mac_session.py +316 -0
- qem/models.py +197 -0
- qem/nomac.py +145 -0
- qem/ssh.py +430 -0
- qem/transfer.py +97 -0
- qem/transport.py +79 -0
- qem/tunnel.py +189 -0
- qem/upload.py +579 -0
- qem/version.py +24 -0
- qem/version_info.py +11 -0
- qem/vnc.py +207 -0
- qem-0.2.0.dist-info/METADATA +85 -0
- qem-0.2.0.dist-info/RECORD +25 -0
- qem-0.2.0.dist-info/WHEEL +4 -0
- qem-0.2.0.dist-info/entry_points.txt +2 -0
- qem-0.2.0.dist-info/licenses/LICENSE +12 -0
- qem-0.2.0.dist-info/licenses/LICENSES/0BSD.txt +5 -0
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
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))
|