minitap-mobile-use 3.3.0__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.
- minitap/mobile_use/__init__.py +0 -0
- minitap/mobile_use/agents/contextor/contextor.md +55 -0
- minitap/mobile_use/agents/contextor/contextor.py +175 -0
- minitap/mobile_use/agents/contextor/types.py +36 -0
- minitap/mobile_use/agents/cortex/cortex.md +135 -0
- minitap/mobile_use/agents/cortex/cortex.py +152 -0
- minitap/mobile_use/agents/cortex/types.py +15 -0
- minitap/mobile_use/agents/executor/executor.md +42 -0
- minitap/mobile_use/agents/executor/executor.py +87 -0
- minitap/mobile_use/agents/executor/tool_node.py +152 -0
- minitap/mobile_use/agents/hopper/hopper.md +15 -0
- minitap/mobile_use/agents/hopper/hopper.py +44 -0
- minitap/mobile_use/agents/orchestrator/human.md +12 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.md +21 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.py +134 -0
- minitap/mobile_use/agents/orchestrator/types.py +11 -0
- minitap/mobile_use/agents/outputter/human.md +25 -0
- minitap/mobile_use/agents/outputter/outputter.py +85 -0
- minitap/mobile_use/agents/outputter/test_outputter.py +167 -0
- minitap/mobile_use/agents/planner/human.md +14 -0
- minitap/mobile_use/agents/planner/planner.md +126 -0
- minitap/mobile_use/agents/planner/planner.py +101 -0
- minitap/mobile_use/agents/planner/types.py +51 -0
- minitap/mobile_use/agents/planner/utils.py +70 -0
- minitap/mobile_use/agents/summarizer/summarizer.py +35 -0
- minitap/mobile_use/agents/video_analyzer/__init__.py +5 -0
- minitap/mobile_use/agents/video_analyzer/human.md +5 -0
- minitap/mobile_use/agents/video_analyzer/video_analyzer.md +37 -0
- minitap/mobile_use/agents/video_analyzer/video_analyzer.py +111 -0
- minitap/mobile_use/clients/browserstack_client.py +477 -0
- minitap/mobile_use/clients/idb_client.py +429 -0
- minitap/mobile_use/clients/ios_client.py +332 -0
- minitap/mobile_use/clients/ios_client_config.py +141 -0
- minitap/mobile_use/clients/ui_automator_client.py +330 -0
- minitap/mobile_use/clients/wda_client.py +526 -0
- minitap/mobile_use/clients/wda_lifecycle.py +367 -0
- minitap/mobile_use/config.py +413 -0
- minitap/mobile_use/constants.py +3 -0
- minitap/mobile_use/context.py +106 -0
- minitap/mobile_use/controllers/__init__.py +0 -0
- minitap/mobile_use/controllers/android_controller.py +524 -0
- minitap/mobile_use/controllers/controller_factory.py +46 -0
- minitap/mobile_use/controllers/device_controller.py +182 -0
- minitap/mobile_use/controllers/ios_controller.py +436 -0
- minitap/mobile_use/controllers/platform_specific_commands_controller.py +199 -0
- minitap/mobile_use/controllers/types.py +106 -0
- minitap/mobile_use/controllers/unified_controller.py +193 -0
- minitap/mobile_use/graph/graph.py +160 -0
- minitap/mobile_use/graph/state.py +115 -0
- minitap/mobile_use/main.py +309 -0
- minitap/mobile_use/sdk/__init__.py +12 -0
- minitap/mobile_use/sdk/agent.py +1294 -0
- minitap/mobile_use/sdk/builders/__init__.py +10 -0
- minitap/mobile_use/sdk/builders/agent_config_builder.py +307 -0
- minitap/mobile_use/sdk/builders/index.py +15 -0
- minitap/mobile_use/sdk/builders/task_request_builder.py +236 -0
- minitap/mobile_use/sdk/constants.py +1 -0
- minitap/mobile_use/sdk/examples/README.md +83 -0
- minitap/mobile_use/sdk/examples/__init__.py +1 -0
- minitap/mobile_use/sdk/examples/app_lock_messaging.py +54 -0
- minitap/mobile_use/sdk/examples/platform_manual_task_example.py +67 -0
- minitap/mobile_use/sdk/examples/platform_minimal_example.py +48 -0
- minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
- minitap/mobile_use/sdk/examples/smart_notification_assistant.py +225 -0
- minitap/mobile_use/sdk/examples/video_transcription_example.py +117 -0
- minitap/mobile_use/sdk/services/cloud_mobile.py +656 -0
- minitap/mobile_use/sdk/services/platform.py +434 -0
- minitap/mobile_use/sdk/types/__init__.py +51 -0
- minitap/mobile_use/sdk/types/agent.py +84 -0
- minitap/mobile_use/sdk/types/exceptions.py +138 -0
- minitap/mobile_use/sdk/types/platform.py +183 -0
- minitap/mobile_use/sdk/types/task.py +269 -0
- minitap/mobile_use/sdk/utils.py +29 -0
- minitap/mobile_use/services/accessibility.py +100 -0
- minitap/mobile_use/services/llm.py +247 -0
- minitap/mobile_use/services/telemetry.py +421 -0
- minitap/mobile_use/tools/index.py +67 -0
- minitap/mobile_use/tools/mobile/back.py +52 -0
- minitap/mobile_use/tools/mobile/erase_one_char.py +56 -0
- minitap/mobile_use/tools/mobile/focus_and_clear_text.py +317 -0
- minitap/mobile_use/tools/mobile/focus_and_input_text.py +153 -0
- minitap/mobile_use/tools/mobile/launch_app.py +86 -0
- minitap/mobile_use/tools/mobile/long_press_on.py +169 -0
- minitap/mobile_use/tools/mobile/open_link.py +62 -0
- minitap/mobile_use/tools/mobile/press_key.py +83 -0
- minitap/mobile_use/tools/mobile/stop_app.py +62 -0
- minitap/mobile_use/tools/mobile/swipe.py +156 -0
- minitap/mobile_use/tools/mobile/tap.py +154 -0
- minitap/mobile_use/tools/mobile/video_recording.py +177 -0
- minitap/mobile_use/tools/mobile/wait_for_delay.py +81 -0
- minitap/mobile_use/tools/scratchpad.py +147 -0
- minitap/mobile_use/tools/test_utils.py +413 -0
- minitap/mobile_use/tools/tool_wrapper.py +16 -0
- minitap/mobile_use/tools/types.py +35 -0
- minitap/mobile_use/tools/utils.py +336 -0
- minitap/mobile_use/utils/app_launch_utils.py +173 -0
- minitap/mobile_use/utils/cli_helpers.py +37 -0
- minitap/mobile_use/utils/cli_selection.py +143 -0
- minitap/mobile_use/utils/conversations.py +31 -0
- minitap/mobile_use/utils/decorators.py +124 -0
- minitap/mobile_use/utils/errors.py +6 -0
- minitap/mobile_use/utils/file.py +13 -0
- minitap/mobile_use/utils/logger.py +183 -0
- minitap/mobile_use/utils/media.py +186 -0
- minitap/mobile_use/utils/recorder.py +52 -0
- minitap/mobile_use/utils/requests_utils.py +37 -0
- minitap/mobile_use/utils/shell_utils.py +20 -0
- minitap/mobile_use/utils/test_ui_hierarchy.py +178 -0
- minitap/mobile_use/utils/time.py +6 -0
- minitap/mobile_use/utils/ui_hierarchy.py +132 -0
- minitap/mobile_use/utils/video.py +281 -0
- minitap_mobile_use-3.3.0.dist-info/METADATA +329 -0
- minitap_mobile_use-3.3.0.dist-info/RECORD +115 -0
- minitap_mobile_use-3.3.0.dist-info/WHEEL +4 -0
- minitap_mobile_use-3.3.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import platform
|
|
3
|
+
import re
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
from minitap.mobile_use.clients.browserstack_client import BrowserStackClientWrapper
|
|
8
|
+
from minitap.mobile_use.clients.idb_client import IdbClientWrapper
|
|
9
|
+
from minitap.mobile_use.clients.ios_client_config import IosClientConfig
|
|
10
|
+
from minitap.mobile_use.clients.wda_client import WdaClientWrapper
|
|
11
|
+
from minitap.mobile_use.utils.logger import get_logger
|
|
12
|
+
from minitap.mobile_use.utils.shell_utils import run_shell_command_on_host
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _run_host_cmd(cmd: list[str]) -> str:
|
|
18
|
+
return run_shell_command_on_host(" ".join(cmd))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Type alias for the union of all client wrappers
|
|
22
|
+
IosClientWrapper = IdbClientWrapper | WdaClientWrapper | BrowserStackClientWrapper
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DeviceType(str, Enum):
|
|
26
|
+
"""Type of iOS device."""
|
|
27
|
+
|
|
28
|
+
SIMULATOR = "SIMULATOR"
|
|
29
|
+
PHYSICAL = "PHYSICAL"
|
|
30
|
+
BROWSERSTACK = "BROWSERSTACK"
|
|
31
|
+
UNKNOWN = "UNKNOWN"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DeviceInfo(TypedDict):
|
|
35
|
+
"""Information about an iOS device."""
|
|
36
|
+
|
|
37
|
+
udid: str
|
|
38
|
+
type: DeviceType
|
|
39
|
+
name: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def format_device_info(device: DeviceInfo) -> str:
|
|
43
|
+
return f"{device['name']} ({device['type'].value}) - {device['udid']}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DeviceNotFoundError(Exception):
|
|
47
|
+
"""Raised when the specified device cannot be found."""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class UnsupportedDeviceError(Exception):
|
|
53
|
+
"""Raised when the device type is not supported."""
|
|
54
|
+
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_device_type(udid: str) -> DeviceType:
|
|
59
|
+
"""Detect whether a device is a simulator or physical device.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
udid: The device UDID to check
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
DeviceType.SIMULATOR if the device is a simulator,
|
|
66
|
+
DeviceType.PHYSICAL if it's a physical device,
|
|
67
|
+
DeviceType.UNKNOWN if detection fails
|
|
68
|
+
"""
|
|
69
|
+
if platform.system() != "Darwin":
|
|
70
|
+
return DeviceType.UNKNOWN
|
|
71
|
+
|
|
72
|
+
# Check if it's a booted simulator
|
|
73
|
+
try:
|
|
74
|
+
cmd = ["xcrun", "simctl", "list", "devices", "--json"]
|
|
75
|
+
output = _run_host_cmd(cmd)
|
|
76
|
+
data = json.loads(output)
|
|
77
|
+
for _runtime, devices in data.get("devices", {}).items():
|
|
78
|
+
for device in devices:
|
|
79
|
+
if device.get("udid") == udid and device.get("state") == "Booted":
|
|
80
|
+
return DeviceType.SIMULATOR
|
|
81
|
+
except (RuntimeError, json.JSONDecodeError, Exception):
|
|
82
|
+
logger.debug("Failed to detect simulator device type")
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
# Check if it's a physical device using idevice_id
|
|
86
|
+
try:
|
|
87
|
+
cmd = ["idevice_id", "-l"]
|
|
88
|
+
output = _run_host_cmd(cmd)
|
|
89
|
+
physical_udids = output.strip().split("\n") if output else []
|
|
90
|
+
if udid in physical_udids:
|
|
91
|
+
return DeviceType.PHYSICAL
|
|
92
|
+
except (RuntimeError, Exception) as e:
|
|
93
|
+
logger.debug(f"Failed to detect physical device type using idevice_id: {e}")
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
# Fallback: try system_profiler for USB devices
|
|
97
|
+
try:
|
|
98
|
+
cmd = ["system_profiler", "SPUSBDataType", "-json"]
|
|
99
|
+
output = _run_host_cmd(cmd)
|
|
100
|
+
if udid in output:
|
|
101
|
+
return DeviceType.PHYSICAL
|
|
102
|
+
except (RuntimeError, Exception) as e:
|
|
103
|
+
logger.debug(f"Failed to detect physical device type using system_profiler: {e}")
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
return DeviceType.UNKNOWN
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_physical_devices() -> list[str]:
|
|
110
|
+
"""Get UDIDs of connected physical iOS devices.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of physical device UDIDs
|
|
114
|
+
"""
|
|
115
|
+
if platform.system() != "Darwin":
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
# Try idevice_id first (libimobiledevice) - most reliable
|
|
119
|
+
try:
|
|
120
|
+
cmd = ["idevice_id", "-l"]
|
|
121
|
+
output = _run_host_cmd(cmd)
|
|
122
|
+
udids = output.strip().split("\n") if output else []
|
|
123
|
+
return [u for u in udids if u]
|
|
124
|
+
except (RuntimeError, Exception):
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
# Fallback to xcrun xctrace - filter out simulators by checking name
|
|
128
|
+
try:
|
|
129
|
+
cmd = ["xcrun", "xctrace", "list", "devices"]
|
|
130
|
+
output = _run_host_cmd(cmd)
|
|
131
|
+
udids: list[str] = []
|
|
132
|
+
for line in output.strip().split("\n") if output else []:
|
|
133
|
+
if "Simulator" in line:
|
|
134
|
+
continue
|
|
135
|
+
match = re.search(r"\(([A-Fa-f0-9-]{36})\)$", line)
|
|
136
|
+
if match:
|
|
137
|
+
udids.append(match.group(1))
|
|
138
|
+
return udids
|
|
139
|
+
except (RuntimeError, Exception):
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_physical_ios_devices() -> list[DeviceInfo]:
|
|
146
|
+
"""Get detailed info about connected physical iOS devices.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
List of DeviceInfo dicts with udid, type, and name
|
|
150
|
+
"""
|
|
151
|
+
if platform.system() != "Darwin":
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
devices: list[DeviceInfo] = []
|
|
155
|
+
|
|
156
|
+
# Primary: idevice_id + ideviceinfo for names (most reliable)
|
|
157
|
+
try:
|
|
158
|
+
cmd = ["idevice_id", "-l"]
|
|
159
|
+
output = _run_host_cmd(cmd)
|
|
160
|
+
for udid in output.strip().split("\n") if output else []:
|
|
161
|
+
if not udid:
|
|
162
|
+
continue
|
|
163
|
+
name = _get_device_name(udid)
|
|
164
|
+
devices.append(
|
|
165
|
+
DeviceInfo(udid=udid, type=DeviceType.PHYSICAL, name=name or "Unknown Device")
|
|
166
|
+
)
|
|
167
|
+
if devices:
|
|
168
|
+
return devices
|
|
169
|
+
except (RuntimeError, Exception):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
# Fallback: xcrun xctrace - filter out simulators by name
|
|
173
|
+
try:
|
|
174
|
+
cmd = ["xcrun", "xctrace", "list", "devices"]
|
|
175
|
+
output = _run_host_cmd(cmd)
|
|
176
|
+
for line in output.strip().split("\n") if output else []:
|
|
177
|
+
if "Simulator" in line:
|
|
178
|
+
continue
|
|
179
|
+
match = re.search(r"^(.+?)\s+\([^)]+\)\s+\(([A-Fa-f0-9-]{36})\)$", line)
|
|
180
|
+
if match:
|
|
181
|
+
devices.append(
|
|
182
|
+
DeviceInfo(
|
|
183
|
+
udid=match.group(2),
|
|
184
|
+
type=DeviceType.PHYSICAL,
|
|
185
|
+
name=match.group(1).strip(),
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
except (RuntimeError, Exception):
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
return devices
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _get_device_name(udid: str) -> str | None:
|
|
195
|
+
"""Get device name using ideviceinfo."""
|
|
196
|
+
try:
|
|
197
|
+
cmd = ["ideviceinfo", "-u", udid, "-k", "DeviceName"]
|
|
198
|
+
output = _run_host_cmd(cmd)
|
|
199
|
+
return output.strip() if output else None
|
|
200
|
+
except (RuntimeError, Exception):
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_simulator_devices() -> list[DeviceInfo]:
|
|
205
|
+
"""Get detailed info about booted iOS simulators.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
List of DeviceInfo dicts with udid, type, and name
|
|
209
|
+
"""
|
|
210
|
+
if platform.system() != "Darwin":
|
|
211
|
+
return []
|
|
212
|
+
|
|
213
|
+
devices: list[DeviceInfo] = []
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
cmd = ["xcrun", "simctl", "list", "devices", "--json"]
|
|
217
|
+
output = _run_host_cmd(cmd)
|
|
218
|
+
data = json.loads(output)
|
|
219
|
+
for runtime, runtime_devices in data.get("devices", {}).items():
|
|
220
|
+
if "ios" not in runtime.lower():
|
|
221
|
+
continue
|
|
222
|
+
for device in runtime_devices:
|
|
223
|
+
if device.get("state") != "Booted":
|
|
224
|
+
continue
|
|
225
|
+
udid = device.get("udid")
|
|
226
|
+
name = device.get("name")
|
|
227
|
+
if not udid:
|
|
228
|
+
continue
|
|
229
|
+
devices.append(
|
|
230
|
+
DeviceInfo(
|
|
231
|
+
udid=udid,
|
|
232
|
+
type=DeviceType.SIMULATOR,
|
|
233
|
+
name=name or "Unknown Simulator",
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
except (RuntimeError, json.JSONDecodeError, Exception):
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
return devices
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def get_all_ios_devices() -> dict[str, DeviceType]:
|
|
243
|
+
"""Get all connected iOS devices (simulators and physical).
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Dictionary mapping UDID to device type
|
|
247
|
+
"""
|
|
248
|
+
devices: dict[str, DeviceType] = {}
|
|
249
|
+
|
|
250
|
+
# Get simulators
|
|
251
|
+
for device in get_simulator_devices():
|
|
252
|
+
devices[device["udid"]] = DeviceType.SIMULATOR
|
|
253
|
+
|
|
254
|
+
# Get physical devices
|
|
255
|
+
for device in get_physical_ios_devices():
|
|
256
|
+
devices[device["udid"]] = DeviceType.PHYSICAL
|
|
257
|
+
|
|
258
|
+
return devices
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_all_ios_devices_detailed() -> list[DeviceInfo]:
|
|
262
|
+
"""Get detailed info about all connected iOS devices.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
List of DeviceInfo dicts with udid, type, and name
|
|
266
|
+
"""
|
|
267
|
+
devices: list[DeviceInfo] = []
|
|
268
|
+
devices.extend(get_simulator_devices())
|
|
269
|
+
devices.extend(get_physical_ios_devices())
|
|
270
|
+
return devices
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def get_ios_client(
|
|
274
|
+
udid: str | None = None,
|
|
275
|
+
config: IosClientConfig | None = None,
|
|
276
|
+
) -> IosClientWrapper:
|
|
277
|
+
"""Factory function to get the appropriate iOS client based on device type.
|
|
278
|
+
|
|
279
|
+
Automatically detects whether the device is a simulator or physical device
|
|
280
|
+
and returns the appropriate client wrapper.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
udid: Optional device UDID
|
|
284
|
+
config: Optional iOS client configuration (WDA/IDB settings). Defaults are used when None.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
IdbClientWrapper for simulators, WdaClientWrapper for physical devices
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
DeviceNotFoundError: If the device cannot be found
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
# Auto-detect and get appropriate client
|
|
294
|
+
client = get_ios_client("device-udid")
|
|
295
|
+
|
|
296
|
+
async with client:
|
|
297
|
+
await client.tap(100, 200)
|
|
298
|
+
screenshot = await client.screenshot()
|
|
299
|
+
"""
|
|
300
|
+
if not udid:
|
|
301
|
+
if config and config.browserstack:
|
|
302
|
+
return BrowserStackClientWrapper(config=config.browserstack)
|
|
303
|
+
raise DeviceNotFoundError("No device UDID provided")
|
|
304
|
+
|
|
305
|
+
device_type = get_device_type(udid)
|
|
306
|
+
resolved_config = config or IosClientConfig()
|
|
307
|
+
|
|
308
|
+
if device_type == DeviceType.SIMULATOR:
|
|
309
|
+
return IdbClientWrapper(
|
|
310
|
+
udid=udid,
|
|
311
|
+
host=resolved_config.idb.host,
|
|
312
|
+
port=resolved_config.idb.port,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if device_type == DeviceType.PHYSICAL:
|
|
316
|
+
return WdaClientWrapper(
|
|
317
|
+
udid=udid,
|
|
318
|
+
config=resolved_config.wda,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Device type is unknown - try to provide helpful error
|
|
322
|
+
all_devices = get_all_ios_devices()
|
|
323
|
+
|
|
324
|
+
if not all_devices:
|
|
325
|
+
raise DeviceNotFoundError(
|
|
326
|
+
f"Device '{udid}' not found. No iOS devices detected.\n"
|
|
327
|
+
"For simulators: Boot a simulator using Xcode or `xcrun simctl boot <udid>`\n"
|
|
328
|
+
"For physical devices: Connect via USB and trust the computer on the device"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
available = ", ".join(f"{u} ({t})" for u, t in all_devices.items())
|
|
332
|
+
raise DeviceNotFoundError(f"Device '{udid}' not found.\nAvailable devices: {available}")
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, SecretStr
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BrowserStackClientConfig(BaseModel):
|
|
7
|
+
model_config = ConfigDict(frozen=True)
|
|
8
|
+
username: str
|
|
9
|
+
access_key: SecretStr
|
|
10
|
+
device_name: str
|
|
11
|
+
platform_version: str
|
|
12
|
+
app_url: str
|
|
13
|
+
hub_url: str | None = None
|
|
14
|
+
project_name: str | None = None
|
|
15
|
+
build_name: str | None = None
|
|
16
|
+
session_name: str | None = None
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def with_overrides(
|
|
20
|
+
cls,
|
|
21
|
+
username: str | None = None,
|
|
22
|
+
access_key: str | None = None,
|
|
23
|
+
device_name: str | None = None,
|
|
24
|
+
platform_version: str | None = None,
|
|
25
|
+
app_url: str | None = None,
|
|
26
|
+
hub_url: str | None = None,
|
|
27
|
+
project_name: str | None = None,
|
|
28
|
+
build_name: str | None = None,
|
|
29
|
+
session_name: str | None = None,
|
|
30
|
+
base: BrowserStackClientConfig | None = None,
|
|
31
|
+
) -> BrowserStackClientConfig:
|
|
32
|
+
"""Create a BrowserStackClientConfig with only specified fields overridden."""
|
|
33
|
+
if base is None:
|
|
34
|
+
raise ValueError("base config is required for BrowserStackClientConfig.with_overrides")
|
|
35
|
+
overrides = {
|
|
36
|
+
k: v
|
|
37
|
+
for k, v in {
|
|
38
|
+
"username": username,
|
|
39
|
+
"access_key": access_key,
|
|
40
|
+
"device_name": device_name,
|
|
41
|
+
"platform_version": platform_version,
|
|
42
|
+
"app_url": app_url,
|
|
43
|
+
"hub_url": hub_url,
|
|
44
|
+
"project_name": project_name,
|
|
45
|
+
"build_name": build_name,
|
|
46
|
+
"session_name": session_name,
|
|
47
|
+
}.items()
|
|
48
|
+
if v is not None
|
|
49
|
+
}
|
|
50
|
+
if not overrides:
|
|
51
|
+
return base
|
|
52
|
+
return base.model_copy(update=overrides)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class WdaClientConfig(BaseModel):
|
|
56
|
+
model_config = ConfigDict(frozen=True)
|
|
57
|
+
wda_url: str = "http://localhost:8100"
|
|
58
|
+
timeout: float = 30.0
|
|
59
|
+
auto_start_iproxy: bool = True
|
|
60
|
+
auto_start_wda: bool = True
|
|
61
|
+
wda_project_path: str | None = None
|
|
62
|
+
wda_startup_timeout: float = 120.0
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def with_overrides(
|
|
66
|
+
cls,
|
|
67
|
+
wda_url: str | None = None,
|
|
68
|
+
timeout: float | None = None,
|
|
69
|
+
auto_start_iproxy: bool | None = None,
|
|
70
|
+
auto_start_wda: bool | None = None,
|
|
71
|
+
wda_project_path: str | None = None,
|
|
72
|
+
wda_startup_timeout: float | None = None,
|
|
73
|
+
) -> WdaClientConfig:
|
|
74
|
+
"""Create a WdaClientConfig with only specified fields overridden.
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
config = WdaClientConfig.with_overrides(
|
|
78
|
+
wda_url="http://localhost:8101",
|
|
79
|
+
auto_start_wda=False,
|
|
80
|
+
)
|
|
81
|
+
"""
|
|
82
|
+
base = cls()
|
|
83
|
+
overrides = {
|
|
84
|
+
k: v
|
|
85
|
+
for k, v in {
|
|
86
|
+
"wda_url": wda_url,
|
|
87
|
+
"timeout": timeout,
|
|
88
|
+
"auto_start_iproxy": auto_start_iproxy,
|
|
89
|
+
"auto_start_wda": auto_start_wda,
|
|
90
|
+
"wda_project_path": wda_project_path,
|
|
91
|
+
"wda_startup_timeout": wda_startup_timeout,
|
|
92
|
+
}.items()
|
|
93
|
+
if v is not None
|
|
94
|
+
}
|
|
95
|
+
if not overrides:
|
|
96
|
+
return base
|
|
97
|
+
return base.model_copy(update=overrides)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class IdbClientConfig(BaseModel):
|
|
101
|
+
model_config = ConfigDict(frozen=True)
|
|
102
|
+
host: str | None = None
|
|
103
|
+
port: int | None = None
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def with_overrides(
|
|
107
|
+
cls,
|
|
108
|
+
host: str | None = None,
|
|
109
|
+
port: int | None = None,
|
|
110
|
+
) -> IdbClientConfig:
|
|
111
|
+
"""Create an IdbClientConfig with only specified fields overridden."""
|
|
112
|
+
base = cls()
|
|
113
|
+
overrides = {k: v for k, v in {"host": host, "port": port}.items() if v is not None}
|
|
114
|
+
if not overrides:
|
|
115
|
+
return base
|
|
116
|
+
return base.model_copy(update=overrides)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class IosClientConfig(BaseModel):
|
|
120
|
+
model_config = ConfigDict(frozen=True)
|
|
121
|
+
wda: WdaClientConfig = WdaClientConfig()
|
|
122
|
+
idb: IdbClientConfig = IdbClientConfig()
|
|
123
|
+
browserstack: BrowserStackClientConfig | None = None
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def with_overrides(
|
|
127
|
+
cls,
|
|
128
|
+
wda: WdaClientConfig | None = None,
|
|
129
|
+
idb: IdbClientConfig | None = None,
|
|
130
|
+
browserstack: BrowserStackClientConfig | None = None,
|
|
131
|
+
) -> IosClientConfig:
|
|
132
|
+
"""Create an IosClientConfig with only specified fields overridden."""
|
|
133
|
+
base = cls()
|
|
134
|
+
overrides = {
|
|
135
|
+
k: v
|
|
136
|
+
for k, v in {"wda": wda, "idb": idb, "browserstack": browserstack}.items()
|
|
137
|
+
if v is not None
|
|
138
|
+
}
|
|
139
|
+
if not overrides:
|
|
140
|
+
return base
|
|
141
|
+
return base.model_copy(update=overrides)
|