uiautodev 0.3.5__tar.gz → 0.3.7__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.
Potentially problematic release.
This version of uiautodev might be problematic. Click here for more details.
- {uiautodev-0.3.5 → uiautodev-0.3.7}/PKG-INFO +2 -1
- {uiautodev-0.3.5 → uiautodev-0.3.7}/pyproject.toml +2 -1
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/app.py +1 -1
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/android.py +39 -51
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/model.py +8 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/router/device.py +8 -4
- {uiautodev-0.3.5 → uiautodev-0.3.7}/LICENSE +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/README.md +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/__init__.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/__main__.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/appium_proxy.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/case.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/cli.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/command_proxy.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/command_types.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/common.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/appium.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/base_driver.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/ios.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/mock.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/driver/udt/udt.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/exceptions.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/provider.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/router/xml.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/static/demo.html +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/utils/common.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/utils/exceptions.py +0 -0
- {uiautodev-0.3.5 → uiautodev-0.3.7}/uiautodev/utils/usbmux.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: uiautodev
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
|
|
5
5
|
Home-page: https://uiauto.dev
|
|
6
6
|
License: MIT
|
|
@@ -25,6 +25,7 @@ Requires-Dist: httpx
|
|
|
25
25
|
Requires-Dist: lxml
|
|
26
26
|
Requires-Dist: pillow
|
|
27
27
|
Requires-Dist: poetry (>=1.8.2,<2.0.0)
|
|
28
|
+
Requires-Dist: pydantic (>=2.6,<3.0)
|
|
28
29
|
Requires-Dist: pygments (>=2)
|
|
29
30
|
Requires-Dist: uiautomator2 (>=2)
|
|
30
31
|
Requires-Dist: uvicorn[standard]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "uiautodev"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.7"
|
|
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>"]
|
|
@@ -22,6 +22,7 @@ httpx = "*"
|
|
|
22
22
|
fastapi = "^0.111.0"
|
|
23
23
|
uvicorn = {version = "*", extras = ["standard"]}
|
|
24
24
|
poetry = "^1.8.2"
|
|
25
|
+
pydantic = "^2.6"
|
|
25
26
|
|
|
26
27
|
[tool.poetry.extras]
|
|
27
28
|
appium = ["appium-python-client", "httppretty"]
|
|
@@ -9,11 +9,10 @@ import logging
|
|
|
9
9
|
import re
|
|
10
10
|
import time
|
|
11
11
|
from functools import cached_property, partial
|
|
12
|
-
from typing import List, Tuple
|
|
12
|
+
from typing import List, Optional, Tuple
|
|
13
13
|
from xml.etree import ElementTree
|
|
14
14
|
|
|
15
15
|
import adbutils
|
|
16
|
-
import requests
|
|
17
16
|
import uiautomator2 as u2
|
|
18
17
|
from PIL import Image
|
|
19
18
|
|
|
@@ -21,7 +20,7 @@ from uiautodev.command_types import CurrentAppResponse
|
|
|
21
20
|
from uiautodev.driver.base_driver import BaseDriver
|
|
22
21
|
from uiautodev.driver.udt.udt import UDT, UDTError
|
|
23
22
|
from uiautodev.exceptions import AndroidDriverException, RequestError
|
|
24
|
-
from uiautodev.model import Node, ShellResponse, WindowSize
|
|
23
|
+
from uiautodev.model import Node, Rect, ShellResponse, WindowSize
|
|
25
24
|
from uiautodev.utils.common import fetch_through_socket
|
|
26
25
|
|
|
27
26
|
logger = logging.getLogger(__name__)
|
|
@@ -29,16 +28,16 @@ logger = logging.getLogger(__name__)
|
|
|
29
28
|
class AndroidDriver(BaseDriver):
|
|
30
29
|
def __init__(self, serial: str):
|
|
31
30
|
super().__init__(serial)
|
|
32
|
-
self.
|
|
31
|
+
self.adb_device = adbutils.device(serial)
|
|
33
32
|
self._try_dump_list = [
|
|
34
33
|
self._get_u2_hierarchy,
|
|
35
|
-
self._get_appium_hierarchy,
|
|
36
34
|
self._get_udt_dump_hierarchy,
|
|
35
|
+
# self._get_appium_hierarchy,
|
|
37
36
|
]
|
|
38
37
|
|
|
39
38
|
@cached_property
|
|
40
39
|
def udt(self) -> UDT:
|
|
41
|
-
return UDT(self.
|
|
40
|
+
return UDT(self.adb_device)
|
|
42
41
|
|
|
43
42
|
@cached_property
|
|
44
43
|
def ud(self) -> u2.Device:
|
|
@@ -46,8 +45,7 @@ class AndroidDriver(BaseDriver):
|
|
|
46
45
|
|
|
47
46
|
def screenshot(self, id: int) -> Image.Image:
|
|
48
47
|
try:
|
|
49
|
-
|
|
50
|
-
return img.convert("RGB")
|
|
48
|
+
return self.adb_device.screenshot() # display_id is not OK now
|
|
51
49
|
except adbutils.AdbError as e:
|
|
52
50
|
logger.warning("screenshot error: %s", str(e))
|
|
53
51
|
if id > 0:
|
|
@@ -56,7 +54,7 @@ class AndroidDriver(BaseDriver):
|
|
|
56
54
|
|
|
57
55
|
def shell(self, command: str) -> ShellResponse:
|
|
58
56
|
try:
|
|
59
|
-
ret = self.
|
|
57
|
+
ret = self.adb_device.shell2(command, rstrip=True, timeout=20)
|
|
60
58
|
if ret.returncode == 0:
|
|
61
59
|
return ShellResponse(output=ret.output, error=None)
|
|
62
60
|
else:
|
|
@@ -66,16 +64,16 @@ class AndroidDriver(BaseDriver):
|
|
|
66
64
|
except Exception as e:
|
|
67
65
|
return ShellResponse(output="", error=f"adb error: {str(e)}")
|
|
68
66
|
|
|
69
|
-
def dump_hierarchy(self) -> Tuple[str, Node]:
|
|
67
|
+
def dump_hierarchy(self, display_id: Optional[int] = 0) -> Tuple[str, Node]:
|
|
70
68
|
"""returns xml string and hierarchy object"""
|
|
71
|
-
wsize = self.device.window_size()
|
|
72
|
-
logger.debug("window size: %s", wsize)
|
|
73
69
|
start = time.time()
|
|
74
70
|
xml_data = self._dump_hierarchy_raw()
|
|
75
71
|
logger.debug("dump_hierarchy cost: %s", time.time() - start)
|
|
76
72
|
|
|
73
|
+
wsize = self.adb_device.window_size()
|
|
74
|
+
logger.debug("window size: %s", wsize)
|
|
77
75
|
return xml_data, parse_xml(
|
|
78
|
-
xml_data, WindowSize(width=wsize[0], height=wsize[1])
|
|
76
|
+
xml_data, WindowSize(width=wsize[0], height=wsize[1]), display_id
|
|
79
77
|
)
|
|
80
78
|
|
|
81
79
|
def _dump_hierarchy_raw(self) -> str:
|
|
@@ -101,31 +99,9 @@ class AndroidDriver(BaseDriver):
|
|
|
101
99
|
def _get_u2_hierarchy(self) -> str:
|
|
102
100
|
d = u2.connect_usb(self.serial)
|
|
103
101
|
return d.dump_hierarchy()
|
|
104
|
-
# c = self.device.create_connection(adbutils.Network.TCP, 9008)
|
|
105
|
-
# try:
|
|
106
|
-
# compressed = False
|
|
107
|
-
# payload = {
|
|
108
|
-
# "jsonrpc": "2.0",
|
|
109
|
-
# "method": "dumpWindowHierarchy",
|
|
110
|
-
# "params": [compressed],
|
|
111
|
-
# "id": 1,
|
|
112
|
-
# }
|
|
113
|
-
# content = fetch_through_socket(
|
|
114
|
-
# c, "/jsonrpc/0", method="POST", json=payload, timeout=5
|
|
115
|
-
# )
|
|
116
|
-
# json_resp = json.loads(content)
|
|
117
|
-
# if "error" in json_resp:
|
|
118
|
-
# raise AndroidDriverException(json_resp["error"])
|
|
119
|
-
# return json_resp["result"]
|
|
120
|
-
# except adbutils.AdbError as e:
|
|
121
|
-
# raise AndroidDriverException(
|
|
122
|
-
# f"Failed to get hierarchy from u2 server: {str(e)}"
|
|
123
|
-
# )
|
|
124
|
-
# finally:
|
|
125
|
-
# c.close()
|
|
126
102
|
|
|
127
103
|
def _get_appium_hierarchy(self) -> str:
|
|
128
|
-
c = self.
|
|
104
|
+
c = self.adb_device.create_connection(adbutils.Network.TCP, 6790)
|
|
129
105
|
try:
|
|
130
106
|
content = fetch_through_socket(c, "/wd/hub/session/0/source", timeout=10)
|
|
131
107
|
return json.loads(content)["value"]
|
|
@@ -140,56 +116,64 @@ class AndroidDriver(BaseDriver):
|
|
|
140
116
|
return self.udt.dump_hierarchy()
|
|
141
117
|
|
|
142
118
|
def tap(self, x: int, y: int):
|
|
143
|
-
self.
|
|
119
|
+
self.adb_device.click(x, y)
|
|
144
120
|
|
|
145
121
|
def window_size(self) -> Tuple[int, int]:
|
|
146
|
-
w, h = self.
|
|
122
|
+
w, h = self.adb_device.window_size()
|
|
147
123
|
return (w, h)
|
|
148
124
|
|
|
149
125
|
def app_install(self, app_path: str):
|
|
150
|
-
self.
|
|
126
|
+
self.adb_device.install(app_path)
|
|
151
127
|
|
|
152
128
|
def app_current(self) -> CurrentAppResponse:
|
|
153
|
-
info = self.
|
|
129
|
+
info = self.adb_device.app_current()
|
|
154
130
|
return CurrentAppResponse(
|
|
155
131
|
package=info.package, activity=info.activity, pid=info.pid
|
|
156
132
|
)
|
|
157
133
|
|
|
158
134
|
def app_launch(self, package: str):
|
|
159
|
-
if self.
|
|
135
|
+
if self.adb_device.package_info(package) is None:
|
|
160
136
|
raise AndroidDriverException(f"App not installed: {package}")
|
|
161
|
-
self.
|
|
137
|
+
self.adb_device.app_start(package)
|
|
162
138
|
|
|
163
139
|
def app_terminate(self, package: str):
|
|
164
|
-
self.
|
|
140
|
+
self.adb_device.app_stop(package)
|
|
165
141
|
|
|
166
142
|
def home(self):
|
|
167
|
-
self.
|
|
143
|
+
self.adb_device.keyevent("HOME")
|
|
168
144
|
|
|
169
145
|
def wake_up(self):
|
|
170
|
-
self.
|
|
146
|
+
self.adb_device.keyevent("WAKEUP")
|
|
171
147
|
|
|
172
148
|
|
|
173
|
-
def parse_xml(xml_data: str, wsize: WindowSize) -> Node:
|
|
149
|
+
def parse_xml(xml_data: str, wsize: WindowSize, display_id: Optional[int] = None) -> Node:
|
|
174
150
|
root = ElementTree.fromstring(xml_data)
|
|
175
|
-
|
|
151
|
+
node = parse_xml_element(root, wsize, display_id)
|
|
152
|
+
if node is None:
|
|
153
|
+
raise AndroidDriverException("Failed to parse xml")
|
|
154
|
+
return node
|
|
176
155
|
|
|
177
156
|
|
|
178
|
-
def parse_xml_element(
|
|
179
|
-
element, wsize: WindowSize, indexes: List[int] = [0]
|
|
180
|
-
) -> Node:
|
|
157
|
+
def parse_xml_element(element, wsize: WindowSize, display_id: Optional[int], indexes: List[int] = [0]) -> Optional[Node]:
|
|
181
158
|
"""
|
|
182
159
|
Recursively parse an XML element into a dictionary format.
|
|
183
160
|
"""
|
|
184
161
|
name = element.tag
|
|
185
162
|
if name == "node":
|
|
186
163
|
name = element.attrib.get("class", "node")
|
|
164
|
+
if display_id is not None:
|
|
165
|
+
elem_display_id = int(element.attrib.get("display-id", display_id))
|
|
166
|
+
if elem_display_id != display_id:
|
|
167
|
+
return
|
|
168
|
+
|
|
187
169
|
bounds = None
|
|
170
|
+
rect = None
|
|
188
171
|
# eg: bounds="[883,2222][1008,2265]"
|
|
189
172
|
if "bounds" in element.attrib:
|
|
190
173
|
bounds = element.attrib["bounds"]
|
|
191
174
|
bounds = list(map(int, re.findall(r"\d+", bounds)))
|
|
192
175
|
assert len(bounds) == 4
|
|
176
|
+
rect = Rect(x=bounds[0], y=bounds[1], width=bounds[2] - bounds[0], height=bounds[3] - bounds[1])
|
|
193
177
|
bounds = (
|
|
194
178
|
bounds[0] / wsize.width,
|
|
195
179
|
bounds[1] / wsize.height,
|
|
@@ -197,16 +181,20 @@ def parse_xml_element(
|
|
|
197
181
|
bounds[3] / wsize.height,
|
|
198
182
|
)
|
|
199
183
|
bounds = map(partial(round, ndigits=4), bounds)
|
|
184
|
+
|
|
200
185
|
elem = Node(
|
|
201
186
|
key="-".join(map(str, indexes)),
|
|
202
187
|
name=name,
|
|
203
188
|
bounds=bounds,
|
|
189
|
+
rect=rect,
|
|
204
190
|
properties={key: element.attrib[key] for key in element.attrib},
|
|
205
191
|
children=[],
|
|
206
192
|
)
|
|
207
193
|
|
|
208
194
|
# Construct xpath for children
|
|
209
195
|
for index, child in enumerate(element):
|
|
210
|
-
|
|
196
|
+
child_node = parse_xml_element(child, wsize, display_id, indexes + [index])
|
|
197
|
+
if child_node:
|
|
198
|
+
elem.children.append(child_node)
|
|
211
199
|
|
|
212
200
|
return elem
|
|
@@ -24,10 +24,18 @@ class ShellResponse(BaseModel):
|
|
|
24
24
|
error: Optional[str] = ""
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class Rect(BaseModel):
|
|
28
|
+
x: int
|
|
29
|
+
y: int
|
|
30
|
+
width: int
|
|
31
|
+
height: int
|
|
32
|
+
|
|
33
|
+
|
|
27
34
|
class Node(BaseModel):
|
|
28
35
|
key: str
|
|
29
36
|
name: str
|
|
30
37
|
bounds: Optional[Tuple[float, float, float, float]] = None
|
|
38
|
+
rect: Optional[Rect] = None
|
|
31
39
|
properties: Dict[str, Union[str, bool]] = []
|
|
32
40
|
children: List[Node] = []
|
|
33
41
|
|
|
@@ -16,7 +16,6 @@ from uiautodev.command_types import Command, CurrentAppResponse, InstallAppReque
|
|
|
16
16
|
from uiautodev.model import DeviceInfo, Node, ShellResponse
|
|
17
17
|
from uiautodev.provider import BaseProvider
|
|
18
18
|
|
|
19
|
-
|
|
20
19
|
logger = logging.getLogger(__name__)
|
|
21
20
|
|
|
22
21
|
class AndroidShellPayload(BaseModel):
|
|
@@ -58,7 +57,7 @@ def make_router(provider: BaseProvider) -> APIRouter:
|
|
|
58
57
|
"""Take a screenshot of device"""
|
|
59
58
|
try:
|
|
60
59
|
driver = provider.get_device_driver(serial)
|
|
61
|
-
pil_img = driver.screenshot(id)
|
|
60
|
+
pil_img = driver.screenshot(id).convert("RGB")
|
|
62
61
|
buf = io.BytesIO()
|
|
63
62
|
pil_img.save(buf, format="JPEG")
|
|
64
63
|
image_bytes = buf.getvalue()
|
|
@@ -68,12 +67,17 @@ def make_router(provider: BaseProvider) -> APIRouter:
|
|
|
68
67
|
return Response(content=str(e), media_type="text/plain", status_code=500)
|
|
69
68
|
|
|
70
69
|
@router.get("/{serial}/hierarchy")
|
|
71
|
-
def dump_hierarchy(serial: str) -> Node:
|
|
70
|
+
def dump_hierarchy(serial: str, format: str = "json") -> Node:
|
|
72
71
|
"""Dump the view hierarchy of an Android device"""
|
|
73
72
|
try:
|
|
74
73
|
driver = provider.get_device_driver(serial)
|
|
75
74
|
xml_data, hierarchy = driver.dump_hierarchy()
|
|
76
|
-
|
|
75
|
+
if format == "xml":
|
|
76
|
+
return Response(content=xml_data, media_type="text/xml")
|
|
77
|
+
elif format == "json":
|
|
78
|
+
return hierarchy
|
|
79
|
+
else:
|
|
80
|
+
return Response(content=f"Invalid format: {format}", media_type="text/plain", status_code=400)
|
|
77
81
|
except Exception as e:
|
|
78
82
|
logger.exception("dump_hierarchy failed")
|
|
79
83
|
return Response(content=str(e), media_type="text/plain", status_code=500)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{uiautodev-0.3.5 → uiautodev-0.3.7}/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
|