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.
Files changed (41) hide show
  1. {use_computer-0.0.14 → use_computer-0.0.16}/PKG-INFO +15 -7
  2. {use_computer-0.0.14 → use_computer-0.0.16}/README.md +14 -6
  3. {use_computer-0.0.14 → use_computer-0.0.16}/examples/_2_hello_ios.py +2 -2
  4. {use_computer-0.0.14 → use_computer-0.0.16}/pyproject.toml +1 -1
  5. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/__init__.py +24 -2
  6. use_computer-0.0.16/use_computer/accessibility.py +39 -0
  7. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/client.py +61 -18
  8. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/ios/input.py +27 -0
  9. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/models.py +26 -0
  10. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/sandbox.py +3 -0
  11. use_computer-0.0.16/use_computer/simulators.py +179 -0
  12. {use_computer-0.0.14 → use_computer-0.0.16}/.env.example +0 -0
  13. {use_computer-0.0.14 → use_computer-0.0.16}/.gitignore +0 -0
  14. {use_computer-0.0.14 → use_computer-0.0.16}/.pre-commit-config.yaml +0 -0
  15. {use_computer-0.0.14 → use_computer-0.0.16}/examples/_1_hello_macos.py +0 -0
  16. {use_computer-0.0.14 → use_computer-0.0.16}/examples/_3_recording.py +0 -0
  17. {use_computer-0.0.14 → use_computer-0.0.16}/examples/_4_file_transfer.py +0 -0
  18. {use_computer-0.0.14 → use_computer-0.0.16}/examples/_5_keepalive.py +0 -0
  19. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/ax_transpile.py +0 -0
  20. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/display.py +0 -0
  21. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/errors.py +0 -0
  22. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/ios/__init__.py +0 -0
  23. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/ios/apps.py +0 -0
  24. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/ios/environment.py +0 -0
  25. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/macos/__init__.py +0 -0
  26. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/macos/keyboard.py +0 -0
  27. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/macos/mouse.py +0 -0
  28. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/parsers.py +0 -0
  29. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/py.typed +0 -0
  30. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/recording.py +0 -0
  31. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/retry.py +0 -0
  32. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/screenshot.py +0 -0
  33. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/__init__.py +0 -0
  34. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/pre_command.sh +0 -0
  35. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/task.toml +0 -0
  36. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_ios.sh +0 -0
  37. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_ios_nograder.sh +0 -0
  38. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_macos.sh +0 -0
  39. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_macos_check.sh +0 -0
  40. {use_computer-0.0.14 → use_computer-0.0.16}/use_computer/tasks/templates/test_macos_nograder.sh +0 -0
  41. {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.14
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="ios"` for the SDK/API route, but `device_type`
44
- and `runtime` can target any installed compatible CoreSimulator pair: iPhone or
45
- iPad with iOS, Apple Watch with watchOS, or Apple TV with tvOS. Vision is
46
- unavailable for now. If omitted, the gateway defaults to iPhone 17 Pro on the
47
- latest installed iOS runtime.
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="ios"` for the SDK/API route, but `device_type`
22
- and `runtime` can target any installed compatible CoreSimulator pair: iPhone or
23
- iPad with iOS, Apple Watch with watchOS, or Apple TV with tvOS. Vision is
24
- unavailable for now. If omitted, the gateway defaults to iPhone 17 Pro on the
25
- latest installed iOS runtime.
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="ios") as ios:
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)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "use-computer"
7
- version = "0.0.14"
7
+ version = "0.0.16"
8
8
  description = "Python SDK for the use.computer macOS and iOS Computer Use API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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(self, *, type: Literal["macos"] = ..., host: str = ...) -> MacOSSandbox: ...
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, *, type: Literal["ios"], device_type: str = ..., runtime: str = ...
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: str = "macos",
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: "macos" (default) or "ios".
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
- device_type: iOS only simulator device type identifier.
125
- runtime: iOS only simulator runtime identifier.
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
- body: dict = {"type": type}
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 type == "ios":
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 type == "ios":
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, *, type: Literal["macos"] = ..., host: str = ...
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, *, type: Literal["ios"], device_type: str = ..., runtime: str = ...
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: str = "macos",
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: "macos" (default) or "ios".
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
- device_type: iOS only simulator device type identifier.
273
- runtime: iOS only simulator runtime identifier.
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
- body: dict = {"type": type}
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 type == "ios":
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 type == "ios":
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