uiautodev 0.5.0__tar.gz → 0.6.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 (33) hide show
  1. {uiautodev-0.5.0 → uiautodev-0.6.0}/PKG-INFO +4 -3
  2. {uiautodev-0.5.0 → uiautodev-0.6.0}/pyproject.toml +1 -1
  3. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/__init__.py +1 -1
  4. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/app.py +25 -7
  5. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/command_proxy.py +1 -3
  6. uiautodev-0.6.0/uiautodev/common.py +54 -0
  7. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/driver/android.py +1 -1
  8. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/driver/base_driver.py +1 -1
  9. uiautodev-0.6.0/uiautodev/driver/harmony.py +224 -0
  10. uiautodev-0.6.0/uiautodev/driver/testdata/layout.json +1 -0
  11. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/model.py +6 -2
  12. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/provider.py +18 -3
  13. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/router/device.py +1 -1
  14. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/utils/common.py +11 -7
  15. uiautodev-0.6.0/uiautodev/utils/envutils.py +9 -0
  16. uiautodev-0.5.0/uiautodev/common.py +0 -25
  17. {uiautodev-0.5.0 → uiautodev-0.6.0}/LICENSE +0 -0
  18. {uiautodev-0.5.0 → uiautodev-0.6.0}/README.md +0 -0
  19. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/__main__.py +0 -0
  20. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/appium_proxy.py +0 -0
  21. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/case.py +0 -0
  22. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/cli.py +0 -0
  23. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/command_types.py +0 -0
  24. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/driver/appium.py +0 -0
  25. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/driver/ios.py +0 -0
  26. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/driver/mock.py +0 -0
  27. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
  28. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/driver/udt/udt.py +0 -0
  29. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/exceptions.py +0 -0
  30. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/router/xml.py +0 -0
  31. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/static/demo.html +0 -0
  32. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/utils/exceptions.py +0 -0
  33. {uiautodev-0.5.0 → uiautodev-0.6.0}/uiautodev/utils/usbmux.py +0 -0
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: uiautodev
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
5
- Home-page: https://uiauto.dev
6
5
  License: MIT
7
6
  Author: codeskyblue
8
7
  Author-email: codeskyblue@gmail.com
@@ -14,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.9
14
13
  Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
16
15
  Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
17
  Requires-Dist: adbutils (>=2.7.0,<3.0.0)
18
18
  Requires-Dist: click (>=8.1.7,<9.0.0)
19
19
  Requires-Dist: construct
@@ -27,6 +27,7 @@ Requires-Dist: pygments (>=2)
27
27
  Requires-Dist: uiautomator2 (>=2)
28
28
  Requires-Dist: uvicorn[standard]
29
29
  Requires-Dist: wdapy (>=0.2.2,<0.3.0)
30
+ Project-URL: Homepage, https://uiauto.dev
30
31
  Description-Content-Type: text/markdown
31
32
 
