wheelmachine 0.1.0__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.

Potentially problematic release.


This version of wheelmachine might be problematic. Click here for more details.

Files changed (35) hide show
  1. wheelmachine-0.1.0/.gitignore +5 -0
  2. wheelmachine-0.1.0/CHANGELOG.md +10 -0
  3. wheelmachine-0.1.0/LICENSE +202 -0
  4. wheelmachine-0.1.0/NOTICE +4 -0
  5. wheelmachine-0.1.0/PKG-INFO +253 -0
  6. wheelmachine-0.1.0/README.md +223 -0
  7. wheelmachine-0.1.0/pyproject.toml +52 -0
  8. wheelmachine-0.1.0/src/wheelmachine/__init__.py +230 -0
  9. wheelmachine-0.1.0/src/wheelmachine/_client.py +550 -0
  10. wheelmachine-0.1.0/src/wheelmachine/_errors.py +215 -0
  11. wheelmachine-0.1.0/src/wheelmachine/_http.py +418 -0
  12. wheelmachine-0.1.0/src/wheelmachine/_login.py +126 -0
  13. wheelmachine-0.1.0/src/wheelmachine/_models.py +1987 -0
  14. wheelmachine-0.1.0/src/wheelmachine/_pagination.py +36 -0
  15. wheelmachine-0.1.0/src/wheelmachine/_resources.py +1323 -0
  16. wheelmachine-0.1.0/src/wheelmachine/_version.py +3 -0
  17. wheelmachine-0.1.0/src/wheelmachine/py.typed +0 -0
  18. wheelmachine-0.1.0/tests/fake_api.py +1550 -0
  19. wheelmachine-0.1.0/tests/support.py +43 -0
  20. wheelmachine-0.1.0/tests/test_apps.py +143 -0
  21. wheelmachine-0.1.0/tests/test_client.py +452 -0
  22. wheelmachine-0.1.0/tests/test_code.py +23 -0
  23. wheelmachine-0.1.0/tests/test_credit.py +62 -0
  24. wheelmachine-0.1.0/tests/test_files.py +138 -0
  25. wheelmachine-0.1.0/tests/test_folders.py +34 -0
  26. wheelmachine-0.1.0/tests/test_keys.py +35 -0
  27. wheelmachine-0.1.0/tests/test_login.py +62 -0
  28. wheelmachine-0.1.0/tests/test_metrics.py +46 -0
  29. wheelmachine-0.1.0/tests/test_models.py +133 -0
  30. wheelmachine-0.1.0/tests/test_network.py +39 -0
  31. wheelmachine-0.1.0/tests/test_orgs.py +185 -0
  32. wheelmachine-0.1.0/tests/test_ports.py +198 -0
  33. wheelmachine-0.1.0/tests/test_requests.py +235 -0
  34. wheelmachine-0.1.0/tests/test_retries.py +236 -0
  35. wheelmachine-0.1.0/tests/test_secrets.py +127 -0
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ .pytest_cache/
3
+ dist/
4
+ build/
5
+ *.egg-info/
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ `wheelmachine`, the Wheelmachine library for Python. Each release is tagged `py-v<version>`.
4
+
5
+ ## 0.1.0
6
+
7
+ The first public release.
8
+
9
+ - The whole Wheelmachine API from Python, method for method, with retries that never make two of anything.
10
+ - Python 3.9 or later, with the standard library alone.
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,4 @@
1
+ Wheelmachine Python library
2
+ Copyright 2026 Antelligen Private Limited
3
+
4
+ Licensed under the Apache License, Version 2.0 (LICENSE).
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.5
2
+ Name: wheelmachine
3
+ Version: 0.1.0
4
+ Summary: The official Python SDK for the Wheelmachine API: machines for agents and people, by the second.
5
+ Project-URL: Homepage, https://wheelmachine.ai
6
+ Project-URL: Documentation, https://docs.wheelmachine.ai/libraries
7
+ Project-URL: Source, https://github.com/Antelligen/wheelmachine/tree/main/frontend/sdk/python
8
+ Project-URL: Issues, https://github.com/Antelligen/wheelmachine/issues
9
+ Project-URL: Changelog, https://github.com/Antelligen/wheelmachine/blob/main/frontend/sdk/python/CHANGELOG.md
10
+ Author-email: Antelligen Private Limited <team@antelligen.com>
11
+ License-Expression: Apache-2.0
12
+ License-File: LICENSE
13
+ License-File: NOTICE
14
+ Keywords: agents,api,cloud,machines,sdk,wheelmachine
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.9
29
+ Description-Content-Type: text/markdown
30
+
31
+ # Wheelmachine for Python
32
+
33
+ The official Python SDK for the Wheelmachine API: machines for agents and people, by the second.
34
+ Python 3.9 or later, and nothing else: it uses only the standard library.
35
+
36
+ ## Install from source
37
+
38
+ ```sh
39
+ pip install ./frontend/sdk/python # from a checkout of this repository
40
+ export WHEELMACHINE_API_KEY=ak_live_...
41
+ ```
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ from pathlib import Path
47
+ from wheelmachine import Wheelmachine
48
+
49
+ client = Wheelmachine() # reads WHEELMACHINE_API_KEY
50
+ machine = client.machines.create(name="demo")
51
+ machine = client.machines.wait(machine.id, "running") # at once if it already is
52
+ print(client.machines.exec(machine.id, ["uname", "-a"]).stdout)
53
+ client.machines.write_file(machine.id, "work/notes.txt", Path("notes.txt")) # a file in...
54
+ client.machines.read_file(machine.id, "work/notes.txt", to="notes-back.txt") # ...and back out
55
+ client.machines.delete(machine.id)
56
+ ```
57
+
58
+ ## Signing in through the browser
59
+
60
+ A program with no key yet can sign in the way `wm login` does: the person approves it in their browser, signed in
61
+ with GitHub or Google, and the program is given a key of its own, once.
62
+
63
+ ```python
64
+ from wheelmachine import DeviceLogin, Wheelmachine
65
+
66
+ login = DeviceLogin.start(name="my-tool on laptop") # scopes=[...] asks for less than everything
67
+ print("Open", login.verification_uri, "and enter", login.user_code)
68
+ key = login.wait() # key.secret is the new key: keep it now
69
+ client = Wheelmachine(api_key=key.secret)
70
+ ```
71
+
72
+ ## Credit
73
+
74
+ ```python
75
+ credit = client.credit.get() # what is left: running time taken from it
76
+ print(credit.balance_usd, credit.empty)
77
+ for entry in client.credit.entries(): # every entry, the newest first
78
+ print(entry.at, entry.kind, entry.amount_usd, entry.machine_name)
79
+ purchase = client.credit.buy(25) # buy $25: pay it at purchase.checkout.url
80
+ print(purchase.checkout.url)
81
+ ```
82
+
83
+ At zero the account's machines are paused, and none starts until the balance is above $0. Credit is bought in $10, $25, $50
84
+ or $100, from Paddle, our reseller, which adds any sales tax or VAT and e-mails the receipt and tax invoice: open
85
+ `checkout.url` in a browser signed in to the dashboard, in the same account, and the credit is there once Paddle
86
+ confirms the payment. In an organisation the credit is the organisation's, read and bought by its owners, admins and
87
+ billing.
88
+
89
+ ## Organisations and the audit
90
+
91
+ ```python
92
+ from datetime import datetime, timezone
93
+
94
+ for org in client.orgs.list(): # with a key of your own account
95
+ print(org.id, org.name, org.role)
96
+ me = client.me() # with a key made in an organisation
97
+ print(me.in_org, me.role, me.teams, me.limits)
98
+ since = datetime(2026, 10, 1, tzinfo=timezone.utc)
99
+ for event in client.audit.list(action="machine.", since=since):
100
+ print(event.at, event.person.name if event.person else event.by, event.action, event.target)
101
+ client.audit.export("csv", since=since, to="audit.csv") # whole: a year at most, 100,000 events at most
102
+ ```
103
+
104
+ `DeviceLogin.start(account="acc_...")` asks for a key made in an organisation. The audit is read with a key that holds
105
+ every scope, and in an organisation by its owners and admins.
106
+
107
+ ## Handing on and forking, in an organisation
108
+
109
+ ```python
110
+ q = client.machines.fork_for(machine.id, "ravi@example.com") # or hand_off(): cleared before it is theirs
111
+ print(q.state, q.waiting_on) # waiting on you ("asker"): approve it in the dashboard
112
+ q = client.requests.get(q.id) # then done and the fork in q.result, or on whom it waits
113
+ asked = client.requests.ask("fork", "ravi", why="for the demo") # Ravi picks which machine as they approve
114
+ client.requests.take_back(asked.id)
115
+ mine = client.machines.fork(machine.id) # a member's own: a MachineRequest until it is approved
116
+ print(client.projects.rules(machine.project).forks) # ask or off
117
+ ```
118
+
119
+ Approving and declining are a person's, signed in to the dashboard, and a team's rules are set there. A key decides
120
+ nothing by itself: a give or an ask made with one waits first for its maker's own OK there. A machine handed on arrives
121
+ paused, its memory kept, until its receiver starts it. A member's own fork waits too, for the OK of a full manager of
122
+ its team, an owner or an admin: `machines.fork` answers it with the request, and the fork is its `result` once made.
123
+
124
+ ## What to know
125
+
126
+ - **The client.** `Wheelmachine(api_key=None, base_url=None, timeout=60.0, max_retries=3, proxy=None)`. The key
127
+ comes from `WHEELMACHINE_API_KEY` and the address from `WHEELMACHINE_BASE_URL` when not given. The key is never
128
+ logged, printed, or put in an error or a repr. One client is safe to share between threads, and keeps its
129
+ connections open between calls; `close()` or a `with` block closes them. HTTPS_PROXY and NO_PROXY are followed.
130
+ - **Errors.** Everything raised is a `WheelmachineError`, with `status`, `code` (the API's stable code), `detail`,
131
+ `request_id` (give it when you ask us about a call) and `retry_after`. By family: `BadRequestError` (400),
132
+ `AuthenticationError` (401), `PaymentRequiredError` (402), `PermissionDeniedError` (403), `NotFoundError` (404),
133
+ `WaitTimeoutError` (408), `ConflictError` (409, 422), `RateLimitError` (429), `InternalServerError` (500),
134
+ `UnavailableError` (502, 503, 504), and `APIConnectionError` / `APITimeoutError` when no answer came.
135
+ - **Retries.** A call refused for a moment (429, 503) is tried again after the API's `Retry-After`, or a backoff with
136
+ full jitter; so is a call that lost its connection - but a command (`exec`), `start`, `pause` and the other
137
+ actions are not sent again once they went out, since they may have run. A call that creates something (a
138
+ machine, fork, backup, project or key, a give or an ask) carries an `Idempotency-Key`, the same on every retry, so a retry never
139
+ makes two; pass your own with `idempotency_key=` to retry across restarts. A 503 `not_open` is something not
140
+ served yet, not a moment's refusal: it is never tried again.
141
+ - **Long calls.** Calls the API holds by design - making a machine ready (`wait_s`), `wait()`, `exec()` (up to 10
142
+ minutes), `archive()` - are given the time they need on top of `timeout`.
143
+ - **Lists** page by themselves: `for m in client.machines.list(state="running", labels={"team": "ml"})`.
144
+ - **Files** are `home/...` or `work/...` in the machine. `write_file` streams bytes, a `pathlib.Path` or a binary
145
+ file object with its length; `read_file` gives bytes, or streams into a path (whole or not at all) or a file object.
146
+ - **Backups** keep a machine's files as they are: `backup = client.machines.backup(machine.id, note="before")`. A fork
147
+ is made from one, starting afresh, at the machine's size or another: `client.machines.fork(machine.id,
148
+ backup=backup, cpus=8)`, or `backup="latest"` for its newest. `client.machines.backups(machine.id)` lists a
149
+ machine's, the newest first, and `client.machines.backups()` every one the key reaches.
150
+ - **Answers** are typed objects with the API's own field names; each keeps its JSON in `raw`, and one a call answered
151
+ with says how it came in `last_response` (`request_id`, `replayed`, `rate_limit`).
152
+ - **What a machine used**: `client.machines.metrics(machine.id, since="6h", step=300)` gives its processors (a share
153
+ of its own size), memory, disk and network a point a step; `since` is also a `timedelta` or an aware `datetime`.
154
+ Its server keeps a point a minute for the last day, in memory, read while the machine runs.
155
+ - **Secrets** are values every command in a machine gets as environment variables:
156
+ `client.secrets.set("API_TOKEN", token)`, then `client.machines.create(secrets=["API_TOKEN"])` or
157
+ `client.machines.update(machine.id, secrets=["API_TOKEN"])` (the names said whole; `[]` takes them away).
158
+ Hold what a machine reaches on the internet with `network=` on `create`/`update` - `{"internet": "closed"}`, or
159
+ `{"internet": "allowed", "allow": ["api.github.com", "*.githubusercontent.com", "140.82.112.0/20"]}` (names for web,
160
+ ranges for other ports); a running machine's policy changes at once, without a restart. `machine.network` shows it.
161
+ `client.secrets.list()` gives names and the machines given each - a value is never answered again, and never put
162
+ in an error. Read the value from somewhere safe (a file, a vault, the environment), never write it in code.
163
+ - **Your own AI keys** are set once, under the names agents read them by: `client.secrets.set("ANTHROPIC_API_KEY",
164
+ key)` keeps it outside every machine - each holds a placeholder, and the key is sent only to its maker's hosts -
165
+ and every machine gets it (`machine.ai_keys`). `every_machine=False` keeps it on the machines given it, and
166
+ `outside=False` makes it an ordinary secret. `client.secrets.presets()` lists the names and where each key goes.
167
+
168
+ ## Ports
169
+
170
+ A program listening on a port in a machine is reached at a web address of its own,
171
+ `https://<port>-<the machine's id without m_>.run.wheelmachine.ai`. HTTP, WebSockets and streams pass through, and a
172
+ paused or archived machine is started by the first visit. The address is private unless the port is opened with
173
+ `public=True`: it answers a call carrying an API key that can see the machine (the key never reaches the program),
174
+ and a browser that opened a link made for it. Anyone else is answered 401.
175
+
176
+ ```python
177
+ import os
178
+ import urllib.request
179
+ import webbrowser
180
+
181
+ port = client.ports.open(machine.id, 8080) # a program in the machine listens on 8080
182
+ headers = {"Authorization": "Bearer " + os.environ["WHEELMACHINE_API_KEY"]}
183
+ print(urllib.request.urlopen(urllib.request.Request(port.url, headers=headers)).status) # in with the key
184
+ link = client.ports.link(machine.id, 8080) # or in with a link, for one browser
185
+ webbrowser.open(link.url) # it opens once, within five minutes
186
+ print([(p.port, p.public) for p in client.ports.list(machine.id)]) # lowest first
187
+ client.ports.close(machine.id, 8080) # the address answers 404 from then on
188
+ ```
189
+
190
+ `link()` also takes `access_s`, how long the browser that opens it stays in (1800 seconds by default, 60 to 3600),
191
+ and `path`, where it lands (`/` by default). A link's `url` is in that answer only, and never in a repr. A port open
192
+ already is made public or private as asked, so `open` is sent again after a lost connection; `link` never is once
193
+ it may have reached the API, not even when refused for a moment, since each call makes a new link. A machine has at
194
+ most 20 ports open, and the ones it uses itself (5900, 6080, 7070, 9222, 9223, 9224) cannot be opened. Until web
195
+ addresses are served, `open` is refused with `UnavailableError` and the code `not_open`.
196
+
197
+ ## Apps
198
+
199
+ A machine carries apps, on by default and turned on and off: a desktop and a code editor in the browser and an agent's
200
+ own page (`kind` `web`), a model server reached from inside the machine (`service`), and agents that each keep a
201
+ session of their own in its terminal (`agent`; a machine made before agents had sessions lists them as `terminal`, run
202
+ by typing their `command`). Gemini CLI starts off, until it is turned on; the code editor is always on, and turning it
203
+ off is refused with `ConflictError`. A web app is reached at the machine's private address for its port; asking for a
204
+ link into one that is on opens that address private when it is not open.
205
+
206
+ ```python
207
+ import webbrowser
208
+
209
+ for app in client.apps.list(machine.id):
210
+ print(app.name, app.kind, app.on)
211
+ desktop = client.apps.turn_on(machine.id, "desktop") # its port opened private, if it was not
212
+ webbrowser.open(desktop.link) # it opens once, within five minutes, on the app's own page
213
+ again = client.apps.link(machine.id, "desktop") # a fresh link, while it is on
214
+ client.apps.show(machine.id, "openclaw") # its window on the machine's own screen, signed in
215
+ client.apps.turn_off(machine.id, "desktop")
216
+ ```
217
+
218
+ An app with a token of its own answers it once, in `token`, in the answer to `turn_on`. `link` and `token` are
219
+ never in a repr. `turn_on` and `link` are never sent again once they may have reached the API; `turn_off` is. A machine
220
+ made before apps came has none: `ConflictError` with the code `no_apps`.
221
+
222
+ ## Terminals
223
+
224
+ An interactive terminal in a machine (a login shell, or one command on a terminal of its own) is a WebSocket, not a
225
+ call this library makes: a WebSocket client is a dependency, and this library has none. Open one at
226
+
227
+ ```
228
+ GET wss://api.wheelmachine.ai/v1/machines/{machine_id}/terminal
229
+ Authorization: Bearer ak_live_...
230
+ ```
231
+
232
+ with the `cloud:machines:exec` scope. The query sets it up: `cols`, `rows`, `user` (`user` or `root`), `term`, `cwd`,
233
+ and `cmd` repeated for each word of a command (left out, a login shell opens). A paused machine is woken for it.
234
+
235
+ Over the socket, each message is one event. **From you:** a *binary* message is typed in (keystrokes, bytes as they
236
+ are); a *text* message `{"resize":{"cols":C,"rows":R}}` sets the window. **From us:** a *binary* message is what the
237
+ program wrote; a final *text* message `{"exit_code":N}` (or `{"reason":...,"message":...}` when it did not end by
238
+ itself) says how it ended, just before the socket closes. We never log what is typed or shown.
239
+
240
+ For a ready-made interactive client, `wm ssh <machine>` (the `wm` command-line tool) drives this endpoint: raw local
241
+ terminal, window changes followed, and your shell's own exit code.
242
+
243
+ For `scp`, `sftp` and editors' remote-SSH, `POST /v1/ssh/certificates` signs a short-lived SSH certificate (give a
244
+ public key and a machine; good for at most an hour, for that machine alone) to present to the SSH server. `wm
245
+ ssh-config <machine>` prints the matching `~/.ssh/config` block.
246
+
247
+ ## Tests
248
+
249
+ ```sh
250
+ cd frontend/sdk/python && python3 -m pytest -q # or: PYTHONPATH=src:tests python3 -m unittest discover -s tests
251
+ ```
252
+
253
+ They run against a stand-in for the API on 127.0.0.1; nothing reaches the network.