uiautodev 0.4.0__tar.gz → 0.5.0__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.
- {uiautodev-0.4.0 → uiautodev-0.5.0}/PKG-INFO +3 -4
- {uiautodev-0.4.0 → uiautodev-0.5.0}/README.md +2 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/pyproject.toml +3 -5
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/__init__.py +1 -1
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/cli.py +1 -1
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/command_proxy.py +19 -8
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/command_types.py +1 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/driver/android.py +25 -50
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/driver/base_driver.py +13 -4
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/driver/ios.py +0 -3
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/model.py +5 -1
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/router/device.py +19 -7
- {uiautodev-0.4.0 → uiautodev-0.5.0}/LICENSE +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/__main__.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/app.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/appium_proxy.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/case.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/common.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/driver/appium.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/driver/mock.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/driver/udt/udt.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/exceptions.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/provider.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/router/xml.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/static/demo.html +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/utils/common.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/utils/exceptions.py +0 -0
- {uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/utils/usbmux.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: uiautodev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
|
|
5
5
|
Home-page: https://uiauto.dev
|
|
6
6
|
License: MIT
|
|
@@ -14,13 +14,10 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.10
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Provides-Extra: appium
|
|
18
17
|
Requires-Dist: adbutils (>=2.7.0,<3.0.0)
|
|
19
|
-
Requires-Dist: appium-python-client (>=4.0.0,<5.0.0) ; extra == "appium"
|
|
20
18
|
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
21
19
|
Requires-Dist: construct
|
|
22
20
|
Requires-Dist: fastapi (>=0.111.0,<0.112.0)
|
|
23
|
-
Requires-Dist: httpretty (>=1.1.4,<2.0.0)
|
|
24
21
|
Requires-Dist: httpx
|
|
25
22
|
Requires-Dist: lxml
|
|
26
23
|
Requires-Dist: pillow
|
|
@@ -38,6 +35,8 @@ Description-Content-Type: text/markdown
|
|
|
38
35
|
|
|
39
36
|
https://uiauto.dev
|
|
40
37
|
|
|
38
|
+
> backup site: https://uiauto.devsleep.com
|
|
39
|
+
|
|
41
40
|
UI Inspector for Android and iOS, help inspector element properties, and auto generate XPath, script.
|
|
42
41
|
|
|
43
42
|
# Install
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "uiautodev"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Mobile UI Automation, include UI hierarchy inspector, script recorder"
|
|
5
5
|
homepage = "https://uiauto.dev"
|
|
6
6
|
authors = ["codeskyblue <codeskyblue@gmail.com>"]
|
|
@@ -15,8 +15,6 @@ construct = "*"
|
|
|
15
15
|
lxml = "*"
|
|
16
16
|
click = "^8.1.7"
|
|
17
17
|
pygments = ">=2"
|
|
18
|
-
httpretty = {version = "^1.1.4", optional = true}
|
|
19
|
-
appium-python-client = {version = "^4.0.0", optional = true}
|
|
20
18
|
uiautomator2 = ">=2"
|
|
21
19
|
httpx = "*"
|
|
22
20
|
fastapi = "^0.111.0"
|
|
@@ -25,8 +23,8 @@ poetry = "^1.8.2"
|
|
|
25
23
|
pydantic = "^2.6"
|
|
26
24
|
wdapy = "^0.2.2"
|
|
27
25
|
|
|
28
|
-
[tool.poetry.extras]
|
|
29
|
-
appium = ["appium-python-client", "httppretty"]
|
|
26
|
+
#[tool.poetry.extras]
|
|
27
|
+
#appium = ["appium-python-client", "httppretty"]
|
|
30
28
|
|
|
31
29
|
[tool.poetry.scripts]
|
|
32
30
|
"uiauto.dev" = "uiautodev.__main__:main"
|
|
@@ -140,7 +140,7 @@ def self_update():
|
|
|
140
140
|
@click.option("-f", "--force", is_flag=True, default=False, help="shutdown alrealy runningserver")
|
|
141
141
|
@click.option("-s", "--no-browser", is_flag=True, default=False, help="silent mode, do not open browser")
|
|
142
142
|
def server(port: int, host: str, reload: bool, force: bool, no_browser: bool):
|
|
143
|
-
|
|
143
|
+
print("uiautodev version:", __version__)
|
|
144
144
|
if force:
|
|
145
145
|
try:
|
|
146
146
|
httpx.get(f"http://{host}:{port}/shutdown", timeout=3)
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import time
|
|
10
10
|
import typing
|
|
11
|
-
from typing import Callable, Dict, Optional
|
|
11
|
+
from typing import Callable, Dict, List, Optional, Union
|
|
12
12
|
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
@@ -17,7 +17,7 @@ from uiautodev.command_types import AppLaunchRequest, AppTerminateRequest, By, C
|
|
|
17
17
|
WindowSizeResponse
|
|
18
18
|
from uiautodev.driver.base_driver import BaseDriver
|
|
19
19
|
from uiautodev.exceptions import ElementNotFoundError
|
|
20
|
-
from uiautodev.model import Node
|
|
20
|
+
from uiautodev.model import Node, AppInfo
|
|
21
21
|
from uiautodev.utils.common import node_travel
|
|
22
22
|
|
|
23
23
|
COMMANDS: Dict[Command, Callable] = {}
|
|
@@ -39,17 +39,21 @@ def get_command_params_type(command: Command) -> Optional[BaseModel]:
|
|
|
39
39
|
return type_hints.get("params")
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def send_command(driver: BaseDriver, command: Command, params=None):
|
|
42
|
+
def send_command(driver: BaseDriver, command: Union[str, Command], params=None):
|
|
43
43
|
if command not in COMMANDS:
|
|
44
44
|
raise NotImplementedError(f"command {command} not implemented")
|
|
45
45
|
func = COMMANDS[command]
|
|
46
|
-
|
|
47
|
-
if
|
|
46
|
+
params_model = get_command_params_type(command)
|
|
47
|
+
if params_model:
|
|
48
48
|
if params is None:
|
|
49
49
|
raise ValueError(f"params is required for {command}")
|
|
50
|
-
if
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
if isinstance(params, dict):
|
|
51
|
+
params = params_model.model_validate(params)
|
|
52
|
+
elif isinstance(params, params_model):
|
|
53
|
+
pass
|
|
54
|
+
else:
|
|
55
|
+
raise TypeError(f"params should be {params_model}", params)
|
|
56
|
+
if not params:
|
|
53
57
|
return func(driver)
|
|
54
58
|
return func(driver, params)
|
|
55
59
|
|
|
@@ -177,3 +181,10 @@ def click_element(driver: BaseDriver, params: FindElementRequest):
|
|
|
177
181
|
center_x = (node.bounds[0] + node.bounds[2]) / 2
|
|
178
182
|
center_y = (node.bounds[1] + node.bounds[3]) / 2
|
|
179
183
|
tap(driver, TapRequest(x=center_x, y=center_y, isPercent=True))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@register(Command.APP_LIST)
|
|
187
|
+
def app_list(driver: BaseDriver) -> List[AppInfo]:
|
|
188
|
+
# added in v0.5.0
|
|
189
|
+
return driver.app_list()
|
|
190
|
+
|
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
"""Created on Fri Mar 01 2024 14:19:29 by codeskyblue
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import json
|
|
8
7
|
import logging
|
|
9
8
|
import re
|
|
10
9
|
import time
|
|
11
10
|
from functools import cached_property, partial
|
|
12
|
-
from typing import List, Optional, Tuple
|
|
11
|
+
from typing import Iterator, List, Optional, Tuple
|
|
13
12
|
from xml.etree import ElementTree
|
|
14
13
|
|
|
15
14
|
import adbutils
|
|
@@ -18,9 +17,8 @@ from PIL import Image
|
|
|
18
17
|
|
|
19
18
|
from uiautodev.command_types import CurrentAppResponse
|
|
20
19
|
from uiautodev.driver.base_driver import BaseDriver
|
|
21
|
-
from uiautodev.driver.udt.udt import UDT, UDTError
|
|
22
20
|
from uiautodev.exceptions import AndroidDriverException, RequestError
|
|
23
|
-
from uiautodev.model import Node, Rect, ShellResponse, WindowSize
|
|
21
|
+
from uiautodev.model import Node, AppInfo, Rect, ShellResponse, WindowSize
|
|
24
22
|
from uiautodev.utils.common import fetch_through_socket
|
|
25
23
|
|
|
26
24
|
logger = logging.getLogger(__name__)
|
|
@@ -29,28 +27,15 @@ class AndroidDriver(BaseDriver):
|
|
|
29
27
|
def __init__(self, serial: str):
|
|
30
28
|
super().__init__(serial)
|
|
31
29
|
self.adb_device = adbutils.device(serial)
|
|
32
|
-
self._try_dump_list = [
|
|
33
|
-
self._get_u2_hierarchy,
|
|
34
|
-
self._get_udt_dump_hierarchy,
|
|
35
|
-
# self._get_appium_hierarchy,
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
@cached_property
|
|
39
|
-
def udt(self) -> UDT:
|
|
40
|
-
return UDT(self.adb_device)
|
|
41
30
|
|
|
42
31
|
@cached_property
|
|
43
32
|
def ud(self) -> u2.Device:
|
|
44
33
|
return u2.connect_usb(self.serial)
|
|
45
34
|
|
|
46
35
|
def screenshot(self, id: int) -> Image.Image:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
logger.warning("screenshot error: %s", str(e))
|
|
51
|
-
if id > 0:
|
|
52
|
-
raise AndroidDriverException("multi-display is not supported yet for uiautomator2")
|
|
53
|
-
return self.ud.screenshot()
|
|
36
|
+
if id > 0:
|
|
37
|
+
raise AndroidDriverException("multi-display is not supported yet for uiautomator2")
|
|
38
|
+
return self.ud.screenshot()
|
|
54
39
|
|
|
55
40
|
def shell(self, command: str) -> ShellResponse:
|
|
56
41
|
try:
|
|
@@ -82,38 +67,12 @@ class AndroidDriver(BaseDriver):
|
|
|
82
67
|
|
|
83
68
|
uiautomator dump errors:
|
|
84
69
|
- ERROR: could not get idle state.
|
|
85
|
-
|
|
86
70
|
"""
|
|
87
|
-
for dump_func in self._try_dump_list[:]:
|
|
88
|
-
try:
|
|
89
|
-
logger.debug(f"try to dump with %s", dump_func.__name__)
|
|
90
|
-
result = dump_func()
|
|
91
|
-
logger.debug("dump success")
|
|
92
|
-
self._try_dump_list.remove(dump_func)
|
|
93
|
-
self._try_dump_list.insert(0, dump_func)
|
|
94
|
-
return result
|
|
95
|
-
except Exception as e:
|
|
96
|
-
logger.exception("unexpected dump error: %s", e)
|
|
97
|
-
raise AndroidDriverException("Failed to dump hierarchy")
|
|
98
|
-
|
|
99
|
-
def _get_u2_hierarchy(self) -> str:
|
|
100
|
-
d = u2.connect_usb(self.serial)
|
|
101
|
-
return d.dump_hierarchy()
|
|
102
|
-
|
|
103
|
-
def _get_appium_hierarchy(self) -> str:
|
|
104
|
-
c = self.adb_device.create_connection(adbutils.Network.TCP, 6790)
|
|
105
71
|
try:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
raise AndroidDriverException(
|
|
110
|
-
f"Failed to get hierarchy from appium server: {str(e)}"
|
|
111
|
-
)
|
|
112
|
-
finally:
|
|
113
|
-
c.close()
|
|
114
|
-
|
|
115
|
-
def _get_udt_dump_hierarchy(self) -> str:
|
|
116
|
-
return self.udt.dump_hierarchy()
|
|
72
|
+
return self.ud.dump_hierarchy()
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.exception("unexpected dump error: %s", e)
|
|
75
|
+
raise AndroidDriverException("Failed to dump hierarchy")
|
|
117
76
|
|
|
118
77
|
def tap(self, x: int, y: int):
|
|
119
78
|
self.adb_device.click(x, y)
|
|
@@ -159,6 +118,22 @@ class AndroidDriver(BaseDriver):
|
|
|
159
118
|
|
|
160
119
|
def volume_mute(self):
|
|
161
120
|
self.adb_device.keyevent("VOLUME_MUTE")
|
|
121
|
+
|
|
122
|
+
def app_list(self) -> List[AppInfo]:
|
|
123
|
+
results = []
|
|
124
|
+
output = self.adb_device.shell(["pm", "list", "packages", '-3'])
|
|
125
|
+
for m in re.finditer(r"^package:([^\s]+)\r?$", output, re.M):
|
|
126
|
+
packageName = m.group(1)
|
|
127
|
+
results.append(AppInfo(packageName=packageName))
|
|
128
|
+
return results
|
|
129
|
+
|
|
130
|
+
def open_app_file(self, package: str) -> Iterator[bytes]:
|
|
131
|
+
line = self.adb_device.shell(f"pm path {package}")
|
|
132
|
+
if not line.startswith("package:"):
|
|
133
|
+
raise AndroidDriverException(f"Failed to get package path: {line}")
|
|
134
|
+
remote_path = line.split(':', 1)[1]
|
|
135
|
+
yield from self.adb_device.sync.iter_content(remote_path)
|
|
136
|
+
|
|
162
137
|
|
|
163
138
|
|
|
164
139
|
def parse_xml(xml_data: str, wsize: WindowSize, display_id: Optional[int] = None) -> Node:
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"""Created on Fri Mar 01 2024 14:18:30 by codeskyblue
|
|
5
5
|
"""
|
|
6
6
|
import abc
|
|
7
|
-
import
|
|
8
|
-
from typing import Tuple
|
|
7
|
+
from io import FileIO
|
|
8
|
+
from typing import Iterator, List, Tuple
|
|
9
9
|
|
|
10
10
|
from PIL import Image
|
|
11
11
|
from pydantic import BaseModel
|
|
12
12
|
|
|
13
13
|
from uiautodev.command_types import CurrentAppResponse
|
|
14
|
-
from uiautodev.model import Node, ShellResponse, WindowSize
|
|
14
|
+
from uiautodev.model import Node, AppInfo, ShellResponse, WindowSize
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class BaseDriver(abc.ABC):
|
|
@@ -93,4 +93,13 @@ class BaseDriver(abc.ABC):
|
|
|
93
93
|
|
|
94
94
|
def wake_up(self):
|
|
95
95
|
""" wake up the device """
|
|
96
|
-
raise NotImplementedError()
|
|
96
|
+
raise NotImplementedError()
|
|
97
|
+
|
|
98
|
+
def app_list(self) -> List[AppInfo]:
|
|
99
|
+
""" list installed packages """
|
|
100
|
+
raise NotImplementedError()
|
|
101
|
+
|
|
102
|
+
def open_app_file(self, package: str) -> Iterator[bytes]:
|
|
103
|
+
""" open app file """
|
|
104
|
+
raise NotImplementedError()
|
|
105
|
+
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import io
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Any, List
|
|
9
|
+
from typing import Any, Dict, List
|
|
10
10
|
|
|
11
11
|
from fastapi import APIRouter, Response
|
|
12
|
+
from fastapi.responses import StreamingResponse
|
|
12
13
|
from pydantic import BaseModel
|
|
13
14
|
|
|
14
15
|
from uiautodev import command_proxy
|
|
@@ -102,14 +103,25 @@ def make_router(provider: BaseProvider) -> APIRouter:
|
|
|
102
103
|
return command_proxy.app_current(driver)
|
|
103
104
|
|
|
104
105
|
@router.post('/{serial}/command/{command}')
|
|
105
|
-
def _command_proxy_other(serial: str, command: Command, params: Any = None):
|
|
106
|
+
def _command_proxy_other(serial: str, command: Command, params: Dict[str, Any] = None):
|
|
106
107
|
"""Run a command on the device"""
|
|
107
108
|
driver = provider.get_device_driver(serial)
|
|
108
|
-
|
|
109
|
-
if params is None:
|
|
110
|
-
response = func(driver)
|
|
111
|
-
else:
|
|
112
|
-
response = func(driver, params)
|
|
109
|
+
response = command_proxy.send_command(driver, command, params)
|
|
113
110
|
return response
|
|
111
|
+
|
|
112
|
+
@router.get('/{serial}/backupApp')
|
|
113
|
+
def _backup_app(serial: str, packageName: str):
|
|
114
|
+
"""Backup app
|
|
115
|
+
|
|
116
|
+
Added in 0.5.0
|
|
117
|
+
"""
|
|
118
|
+
driver = provider.get_device_driver(serial)
|
|
119
|
+
file_name = f"{packageName}.apk"
|
|
120
|
+
headers = {
|
|
121
|
+
'Content-Disposition': f'attachment; filename="{file_name}"'
|
|
122
|
+
}
|
|
123
|
+
return StreamingResponse(driver.open_app_file(packageName), headers=headers)
|
|
124
|
+
|
|
125
|
+
|
|
114
126
|
|
|
115
127
|
return router
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{uiautodev-0.4.0 → uiautodev-0.5.0}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|