uiautodev 0.3.6__tar.gz → 0.4.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.

Potentially problematic release.


This version of uiautodev might be problematic. Click here for more details.

Files changed (30) hide show
  1. {uiautodev-0.3.6 → uiautodev-0.4.0}/PKG-INFO +4 -2
  2. {uiautodev-0.3.6 → uiautodev-0.4.0}/pyproject.toml +4 -2
  3. uiautodev-0.4.0/uiautodev/__init__.py +8 -0
  4. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/command_proxy.py +25 -0
  5. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/command_types.py +7 -0
  6. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/driver/android.py +40 -35
  7. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/driver/base_driver.py +20 -0
  8. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/driver/ios.py +20 -11
  9. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/model.py +8 -0
  10. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/router/device.py +8 -4
  11. uiautodev-0.3.6/uiautodev/__init__.py +0 -12
  12. {uiautodev-0.3.6 → uiautodev-0.4.0}/LICENSE +0 -0
  13. {uiautodev-0.3.6 → uiautodev-0.4.0}/README.md +0 -0
  14. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/__main__.py +0 -0
  15. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/app.py +0 -0
  16. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/appium_proxy.py +0 -0
  17. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/case.py +0 -0
  18. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/cli.py +0 -0
  19. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/common.py +0 -0
  20. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/driver/appium.py +0 -0
  21. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/driver/mock.py +0 -0
  22. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
  23. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/driver/udt/udt.py +0 -0
  24. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/exceptions.py +0 -0
  25. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/provider.py +0 -0
  26. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/router/xml.py +0 -0
  27. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/static/demo.html +0 -0
  28. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/utils/common.py +0 -0
  29. {uiautodev-0.3.6 → uiautodev-0.4.0}/uiautodev/utils/exceptions.py +0 -0
  30. {uiautodev-0.3.6 → uiautodev-0.4.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.6
3
+ Version: 0.4.0
4
4
  Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
5
5
  Home-page: https://uiauto.dev
6
6
  License: MIT
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Provides-Extra: appium
18
- Requires-Dist: adbutils (>=2.6.0,<3.0.0)
18
+ Requires-Dist: adbutils (>=2.7.0,<3.0.0)
19
19
  Requires-Dist: appium-python-client (>=4.0.0,<5.0.0) ; extra == "appium"
20
20
  Requires-Dist: click (>=8.1.7,<9.0.0)
21
21
  Requires-Dist: construct
@@ -25,9 +25,11 @@ 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]
32
+ Requires-Dist: wdapy (>=0.2.2,<0.3.0)
31
33
  Description-Content-Type: text/markdown
32
34
 
33
35
  # uiautodev
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "uiautodev"
3
- version = "0.3.6"
3
+ version = "0.4.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>"]
@@ -10,7 +10,7 @@ readme = "README.md"
10
10
  [tool.poetry.dependencies]
11
11
  python = "^3.8"
12
12
  pillow = "*"
13
- adbutils = "^2.6.0"
13
+ adbutils = "^2.7.0"
14
14
  construct = "*"
15
15
  lxml = "*"
16
16
  click = "^8.1.7"
@@ -22,6 +22,8 @@ httpx = "*"
22
22
  fastapi = "^0.111.0"
23
23
  uvicorn = {version = "*", extras = ["standard"]}
24
24
  poetry = "^1.8.2"
25
+ pydantic = "^2.6"
26
+ wdapy = "^0.2.2"
25
27
 
26
28
  [tool.poetry.extras]
27
29
  appium = ["appium-python-client", "httppretty"]
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """Created on Mon Mar 04 2024 14:28:53 by codeskyblue
5
+ """
6
+
7
+ # version is auto managed by poetry
8
+ __version__ = "0.4.0"
@@ -105,6 +105,31 @@ def home(driver: BaseDriver):
105
105
  driver.home()
106
106
 
107
107
 
