use-computer 0.0.1__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.
mmini/client.py ADDED
@@ -0,0 +1,362 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ import time
6
+ from dataclasses import dataclass
7
+ from typing import Any, Literal, overload
8
+
9
+ import httpx
10
+
11
+ from mmini.errors import _araise_if_unsupported, _raise_if_unsupported
12
+ from mmini.retry import AsyncRetryTransport, RetryTransport
13
+ from mmini.sandbox import (
14
+ AsyncIOSSandbox,
15
+ AsyncMacOSSandbox,
16
+ AsyncSandbox,
17
+ IOSSandbox,
18
+ MacOSSandbox,
19
+ Sandbox,
20
+ SandboxType,
21
+ )
22
+ from mmini.tasks import TasksClient
23
+
24
+ DEFAULT_BASE_URL = "https://api.use.computer"
25
+
26
+
27
+ def _resolve_api_key(api_key: str | None) -> str | None:
28
+ if api_key is not None:
29
+ return api_key
30
+ return os.getenv("USE_COMPUTER_API_KEY") or os.getenv("MMINI_API_KEY")
31
+
32
+
33
+ def _resolve_base_url(base_url: str | None) -> str:
34
+ return (base_url or os.getenv("USE_COMPUTER_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
35
+
36
+
37
+ @dataclass
38
+ class RunStatus:
39
+ """Status of an ad-hoc or model run."""
40
+
41
+ id: str
42
+ status: str # "starting" | "running" | "completed" | "failed"
43
+ model: str = ""
44
+ task_id: str = ""
45
+ sandbox_id: str = ""
46
+ mini_ip: str = ""
47
+ mini_hostname: str = ""
48
+ max_steps: int = 0
49
+ reward: float | None = None
50
+ error: str = ""
51
+ n_steps: int | None = None
52
+ n_actions: int | None = None
53
+ started_at: str = ""
54
+ finished_at: str | None = None
55
+
56
+ @property
57
+ def done(self) -> bool:
58
+ return self.status in ("completed", "failed")
59
+
60
+ @staticmethod
61
+ def _from_dict(d: dict[str, Any]) -> RunStatus:
62
+ return RunStatus(
63
+ id=d.get("id", ""),
64
+ status=d.get("status", ""),
65
+ model=d.get("model", ""),
66
+ task_id=d.get("task_id", ""),
67
+ sandbox_id=d.get("sandbox_id", ""),
68
+ mini_ip=d.get("mini_ip", ""),
69
+ mini_hostname=d.get("mini_hostname", ""),
70
+ max_steps=d.get("max_steps", 0),
71
+ reward=d.get("reward"),
72
+ error=d.get("error", ""),
73
+ n_steps=d.get("n_steps"),
74
+ n_actions=d.get("n_actions"),
75
+ started_at=d.get("started_at", ""),
76
+ finished_at=d.get("finished_at"),
77
+ )
78
+
79
+
80
+ class Mmini:
81
+ """mmini client. Creates and manages macOS and iOS sandboxes."""
82
+
83
+ def __init__(self, api_key: str | None = None, base_url: str | None = None):
84
+ self._base_url = _resolve_base_url(base_url)
85
+ self._api_key = _resolve_api_key(api_key)
86
+ headers: dict[str, str] = {}
87
+ if self._api_key:
88
+ headers["Authorization"] = f"Bearer {self._api_key}"
89
+ self._http = httpx.Client(
90
+ base_url=self._base_url,
91
+ headers=headers,
92
+ timeout=60.0,
93
+ transport=RetryTransport(httpx.HTTPTransport()),
94
+ event_hooks={"response": [_raise_if_unsupported]},
95
+ )
96
+ self.tasks = TasksClient(self._http)
97
+
98
+ def platforms(self) -> dict:
99
+ """Discover available platforms, images, runtimes, and device types."""
100
+ resp = self._http.get("/v1/platforms")
101
+ resp.raise_for_status()
102
+ return resp.json()
103
+
104
+ @overload
105
+ def create(self, *, type: Literal["macos"] = ..., host: str = ...) -> MacOSSandbox: ...
106
+ @overload
107
+ def create(
108
+ self, *, type: Literal["ios"], device_type: str = ..., runtime: str = ...
109
+ ) -> IOSSandbox: ...
110
+
111
+ def create(
112
+ self,
113
+ *,
114
+ type: str = "macos",
115
+ host: str = "",
116
+ device_type: str = "",
117
+ runtime: str = "",
118
+ ) -> MacOSSandbox | IOSSandbox:
119
+ """Create a new sandbox.
120
+
121
+ Args:
122
+ type: "macos" (default) or "ios".
123
+ host: Pin sandbox to a specific host machine (e.g. "mm001").
124
+ device_type: iOS only — simulator device type identifier.
125
+ runtime: iOS only — simulator runtime identifier.
126
+ """
127
+ body: dict = {"type": type}
128
+ if host:
129
+ body["host"] = host
130
+ if type == "ios":
131
+ if device_type:
132
+ body["device_type"] = device_type
133
+ if runtime:
134
+ body["runtime"] = runtime
135
+
136
+ resp = self._http.post("/v1/sandboxes", json=body, timeout=180.0)
137
+ resp.raise_for_status()
138
+ data = resp.json()
139
+ sid = data["sandbox_id"]
140
+
141
+ if type == "ios":
142
+ return IOSSandbox(sandbox_id=sid, http=self._http)
143
+ return MacOSSandbox(
144
+ sandbox_id=sid,
145
+ http=self._http,
146
+ vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
147
+ vm_ip=data.get("vm_ip", ""),
148
+ host=data.get("host", ""),
149
+ ssh_url=f"{self._base_url}{data.get('ssh_url', '')}",
150
+ )
151
+
152
+ def get(self, sandbox_id: str) -> Sandbox:
153
+ """Get an existing sandbox by ID. Returns base Sandbox (use create() for typed access)."""
154
+ resp = self._http.get(f"/v1/sandboxes/{sandbox_id}")
155
+ resp.raise_for_status()
156
+ data = resp.json()
157
+ sb_type = SandboxType(data.get("type", "macos"))
158
+ if sb_type == SandboxType.IOS:
159
+ return IOSSandbox(sandbox_id=data["sandbox_id"], http=self._http)
160
+ return MacOSSandbox(
161
+ sandbox_id=data["sandbox_id"],
162
+ http=self._http,
163
+ vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
164
+ ssh_url=f"{self._base_url}{data.get('ssh_url', '')}",
165
+ )
166
+
167
+ def adhoc_run(
168
+ self,
169
+ instruction: str,
170
+ *,
171
+ platform: str = "macos",
172
+ model: str = "claude-sonnet-4-6",
173
+ max_steps: int = 50,
174
+ files: list[dict[str, str]] | None = None,
175
+ poll: bool = True,
176
+ poll_interval: float = 3.0,
177
+ ) -> RunStatus:
178
+ """Run an ad-hoc task: spin up a sandbox and drive a model against a free-form instruction.
179
+
180
+ Args:
181
+ instruction: What the agent should do.
182
+ platform: "macos" or "ios".
183
+ model: Model to use.
184
+ max_steps: Max agent steps.
185
+ files: Optional list of {remote_path, content_b64} to pre-upload.
186
+ poll: If True, block until the run finishes.
187
+ poll_interval: Seconds between status polls.
188
+ """
189
+ body: dict[str, Any] = {
190
+ "instruction": instruction,
191
+ "platform": platform,
192
+ "model": model,
193
+ "max_steps": max_steps,
194
+ }
195
+ if files:
196
+ body["files"] = files
197
+ resp = self._http.post("/admin/adhoc-runs/", json=body, timeout=30.0)
198
+ resp.raise_for_status()
199
+ status = RunStatus._from_dict(resp.json())
200
+ if not poll:
201
+ return status
202
+ return self._poll_run(status.id, poll_interval)
203
+
204
+ def get_run(self, run_id: str) -> RunStatus:
205
+ """Get current status of a model run."""
206
+ resp = self._http.get(f"/admin/tasks/model-runs/{run_id}")
207
+ resp.raise_for_status()
208
+ return RunStatus._from_dict(resp.json())
209
+
210
+ def _poll_run(self, run_id: str, interval: float = 3.0) -> RunStatus:
211
+ while True:
212
+ status = self.get_run(run_id)
213
+ if status.done:
214
+ return status
215
+ time.sleep(interval)
216
+
217
+ def close(self):
218
+ self._http.close()
219
+
220
+ def __enter__(self):
221
+ return self
222
+
223
+ def __exit__(self, *_):
224
+ self.close()
225
+
226
+
227
+ class AsyncMmini:
228
+ """Async mmini client."""
229
+
230
+ def __init__(self, api_key: str | None = None, base_url: str | None = None):
231
+ self._base_url = _resolve_base_url(base_url)
232
+ self._api_key = _resolve_api_key(api_key)
233
+ headers: dict[str, str] = {}
234
+ if self._api_key:
235
+ headers["Authorization"] = f"Bearer {self._api_key}"
236
+ self._http = httpx.AsyncClient(
237
+ base_url=self._base_url,
238
+ headers=headers,
239
+ timeout=60.0,
240
+ transport=AsyncRetryTransport(httpx.AsyncHTTPTransport()),
241
+ event_hooks={"response": [_araise_if_unsupported]},
242
+ )
243
+
244
+ async def platforms(self) -> dict:
245
+ """Discover available platforms, images, runtimes, and device types."""
246
+ resp = await self._http.get("/v1/platforms")
247
+ resp.raise_for_status()
248
+ return resp.json()
249
+
250
+ @overload
251
+ async def create(
252
+ self, *, type: Literal["macos"] = ..., host: str = ...
253
+ ) -> AsyncMacOSSandbox: ...
254
+ @overload
255
+ async def create(
256
+ self, *, type: Literal["ios"], device_type: str = ..., runtime: str = ...
257
+ ) -> AsyncIOSSandbox: ...
258
+
259
+ async def create(
260
+ self,
261
+ *,
262
+ type: str = "macos",
263
+ host: str = "",
264
+ device_type: str = "",
265
+ runtime: str = "",
266
+ ) -> AsyncMacOSSandbox | AsyncIOSSandbox:
267
+ """Create a new sandbox.
268
+
269
+ Args:
270
+ type: "macos" (default) or "ios".
271
+ host: Pin sandbox to a specific host machine (e.g. "mm001").
272
+ device_type: iOS only — simulator device type identifier.
273
+ runtime: iOS only — simulator runtime identifier.
274
+ """
275
+ body: dict = {"type": type}
276
+ if host:
277
+ body["host"] = host
278
+ if type == "ios":
279
+ if device_type:
280
+ body["device_type"] = device_type
281
+ if runtime:
282
+ body["runtime"] = runtime
283
+
284
+ resp = await self._http.post("/v1/sandboxes", json=body, timeout=180.0)
285
+ resp.raise_for_status()
286
+ data = resp.json()
287
+ sid = data["sandbox_id"]
288
+
289
+ if type == "ios":
290
+ return AsyncIOSSandbox(sandbox_id=sid, http=self._http)
291
+ return AsyncMacOSSandbox(
292
+ sandbox_id=sid,
293
+ http=self._http,
294
+ vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
295
+ ssh_url=f"{self._base_url}{data.get('ssh_url', '')}",
296
+ vm_ip=data.get("vm_ip", ""),
297
+ host=data.get("host", ""),
298
+ )
299
+
300
+ async def get(self, sandbox_id: str) -> AsyncSandbox:
301
+ """Get an existing sandbox by ID."""
302
+ resp = await self._http.get(f"/v1/sandboxes/{sandbox_id}")
303
+ resp.raise_for_status()
304
+ data = resp.json()
305
+ sb_type = SandboxType(data.get("type", "macos"))
306
+ if sb_type == SandboxType.IOS:
307
+ return AsyncIOSSandbox(sandbox_id=data["sandbox_id"], http=self._http)
308
+ return AsyncMacOSSandbox(
309
+ sandbox_id=data["sandbox_id"],
310
+ http=self._http,
311
+ vnc_url=f"{self._base_url}{data.get('vnc_url', '')}",
312
+ ssh_url=f"{self._base_url}{data.get('ssh_url', '')}",
313
+ )
314
+
315
+ async def adhoc_run(
316
+ self,
317
+ instruction: str,
318
+ *,
319
+ platform: str = "macos",
320
+ model: str = "claude-sonnet-4-6",
321
+ max_steps: int = 50,
322
+ files: list[dict[str, str]] | None = None,
323
+ poll: bool = True,
324
+ poll_interval: float = 3.0,
325
+ ) -> RunStatus:
326
+ """Run an ad-hoc task and drive a model against a free-form instruction."""
327
+ body: dict[str, Any] = {
328
+ "instruction": instruction,
329
+ "platform": platform,
330
+ "model": model,
331
+ "max_steps": max_steps,
332
+ }
333
+ if files:
334
+ body["files"] = files
335
+ resp = await self._http.post("/admin/adhoc-runs/", json=body, timeout=30.0)
336
+ resp.raise_for_status()
337
+ status = RunStatus._from_dict(resp.json())
338
+ if not poll:
339
+ return status
340
+ return await self._poll_run(status.id, poll_interval)
341
+
342
+ async def get_run(self, run_id: str) -> RunStatus:
343
+ """Get current status of a model run."""
344
+ resp = await self._http.get(f"/admin/tasks/model-runs/{run_id}")
345
+ resp.raise_for_status()
346
+ return RunStatus._from_dict(resp.json())
347
+
348
+ async def _poll_run(self, run_id: str, interval: float = 3.0) -> RunStatus:
349
+ while True:
350
+ status = await self.get_run(run_id)
351
+ if status.done:
352
+ return status
353
+ await asyncio.sleep(interval)
354
+
355
+ async def close(self):
356
+ await self._http.aclose()
357
+
358
+ async def __aenter__(self):
359
+ return self
360
+
361
+ async def __aexit__(self, *_):
362
+ await self.close()
mmini/display.py ADDED
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from mmini.models import DisplayInfo
8
+
9
+
10
+ class Display:
11
+ def __init__(self, http: httpx.Client, prefix: str):
12
+ self._http = http
13
+ self._prefix = prefix
14
+
15
+ def get_info(self) -> DisplayInfo:
16
+ resp = self._http.get(f"{self._prefix}/display/info")
17
+ resp.raise_for_status()
18
+ return DisplayInfo.from_dict(resp.json())
19
+
20
+ def get_windows(self) -> list[dict[str, Any]]:
21
+ resp = self._http.get(f"{self._prefix}/display/windows")
22
+ resp.raise_for_status()
23
+ return resp.json()
24
+
25
+
26
+ class AsyncDisplay:
27
+ def __init__(self, http: httpx.AsyncClient, prefix: str):
28
+ self._http = http
29
+ self._prefix = prefix
30
+
31
+ async def get_info(self) -> DisplayInfo:
32
+ resp = await self._http.get(f"{self._prefix}/display/info")
33
+ resp.raise_for_status()
34
+ return DisplayInfo.from_dict(resp.json())
35
+
36
+ async def get_windows(self) -> list[dict[str, Any]]:
37
+ resp = await self._http.get(f"{self._prefix}/display/windows")
38
+ resp.raise_for_status()
39
+ return resp.json()
mmini/errors.py ADDED
@@ -0,0 +1,68 @@
1
+ """Typed exceptions for mmini SDK errors that callers should catch by class
2
+ rather than by parsing HTTP status codes or response bodies."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import httpx
7
+
8
+
9
+ class MminiError(Exception):
10
+ """Base class for typed mmini SDK errors."""
11
+
12
+
13
+ class PlatformNotSupportedError(MminiError):
14
+ """Raised when an action is not supported on the sandbox's platform.
15
+
16
+ The gateway returns 501 with a JSON body of the shape:
17
+ {"error": "...", "sandbox_type": "ios", "action": "POST /mouse/click",
18
+ "hint": "use POST /tap"}
19
+ """
20
+
21
+ def __init__(self, action: str, sandbox_type: str, hint: str, response: httpx.Response):
22
+ self.action = action
23
+ self.sandbox_type = sandbox_type
24
+ self.hint = hint
25
+ self.response = response
26
+ super().__init__(f"{action} not supported on {sandbox_type} sandbox: {hint}")
27
+
28
+
29
+ def _raise_if_unsupported(resp: httpx.Response) -> None:
30
+ """httpx event hook: convert 501-with-platform-shape into a typed error.
31
+
32
+ httpx hooks run before the body is read on streaming responses, so we
33
+ only touch the body when the status is 501 — anything else is left
34
+ untouched for raise_for_status() to handle normally.
35
+ """
36
+ if resp.status_code != 501:
37
+ return
38
+ try:
39
+ resp.read()
40
+ data = resp.json()
41
+ except Exception:
42
+ return
43
+ if not isinstance(data, dict) or data.get("error") != "not supported on iOS sandbox":
44
+ return
45
+ raise PlatformNotSupportedError(
46
+ action=data.get("action", ""),
47
+ sandbox_type=data.get("sandbox_type", ""),
48
+ hint=data.get("hint", ""),
49
+ response=resp,
50
+ )
51
+
52
+
53
+ async def _araise_if_unsupported(resp: httpx.Response) -> None:
54
+ if resp.status_code != 501:
55
+ return
56
+ try:
57
+ await resp.aread()
58
+ data = resp.json()
59
+ except Exception:
60
+ return
61
+ if not isinstance(data, dict) or data.get("error") != "not supported on iOS sandbox":
62
+ return
63
+ raise PlatformNotSupportedError(
64
+ action=data.get("action", ""),
65
+ sandbox_type=data.get("sandbox_type", ""),
66
+ hint=data.get("hint", ""),
67
+ response=resp,
68
+ )
mmini/ios/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ from mmini.ios.apps import Apps, AsyncApps
2
+ from mmini.ios.environment import AsyncEnvironment, Environment
3
+ from mmini.ios.input import AsyncInput, Button, Input, Key
4
+
5
+ __all__ = [
6
+ "Apps",
7
+ "AsyncApps",
8
+ "AsyncEnvironment",
9
+ "AsyncInput",
10
+ "Button",
11
+ "Environment",
12
+ "Input",
13
+ "Key",
14
+ ]
mmini/ios/apps.py ADDED
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from mmini.models import ActionResult
6
+
7
+
8
+ class Apps:
9
+ def __init__(self, http: httpx.Client, prefix: str):
10
+ self._http = http
11
+ self._prefix = prefix
12
+
13
+ def open_url(self, url: str) -> ActionResult:
14
+ resp = self._http.post(f"{self._prefix}/openurl", json={"url": url})
15
+ resp.raise_for_status()
16
+ return ActionResult.from_dict(resp.json())
17
+
18
+ def install(self, app_path: str) -> ActionResult:
19
+ resp = self._http.post(f"{self._prefix}/install", json={"appPath": app_path})
20
+ resp.raise_for_status()
21
+ return ActionResult.from_dict(resp.json())
22
+
23
+ def launch(self, bundle_id: str) -> ActionResult:
24
+ resp = self._http.post(f"{self._prefix}/launch", json={"bundleId": bundle_id})
25
+ resp.raise_for_status()
26
+ return ActionResult.from_dict(resp.json())
27
+
28
+ def terminate(self, bundle_id: str) -> ActionResult:
29
+ resp = self._http.post(f"{self._prefix}/terminate", json={"bundleId": bundle_id})
30
+ resp.raise_for_status()
31
+ return ActionResult.from_dict(resp.json())
32
+
33
+
34
+ class AsyncApps:
35
+ def __init__(self, http: httpx.AsyncClient, prefix: str):
36
+ self._http = http
37
+ self._prefix = prefix
38
+
39
+ async def open_url(self, url: str) -> ActionResult:
40
+ resp = await self._http.post(f"{self._prefix}/openurl", json={"url": url})
41
+ resp.raise_for_status()
42
+ return ActionResult.from_dict(resp.json())
43
+
44
+ async def install(self, app_path: str) -> ActionResult:
45
+ resp = await self._http.post(f"{self._prefix}/install", json={"appPath": app_path})
46
+ resp.raise_for_status()
47
+ return ActionResult.from_dict(resp.json())
48
+
49
+ async def launch(self, bundle_id: str) -> ActionResult:
50
+ resp = await self._http.post(f"{self._prefix}/launch", json={"bundleId": bundle_id})
51
+ resp.raise_for_status()
52
+ return ActionResult.from_dict(resp.json())
53
+
54
+ async def terminate(self, bundle_id: str) -> ActionResult:
55
+ resp = await self._http.post(f"{self._prefix}/terminate", json={"bundleId": bundle_id})
56
+ resp.raise_for_status()
57
+ return ActionResult.from_dict(resp.json())
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from mmini.models import ActionResult
6
+
7
+
8
+ class Environment:
9
+ def __init__(self, http: httpx.Client, prefix: str):
10
+ self._http = http
11
+ self._prefix = prefix
12
+
13
+ def set_location(self, lat: float, lon: float) -> ActionResult:
14
+ resp = self._http.post(f"{self._prefix}/location", json={"lat": lat, "lon": lon})
15
+ resp.raise_for_status()
16
+ return ActionResult.from_dict(resp.json())
17
+
18
+ def clear_location(self) -> ActionResult:
19
+ resp = self._http.post(f"{self._prefix}/location", json={"action": "clear"})
20
+ resp.raise_for_status()
21
+ return ActionResult.from_dict(resp.json())
22
+
23
+ def set_appearance(self, mode: str) -> ActionResult:
24
+ """Set dark or light mode."""
25
+ resp = self._http.post(f"{self._prefix}/appearance", json={"mode": mode})
26
+ resp.raise_for_status()
27
+ return ActionResult.from_dict(resp.json())
28
+
29
+
30
+ class AsyncEnvironment:
31
+ def __init__(self, http: httpx.AsyncClient, prefix: str):
32
+ self._http = http
33
+ self._prefix = prefix
34
+
35
+ async def set_location(self, lat: float, lon: float) -> ActionResult:
36
+ resp = await self._http.post(f"{self._prefix}/location", json={"lat": lat, "lon": lon})
37
+ resp.raise_for_status()
38
+ return ActionResult.from_dict(resp.json())
39
+
40
+ async def clear_location(self) -> ActionResult:
41
+ resp = await self._http.post(f"{self._prefix}/location", json={"action": "clear"})
42
+ resp.raise_for_status()
43
+ return ActionResult.from_dict(resp.json())
44
+
45
+ async def set_appearance(self, mode: str) -> ActionResult:
46
+ """Set dark or light mode."""
47
+ resp = await self._http.post(f"{self._prefix}/appearance", json={"mode": mode})
48
+ resp.raise_for_status()
49
+ return ActionResult.from_dict(resp.json())