use-computer 0.0.14__tar.gz → 0.0.16__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.
- {use_computer-0.0.14 → use_computer-0.0.16}/PKG-INFO +15 -7
- {use_computer-0.0.14 → use_computer-0.0.16}/README.md +14 -6
- {use_computer-0.0.14 → use_computer-0.0.16}/examples/_2_hello_ios.py +2 -2
- {use_computer-0.0.14 → use_computer-0.0.16}/pyproject.toml +1 -1
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/__init__.py +24 -2
- use_computer-0.0.16/use_computer/accessibility.py +39 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/client.py +61 -18
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/ios/input.py +27 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/models.py +26 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/sandbox.py +3 -0
- use_computer-0.0.16/use_computer/simulators.py +179 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/.env.example +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/.gitignore +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/.pre-commit-config.yaml +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/examples/_1_hello_macos.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/examples/_3_recording.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/examples/_4_file_transfer.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/examples/_5_keepalive.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/ax_transpile.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/display.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/errors.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/ios/__init__.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/ios/apps.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/ios/environment.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/macos/__init__.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/macos/keyboard.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/macos/mouse.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/parsers.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/py.typed +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/recording.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/retry.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/screenshot.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/__init__.py +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/pre_command.sh +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/task.toml +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_ios.sh +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_ios_nograder.sh +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_macos.sh +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_macos_check.sh +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_macos_nograder.sh +0 -0
- {use_computer-0.0.14 → use_computer-0.0.16}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: use-computer
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.16
|
|
4
4
|
Summary: Python SDK for the use.computer macOS and iOS Computer Use API
|
|
5
5
|
Project-URL: Homepage, https://use.computer
|
|
6
6
|
Project-URL: Documentation, https://api.use.computer/docs
|
|
@@ -30,21 +30,29 @@ export USE_COMPUTER_API_KEY=mk_live_...
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
```python
|
|
33
|
-
from use_computer import Computer
|
|
33
|
+
from use_computer import Computer, SandboxType, SimulatorFamily
|
|
34
34
|
|
|
35
35
|
with Computer().create() as mac:
|
|
36
36
|
mac.exec_ssh("open -a TextEdit")
|
|
37
37
|
mac.keyboard.type("hello")
|
|
38
38
|
png = mac.screenshot.take_full_screen()
|
|
39
|
+
|
|
40
|
+
with Computer().create(type=SandboxType.IOS, family=SimulatorFamily.TV) as tv:
|
|
41
|
+
tv.screenshot.take_full_screen()
|
|
42
|
+
tv.input.press_remote("select")
|
|
39
43
|
```
|
|
40
44
|
|
|
41
45
|
Full DSL reference (macOS + simulator): [docs.use.computer/docs/sdk](https://docs.use.computer/docs/sdk)
|
|
42
46
|
|
|
43
|
-
Simulator sandboxes use `type=
|
|
44
|
-
and `runtime` can target any installed compatible CoreSimulator
|
|
45
|
-
iPad with iOS, Apple Watch with watchOS,
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
Simulator sandboxes use `type=SandboxType.IOS` for the SDK route, but
|
|
48
|
+
`device_type` and `runtime` can target any installed compatible CoreSimulator
|
|
49
|
+
pair: iPhone or iPad with iOS, Apple Watch with watchOS, Apple TV with tvOS, or
|
|
50
|
+
Apple Vision with visionOS. Prefer `family=SimulatorFamily.TV`/`WATCH`/`VISION`
|
|
51
|
+
unless you need to pin raw CoreSimulator identifiers. Raw strings like
|
|
52
|
+
`type="ios"` still work for compatibility. If omitted, the gateway defaults to
|
|
53
|
+
iPhone 17 Pro on the latest installed iOS runtime.
|
|
54
|
+
Known-incompatible simulator types are filtered from family selection,
|
|
55
|
+
including the non-4K Apple Vision Pro type on current fleet runtimes.
|
|
48
56
|
|
|
49
57
|
## Examples
|
|
50
58
|
|
|
@@ -8,21 +8,29 @@ export USE_COMPUTER_API_KEY=mk_live_...
|
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
```python
|
|
11
|
-
from use_computer import Computer
|
|
11
|
+
from use_computer import Computer, SandboxType, SimulatorFamily
|
|
12
12
|
|
|
13
13
|
with Computer().create() as mac:
|
|
14
14
|
mac.exec_ssh("open -a TextEdit")
|
|
15
15
|
mac.keyboard.type("hello")
|
|
16
16
|
png = mac.screenshot.take_full_screen()
|
|
17
|
+
|
|
18
|
+
with Computer().create(type=SandboxType.IOS, family=SimulatorFamily.TV) as tv:
|
|
19
|
+
tv.screenshot.take_full_screen()
|
|
20
|
+
tv.input.press_remote("select")
|
|
17
21
|
```
|
|
18
22
|
|
|
19
23
|
Full DSL reference (macOS + simulator): [docs.use.computer/docs/sdk](https://docs.use.computer/docs/sdk)
|
|
20
24
|
|
|
21
|
-
Simulator sandboxes use `type=
|
|
22
|
-
and `runtime` can target any installed compatible CoreSimulator
|
|
23
|
-
iPad with iOS, Apple Watch with watchOS,
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
Simulator sandboxes use `type=SandboxType.IOS` for the SDK route, but
|
|
26
|
+
`device_type` and `runtime` can target any installed compatible CoreSimulator
|
|
27
|
+
pair: iPhone or iPad with iOS, Apple Watch with watchOS, Apple TV with tvOS, or
|
|
28
|
+
Apple Vision with visionOS. Prefer `family=SimulatorFamily.TV`/`WATCH`/`VISION`
|
|
29
|
+
unless you need to pin raw CoreSimulator identifiers. Raw strings like
|
|
30
|
+
`type="ios"` still work for compatibility. If omitted, the gateway defaults to
|
|
31
|
+
iPhone 17 Pro on the latest installed iOS runtime.
|
|
32
|
+
Known-incompatible simulator types are filtered from family selection,
|
|
33
|
+
including the non-4K Apple Vision Pro type on current fleet runtimes.
|
|
26
34
|
|
|
27
35
|
## Examples
|
|
28
36
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from use_computer import Computer
|
|
1
|
+
from use_computer import Computer, SandboxType
|
|
2
2
|
|
|
3
|
-
with Computer().create(type=
|
|
3
|
+
with Computer().create(type=SandboxType.IOS) as ios:
|
|
4
4
|
ios.apps.open_url("https://example.com")
|
|
5
5
|
png = ios.screenshot.take_full_screen()
|
|
6
6
|
open("hello_ios.png", "wb").write(png)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from use_computer.client import AsyncComputer, Computer, RunStatus
|
|
2
2
|
from use_computer.errors import PlatformNotSupportedError, UseComputerError
|
|
3
|
+
from use_computer.ios.input import Button, Key, RemoteButton
|
|
3
4
|
from use_computer.models import (
|
|
5
|
+
AccessibilityTree,
|
|
4
6
|
ActionResult,
|
|
5
7
|
ActResult,
|
|
6
8
|
CursorPosition,
|
|
@@ -18,31 +20,51 @@ from use_computer.sandbox import (
|
|
|
18
20
|
Sandbox,
|
|
19
21
|
SandboxType,
|
|
20
22
|
)
|
|
23
|
+
from use_computer.simulators import (
|
|
24
|
+
SimulatorChoice,
|
|
25
|
+
SimulatorFamily,
|
|
26
|
+
family_for_device,
|
|
27
|
+
is_usable_device_type,
|
|
28
|
+
required_runtime_os,
|
|
29
|
+
runtime_os,
|
|
30
|
+
select_simulator,
|
|
31
|
+
)
|
|
21
32
|
from use_computer.tasks import Task, TasksClient, TaskSummary
|
|
22
33
|
|
|
23
34
|
__all__ = [
|
|
24
35
|
"Action",
|
|
36
|
+
"AccessibilityTree",
|
|
25
37
|
"ActionResult",
|
|
26
38
|
"ActResult",
|
|
27
39
|
"AsyncIOSSandbox",
|
|
28
40
|
"AsyncMacOSSandbox",
|
|
29
41
|
"AsyncComputer",
|
|
30
42
|
"AsyncSandbox",
|
|
43
|
+
"Button",
|
|
44
|
+
"Computer",
|
|
31
45
|
"CursorPosition",
|
|
32
46
|
"DisplayInfo",
|
|
33
47
|
"ExecResult",
|
|
34
48
|
"IOSSandbox",
|
|
49
|
+
"Key",
|
|
35
50
|
"MacOSSandbox",
|
|
36
|
-
"Computer",
|
|
37
|
-
"UseComputerError",
|
|
38
51
|
"PlatformNotSupportedError",
|
|
39
52
|
"RecordingInfo",
|
|
53
|
+
"RemoteButton",
|
|
40
54
|
"RunStatus",
|
|
41
55
|
"Sandbox",
|
|
42
56
|
"SandboxType",
|
|
57
|
+
"SimulatorChoice",
|
|
58
|
+
"SimulatorFamily",
|
|
43
59
|
"Task",
|
|
44
60
|
"TaskSummary",
|
|
45
61
|
"TasksClient",
|
|
62
|
+
"UseComputerError",
|
|
63
|
+
"family_for_device",
|
|
64
|
+
"is_usable_device_type",
|
|
46
65
|
"parse_pyautogui",
|
|
47
66
|
"parse_xdotool",
|
|
67
|
+
"required_runtime_os",
|
|
68
|
+
"runtime_os",
|
|
69
|
+
"select_simulator",
|
|
48
70
|
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from use_computer.models import AccessibilityTree
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Accessibility:
|
|
9
|
+
def __init__(self, http: httpx.Client, prefix: str):
|
|
10
|
+
self._http = http
|
|
11
|
+
self._prefix = prefix
|
|
12
|
+
|
|
13
|
+
def get_tree(self, *, best_effort: bool = True) -> AccessibilityTree:
|
|
14
|
+
params = {"best_effort": "1"} if best_effort else None
|
|
15
|
+
resp = self._http.get(f"{self._prefix}/display/windows", params=params)
|
|
16
|
+
if best_effort and resp.status_code >= 400:
|
|
17
|
+
return AccessibilityTree(available=False, error=resp.text)
|
|
18
|
+
resp.raise_for_status()
|
|
19
|
+
data = resp.json()
|
|
20
|
+
if isinstance(data, dict) and "available" in data:
|
|
21
|
+
return AccessibilityTree.from_dict(data)
|
|
22
|
+
return AccessibilityTree(available=True, tree=data)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AsyncAccessibility:
|
|
26
|
+
def __init__(self, http: httpx.AsyncClient, prefix: str):
|
|
27
|
+
self._http = http
|
|
28
|
+
self._prefix = prefix
|
|
29
|
+
|
|
30
|
+
async def get_tree(self, *, best_effort: bool = True) -> AccessibilityTree:
|
|
31
|
+
params = {"best_effort": "1"} if best_effort else None
|
|
32
|
+
resp = await self._http.get(f"{self._prefix}/display/windows", params=params)
|
|
33
|
+
if best_effort and resp.status_code >= 400:
|
|
34
|
+
return AccessibilityTree(available=False, error=resp.text)
|
|
35
|
+
resp.raise_for_status()
|
|
36
|
+
data = resp.json()
|
|
37
|
+
if isinstance(data, dict) and "available" in data:
|
|
38
|
+
return AccessibilityTree.from_dict(data)
|
|
39
|
+
return AccessibilityTree(available=True, tree=data)
|
|
@@ -19,10 +19,13 @@ from use_computer.sandbox import (
|
|
|
19
19
|
Sandbox,
|
|
20
20
|
SandboxType,
|
|
21
21
|
)
|
|
22
|
+
from use_computer.simulators import SimulatorFamily, select_simulator
|
|
22
23
|
from use_computer.tasks import TasksClient
|
|
23
24
|
|
|
24
25
|
DEFAULT_BASE_URL = "https://api.use.computer"
|
|
25
26
|
|
|
27
|
+
SandboxTypeInput = SandboxType | Literal["macos", "ios"]
|
|
28
|
+
|
|
26
29
|
|
|
27
30
|
def _resolve_api_key(api_key: str | None) -> str | None:
|
|
28
31
|
if api_key is not None:
|
|
@@ -34,6 +37,12 @@ def _resolve_base_url(base_url: str | None) -> str:
|
|
|
34
37
|
return (base_url or os.getenv("USE_COMPUTER_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
35
38
|
|
|
36
39
|
|
|
40
|
+
def _normalize_sandbox_type(type: SandboxTypeInput) -> SandboxType:
|
|
41
|
+
if isinstance(type, SandboxType):
|
|
42
|
+
return type
|
|
43
|
+
return SandboxType(type)
|
|
44
|
+
|
|
45
|
+
|
|
37
46
|
@dataclass
|
|
38
47
|
class RunStatus:
|
|
39
48
|
"""Status of an ad-hoc or model run."""
|
|
@@ -102,32 +111,50 @@ class Computer:
|
|
|
102
111
|
return resp.json()
|
|
103
112
|
|
|
104
113
|
@overload
|
|
105
|
-
def create(
|
|
114
|
+
def create(
|
|
115
|
+
self,
|
|
116
|
+
*,
|
|
117
|
+
type: Literal["macos", SandboxType.MACOS] = ...,
|
|
118
|
+
host: str = ...,
|
|
119
|
+
) -> MacOSSandbox: ...
|
|
120
|
+
|
|
106
121
|
@overload
|
|
107
122
|
def create(
|
|
108
|
-
self,
|
|
123
|
+
self,
|
|
124
|
+
*,
|
|
125
|
+
type: Literal["ios", SandboxType.IOS],
|
|
126
|
+
family: SimulatorFamily | str = ...,
|
|
127
|
+
device_type: str = ...,
|
|
128
|
+
runtime: str = ...,
|
|
109
129
|
) -> IOSSandbox: ...
|
|
110
130
|
|
|
111
131
|
def create(
|
|
112
132
|
self,
|
|
113
133
|
*,
|
|
114
|
-
type:
|
|
134
|
+
type: SandboxTypeInput = SandboxType.MACOS,
|
|
115
135
|
host: str = "",
|
|
136
|
+
family: SimulatorFamily | str | None = None,
|
|
116
137
|
device_type: str = "",
|
|
117
138
|
runtime: str = "",
|
|
118
139
|
) -> MacOSSandbox | IOSSandbox:
|
|
119
140
|
"""Create a new sandbox.
|
|
120
141
|
|
|
121
142
|
Args:
|
|
122
|
-
type:
|
|
143
|
+
type: SandboxType.MACOS (default), SandboxType.IOS, or the raw API string.
|
|
123
144
|
host: Pin sandbox to a specific host machine (e.g. "mm001").
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
family: Simulator family enum/alias, for example SimulatorFamily.TV.
|
|
146
|
+
device_type: Simulator device type identifier, for exact pinning.
|
|
147
|
+
runtime: Simulator runtime identifier, for exact pinning.
|
|
126
148
|
"""
|
|
127
|
-
|
|
149
|
+
sandbox_type = _normalize_sandbox_type(type)
|
|
150
|
+
body: dict = {"type": sandbox_type.value}
|
|
128
151
|
if host:
|
|
129
152
|
body["host"] = host
|
|
130
|
-
if
|
|
153
|
+
if sandbox_type == SandboxType.IOS:
|
|
154
|
+
if family and (not device_type or not runtime):
|
|
155
|
+
choice = select_simulator(self.platforms(), family)
|
|
156
|
+
device_type = device_type or choice.device_type
|
|
157
|
+
runtime = runtime or choice.runtime
|
|
131
158
|
if device_type:
|
|
132
159
|
body["device_type"] = device_type
|
|
133
160
|
if runtime:
|
|
@@ -138,7 +165,7 @@ class Computer:
|
|
|
138
165
|
data = resp.json()
|
|
139
166
|
sid = data["sandbox_id"]
|
|
140
167
|
|
|
141
|
-
if
|
|
168
|
+
if sandbox_type == SandboxType.IOS:
|
|
142
169
|
return IOSSandbox(sandbox_id=sid, http=self._http)
|
|
143
170
|
return MacOSSandbox(
|
|
144
171
|
sandbox_id=sid,
|
|
@@ -249,33 +276,49 @@ class AsyncComputer:
|
|
|
249
276
|
|
|
250
277
|
@overload
|
|
251
278
|
async def create(
|
|
252
|
-
self,
|
|
279
|
+
self,
|
|
280
|
+
*,
|
|
281
|
+
type: Literal["macos", SandboxType.MACOS] = ...,
|
|
282
|
+
host: str = ...,
|
|
253
283
|
) -> AsyncMacOSSandbox: ...
|
|
284
|
+
|
|
254
285
|
@overload
|
|
255
286
|
async def create(
|
|
256
|
-
self,
|
|
287
|
+
self,
|
|
288
|
+
*,
|
|
289
|
+
type: Literal["ios", SandboxType.IOS],
|
|
290
|
+
family: SimulatorFamily | str = ...,
|
|
291
|
+
device_type: str = ...,
|
|
292
|
+
runtime: str = ...,
|
|
257
293
|
) -> AsyncIOSSandbox: ...
|
|
258
294
|
|
|
259
295
|
async def create(
|
|
260
296
|
self,
|
|
261
297
|
*,
|
|
262
|
-
type:
|
|
298
|
+
type: SandboxTypeInput = SandboxType.MACOS,
|
|
263
299
|
host: str = "",
|
|
300
|
+
family: SimulatorFamily | str | None = None,
|
|
264
301
|
device_type: str = "",
|
|
265
302
|
runtime: str = "",
|
|
266
303
|
) -> AsyncMacOSSandbox | AsyncIOSSandbox:
|
|
267
304
|
"""Create a new sandbox.
|
|
268
305
|
|
|
269
306
|
Args:
|
|
270
|
-
type:
|
|
307
|
+
type: SandboxType.MACOS (default), SandboxType.IOS, or the raw API string.
|
|
271
308
|
host: Pin sandbox to a specific host machine (e.g. "mm001").
|
|
272
|
-
|
|
273
|
-
|
|
309
|
+
family: Simulator family enum/alias, for example SimulatorFamily.TV.
|
|
310
|
+
device_type: Simulator device type identifier, for exact pinning.
|
|
311
|
+
runtime: Simulator runtime identifier, for exact pinning.
|
|
274
312
|
"""
|
|
275
|
-
|
|
313
|
+
sandbox_type = _normalize_sandbox_type(type)
|
|
314
|
+
body: dict = {"type": sandbox_type.value}
|
|
276
315
|
if host:
|
|
277
316
|
body["host"] = host
|
|
278
|
-
if
|
|
317
|
+
if sandbox_type == SandboxType.IOS:
|
|
318
|
+
if family and (not device_type or not runtime):
|
|
319
|
+
choice = select_simulator(await self.platforms(), family)
|
|
320
|
+
device_type = device_type or choice.device_type
|
|
321
|
+
runtime = runtime or choice.runtime
|
|
279
322
|
if device_type:
|
|
280
323
|
body["device_type"] = device_type
|
|
281
324
|
if runtime:
|
|
@@ -286,7 +329,7 @@ class AsyncComputer:
|
|
|
286
329
|
data = resp.json()
|
|
287
330
|
sid = data["sandbox_id"]
|
|
288
331
|
|
|
289
|
-
if
|
|
332
|
+
if sandbox_type == SandboxType.IOS:
|
|
290
333
|
return AsyncIOSSandbox(sandbox_id=sid, http=self._http)
|
|
291
334
|
return AsyncMacOSSandbox(
|
|
292
335
|
sandbox_id=sid,
|
|
@@ -17,6 +17,19 @@ class Button(str, Enum):
|
|
|
17
17
|
APPLE_PAY = "apple-pay"
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class RemoteButton(str, Enum):
|
|
21
|
+
"""Apple TV remote-style controls."""
|
|
22
|
+
|
|
23
|
+
UP = "up"
|
|
24
|
+
DOWN = "down"
|
|
25
|
+
LEFT = "left"
|
|
26
|
+
RIGHT = "right"
|
|
27
|
+
SELECT = "select"
|
|
28
|
+
MENU = "menu"
|
|
29
|
+
HOME = "home"
|
|
30
|
+
PLAY_PAUSE = "play-pause"
|
|
31
|
+
|
|
32
|
+
|
|
20
33
|
class Key(int, Enum):
|
|
21
34
|
"""Common HID keycodes for iOS simulator key presses."""
|
|
22
35
|
|
|
@@ -79,6 +92,13 @@ class Input:
|
|
|
79
92
|
resp.raise_for_status()
|
|
80
93
|
return ActionResult.from_dict(resp.json())
|
|
81
94
|
|
|
95
|
+
def press_remote(self, button: RemoteButton | str) -> ActionResult:
|
|
96
|
+
"""Press an Apple TV remote-style control."""
|
|
97
|
+
val = button.value if isinstance(button, RemoteButton) else button
|
|
98
|
+
resp = self._http.post(f"{self._prefix}/remote", json={"button": val})
|
|
99
|
+
resp.raise_for_status()
|
|
100
|
+
return ActionResult.from_dict(resp.json())
|
|
101
|
+
|
|
82
102
|
|
|
83
103
|
class AsyncInput:
|
|
84
104
|
def __init__(self, http: httpx.AsyncClient, prefix: str):
|
|
@@ -116,3 +136,10 @@ class AsyncInput:
|
|
|
116
136
|
resp = await self._http.post(f"{self._prefix}/key", json={"keycode": val})
|
|
117
137
|
resp.raise_for_status()
|
|
118
138
|
return ActionResult.from_dict(resp.json())
|
|
139
|
+
|
|
140
|
+
async def press_remote(self, button: RemoteButton | str) -> ActionResult:
|
|
141
|
+
"""Press an Apple TV remote-style control."""
|
|
142
|
+
val = button.value if isinstance(button, RemoteButton) else button
|
|
143
|
+
resp = await self._http.post(f"{self._prefix}/remote", json={"button": val})
|
|
144
|
+
resp.raise_for_status()
|
|
145
|
+
return ActionResult.from_dict(resp.json())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
@dataclass
|
|
@@ -29,6 +30,9 @@ class DisplayInfo:
|
|
|
29
30
|
width: int
|
|
30
31
|
height: int
|
|
31
32
|
scale: float = 1.0
|
|
33
|
+
platform: str = ""
|
|
34
|
+
device_type: str = ""
|
|
35
|
+
runtime: str = ""
|
|
32
36
|
|
|
33
37
|
@classmethod
|
|
34
38
|
def from_dict(cls, d: dict) -> DisplayInfo:
|
|
@@ -36,6 +40,28 @@ class DisplayInfo:
|
|
|
36
40
|
width=d.get("width", 0),
|
|
37
41
|
height=d.get("height", 0),
|
|
38
42
|
scale=d.get("scale", 1.0),
|
|
43
|
+
platform=d.get("platform", ""),
|
|
44
|
+
device_type=(
|
|
45
|
+
d.get("device_type") or d.get("deviceType") or d.get("deviceTypeIdentifier", "")
|
|
46
|
+
),
|
|
47
|
+
runtime=d.get("runtime", ""),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class AccessibilityTree:
|
|
53
|
+
"""Best-effort simulator accessibility tree result."""
|
|
54
|
+
|
|
55
|
+
available: bool
|
|
56
|
+
tree: Any = None
|
|
57
|
+
error: str = ""
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_dict(cls, d: dict) -> AccessibilityTree:
|
|
61
|
+
return cls(
|
|
62
|
+
available=bool(d.get("available", False)),
|
|
63
|
+
tree=d.get("tree"),
|
|
64
|
+
error=d.get("error", ""),
|
|
39
65
|
)
|
|
40
66
|
|
|
41
67
|
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
|
|
12
|
+
from use_computer.accessibility import Accessibility, AsyncAccessibility
|
|
12
13
|
from use_computer.display import AsyncDisplay, Display
|
|
13
14
|
from use_computer.ios.apps import Apps, AsyncApps
|
|
14
15
|
from use_computer.ios.environment import AsyncEnvironment, Environment
|
|
@@ -104,6 +105,7 @@ class Sandbox:
|
|
|
104
105
|
self.screenshot = Screenshot(http, self._prefix)
|
|
105
106
|
self.recording = Recording(http, self._prefix)
|
|
106
107
|
self.display = Display(http, self._prefix)
|
|
108
|
+
self.accessibility = Accessibility(http, self._prefix)
|
|
107
109
|
|
|
108
110
|
def upload(self, local_path: str | Path, remote_path: str) -> None:
|
|
109
111
|
with open(local_path, "rb") as f:
|
|
@@ -279,6 +281,7 @@ class AsyncSandbox:
|
|
|
279
281
|
self.screenshot = AsyncScreenshot(http, self._prefix)
|
|
280
282
|
self.recording = AsyncRecording(http, self._prefix)
|
|
281
283
|
self.display = AsyncDisplay(http, self._prefix)
|
|
284
|
+
self.accessibility = AsyncAccessibility(http, self._prefix)
|
|
282
285
|
|
|
283
286
|
async def upload(self, local_path: str | Path, remote_path: str) -> None:
|
|
284
287
|
with open(local_path, "rb") as f:
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SimulatorFamily(str, Enum):
|
|
10
|
+
"""CoreSimulator device families supported by the API."""
|
|
11
|
+
|
|
12
|
+
IPHONE = "iPhone"
|
|
13
|
+
IPAD = "iPad"
|
|
14
|
+
WATCH = "Apple Watch"
|
|
15
|
+
TV = "Apple TV"
|
|
16
|
+
VISION = "Apple Vision"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class SimulatorChoice:
|
|
21
|
+
"""Resolved CoreSimulator device/runtime pair."""
|
|
22
|
+
|
|
23
|
+
family: SimulatorFamily
|
|
24
|
+
device_type: str
|
|
25
|
+
runtime: str
|
|
26
|
+
device_name: str = ""
|
|
27
|
+
runtime_name: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_FAMILY_RUNTIME_OS: dict[SimulatorFamily, str] = {
|
|
31
|
+
SimulatorFamily.IPHONE: "iOS",
|
|
32
|
+
SimulatorFamily.IPAD: "iOS",
|
|
33
|
+
SimulatorFamily.WATCH: "watchOS",
|
|
34
|
+
SimulatorFamily.TV: "tvOS",
|
|
35
|
+
SimulatorFamily.VISION: "visionOS",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_RUNTIME_OS_ALIASES = {
|
|
39
|
+
"xrOS": "visionOS",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_simulator_family(family: SimulatorFamily | str) -> SimulatorFamily:
|
|
44
|
+
"""Normalize SDK enum values and common user-facing aliases."""
|
|
45
|
+
|
|
46
|
+
if isinstance(family, SimulatorFamily):
|
|
47
|
+
return family
|
|
48
|
+
compact = re.sub(r"[^a-z0-9]", "", family.lower())
|
|
49
|
+
aliases = {
|
|
50
|
+
"iphone": SimulatorFamily.IPHONE,
|
|
51
|
+
"ios": SimulatorFamily.IPHONE,
|
|
52
|
+
"ipad": SimulatorFamily.IPAD,
|
|
53
|
+
"watch": SimulatorFamily.WATCH,
|
|
54
|
+
"applewatch": SimulatorFamily.WATCH,
|
|
55
|
+
"watchos": SimulatorFamily.WATCH,
|
|
56
|
+
"tv": SimulatorFamily.TV,
|
|
57
|
+
"appletv": SimulatorFamily.TV,
|
|
58
|
+
"tvos": SimulatorFamily.TV,
|
|
59
|
+
"vision": SimulatorFamily.VISION,
|
|
60
|
+
"applevision": SimulatorFamily.VISION,
|
|
61
|
+
"visionpro": SimulatorFamily.VISION,
|
|
62
|
+
"applevisionpro": SimulatorFamily.VISION,
|
|
63
|
+
"visionos": SimulatorFamily.VISION,
|
|
64
|
+
"xros": SimulatorFamily.VISION,
|
|
65
|
+
}
|
|
66
|
+
try:
|
|
67
|
+
return aliases[compact]
|
|
68
|
+
except KeyError as exc:
|
|
69
|
+
raise ValueError(f"unknown simulator family: {family!r}") from exc
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def family_for_device(device: dict[str, Any] | str) -> SimulatorFamily | None:
|
|
73
|
+
"""Return the simulator family for a device type dict, identifier, or name."""
|
|
74
|
+
|
|
75
|
+
text = (
|
|
76
|
+
device
|
|
77
|
+
if isinstance(device, str)
|
|
78
|
+
else f"{device.get('name', '')} {device.get('identifier', '')}"
|
|
79
|
+
)
|
|
80
|
+
if re.search(r"Apple[- ]Watch", text, re.I):
|
|
81
|
+
return SimulatorFamily.WATCH
|
|
82
|
+
if re.search(r"Apple[- ]TV", text, re.I):
|
|
83
|
+
return SimulatorFamily.TV
|
|
84
|
+
if re.search(r"Apple[- ]Vision", text, re.I):
|
|
85
|
+
return SimulatorFamily.VISION
|
|
86
|
+
if re.search(r"iPad", text, re.I):
|
|
87
|
+
return SimulatorFamily.IPAD
|
|
88
|
+
if re.search(r"iPhone", text, re.I):
|
|
89
|
+
return SimulatorFamily.IPHONE
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def runtime_os(runtime: dict[str, Any] | str) -> str:
|
|
94
|
+
"""Return the normalized OS family for a runtime dict or identifier."""
|
|
95
|
+
|
|
96
|
+
identifier = runtime if isinstance(runtime, str) else runtime.get("identifier", "")
|
|
97
|
+
match = re.search(r"SimRuntime\.([^-]+)-", identifier)
|
|
98
|
+
if not match:
|
|
99
|
+
return ""
|
|
100
|
+
os_name = match.group(1)
|
|
101
|
+
return _RUNTIME_OS_ALIASES.get(os_name, os_name)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def required_runtime_os(family: SimulatorFamily | str) -> str:
|
|
105
|
+
"""Return the runtime OS required by a simulator family."""
|
|
106
|
+
|
|
107
|
+
return _FAMILY_RUNTIME_OS[normalize_simulator_family(family)]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def select_simulator(
|
|
111
|
+
platforms: dict[str, Any],
|
|
112
|
+
family: SimulatorFamily | str,
|
|
113
|
+
*,
|
|
114
|
+
device_name: str = "",
|
|
115
|
+
) -> SimulatorChoice:
|
|
116
|
+
"""Select a compatible device/runtime pair from `/v1/platforms` output."""
|
|
117
|
+
|
|
118
|
+
fam = normalize_simulator_family(family)
|
|
119
|
+
ios = platforms.get("ios") or {}
|
|
120
|
+
devices = [
|
|
121
|
+
d
|
|
122
|
+
for d in ios.get("device_types") or []
|
|
123
|
+
if family_for_device(d) == fam and is_usable_device_type(d)
|
|
124
|
+
]
|
|
125
|
+
if device_name:
|
|
126
|
+
needle = device_name.lower()
|
|
127
|
+
devices = [d for d in devices if needle in (d.get("name") or "").lower()]
|
|
128
|
+
if not devices:
|
|
129
|
+
raise ValueError(f"no {fam.value} simulator devices available")
|
|
130
|
+
|
|
131
|
+
want_os = required_runtime_os(fam)
|
|
132
|
+
runtimes = [
|
|
133
|
+
rt
|
|
134
|
+
for rt in ios.get("runtimes") or []
|
|
135
|
+
if rt.get("isAvailable", True) and runtime_os(rt) == want_os
|
|
136
|
+
]
|
|
137
|
+
if not runtimes:
|
|
138
|
+
raise ValueError(f"no {want_os} runtime available for {fam.value}")
|
|
139
|
+
|
|
140
|
+
devices.sort(key=lambda d: d.get("name") or d.get("identifier") or "")
|
|
141
|
+
runtimes.sort(
|
|
142
|
+
key=lambda rt: (rt.get("version") or "", rt.get("identifier") or ""),
|
|
143
|
+
reverse=True,
|
|
144
|
+
)
|
|
145
|
+
device = _default_device(fam, devices)
|
|
146
|
+
runtime = runtimes[0]
|
|
147
|
+
return SimulatorChoice(
|
|
148
|
+
family=fam,
|
|
149
|
+
device_type=device.get("identifier", ""),
|
|
150
|
+
runtime=runtime.get("identifier", ""),
|
|
151
|
+
device_name=device.get("name", ""),
|
|
152
|
+
runtime_name=runtime.get("name", ""),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _default_device(family: SimulatorFamily, devices: list[dict[str, Any]]) -> dict[str, Any]:
|
|
157
|
+
if family == SimulatorFamily.IPHONE:
|
|
158
|
+
for d in devices:
|
|
159
|
+
name = d.get("name") or ""
|
|
160
|
+
if re.search(r"17 Pro\b", name) and "Max" not in name:
|
|
161
|
+
return d
|
|
162
|
+
if family == SimulatorFamily.VISION:
|
|
163
|
+
for d in devices:
|
|
164
|
+
if "Apple-Vision-Pro-4K" in (d.get("identifier") or ""):
|
|
165
|
+
return d
|
|
166
|
+
return devices[0]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def is_usable_device_type(device: dict[str, Any] | str) -> bool:
|
|
170
|
+
"""Return false for simulator device types known to fail with current runtimes."""
|
|
171
|
+
|
|
172
|
+
text = (
|
|
173
|
+
device
|
|
174
|
+
if isinstance(device, str)
|
|
175
|
+
else f"{device.get('name', '')} {device.get('identifier', '')}"
|
|
176
|
+
)
|
|
177
|
+
if re.search(r"Apple[- ]Vision[- ]Pro", text, re.I):
|
|
178
|
+
return "Apple-Vision-Pro-4K" in text or re.search(r"\b4K\b", text, re.I) is not None
|
|
179
|
+
return True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_ios_nograder.sh
RENAMED
|
File without changes
|
|
File without changes
|
{use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_macos_check.sh
RENAMED
|
File without changes
|
{use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_macos_nograder.sh
RENAMED
|
File without changes
|
|
File without changes
|