108
+ @register(Command.BACK)
109
+ def back(driver: BaseDriver):
110
+ driver.back()
111
+
112
+
113
+ @register(Command.APP_SWITCH)
114
+ def app_switch(driver: BaseDriver):
115
+ driver.app_switch()
116
+
117
+
118
+ @register(Command.VOLUME_UP)
119
+ def volume_up(driver: BaseDriver):
120
+ driver.volume_up()
121
+
122
+
123
+ @register(Command.VOLUME_DOWN)
124
+ def volume_down(driver: BaseDriver):
125
+ driver.volume_down()
126
+
127
+
128
+ @register(Command.VOLUME_MUTE)
129
+ def volume_mute(driver: BaseDriver):
130
+ driver.volume_mute()
131
+
132
+
108
133
  @register(Command.DUMP)
109
134
  def dump(driver: BaseDriver) -> DumpResponse:
110
135
  source, _ = driver.dump_hierarchy()
@@ -32,6 +32,13 @@ class Command(str, enum.Enum):
32
32
 
33
33
  LIST = "list"
34
34
 
35
+ # 0.4.0
36
+ BACK = "back"
37
+ APP_SWITCH = "appSwitch"
38
+ VOLUME_UP = "volumeUp"
39
+ VOLUME_DOWN = "volumeDown"
40
+ VOLUME_MUTE = "volumeMute"
41
+
35
42
 
36
43
  class TapRequest(BaseModel):
37
44
  x: Union[int, float]
@@ -9,7 +9,7 @@ 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
@@ -20,7 +20,7 @@ from uiautodev.command_types import CurrentAppResponse
20
20
  from uiautodev.driver.base_driver import BaseDriver
21
21
  from uiautodev.driver.udt.udt import UDT, UDTError
22
22
  from uiautodev.exceptions import AndroidDriverException, RequestError
23
- from uiautodev.model import Node, ShellResponse, WindowSize
23
+ from uiautodev.model import Node, Rect, ShellResponse, WindowSize
24
24
  from uiautodev.utils.common import fetch_through_socket
25
25
 
26
26
  logger = logging.getLogger(__name__)
@@ -31,8 +31,8 @@ class AndroidDriver(BaseDriver):
31
31
  self.adb_device = adbutils.device(serial)
32
32
  self._try_dump_list = [
33
33
  self._get_u2_hierarchy,
34
- self._get_appium_hierarchy,
35
34
  self._get_udt_dump_hierarchy,
35
+ # self._get_appium_hierarchy,
36
36
  ]
37
37
 
38
38
  @cached_property
@@ -64,16 +64,16 @@ class AndroidDriver(BaseDriver):
64
64
  except Exception as e:
65
65
  return ShellResponse(output="", error=f"adb error: {str(e)}")
66
66
 
67
- def dump_hierarchy(self) -> Tuple[str, Node]:
67
+ def dump_hierarchy(self, display_id: Optional[int] = 0) -> Tuple[str, Node]:
68
68
  """returns xml string and hierarchy object"""
69
- wsize = self.adb_device.window_size()
70
- logger.debug("window size: %s", wsize)
71
69
  start = time.time()
72
70
  xml_data = self._dump_hierarchy_raw()
73
71
  logger.debug("dump_hierarchy cost: %s", time.time() - start)
74
72
 
73
+ wsize = self.adb_device.window_size()
74
+ logger.debug("window size: %s", wsize)
75
75
  return xml_data, parse_xml(
76
- xml_data, WindowSize(width=wsize[0], height=wsize[1])
76
+ xml_data, WindowSize(width=wsize[0], height=wsize[1]), display_id
77
77
  )
78
78
 
79
79
  def _dump_hierarchy_raw(self) -> str:
@@ -99,28 +99,6 @@ class AndroidDriver(BaseDriver):
99
99
  def _get_u2_hierarchy(self) -> str:
100
100
  d = u2.connect_usb(self.serial)
101
101
  return d.dump_hierarchy()
102
- # c = self.device.create_connection(adbutils.Network.TCP, 9008)
103
- # try:
104
- # compressed = False
105
- # payload = {
106
- # "jsonrpc": "2.0",
107
- # "method": "dumpWindowHierarchy",
108
- # "params": [compressed],
109
- # "id": 1,
110
- # }
111
- # content = fetch_through_socket(
112
- # c, "/jsonrpc/0", method="POST", json=payload, timeout=5
113
- # )
114
- # json_resp = json.loads(content)
115
- # if "error" in json_resp:
116
- # raise AndroidDriverException(json_resp["error"])
117
- # return json_resp["result"]
118
- # except adbutils.AdbError as e:
119
- # raise AndroidDriverException(
120
- # f"Failed to get hierarchy from u2 server: {str(e)}"
121
- # )
122
- # finally:
123
- # c.close()
124
102
 
