minitap-mobile-use 0.0.1.dev0__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.
Potentially problematic release.
This version of minitap-mobile-use might be problematic. Click here for more details.
- minitap/mobile_use/__init__.py +0 -0
- minitap/mobile_use/agents/contextor/contextor.py +42 -0
- minitap/mobile_use/agents/cortex/cortex.md +93 -0
- minitap/mobile_use/agents/cortex/cortex.py +107 -0
- minitap/mobile_use/agents/cortex/types.py +11 -0
- minitap/mobile_use/agents/executor/executor.md +73 -0
- minitap/mobile_use/agents/executor/executor.py +84 -0
- minitap/mobile_use/agents/executor/executor_context_cleaner.py +27 -0
- minitap/mobile_use/agents/executor/utils.py +11 -0
- minitap/mobile_use/agents/hopper/hopper.md +13 -0
- minitap/mobile_use/agents/hopper/hopper.py +45 -0
- minitap/mobile_use/agents/orchestrator/human.md +13 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.md +18 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.py +114 -0
- minitap/mobile_use/agents/orchestrator/types.py +14 -0
- minitap/mobile_use/agents/outputter/human.md +25 -0
- minitap/mobile_use/agents/outputter/outputter.py +75 -0
- minitap/mobile_use/agents/outputter/test_outputter.py +107 -0
- minitap/mobile_use/agents/planner/human.md +12 -0
- minitap/mobile_use/agents/planner/planner.md +64 -0
- minitap/mobile_use/agents/planner/planner.py +64 -0
- minitap/mobile_use/agents/planner/types.py +44 -0
- minitap/mobile_use/agents/planner/utils.py +45 -0
- minitap/mobile_use/agents/summarizer/summarizer.py +34 -0
- minitap/mobile_use/clients/device_hardware_client.py +23 -0
- minitap/mobile_use/clients/ios_client.py +44 -0
- minitap/mobile_use/clients/screen_api_client.py +53 -0
- minitap/mobile_use/config.py +285 -0
- minitap/mobile_use/constants.py +2 -0
- minitap/mobile_use/context.py +65 -0
- minitap/mobile_use/controllers/__init__.py +0 -0
- minitap/mobile_use/controllers/mobile_command_controller.py +379 -0
- minitap/mobile_use/controllers/platform_specific_commands_controller.py +74 -0
- minitap/mobile_use/graph/graph.py +149 -0
- minitap/mobile_use/graph/state.py +73 -0
- minitap/mobile_use/main.py +122 -0
- minitap/mobile_use/sdk/__init__.py +12 -0
- minitap/mobile_use/sdk/agent.py +524 -0
- minitap/mobile_use/sdk/builders/__init__.py +10 -0
- minitap/mobile_use/sdk/builders/agent_config_builder.py +213 -0
- minitap/mobile_use/sdk/builders/index.py +15 -0
- minitap/mobile_use/sdk/builders/task_request_builder.py +218 -0
- minitap/mobile_use/sdk/constants.py +14 -0
- minitap/mobile_use/sdk/examples/README.md +45 -0
- minitap/mobile_use/sdk/examples/__init__.py +1 -0
- minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
- minitap/mobile_use/sdk/examples/smart_notification_assistant.py +177 -0
- minitap/mobile_use/sdk/types/__init__.py +49 -0
- minitap/mobile_use/sdk/types/agent.py +73 -0
- minitap/mobile_use/sdk/types/exceptions.py +74 -0
- minitap/mobile_use/sdk/types/task.py +191 -0
- minitap/mobile_use/sdk/utils.py +28 -0
- minitap/mobile_use/servers/config.py +19 -0
- minitap/mobile_use/servers/device_hardware_bridge.py +212 -0
- minitap/mobile_use/servers/device_screen_api.py +143 -0
- minitap/mobile_use/servers/start_servers.py +151 -0
- minitap/mobile_use/servers/stop_servers.py +215 -0
- minitap/mobile_use/servers/utils.py +11 -0
- minitap/mobile_use/services/accessibility.py +100 -0
- minitap/mobile_use/services/llm.py +143 -0
- minitap/mobile_use/tools/index.py +54 -0
- minitap/mobile_use/tools/mobile/back.py +52 -0
- minitap/mobile_use/tools/mobile/copy_text_from.py +77 -0
- minitap/mobile_use/tools/mobile/erase_text.py +124 -0
- minitap/mobile_use/tools/mobile/input_text.py +74 -0
- minitap/mobile_use/tools/mobile/launch_app.py +59 -0
- minitap/mobile_use/tools/mobile/list_packages.py +78 -0
- minitap/mobile_use/tools/mobile/long_press_on.py +62 -0
- minitap/mobile_use/tools/mobile/open_link.py +59 -0
- minitap/mobile_use/tools/mobile/paste_text.py +66 -0
- minitap/mobile_use/tools/mobile/press_key.py +58 -0
- minitap/mobile_use/tools/mobile/run_flow.py +57 -0
- minitap/mobile_use/tools/mobile/stop_app.py +58 -0
- minitap/mobile_use/tools/mobile/swipe.py +56 -0
- minitap/mobile_use/tools/mobile/take_screenshot.py +70 -0
- minitap/mobile_use/tools/mobile/tap.py +66 -0
- minitap/mobile_use/tools/mobile/wait_for_animation_to_end.py +68 -0
- minitap/mobile_use/tools/tool_wrapper.py +33 -0
- minitap/mobile_use/utils/cli_helpers.py +40 -0
- minitap/mobile_use/utils/cli_selection.py +144 -0
- minitap/mobile_use/utils/conversations.py +31 -0
- minitap/mobile_use/utils/decorators.py +123 -0
- minitap/mobile_use/utils/errors.py +6 -0
- minitap/mobile_use/utils/file.py +13 -0
- minitap/mobile_use/utils/logger.py +184 -0
- minitap/mobile_use/utils/media.py +73 -0
- minitap/mobile_use/utils/recorder.py +55 -0
- minitap/mobile_use/utils/requests_utils.py +37 -0
- minitap/mobile_use/utils/shell_utils.py +20 -0
- minitap/mobile_use/utils/time.py +6 -0
- minitap/mobile_use/utils/ui_hierarchy.py +30 -0
- minitap_mobile_use-0.0.1.dev0.dist-info/METADATA +274 -0
- minitap_mobile_use-0.0.1.dev0.dist-info/RECORD +95 -0
- minitap_mobile_use-0.0.1.dev0.dist-info/WHEEL +4 -0
- minitap_mobile_use-0.0.1.dev0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from typing import IO
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def strip_json_comments(text: str) -> str:
|
|
7
|
+
text = re.sub(r"//.*?$", "", text, flags=re.MULTILINE)
|
|
8
|
+
text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
|
|
9
|
+
return text
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_jsonc(file: IO) -> dict:
|
|
13
|
+
return json.loads(strip_json_comments(file.read()))
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
from colorama import Fore, Style, init
|
|
8
|
+
|
|
9
|
+
init(autoreset=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LogLevel(Enum):
|
|
13
|
+
DEBUG = ("DEBUG", Fore.MAGENTA, "🔍")
|
|
14
|
+
INFO = ("INFO", Fore.WHITE, "ℹ")
|
|
15
|
+
SUCCESS = ("SUCCESS", Fore.GREEN, "✓")
|
|
16
|
+
WARNING = ("WARNING", Fore.YELLOW, "⚠")
|
|
17
|
+
ERROR = ("ERROR", Fore.RED, "❌")
|
|
18
|
+
CRITICAL = ("CRITICAL", Fore.RED + Style.BRIGHT, "💥")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MobileUseLogger:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
name: str,
|
|
25
|
+
log_file: Optional[Union[str, Path]] = None,
|
|
26
|
+
console_level: str = "INFO",
|
|
27
|
+
file_level: str = "DEBUG",
|
|
28
|
+
enable_file_logging: bool = True,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the MobileUse logger.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name: Logger name (usually __name__)
|
|
35
|
+
log_file: Path to log file (defaults to logs/{name}.log)
|
|
36
|
+
console_level: Minimum level for console output
|
|
37
|
+
file_level: Minimum level for file output
|
|
38
|
+
enable_file_logging: Whether to enable file logging
|
|
39
|
+
"""
|
|
40
|
+
self.name = name
|
|
41
|
+
self.logger = logging.getLogger(name)
|
|
42
|
+
self.logger.setLevel(logging.DEBUG)
|
|
43
|
+
|
|
44
|
+
self.logger.handlers.clear()
|
|
45
|
+
|
|
46
|
+
self._setup_console_handler(console_level)
|
|
47
|
+
|
|
48
|
+
if enable_file_logging:
|
|
49
|
+
self._setup_file_handler(log_file, file_level)
|
|
50
|
+
|
|
51
|
+
def _setup_console_handler(self, level: str):
|
|
52
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
53
|
+
console_handler.setLevel(getattr(logging, level.upper()))
|
|
54
|
+
|
|
55
|
+
console_formatter = ColoredFormatter()
|
|
56
|
+
console_handler.setFormatter(console_formatter)
|
|
57
|
+
|
|
58
|
+
self.logger.addHandler(console_handler)
|
|
59
|
+
|
|
60
|
+
def _setup_file_handler(self, log_file: Optional[Union[str, Path]], level: str):
|
|
61
|
+
if log_file is None:
|
|
62
|
+
log_file = Path("logs") / f"{self.name.replace('.', '_')}.log"
|
|
63
|
+
|
|
64
|
+
log_file = Path(log_file)
|
|
65
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
|
68
|
+
file_handler.setLevel(getattr(logging, level.upper()))
|
|
69
|
+
|
|
70
|
+
file_formatter = logging.Formatter(
|
|
71
|
+
fmt="%(asctime)s | %(name)s | %(levelname)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
72
|
+
)
|
|
73
|
+
file_handler.setFormatter(file_formatter)
|
|
74
|
+
|
|
75
|
+
self.logger.addHandler(file_handler)
|
|
76
|
+
|
|
77
|
+
def debug(self, message: str, **kwargs):
|
|
78
|
+
self.logger.debug(message, extra={"log_level": LogLevel.DEBUG}, **kwargs)
|
|
79
|
+
|
|
80
|
+
def info(self, message: str, **kwargs):
|
|
81
|
+
self.logger.info(message, extra={"log_level": LogLevel.INFO}, **kwargs)
|
|
82
|
+
|
|
83
|
+
def success(self, message: str, **kwargs):
|
|
84
|
+
self.logger.info(message, extra={"log_level": LogLevel.SUCCESS}, **kwargs)
|
|
85
|
+
|
|
86
|
+
def warning(self, message: str, **kwargs):
|
|
87
|
+
self.logger.warning(message, extra={"log_level": LogLevel.WARNING}, **kwargs)
|
|
88
|
+
|
|
89
|
+
def error(self, message: str, **kwargs):
|
|
90
|
+
self.logger.error(message, extra={"log_level": LogLevel.ERROR}, **kwargs)
|
|
91
|
+
|
|
92
|
+
def critical(self, message: str, **kwargs):
|
|
93
|
+
self.logger.critical(message, extra={"log_level": LogLevel.CRITICAL}, **kwargs)
|
|
94
|
+
|
|
95
|
+
def header(self, message: str, **_kwargs):
|
|
96
|
+
separator = "=" * 60
|
|
97
|
+
colored_separator = f"{Fore.CYAN}{separator}{Style.RESET_ALL}"
|
|
98
|
+
colored_message = f"{Fore.CYAN}{message}{Style.RESET_ALL}"
|
|
99
|
+
|
|
100
|
+
print(colored_separator)
|
|
101
|
+
print(colored_message)
|
|
102
|
+
print(colored_separator)
|
|
103
|
+
self.logger.info(f"\n{separator}\n{message}\n{separator}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ColoredFormatter(logging.Formatter):
|
|
107
|
+
def format(self, record):
|
|
108
|
+
log_level = getattr(record, "log_level", LogLevel.INFO)
|
|
109
|
+
_level_name, color, symbol = log_level.value
|
|
110
|
+
|
|
111
|
+
colored_message = f"{color}{symbol} {record.getMessage()}{Style.RESET_ALL}"
|
|
112
|
+
|
|
113
|
+
return colored_message
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
_loggers = {}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_logger(
|
|
120
|
+
name: str,
|
|
121
|
+
log_file: Optional[Union[str, Path]] = None,
|
|
122
|
+
console_level: str = "INFO",
|
|
123
|
+
file_level: str = "DEBUG",
|
|
124
|
+
enable_file_logging: bool = False,
|
|
125
|
+
) -> MobileUseLogger:
|
|
126
|
+
"""
|
|
127
|
+
Get or create a logger instance.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
name: Logger name (usually __name__)
|
|
131
|
+
log_file: Path to log file (defaults to logs/{name}.log)
|
|
132
|
+
console_level: Minimum level for console output
|
|
133
|
+
file_level: Minimum level for file output
|
|
134
|
+
enable_file_logging: Whether to enable file logging
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
MobileUseLogger instance
|
|
138
|
+
"""
|
|
139
|
+
if name not in _loggers:
|
|
140
|
+
_loggers[name] = MobileUseLogger(
|
|
141
|
+
name=name,
|
|
142
|
+
log_file=log_file,
|
|
143
|
+
console_level=console_level,
|
|
144
|
+
file_level=file_level,
|
|
145
|
+
enable_file_logging=enable_file_logging,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return _loggers[name]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def log_debug(message: str, logger_name: str = "mobile-use"):
|
|
152
|
+
get_logger(logger_name).debug(message)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def log_info(message: str, logger_name: str = "mobile-use"):
|
|
156
|
+
get_logger(logger_name).info(message)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def log_success(message: str, logger_name: str = "mobile-use"):
|
|
160
|
+
get_logger(logger_name).success(message)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def log_warning(message: str, logger_name: str = "mobile-use"):
|
|
164
|
+
get_logger(logger_name).warning(message)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def log_error(message: str, logger_name: str = "mobile-use"):
|
|
168
|
+
get_logger(logger_name).error(message)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def log_critical(message: str, logger_name: str = "mobile-use"):
|
|
172
|
+
get_logger(logger_name).critical(message)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def log_header(message: str, logger_name: str = "mobile-use"):
|
|
176
|
+
get_logger(logger_name).header(message)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_server_logger() -> MobileUseLogger:
|
|
180
|
+
return get_logger(
|
|
181
|
+
name="mobile-use.servers",
|
|
182
|
+
console_level="INFO",
|
|
183
|
+
file_level="DEBUG",
|
|
184
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def compress_base64_jpeg(base64_str: str, quality: int = 50) -> str:
|
|
10
|
+
if base64_str.startswith("data:image"):
|
|
11
|
+
base64_str = base64_str.split(",")[1]
|
|
12
|
+
|
|
13
|
+
image_data = base64.b64decode(base64_str)
|
|
14
|
+
image = Image.open(BytesIO(image_data))
|
|
15
|
+
|
|
16
|
+
compressed_io = BytesIO()
|
|
17
|
+
image.save(compressed_io, format="JPEG", quality=quality, optimize=True)
|
|
18
|
+
|
|
19
|
+
compressed_base64 = base64.b64encode(compressed_io.getvalue()).decode("utf-8")
|
|
20
|
+
return compressed_base64
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_gif_from_trace_folder(trace_folder_path: Path):
|
|
24
|
+
images = []
|
|
25
|
+
image_files = []
|
|
26
|
+
|
|
27
|
+
for file in trace_folder_path.iterdir():
|
|
28
|
+
if file.suffix == ".jpeg":
|
|
29
|
+
image_files.append(file)
|
|
30
|
+
|
|
31
|
+
image_files.sort(key=lambda f: int(f.stem))
|
|
32
|
+
|
|
33
|
+
print("Found " + str(len(image_files)) + " images to compile")
|
|
34
|
+
|
|
35
|
+
for file in image_files:
|
|
36
|
+
with open(file, "rb") as f:
|
|
37
|
+
image = Image.open(f).convert("RGB")
|
|
38
|
+
images.append(image)
|
|
39
|
+
|
|
40
|
+
if len(images) == 0:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
gif_path = trace_folder_path / "trace.gif"
|
|
44
|
+
images[0].save(gif_path, save_all=True, append_images=images[1:], loop=0)
|
|
45
|
+
print("GIF created at " + str(gif_path))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def remove_images_from_trace_folder(trace_folder_path: Path):
|
|
49
|
+
for file in trace_folder_path.iterdir():
|
|
50
|
+
if file.suffix == ".jpeg":
|
|
51
|
+
file.unlink()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create_steps_json_from_trace_folder(trace_folder_path: Path):
|
|
55
|
+
steps = []
|
|
56
|
+
for file in trace_folder_path.iterdir():
|
|
57
|
+
if file.suffix == ".json":
|
|
58
|
+
with open(file, "r", encoding="utf-8", errors="ignore") as f:
|
|
59
|
+
json_content = f.read()
|
|
60
|
+
steps.append({"timestamp": int(file.stem), "data": json_content})
|
|
61
|
+
|
|
62
|
+
steps.sort(key=lambda f: f["timestamp"])
|
|
63
|
+
|
|
64
|
+
print("Found " + str(len(steps)) + " steps to compile")
|
|
65
|
+
|
|
66
|
+
with open(trace_folder_path / "steps.json", "w", encoding="utf-8", errors="ignore") as f:
|
|
67
|
+
f.write(json.dumps(steps))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def remove_steps_json_from_trace_folder(trace_folder_path: Path):
|
|
71
|
+
for file in trace_folder_path.iterdir():
|
|
72
|
+
if file.suffix == ".json" and file.name != "steps.json":
|
|
73
|
+
file.unlink()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from langchain_core.messages import BaseMessage
|
|
6
|
+
from minitap.mobile_use.config import record_events
|
|
7
|
+
from minitap.mobile_use.context import MobileUseContext
|
|
8
|
+
from minitap.mobile_use.controllers.mobile_command_controller import take_screenshot
|
|
9
|
+
from minitap.mobile_use.utils.logger import get_logger
|
|
10
|
+
from minitap.mobile_use.utils.media import compress_base64_jpeg
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def record_interaction(ctx: MobileUseContext, response: BaseMessage):
|
|
16
|
+
if not ctx.execution_setup:
|
|
17
|
+
raise ValueError("No execution setup found")
|
|
18
|
+
|
|
19
|
+
logger.info("Recording interaction")
|
|
20
|
+
screenshot_base64 = take_screenshot(ctx)
|
|
21
|
+
logger.info("Screenshot taken")
|
|
22
|
+
try:
|
|
23
|
+
compressed_screenshot_base64 = compress_base64_jpeg(screenshot_base64, 20)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
logger.error(f"Error compressing screenshot: {e}")
|
|
26
|
+
return "Could not record this interaction"
|
|
27
|
+
timestamp = time.time()
|
|
28
|
+
folder = ctx.execution_setup.traces_path.joinpath(ctx.execution_setup.trace_id).resolve()
|
|
29
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
try:
|
|
31
|
+
with open(
|
|
32
|
+
folder.joinpath(f"{int(timestamp)}.jpeg").resolve(),
|
|
33
|
+
"wb",
|
|
34
|
+
) as f:
|
|
35
|
+
f.write(base64.b64decode(compressed_screenshot_base64))
|
|
36
|
+
|
|
37
|
+
with open(
|
|
38
|
+
folder.joinpath(f"{int(timestamp)}.json").resolve(),
|
|
39
|
+
"w",
|
|
40
|
+
encoding="utf-8",
|
|
41
|
+
) as f:
|
|
42
|
+
f.write(response.model_dump_json())
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.error(f"Error recording interaction: {e}")
|
|
45
|
+
return "Screenshot recorded successfully"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def log_agent_thoughts(agents_thoughts: list[str], output_path: Path | None):
|
|
49
|
+
if len(agents_thoughts) > 0:
|
|
50
|
+
last_agents_thoughts = agents_thoughts[-1]
|
|
51
|
+
previous_last_agents_thoughts = agents_thoughts[-2] if len(agents_thoughts) > 1 else None
|
|
52
|
+
if previous_last_agents_thoughts != last_agents_thoughts:
|
|
53
|
+
logger.info(f"💭 {last_agents_thoughts}")
|
|
54
|
+
if output_path:
|
|
55
|
+
record_events(output_path=output_path, events=agents_thoughts)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from minitap.mobile_use.utils.logger import get_logger
|
|
3
|
+
|
|
4
|
+
logger = get_logger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def curl_from_request(req: requests.PreparedRequest) -> str:
|
|
8
|
+
"""Converts a requests.PreparedRequest object to a valid cURL command string."""
|
|
9
|
+
command = ["curl", f"-X {req.method}"]
|
|
10
|
+
|
|
11
|
+
for key, value in req.headers.items():
|
|
12
|
+
command.append(f'-H "{key}: {value}"')
|
|
13
|
+
|
|
14
|
+
if req.body:
|
|
15
|
+
body = req.body
|
|
16
|
+
if isinstance(body, bytes):
|
|
17
|
+
body = body.decode("utf-8")
|
|
18
|
+
# Escape single quotes in the body for shell safety
|
|
19
|
+
body = body.replace("'", "'\\''")
|
|
20
|
+
command.append(f"-d '{body}'")
|
|
21
|
+
|
|
22
|
+
command.append(f"'{req.url}'")
|
|
23
|
+
|
|
24
|
+
return " ".join(command)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def logging_hook(response, *args, **kwargs):
|
|
28
|
+
"""Hook to log the request as a cURL command."""
|
|
29
|
+
curl_command = curl_from_request(response.request)
|
|
30
|
+
logger.debug(f"\n--- cURL Command ---\n{curl_command}\n--------------------")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_session_with_curl_logging() -> requests.Session:
|
|
34
|
+
"""Returns a requests.Session with cURL logging enabled."""
|
|
35
|
+
session = requests.Session()
|
|
36
|
+
session.hooks["response"] = [logging_hook]
|
|
37
|
+
return session
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def run_shell_command_on_host(command: str) -> str:
|
|
5
|
+
"""Helper to run a shell command on the host and return the output."""
|
|
6
|
+
try:
|
|
7
|
+
result = subprocess.run(
|
|
8
|
+
command,
|
|
9
|
+
shell=True,
|
|
10
|
+
check=True,
|
|
11
|
+
capture_output=True,
|
|
12
|
+
text=True,
|
|
13
|
+
)
|
|
14
|
+
return result.stdout.strip()
|
|
15
|
+
except subprocess.CalledProcessError as e:
|
|
16
|
+
# Log the error and stderr for better debugging
|
|
17
|
+
error_message = (
|
|
18
|
+
f"Command '{command}' failed with exit code {e.returncode}.\nStderr: {e.stderr.strip()}"
|
|
19
|
+
)
|
|
20
|
+
raise RuntimeError(error_message) from e
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def find_element_by_resource_id(ui_hierarchy: list[dict], resource_id: str) -> Optional[dict]:
|
|
5
|
+
"""
|
|
6
|
+
Find a UI element by its resource-id in the UI hierarchy.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
ui_hierarchy: List of UI element dictionaries
|
|
10
|
+
resource_id: The resource-id to search for
|
|
11
|
+
(e.g., "com.google.android.settings.intelligence:id/open_search_view_edit_text")
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
The complete UI element dictionary if found, None otherwise
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def search_recursive(elements: list[dict]) -> Optional[dict]:
|
|
18
|
+
for element in elements:
|
|
19
|
+
if isinstance(element, dict):
|
|
20
|
+
if element.get("resourceId") == resource_id:
|
|
21
|
+
return element
|
|
22
|
+
|
|
23
|
+
children = element.get("children", [])
|
|
24
|
+
if children:
|
|
25
|
+
result = search_recursive(children)
|
|
26
|
+
if result:
|
|
27
|
+
return result
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
return search_recursive(ui_hierarchy)
|