32
33
  # uiautodev
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "uiautodev"
3
- version = "0.5.0"
3
+ version = "0.6.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>"]
@@ -5,4 +5,4 @@
5
5
  """
6
6
 
7
7
  # version is auto managed by poetry
8
- __version__ = "0.5.0"
8
+ __version__ = "0.6.0"
@@ -11,16 +11,19 @@ import signal
11
11
  from pathlib import Path
12
12
  from typing import List
13
13
 
14
- from fastapi import FastAPI
14
+ import uvicorn
15
+ from fastapi import FastAPI, File, UploadFile
15
16
  from fastapi.middleware.cors import CORSMiddleware
16
- from fastapi.responses import FileResponse, RedirectResponse
17
+ from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
17
18
  from pydantic import BaseModel
18
19
 
19
20
  from uiautodev import __version__
20
- from uiautodev.common import get_webpage_url
21
- from uiautodev.provider import AndroidProvider, IOSProvider, MockProvider
21
+ from uiautodev.common import convert_bytes_to_image, get_webpage_url, ocr_image
22
+ from uiautodev.model import Node
23
+ from uiautodev.provider import AndroidProvider, HarmonyProvider, IOSProvider, MockProvider
22
24
  from uiautodev.router.device import make_router
23
25
  from uiautodev.router.xml import router as xml_router
26
+ from uiautodev.utils.envutils import Environment
24
27
 
25
28
  logger = logging.getLogger(__name__)
26
29
 
@@ -36,16 +39,19 @@ app.add_middleware(
36
39
 
37
40
  android_router = make_router(AndroidProvider())
38
41
  ios_router = make_router(IOSProvider())
42
+ harmony_router = make_router(HarmonyProvider())
39
43
  mock_router = make_router(MockProvider())
40
44
 
41
45
  app.include_router(mock_router, prefix="/api/mock", tags=["mock"])
42
46
 
43
- if os.environ.get("UIAUTODEV_MOCK"):
47
+ if Environment.UIAUTODEV_MOCK:
44
48
  app.include_router(mock_router, prefix="/api/android", tags=["mock"])
45
49
  app.include_router(mock_router, prefix="/api/ios", tags=["mock"])
50
+ app.include_router(mock_router, prefix="/api/harmony", tags=["mock"])
46
51
  else:
47
52
  app.include_router(android_router, prefix="/api/android", tags=["android"])
48
53
  app.include_router(ios_router, prefix="/api/ios", tags=["ios"])
54
+ app.include_router(harmony_router, prefix="/api/harmony", tags=["harmony"])
49
55
 
50
56
  app.include_router(xml_router, prefix="/api/xml", tags=["xml"])
51
57
 
@@ -68,10 +74,18 @@ def info() -> InfoResponse:
68
74
  platform=platform.system(), # Linux | Darwin | Windows
69
75
  code_language="Python",
70
76
  cwd=os.getcwd(),
71
- drivers=["android", "ios"],
77
+ drivers=["android", "ios", "harmony"],
72
78
  )
73
79
 
74
80
 
81
+ @app.post('/api/ocr_image')
82
+ async def _ocr_image(file: UploadFile = File(...)) -> List[Node]:
83
+ """OCR an image"""
84
+ image_data = await file.read()
85
+ image = convert_bytes_to_image(image_data)
86
+ return ocr_image(image)
87
+
88
+
75
89
  @app.get("/shutdown")
76
90
  def shutdown() -> str:
77
91
  """Shutdown the server"""
@@ -80,7 +94,7 @@ def shutdown() -> str:
80
94
 
81
95
 
82
96
  @app.get("/demo")
83
- def demo() -> str:
97
+ def demo():
84
98
  """Demo endpoint"""
85
99
  static_dir = Path(__file__).parent / "static"
86
100
  print(static_dir / "demo.html")
@@ -93,3 +107,7 @@ def index_redirect():
93
107
  url = get_webpage_url()
94
108
  logger.debug("redirect to %s", url)
95
109
  return RedirectResponse(url)
110
+
111
+
112
+ if __name__ == '__main__':
113
+ uvicorn.run("uiautodev.app:app", port=4000, reload=True, use_colors=True)
@@ -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, AppInfo
20
+ from uiautodev.model import AppInfo, Node
21
21
  from uiautodev.utils.common import node_travel
22
22
 
23
23
  COMMANDS: Dict[Command, Callable] = {}
@@ -61,8 +61,6 @@ def send_command(driver: BaseDriver, command: Union[str, Command], params=None):
61
61
  @register(Command.TAP)
62
62
  def tap(driver: BaseDriver, params: TapRequest):
63
63
  """Tap on the screen