125
103
  def _get_appium_hierarchy(self) -> str:
126
104
  c = self.adb_device.create_connection(adbutils.Network.TCP, 6790)
@@ -166,28 +144,51 @@ class AndroidDriver(BaseDriver):
166
144
 
167
145
  def wake_up(self):
168
146
  self.adb_device.keyevent("WAKEUP")
147
+
148
+ def back(self):
149
+ self.adb_device.keyevent("BACK")
150
+
151
+ def app_switch(self):
152
+ self.adb_device.keyevent("APP_SWITCH")
153
+
154
+ def volume_up(self):
155
+ self.adb_device.keyevent("VOLUME_UP")
156
+
157
+ def volume_down(self):
158
+ self.adb_device.keyevent("VOLUME_DOWN")
159
+
160
+ def volume_mute(self):
161
+ self.adb_device.keyevent("VOLUME_MUTE")
169
162
 
170
163
 
171
- def parse_xml(xml_data: str, wsize: WindowSize) -> Node:
164
+ def parse_xml(xml_data: str, wsize: WindowSize, display_id: Optional[int] = None) -> Node:
172
165
  root = ElementTree.fromstring(xml_data)
173
- return parse_xml_element(root, wsize)
166
+ node = parse_xml_element(root, wsize, display_id)
167
+ if node is None:
168
+ raise AndroidDriverException("Failed to parse xml")
169
+ return node
174
170
 
175
171
 
176
- def parse_xml_element(
177
- element, wsize: WindowSize, indexes: List[int] = [0]
178
- ) -> Node:
172
+ def parse_xml_element(element, wsize: WindowSize, display_id: Optional[int], indexes: List[int] = [0]) -> Optional[Node]:
179
173
  """
180
174
  Recursively parse an XML element into a dictionary format.
181
175
  """
182
176
  name = element.tag
183
177
  if name == "node":
184
178
  name = element.attrib.get("class", "node")
179
+ if display_id is not None:
180
+ elem_display_id = int(element.attrib.get("display-id", display_id))
181
+ if elem_display_id != display_id:
182
+ return
183
+
185
184
  bounds = None
185
+ rect = None
186
186
  # eg: bounds="[883,2222][1008,2265]"
187
187
  if "bounds" in element.attrib:
188
188
  bounds = element.attrib["bounds"]
189
189
  bounds = list(map(int, re.findall(r"\d+", bounds)))
190
190
  assert len(bounds) == 4
