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.
- wheelmachine-0.1.0/.gitignore +5 -0
- wheelmachine-0.1.0/CHANGELOG.md +10 -0
- wheelmachine-0.1.0/LICENSE +202 -0
- wheelmachine-0.1.0/NOTICE +4 -0
- wheelmachine-0.1.0/PKG-INFO +253 -0
- wheelmachine-0.1.0/README.md +223 -0
- wheelmachine-0.1.0/pyproject.toml +52 -0
- wheelmachine-0.1.0/src/wheelmachine/__init__.py +230 -0
- wheelmachine-0.1.0/src/wheelmachine/_client.py +550 -0
- wheelmachine-0.1.0/src/wheelmachine/_errors.py +215 -0
- wheelmachine-0.1.0/src/wheelmachine/_http.py +418 -0
- wheelmachine-0.1.0/src/wheelmachine/_login.py +126 -0
- wheelmachine-0.1.0/src/wheelmachine/_models.py +1987 -0
- wheelmachine-0.1.0/src/wheelmachine/_pagination.py +36 -0
- wheelmachine-0.1.0/src/wheelmachine/_resources.py +1323 -0
- wheelmachine-0.1.0/src/wheelmachine/_version.py +3 -0
- wheelmachine-0.1.0/src/wheelmachine/py.typed +0 -0
- wheelmachine-0.1.0/tests/fake_api.py +1550 -0
- wheelmachine-0.1.0/tests/support.py +43 -0
- wheelmachine-0.1.0/tests/test_apps.py +143 -0
- wheelmachine-0.1.0/tests/test_client.py +452 -0
- wheelmachine-0.1.0/tests/test_code.py +23 -0
- wheelmachine-0.1.0/tests/test_credit.py +62 -0
- wheelmachine-0.1.0/tests/test_files.py +138 -0
- wheelmachine-0.1.0/tests/test_folders.py +34 -0
- wheelmachine-0.1.0/tests/test_keys.py +35 -0
- wheelmachine-0.1.0/tests/test_login.py +62 -0
- wheelmachine-0.1.0/tests/test_metrics.py +46 -0
- wheelmachine-0.1.0/tests/test_models.py +133 -0
- wheelmachine-0.1.0/tests/test_network.py +39 -0
- wheelmachine-0.1.0/tests/test_orgs.py +185 -0
- wheelmachine-0.1.0/tests/test_ports.py +198 -0
- wheelmachine-0.1.0/tests/test_requests.py +235 -0
- wheelmachine-0.1.0/tests/test_retries.py +236 -0
- wheelmachine-0.1.0/tests/test_secrets.py +127 -0
|
@@ -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,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.
|