64
- :param x: x coordinate
65
- :param y: y coordinate
66
64
  """
67
65
  x = params.x
68
66
  y = params.y
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """Created on Thu May 09 2024 11:33:17 by codeskyblue
5
+ """
6
+
7
+
8
+ import io
9
+ import locale
10
+ import logging
11
+ from typing import List
12
+
13
+ from PIL import Image
14
+
15
+ from uiautodev.model import Node, OCRNode
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ def is_chinese_language() -> bool:
20
+ language_code, _ = locale.getdefaultlocale()
21
+
22
+ # Check if the language code starts with 'zh' (Chinese)
23
+ if language_code and language_code.startswith('zh'):
24
+ return True
25
+ else:
26
+ return False
27
+
28
+
29
+ def get_webpage_url() -> str:
30
+ web_url = "https://uiauto.dev"
31
+ if is_chinese_language():
32
+ web_url = "https://uiauto.devsleep.com"
33
+ return web_url
34
+
35
+
36
+ def convert_bytes_to_image(byte_data: bytes) -> Image.Image:
37
+ return Image.open(io.BytesIO(byte_data))
38
+
39
+
40
+ def ocr_image(image: Image.Image) -> List[OCRNode]:
41
+ # Placeholder for OCR implementation
42
+ w, h = image.size
43
+ try:
44
+ from ocrmac import ocrmac
45
+ except ImportError:
46
+ logger.error("OCR is not supported on this platform")
47
+ return []
48
+ result = ocrmac.OCR(image).recognize()
49
+ nodes = []
50
+ for index, (text, confidence, pbounds) in enumerate(result):
51
+ print(f"OCR result: {text}, confidence: {confidence}, bounds: {pbounds}")
52
+ # bounds = int(pbounds[0]*w), int(pbounds[1]*h), int(pbounds[2]*w), int(pbounds[3]*h)
53
+ nodes.append(OCRNode(key=str(index), name=text, bounds=pbounds, confidence=confidence))
54
+ return nodes
@@ -18,7 +18,7 @@ from PIL import Image
18
18
  from uiautodev.command_types import CurrentAppResponse
19
19
  from uiautodev.driver.base_driver import BaseDriver
20
20
  from uiautodev.exceptions import AndroidDriverException, RequestError
21
- from uiautodev.model import Node, AppInfo, Rect, ShellResponse, WindowSize
21
+ from uiautodev.model import AppInfo, Node, Rect, ShellResponse, WindowSize
22
22
  from uiautodev.utils.common import fetch_through_socket
23
23
 
24
24
  logger = logging.getLogger(__name__)
@@ -11,7 +11,7 @@ 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, AppInfo, ShellResponse, WindowSize
14
+ from uiautodev.model import AppInfo, Node, ShellResponse, WindowSize
15
15
 
16
16
 
17
17
  class BaseDriver(abc.ABC):
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ import json
4
+ import logging
5
+ import os
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ import time
11
+ import uuid
12
+ from pathlib import Path
13
+ from typing import List, Optional, Tuple, Union, final
14
+
15
+ from PIL import Image
16
+
17
+ from uiautodev.command_types import CurrentAppResponse
18
+ from uiautodev.driver.base_driver import BaseDriver
19
+ from uiautodev.model import AppInfo, Node, Rect, ShellResponse, WindowSize
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ StrOrPath = Union[str, Path]
24
+
25
+
26
+ def run_command(command: str, timeout: int = 60) -> str:
27
+ try:
28
+ result = subprocess.run(
29
+ command,
30
+ shell=True,
31
+ capture_output=True,
32
+ timeout=timeout,
33
+ text=True,
34
+ input='' # this avoid stdout: "FreeChannelContinue handle->data is nullptr"
35
+ )
36
+ # the hdc shell stderr is (不仅没啥用,还没办法去掉)
37
+ # Remote PTY will not be allocated because stdin is not a terminal.
38
+ # Use multiple -t options to force remote PTY allocation.
39
+ output = result.stdout.strip()
40
+ return output
41
+ except subprocess.TimeoutExpired as e:
42
+ raise TimeoutError(f"{command:r} timeout {e}")
43
+
44
+
45
+ class HDCError(Exception):
46
+ pass
47
+
48
+
49
+ class HDC:
50
+ def __init__(self):
51
+ self.hdc = 'hdc'
52
+ self.tmpdir = tempfile.TemporaryDirectory()
53
+
54
+ def __del__(self):
55
+ self.tmpdir.cleanup()
56
+
57
+ def list_device(self) -> List[str]:
58
+ command = f"{self.hdc} list targets"
59
+ result = run_command(command)
60
+ if result and not "Empty" in result:
61
+ devices = []
62
+ for line in result.strip().split("\n"):
63
+ serial = line.strip().split('\t', 1)[0]
64
+ devices.append(serial)
65
+ return devices
66
+ else:
67
+ return []
68
+
69
+ def shell(self, serial: str, command: str) -> str:
70
+ command = f"{self.hdc} -t {serial} shell \"{command}\""
71
+ result = run_command(command)
72
+ return result.strip()
73
+
74
+ def get_model(self, serial: str) -> str:
75
+ return self.shell(serial, "param get const.product.model")
76
+
77
+ def pull(self, serial: str, remote: StrOrPath, local: StrOrPath):
78
+ if isinstance(remote, Path):
79
+ remote = remote.as_posix()
80
+ command = f"{self.hdc} -t {serial} file recv {remote} {local}"
81
+ output = run_command(command)
82
+ if not os.path.exists(local):
83
+ raise HDCError(f"device file: {remote} not found", output)
84
+
85
+ def push(self, serial: str, local: StrOrPath, remote: StrOrPath) -> str:
86
+ if isinstance(remote, Path):
87
+ remote = remote.as_posix()
88
+ command = f"{self.hdc} -t {serial} file send {local} {remote}"
89
+ return run_command(command)
90
+
91
+ def screenshot(self, serial: str) -> Image.Image:
92
+ device_path = f'/data/local/tmp/screenshot-{int(time.time()*1000)}.png'
93
+ self.shell(serial, f"uitest screenCap -p {device_path}")
94
+ try:
95
+ local_path = os.path.join(self.tmpdir.name, f"{uuid.uuid4()}.png")
96
+ self.pull(serial, device_path, local_path)
97
+ with Image.open(local_path) as image:
98
+ image.load()
99
+ return image
100
+ finally:
101
+ self.shell(serial, f"rm {device_path}")
102
+
103
+ def dump_layout(self, serial: str) -> dict:
104
+ name = "{}.json".format(int(time.time() * 1000))
105
+ remote_path = f"/data/local/tmp/layout-{name}.json"
106
+ temp_path = os.path.join(self.tmpdir.name, f"layout-{name}.json")
107
+ output = self.shell(serial, f"uitest dumpLayout -p {remote_path}")
108
+ self.pull(serial, remote_path, temp_path)
109
+ # mock
110
+ # temp_path = Path(__file__).parent / 'testdata/layout.json'
111
+ try:
112
+ with open(temp_path, "rb") as f:
113
+ json_content = json.load(f)
114
+ return json_content
115
+ except json.JSONDecodeError:
116
+ raise HDCError(f"failed to dump layout: {output}")
117
+ finally:
118
+ self.shell(serial, f"rm {remote_path}")
119
+
120
+
121
+ class HarmonyDriver(BaseDriver):
122
+ def __init__(self, hdc: HDC, serial: str):
123
+ super().__init__(serial)
124
+ self.hdc = hdc
125
+
126
+ def screenshot(self, id: int = 0) -> Image.Image:
127
+ return self.hdc.screenshot(self.serial)
128
+
129
+ def window_size(self) -> WindowSize:
130
+ result = self.hdc.shell(self.serial, "hidumper -s 10 -a screen")
131
+ pattern = r"activeMode:\s*(\d+x\d+)"
132
+ match = re.search(pattern, result)
133
+ if match:
134
+ resolution = match.group(1).split("x")
135
+ return WindowSize(width=int(resolution[0]), height=int(resolution[1]))
136
+ else:
137
+ image = self.screenshot()
138
+ return WindowSize(width=image.width, height=image.height)
139
+
140
+ def dump_hierarchy(self) -> Tuple[str, Node]:
141
+ """returns xml string and hierarchy object"""
142
+ layout = self.hdc.dump_layout(self.serial)
143
+ return json.dumps(layout), parse_json_element(layout)
144
+
145
+ def tap(self, x: int, y: int):
146
+ self.hdc.shell(self.serial, f"uinput -T -c {x} {y}")
147
+
148
+ def app_current(self) -> Optional[CurrentAppResponse]:
149
+ echo = self.hdc.shell(self.serial, "hidumper -s WindowManagerService -a '-a'")
150
+ focus_window = re.search(r"Focus window: (\d+)", echo)
151
+ if focus_window:
152
+ focus_window = focus_window.group(1)
153
+ mission_echo = self.hdc.shell(self.serial, "aa dump -a")
154
+ pkg_names = re.findall(r"Mission ID #(\d+)\s+mission name #\[(.*?)\]", mission_echo)
155
+ if focus_window and pkg_names:
156
+ for mission in pkg_names:
157
+ mission_id = mission[0]
158
+ if focus_window == mission_id:
159
+ mission_name = mission[1]
160
+ pkg_name = mission_name.split(":")[0].replace("#", "")
161
+ ability_name = mission_name.split(":")[-1]
162
+ pid = self.hdc.shell(self.serial, f"pidof {pkg_name}").strip()
163
+ return CurrentAppResponse(package=pkg_name, activity=ability_name, pid=int(pid))
164
+ else:
165
+ return None
166
+
167
+ def shell(self, command: str) -> ShellResponse:
168
+ result = self.hdc.shell(self.serial, command)
169
+ return ShellResponse(output=result)
170
+
171
+ def home(self):
172
+ self.hdc.shell(self.serial, "uinput -K -d 1 -u 1")
173
+
174
+ def back(self):
175
+ self.hdc.shell(self.serial, "uinput -K -d 2 -u 2")
176
+
177
+ def volume_up(self):
178
+ self.hdc.shell(self.serial, "uinput -K -d 16 -u 16")
179
+
180
+ def volume_down(self):
181
+ self.hdc.shell(self.serial, "uinput -K -d 17 -u 17")
182
+
183
+ def volume_mute(self):
184
+ self.hdc.shell(self.serial, "uinput -K -d 22 -u 22")
185
+
186
+ def app_switch(self):
187
+ self.hdc.shell(self.serial, "uinput -K -d 2076 -d 2049 -u 2076 -u 2049")
188
+
189
+ def app_list(self) -> List[AppInfo]:
190
+ results = []
191
+ output = self.hdc.shell(self.serial, "bm dump -a")
192
+ for i in output.split("\n"):
193
+ if "ID" in i:
194
+ continue
195
+ else:
196
+ results.append(AppInfo(packageName=i.strip()))
197
+ return results
198
+
199
+
200
+ def parse_json_element(element, indexes: List[int] = [0]) -> Node:
201
+ """
202
+ Recursively parse an json element into a dictionary format.
203
+ """
204
+ attributes = element.get("attributes", {})
205
+ name = attributes.get("type", "")
206
+ bounds = attributes.get("bounds", "")
207
+ bounds = list(map(int, re.findall(r"\d+", bounds)))
208
+ assert len(bounds) == 4
209
+ rect = Rect(x=bounds[0], y=bounds[1], width=bounds[2] - bounds[0], height=bounds[3] - bounds[1])
210
+ elem = Node(
211
+ key="-".join(map(str, indexes)),
212
+ name=name,
213
+ bounds=None,
214
+ rect=rect,
215
+ properties={key: attributes[key] for key in attributes},
216
+ children=[],
217
+ )
218
+ # Construct xpath for children
219
+ for index, child in enumerate(element.get("children", [])):
220
+ child_node = parse_json_element(child, indexes + [index])
221
+ if child_node:
222
+ elem.children.append(child_node)
223
+
224
+ return elem