191
+ rect = Rect(x=bounds[0], y=bounds[1], width=bounds[2] - bounds[0], height=bounds[3] - bounds[1])
191
192
  bounds = (
192
193
  bounds[0] / wsize.width,
193
194
  bounds[1] / wsize.height,
@@ -195,16 +196,20 @@ def parse_xml_element(
195
196
  bounds[3] / wsize.height,
196
197
  )
197
198
  bounds = map(partial(round, ndigits=4), bounds)
199
+
198
200
  elem = Node(
199
201
  key="-".join(map(str, indexes)),
200
202
  name=name,
201
203
  bounds=bounds,
204
+ rect=rect,
202
205
  properties={key: element.attrib[key] for key in element.attrib},
203
206
  children=[],
204
207
  )
205
208
 
206
209
  # Construct xpath for children
207
210
  for index, child in enumerate(element):
208
- elem.children.append(parse_xml_element(child, wsize, indexes + [index]))
211
+ child_node = parse_xml_element(child, wsize, display_id, indexes + [index])
212
+ if child_node:
213
+ elem.children.append(child_node)
209
214
 
210
215
  return elem
@@ -70,6 +70,26 @@ class BaseDriver(abc.ABC):
70
70
  def home(self):
71
71
  """ press home button """
72
72
  raise NotImplementedError()
73
+
74
+ def back(self):
75
+ """ press back button """
76
+ raise NotImplementedError()
77
+
78
+ def app_switch(self):
79
+ """ switch app """
80
+ raise NotImplementedError()
81
+
82
+ def volume_up(self):
83
+ """ volume up """
84
+ raise NotImplementedError()
85
+
86
+ def volume_down(self):
87
+ """ volume down """
88
+ raise NotImplementedError()
89
+
90
+ def volume_mute(self):
91
+ """ volume mute """
92
+ raise NotImplementedError()
73
93
 
74
94
  def wake_up(self):
75
95
  """ wake up the device """
@@ -13,13 +13,14 @@ from functools import partial
13
13
  from typing import List, Optional, Tuple
14
14
  from xml.etree import ElementTree
15
15
 
16
+ import wdapy
16
17
  from PIL import Image
17
18
 
18
19
  from uiautodev.command_types import CurrentAppResponse
19
20
  from uiautodev.driver.base_driver import BaseDriver
20
21
  from uiautodev.exceptions import IOSDriverException
21
22
  from uiautodev.model import Node, WindowSize
22
- from uiautodev.utils.usbmux import MuxDevice, select_device
23
+ from uiautodev.utils.usbmux import select_device
23
24
 
24
25
 
25
26
  class IOSDriver(BaseDriver):
@@ -27,6 +28,7 @@ class IOSDriver(BaseDriver):
27
28
  """ serial is the udid of the ios device """
28
29
  super().__init__(serial)
29
30
  self.device = select_device(serial)
31
+ self.wda = wdapy.AppiumUSBClient(self.device.serial)
30
32
 
31
33
  def _request(self, method: str, path: str, payload: Optional[dict] = None) -> bytes:
32
34
  conn = self.device.make_http_connection(port=8100)
@@ -56,29 +58,36 @@ class IOSDriver(BaseDriver):
56
58
  return self._request_json("GET", "/status")
57
59
 
58
60
  def screenshot(self, id: int = 0) -> Image.Image:
59
- png_base64 = self._request_json_value("GET", "/screenshot")
60
- png_data = base64.b64decode(png_base64)
61
- return Image.open(io.BytesIO(png_data))
61
+ return self.wda.screenshot()
62
62
 
63
63
  def window_size(self):
64
- return self._request_json_value("GET", "/window/size")
64
+ return self.wda.window_size()
65
65
 
66
66
  def dump_hierarchy(self) -> Tuple[str, Node]:
67
67
  """returns xml string and hierarchy object"""
68
- xml_data = self._request_json_value("GET", "/source")
68
+ t = self.wda.sourcetree()
69
+ xml_data = t.value
69
70
  root = ElementTree.fromstring(xml_data)
70
71
  return xml_data, parse_xml_element(root, WindowSize(width=1, height=1))
71
72
 
72
73
  def tap(self, x: int, y: int):
73
- self._request("POST", f"/wda/tap/0", {"x": x, "y": y})
74
+ self.wda.tap(x, y)
74
75
 
75
76
  def app_current(self) -> CurrentAppResponse:
76
- # {'processArguments': {'env': {}, 'args': []}, 'name': '', 'pid': 32, 'bundleId': 'com.apple.springboard'}
77
- value = self._request_json_value("GET", "/wda/activeAppInfo")
78
- return CurrentAppResponse(package=value["bundleId"], pid=value["pid"])
77
+ info = self.wda.app_current()
78
+ return CurrentAppResponse(package=info.bundle_id, pid=info.pid)
79
79
 
80
80
  def home(self):
81
- self._request("POST", "/wda/homescreen")
81
+ self.wda.homescreen()
82
+
83
+ def app_switch(self):
84
+ raise NotImplementedError()
85
+
86
+ def volume_up(self):
87
+ self.wda.volume_up()
88
+
89
+ def volume_down(self):
90
+ self.wda.volume_down()
82
91
 
83
92
 
84
93
  def parse_xml_element(element, wsize: WindowSize, indexes: List[int]=[0]) -> Node:
@@ -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
- return hierarchy
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)
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
-
4
- """Created on Mon Mar 04 2024 14:28:53 by codeskyblue
5
- """
6
-
7
- from importlib.metadata import PackageNotFoundError, version
8
-
9
- try:
10
- __version__ = version("uiautodev")
11
- except PackageNotFoundError:
12
- __version__ = "0.0.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes