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.
Files changed (115) hide show
  1. minitap/mobile_use/__init__.py +0 -0
  2. minitap/mobile_use/agents/contextor/contextor.md +55 -0
  3. minitap/mobile_use/agents/contextor/contextor.py +175 -0
  4. minitap/mobile_use/agents/contextor/types.py +36 -0
  5. minitap/mobile_use/agents/cortex/cortex.md +135 -0
  6. minitap/mobile_use/agents/cortex/cortex.py +152 -0
  7. minitap/mobile_use/agents/cortex/types.py +15 -0
  8. minitap/mobile_use/agents/executor/executor.md +42 -0
  9. minitap/mobile_use/agents/executor/executor.py +87 -0
  10. minitap/mobile_use/agents/executor/tool_node.py +152 -0
  11. minitap/mobile_use/agents/hopper/hopper.md +15 -0
  12. minitap/mobile_use/agents/hopper/hopper.py +44 -0
  13. minitap/mobile_use/agents/orchestrator/human.md +12 -0
  14. minitap/mobile_use/agents/orchestrator/orchestrator.md +21 -0
  15. minitap/mobile_use/agents/orchestrator/orchestrator.py +134 -0
  16. minitap/mobile_use/agents/orchestrator/types.py +11 -0
  17. minitap/mobile_use/agents/outputter/human.md +25 -0
  18. minitap/mobile_use/agents/outputter/outputter.py +85 -0
  19. minitap/mobile_use/agents/outputter/test_outputter.py +167 -0
  20. minitap/mobile_use/agents/planner/human.md +14 -0
  21. minitap/mobile_use/agents/planner/planner.md +126 -0
  22. minitap/mobile_use/agents/planner/planner.py +101 -0
  23. minitap/mobile_use/agents/planner/types.py +51 -0
  24. minitap/mobile_use/agents/planner/utils.py +70 -0
  25. minitap/mobile_use/agents/summarizer/summarizer.py +35 -0
  26. minitap/mobile_use/agents/video_analyzer/__init__.py +5 -0
  27. minitap/mobile_use/agents/video_analyzer/human.md +5 -0
  28. minitap/mobile_use/agents/video_analyzer/video_analyzer.md +37 -0
  29. minitap/mobile_use/agents/video_analyzer/video_analyzer.py +111 -0
  30. minitap/mobile_use/clients/browserstack_client.py +477 -0
  31. minitap/mobile_use/clients/idb_client.py +429 -0
  32. minitap/mobile_use/clients/ios_client.py +332 -0
  33. minitap/mobile_use/clients/ios_client_config.py +141 -0
  34. minitap/mobile_use/clients/ui_automator_client.py +330 -0
  35. minitap/mobile_use/clients/wda_client.py +526 -0
  36. minitap/mobile_use/clients/wda_lifecycle.py +367 -0
  37. minitap/mobile_use/config.py +413 -0
  38. minitap/mobile_use/constants.py +3 -0
  39. minitap/mobile_use/context.py +106 -0
  40. minitap/mobile_use/controllers/__init__.py +0 -0
  41. minitap/mobile_use/controllers/android_controller.py +524 -0
  42. minitap/mobile_use/controllers/controller_factory.py +46 -0
  43. minitap/mobile_use/controllers/device_controller.py +182 -0
  44. minitap/mobile_use/controllers/ios_controller.py +436 -0
  45. minitap/mobile_use/controllers/platform_specific_commands_controller.py +199 -0
  46. minitap/mobile_use/controllers/types.py +106 -0
  47. minitap/mobile_use/controllers/unified_controller.py +193 -0
  48. minitap/mobile_use/graph/graph.py +160 -0
  49. minitap/mobile_use/graph/state.py +115 -0
  50. minitap/mobile_use/main.py +309 -0
  51. minitap/mobile_use/sdk/__init__.py +12 -0
  52. minitap/mobile_use/sdk/agent.py +1294 -0
  53. minitap/mobile_use/sdk/builders/__init__.py +10 -0
  54. minitap/mobile_use/sdk/builders/agent_config_builder.py +307 -0
  55. minitap/mobile_use/sdk/builders/index.py +15 -0
  56. minitap/mobile_use/sdk/builders/task_request_builder.py +236 -0
  57. minitap/mobile_use/sdk/constants.py +1 -0
  58. minitap/mobile_use/sdk/examples/README.md +83 -0
  59. minitap/mobile_use/sdk/examples/__init__.py +1 -0
  60. minitap/mobile_use/sdk/examples/app_lock_messaging.py +54 -0
  61. minitap/mobile_use/sdk/examples/platform_manual_task_example.py +67 -0
  62. minitap/mobile_use/sdk/examples/platform_minimal_example.py +48 -0
  63. minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
  64. minitap/mobile_use/sdk/examples/smart_notification_assistant.py +225 -0
  65. minitap/mobile_use/sdk/examples/video_transcription_example.py +117 -0
  66. minitap/mobile_use/sdk/services/cloud_mobile.py +656 -0
  67. minitap/mobile_use/sdk/services/platform.py +434 -0
  68. minitap/mobile_use/sdk/types/__init__.py +51 -0
  69. minitap/mobile_use/sdk/types/agent.py +84 -0
  70. minitap/mobile_use/sdk/types/exceptions.py +138 -0
  71. minitap/mobile_use/sdk/types/platform.py +183 -0
  72. minitap/mobile_use/sdk/types/task.py +269 -0
  73. minitap/mobile_use/sdk/utils.py +29 -0
  74. minitap/mobile_use/services/accessibility.py +100 -0
  75. minitap/mobile_use/services/llm.py +247 -0
  76. minitap/mobile_use/services/telemetry.py +421 -0
  77. minitap/mobile_use/tools/index.py +67 -0
  78. minitap/mobile_use/tools/mobile/back.py +52 -0
  79. minitap/mobile_use/tools/mobile/erase_one_char.py +56 -0
  80. minitap/mobile_use/tools/mobile/focus_and_clear_text.py +317 -0
  81. minitap/mobile_use/tools/mobile/focus_and_input_text.py +153 -0
  82. minitap/mobile_use/tools/mobile/launch_app.py +86 -0
  83. minitap/mobile_use/tools/mobile/long_press_on.py +169 -0
  84. minitap/mobile_use/tools/mobile/open_link.py +62 -0
  85. minitap/mobile_use/tools/mobile/press_key.py +83 -0
  86. minitap/mobile_use/tools/mobile/stop_app.py +62 -0
  87. minitap/mobile_use/tools/mobile/swipe.py +156 -0
  88. minitap/mobile_use/tools/mobile/tap.py +154 -0
  89. minitap/mobile_use/tools/mobile/video_recording.py +177 -0
  90. minitap/mobile_use/tools/mobile/wait_for_delay.py +81 -0
  91. minitap/mobile_use/tools/scratchpad.py +147 -0
  92. minitap/mobile_use/tools/test_utils.py +413 -0
  93. minitap/mobile_use/tools/tool_wrapper.py +16 -0
  94. minitap/mobile_use/tools/types.py +35 -0
  95. minitap/mobile_use/tools/utils.py +336 -0
  96. minitap/mobile_use/utils/app_launch_utils.py +173 -0
  97. minitap/mobile_use/utils/cli_helpers.py +37 -0
  98. minitap/mobile_use/utils/cli_selection.py +143 -0
  99. minitap/mobile_use/utils/conversations.py +31 -0
  100. minitap/mobile_use/utils/decorators.py +124 -0
  101. minitap/mobile_use/utils/errors.py +6 -0
  102. minitap/mobile_use/utils/file.py +13 -0
  103. minitap/mobile_use/utils/logger.py +183 -0
  104. minitap/mobile_use/utils/media.py +186 -0
  105. minitap/mobile_use/utils/recorder.py +52 -0
  106. minitap/mobile_use/utils/requests_utils.py +37 -0
  107. minitap/mobile_use/utils/shell_utils.py +20 -0
  108. minitap/mobile_use/utils/test_ui_hierarchy.py +178 -0
  109. minitap/mobile_use/utils/time.py +6 -0
  110. minitap/mobile_use/utils/ui_hierarchy.py +132 -0
  111. minitap/mobile_use/utils/video.py +281 -0
  112. minitap_mobile_use-3.3.0.dist-info/METADATA +329 -0
  113. minitap_mobile_use-3.3.0.dist-info/RECORD +115 -0
  114. minitap_mobile_use-3.3.0.dist-info/WHEEL +4 -0
  115. 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)