uiautodev 0.5.0__tar.gz → 0.13.3__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.5.0 → uiautodev-0.13.3}/PKG-INFO +52 -24
- uiautodev-0.13.3/README.md +76 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/pyproject.toml +17 -14
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/__init__.py +1 -1
- uiautodev-0.13.3/uiautodev/app.py +216 -0
- uiautodev-0.13.3/uiautodev/binaries/scrcpy_server.jar +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/cli.py +61 -33
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/command_proxy.py +12 -6
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/command_types.py +7 -1
- uiautodev-0.13.3/uiautodev/common.py +56 -0
- uiautodev-0.13.3/uiautodev/driver/android/__init__.py +2 -0
- uiautodev-0.5.0/uiautodev/driver/android.py → uiautodev-0.13.3/uiautodev/driver/android/adb_driver.py +80 -74
- uiautodev-0.13.3/uiautodev/driver/android/common.py +61 -0
- uiautodev-0.13.3/uiautodev/driver/android/u2_driver.py +68 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/driver/base_driver.py +9 -4
- uiautodev-0.13.3/uiautodev/driver/harmony.py +364 -0
- uiautodev-0.13.3/uiautodev/driver/testdata/layout.json +1 -0
- uiautodev-0.13.3/uiautodev/exceptions.py +26 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/model.py +10 -3
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/provider.py +34 -11
- uiautodev-0.13.3/uiautodev/remote/android_input.py +74 -0
- uiautodev-0.13.3/uiautodev/remote/harmony_mjpeg.py +199 -0
- uiautodev-0.13.3/uiautodev/remote/keycode.py +350 -0
- uiautodev-0.13.3/uiautodev/remote/scrcpy.py +179 -0
- uiautodev-0.13.3/uiautodev/remote/touch_controller.py +123 -0
- uiautodev-0.13.3/uiautodev/router/android.py +42 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/router/device.py +6 -20
- uiautodev-0.13.3/uiautodev/router/proxy.py +178 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/utils/common.py +11 -7
- uiautodev-0.13.3/uiautodev/utils/envutils.py +9 -0
- uiautodev-0.5.0/README.md +0 -54
- uiautodev-0.5.0/uiautodev/app.py +0 -95
- uiautodev-0.5.0/uiautodev/common.py +0 -25
- uiautodev-0.5.0/uiautodev/exceptions.py +0 -32
- {uiautodev-0.5.0 → uiautodev-0.13.3}/LICENSE +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/__main__.py +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/appium_proxy.py +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/case.py +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/driver/appium.py +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/driver/ios.py +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/driver/mock.py +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/driver/udt/udt.py +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/router/xml.py +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/static/demo.html +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/utils/exceptions.py +0 -0
- {uiautodev-0.5.0 → uiautodev-0.13.3}/uiautodev/utils/usbmux.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: uiautodev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.3
|
|
4
4
|
Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
|
|
5
|
-
Home-page: https://uiauto.dev
|
|
6
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
7
|
Author: codeskyblue
|
|
8
8
|
Author-email: codeskyblue@gmail.com
|
|
9
9
|
Requires-Python: >=3.8,<4.0
|
|
@@ -14,19 +14,24 @@ 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
|
-
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Dist: Pillow
|
|
20
|
+
Requires-Dist: adbutils (>=2.8.10,<3)
|
|
18
21
|
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
19
22
|
Requires-Dist: construct
|
|
20
|
-
Requires-Dist: fastapi (>=0.
|
|
23
|
+
Requires-Dist: fastapi (>=0.115.12,<1)
|
|
21
24
|
Requires-Dist: httpx
|
|
22
25
|
Requires-Dist: lxml
|
|
23
|
-
Requires-Dist: pillow
|
|
24
|
-
Requires-Dist: poetry (>=1.8.2,<2.0.0)
|
|
25
26
|
Requires-Dist: pydantic (>=2.6,<3.0)
|
|
26
27
|
Requires-Dist: pygments (>=2)
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist:
|
|
28
|
+
Requires-Dist: python-multipart (>=0.0.18)
|
|
29
|
+
Requires-Dist: rich
|
|
30
|
+
Requires-Dist: uiautomator2 (>=3.2.0,<4)
|
|
31
|
+
Requires-Dist: uvicorn (>=0.33.0)
|
|
32
|
+
Requires-Dist: wdapy (>0.2.2,<1)
|
|
33
|
+
Requires-Dist: websockets (>=10.4)
|
|
34
|
+
Project-URL: Homepage, https://uiauto.dev
|
|
30
35
|
Description-Content-Type: text/markdown
|
|
31
36
|
|
|
32
37
|
# uiautodev
|
|
@@ -35,15 +40,21 @@ Description-Content-Type: text/markdown
|
|
|
35
40
|
|
|
36
41
|
https://uiauto.dev
|
|
37
42
|
|
|
38
|
-
>
|
|
43
|
+
> ~~In China visit: https://uiauto.devsleep.com~~
|
|
39
44
|
|
|
40
|
-
UI Inspector for Android and
|
|
45
|
+
UI Inspector for Android, iOS and Harmony help inspector element properties, and auto generate XPath, script.
|
|
41
46
|
|
|
42
47
|
# Install
|
|
43
48
|
```bash
|
|
44
49
|
pip install uiautodev
|
|
45
50
|
```
|
|
46
51
|
|
|
52
|
+
To enable Harmony support, run the following command to install its dependencies:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
uiautodev install-harmony
|
|
56
|
+
```
|
|
57
|
+
|
|
47
58
|
# Usage
|
|
48
59
|
```bash
|
|
49
60
|
Usage: uiauto.dev [OPTIONS] COMMAND [ARGS]...
|
|
@@ -53,12 +64,12 @@ Options:
|
|
|
53
64
|
-h, --help Show this message and exit.
|
|
54
65
|
|
|
55
66
|
Commands:
|
|
67
|
+
server start uiauto.dev local server [Default]
|
|
56
68
|
android COMMAND: tap, tapElement, installApp, currentApp,...
|
|
57
|
-
appium COMMAND: tap, tapElement, installApp, currentApp,...
|
|
58
69
|
ios COMMAND: tap, tapElement, installApp, currentApp,...
|
|
59
70
|
self-update Update uiautodev to latest version
|
|
60
|
-
server start uiauto.dev local server [Default]
|
|
61
71
|
version Print version
|
|
72
|
+
shutdown Shutdown server
|
|
62
73
|
```
|
|
63
74
|
|
|
64
75
|
```bash
|
|
@@ -66,20 +77,37 @@ Commands:
|
|
|
66
77
|
uiauto.dev
|
|
67
78
|
```
|
|
68
79
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
# Environment
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
# Default driver is uiautomator2
|
|
84
|
+
# Set the environment variable below to switch to adb driver
|
|
85
|
+
export UIAUTODEV_USE_ADB_DRIVER=1
|
|
86
|
+
```
|
|
73
87
|
|
|
74
|
-
#
|
|
75
|
-
poetry install
|
|
88
|
+
# Offline mode
|
|
76
89
|
|
|
77
|
-
|
|
78
|
-
make format
|
|
90
|
+
Start with
|
|
79
91
|
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
```sh
|
|
93
|
+
uiautodev server --offline
|
|
94
|
+
|
|
95
|
+
# Specify server url (optional)
|
|
96
|
+
uiautodev server --offline --server-url https://uiauto.dev
|
|
82
97
|
```
|
|
83
98
|
|
|
99
|
+
Visit <http://localhost:20242> once, and then disconnecting from the internet will not affect usage.
|
|
100
|
+
|
|
101
|
+
> All frontend resources will be saved to cache/ dir.
|
|
102
|
+
|
|
103
|
+
# DEVELOP
|
|
104
|
+
|
|
105
|
+
see [DEVELOP.md](DEVELOP.md)
|
|
106
|
+
|
|
107
|
+
# Links
|
|
108
|
+
- https://app.tangoapp.dev/ 基于webadb的手机远程控制项目
|
|
109
|
+
- https://docs.tangoapp.dev/scrcpy/video/web-codecs/ H264解码器
|
|
110
|
+
|
|
84
111
|
# LICENSE
|
|
85
112
|
[MIT](LICENSE)
|
|
113
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# uiautodev
|
|
2
|
+
[](https://codecov.io/gh/codeskyblue/appinspector)
|
|
3
|
+
[](https://badge.fury.io/py/uiautodev)
|
|
4
|
+
|
|
5
|
+
https://uiauto.dev
|
|
6
|
+
|
|
7
|
+
> ~~In China visit: https://uiauto.devsleep.com~~
|
|
8
|
+
|
|
9
|
+
UI Inspector for Android, iOS and Harmony help inspector element properties, and auto generate XPath, script.
|
|
10
|
+
|
|
11
|
+
# Install
|
|
12
|
+
```bash
|
|
13
|
+
pip install uiautodev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
To enable Harmony support, run the following command to install its dependencies:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
uiautodev install-harmony
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
# Usage
|
|
23
|
+
```bash
|
|
24
|
+
Usage: uiauto.dev [OPTIONS] COMMAND [ARGS]...
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
-v, --verbose verbose mode
|
|
28
|
+
-h, --help Show this message and exit.
|
|
29
|
+
|
|
30
|
+
Commands:
|
|
31
|
+
server start uiauto.dev local server [Default]
|
|
32
|
+
android COMMAND: tap, tapElement, installApp, currentApp,...
|
|
33
|
+
ios COMMAND: tap, tapElement, installApp, currentApp,...
|
|
34
|
+
self-update Update uiautodev to latest version
|
|
35
|
+
version Print version
|
|
36
|
+
shutdown Shutdown server
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# run local server and open browser
|
|
41
|
+
uiauto.dev
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
# Environment
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
# Default driver is uiautomator2
|
|
48
|
+
# Set the environment variable below to switch to adb driver
|
|
49
|
+
export UIAUTODEV_USE_ADB_DRIVER=1
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
# Offline mode
|
|
53
|
+
|
|
54
|
+
Start with
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
uiautodev server --offline
|
|
58
|
+
|
|
59
|
+
# Specify server url (optional)
|
|
60
|
+
uiautodev server --offline --server-url https://uiauto.dev
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Visit <http://localhost:20242> once, and then disconnecting from the internet will not affect usage.
|
|
64
|
+
|
|
65
|
+
> All frontend resources will be saved to cache/ dir.
|
|
66
|
+
|
|
67
|
+
# DEVELOP
|
|
68
|
+
|
|
69
|
+
see [DEVELOP.md](DEVELOP.md)
|
|
70
|
+
|
|
71
|
+
# Links
|
|
72
|
+
- https://app.tangoapp.dev/ 基于webadb的手机远程控制项目
|
|
73
|
+
- https://docs.tangoapp.dev/scrcpy/video/web-codecs/ H264解码器
|
|
74
|
+
|
|
75
|
+
# LICENSE
|
|
76
|
+
[MIT](LICENSE)
|
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "uiautodev"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.13.3"
|
|
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>"]
|
|
7
7
|
license = "MIT"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
|
|
10
|
+
include = [
|
|
11
|
+
{path = "uiautodev/binaries/scrcpy.jar"}
|
|
12
|
+
]
|
|
13
|
+
|
|
10
14
|
[tool.poetry.dependencies]
|
|
11
15
|
python = "^3.8"
|
|
12
|
-
|
|
13
|
-
adbutils = "^2.7.0"
|
|
14
|
-
construct = "*"
|
|
15
|
-
lxml = "*"
|
|
16
|
+
adbutils = ">=2.8.10,<3"
|
|
16
17
|
click = "^8.1.7"
|
|
17
18
|
pygments = ">=2"
|
|
18
|
-
uiautomator2 = ">=2"
|
|
19
|
-
|
|
20
|
-
fastapi = "^0.111.0"
|
|
21
|
-
uvicorn = {version = "*", extras = ["standard"]}
|
|
22
|
-
poetry = "^1.8.2"
|
|
19
|
+
uiautomator2 = ">=3.2.0,<4"
|
|
20
|
+
fastapi = ">=0.115.12,<1"
|
|
23
21
|
pydantic = "^2.6"
|
|
24
|
-
wdapy = "
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
wdapy = ">0.2.2,<1"
|
|
23
|
+
websockets = ">=10.4"
|
|
24
|
+
Pillow = "*"
|
|
25
|
+
construct = "*"
|
|
26
|
+
lxml = "*"
|
|
27
|
+
httpx = "*"
|
|
28
|
+
uvicorn = ">=0.33.0"
|
|
29
|
+
rich = "*"
|
|
30
|
+
python-multipart = ">=0.0.18"
|
|
28
31
|
|
|
29
32
|
[tool.poetry.scripts]
|
|
30
33
|
"uiauto.dev" = "uiautodev.__main__:main"
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Created on Sun Feb 18 2024 13:48:55 by codeskyblue"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import signal
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List
|
|
12
|
+
|
|
13
|
+
import adbutils
|
|
14
|
+
import httpx
|
|
15
|
+
import uvicorn
|
|
16
|
+
from fastapi import FastAPI, File, Request, Response, UploadFile, WebSocket
|
|
17
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
18
|
+
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
from starlette.websockets import WebSocketDisconnect
|
|
21
|
+
|
|
22
|
+
from uiautodev import __version__
|
|
23
|
+
from uiautodev.common import convert_bytes_to_image, get_webpage_url, ocr_image
|
|
24
|
+
from uiautodev.driver.android import ADBAndroidDriver, U2AndroidDriver
|
|
25
|
+
from uiautodev.model import Node
|
|
26
|
+
from uiautodev.provider import AndroidProvider, HarmonyProvider, IOSProvider, MockProvider
|
|
27
|
+
from uiautodev.remote.scrcpy import ScrcpyServer
|
|
28
|
+
from uiautodev.router.android import router as android_device_router
|
|
29
|
+
from uiautodev.router.device import make_router
|
|
30
|
+
from uiautodev.router.proxy import make_reverse_proxy
|
|
31
|
+
from uiautodev.router.proxy import router as proxy_router
|
|
32
|
+
from uiautodev.router.xml import router as xml_router
|
|
33
|
+
from uiautodev.utils.envutils import Environment
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
app = FastAPI()
|
|
38
|
+
|
|
39
|
+
app.add_middleware(
|
|
40
|
+
CORSMiddleware,
|
|
41
|
+
allow_origins=["*"],
|
|
42
|
+
allow_credentials=True,
|
|
43
|
+
allow_methods=["GET", "POST"],
|
|
44
|
+
allow_headers=["*"],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
android_default_driver = U2AndroidDriver
|
|
48
|
+
if os.getenv("UIAUTODEV_USE_ADB_DRIVER") in ("1", "true", "True"):
|
|
49
|
+
android_default_driver = ADBAndroidDriver
|
|
50
|
+
|
|
51
|
+
android_router = make_router(AndroidProvider(driver_class=android_default_driver))
|
|
52
|
+
android_adb_router = make_router(AndroidProvider(driver_class=ADBAndroidDriver))
|
|
53
|
+
ios_router = make_router(IOSProvider())
|
|
54
|
+
harmony_router = make_router(HarmonyProvider())
|
|
55
|
+
mock_router = make_router(MockProvider())
|
|
56
|
+
|
|
57
|
+
app.include_router(mock_router, prefix="/api/mock", tags=["mock"])
|
|
58
|
+
|
|
59
|
+
if Environment.UIAUTODEV_MOCK:
|
|
60
|
+
app.include_router(mock_router, prefix="/api/android", tags=["mock"])
|
|
61
|
+
app.include_router(mock_router, prefix="/api/ios", tags=["mock"])
|
|
62
|
+
app.include_router(mock_router, prefix="/api/harmony", tags=["mock"])
|
|
63
|
+
else:
|
|
64
|
+
app.include_router(android_router, prefix="/api/android", tags=["android"])
|
|
65
|
+
app.include_router(android_adb_router, prefix="/api/android_adb", tags=["android_adb"])
|
|
66
|
+
app.include_router(ios_router, prefix="/api/ios", tags=["ios"])
|
|
67
|
+
app.include_router(harmony_router, prefix="/api/harmony", tags=["harmony"])
|
|
68
|
+
|
|
69
|
+
app.include_router(xml_router, prefix="/api/xml", tags=["xml"])
|
|
70
|
+
app.include_router(android_device_router, prefix="/api/android", tags=["android"])
|
|
71
|
+
app.include_router(proxy_router, tags=["proxy"])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.get("/api/{platform}/features")
|
|
75
|
+
def get_features(platform: str) -> Dict[str, bool]:
|
|
76
|
+
"""Get features supported by the specified platform"""
|
|
77
|
+
features = {}
|
|
78
|
+
# 获取所有带有指定平台tag的路由
|
|
79
|
+
from starlette.routing import Route
|
|
80
|
+
|
|
81
|
+
for route in app.routes:
|
|
82
|
+
_route: Route = route # type: ignore
|
|
83
|
+
if hasattr(_route, "tags") and platform in _route.tags:
|
|
84
|
+
if _route.path.startswith(f"/api/{platform}/{{serial}}/"):
|
|
85
|
+
# 提取特性名称
|
|
86
|
+
parts = _route.path.split("/")
|
|
87
|
+
feature_name = parts[-1]
|
|
88
|
+
if not feature_name.startswith("{"):
|
|
89
|
+
features[feature_name] = True
|
|
90
|
+
return features
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class InfoResponse(BaseModel):
|
|
94
|
+
version: str
|
|
95
|
+
description: str
|
|
96
|
+
platform: str
|
|
97
|
+
code_language: str
|
|
98
|
+
cwd: str
|
|
99
|
+
drivers: List[str]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@app.get("/api/info")
|
|
103
|
+
def info() -> InfoResponse:
|
|
104
|
+
"""Information about the application"""
|
|
105
|
+
return InfoResponse(
|
|
106
|
+
version=__version__,
|
|
107
|
+
description="client for https://uiauto.dev",
|
|
108
|
+
platform=platform.system(), # Linux | Darwin | Windows
|
|
109
|
+
code_language="Python",
|
|
110
|
+
cwd=os.getcwd(),
|
|
111
|
+
drivers=["android", "ios", "harmony"],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.post("/api/ocr_image")
|
|
116
|
+
async def _ocr_image(file: UploadFile = File(...)) -> List[Node]:
|
|
117
|
+
"""OCR an image"""
|
|
118
|
+
image_data = await file.read()
|
|
119
|
+
image = convert_bytes_to_image(image_data)
|
|
120
|
+
return ocr_image(image)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.get("/shutdown")
|
|
124
|
+
def shutdown() -> str:
|
|
125
|
+
"""Shutdown the server"""
|
|
126
|
+
os.kill(os.getpid(), signal.SIGINT)
|
|
127
|
+
return "Server shutting down..."
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.get("/demo")
|
|
131
|
+
def demo():
|
|
132
|
+
"""Demo endpoint"""
|
|
133
|
+
static_dir = Path(__file__).parent / "static"
|
|
134
|
+
print(static_dir / "demo.html")
|
|
135
|
+
return FileResponse(static_dir / "demo.html")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.get("/redirect")
|
|
139
|
+
def index_redirect():
|
|
140
|
+
"""redirect to official homepage"""
|
|
141
|
+
url = get_webpage_url()
|
|
142
|
+
logger.debug("redirect to %s", url)
|
|
143
|
+
return RedirectResponse(url)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.get("/api/auth/me")
|
|
147
|
+
def mock_auth_me():
|
|
148
|
+
# 401 {"detail":"Authentication required"}
|
|
149
|
+
return JSONResponse(status_code=401, content={"detail": "Authentication required"})
|
|
150
|
+
|
|
151
|
+
@app.websocket("/ws/android/scrcpy/{serial}")
|
|
152
|
+
async def handle_android_ws(websocket: WebSocket, serial: str):
|
|
153
|
+
"""
|
|
154
|
+
Args:
|
|
155
|
+
serial: device serial
|
|
156
|
+
websocket: WebSocket
|
|
157
|
+
"""
|
|
158
|
+
await websocket.accept()
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
logger.info(f"WebSocket serial: {serial}")
|
|
162
|
+
device = adbutils.device(serial)
|
|
163
|
+
server = ScrcpyServer(device)
|
|
164
|
+
await server.handle_unified_websocket(websocket, serial)
|
|
165
|
+
except WebSocketDisconnect:
|
|
166
|
+
logger.info(f"WebSocket disconnected by client.")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.exception(f"WebSocket error for serial={serial}: {e}")
|
|
169
|
+
await websocket.close(code=1000, reason=str(e))
|
|
170
|
+
finally:
|
|
171
|
+
logger.info(f"WebSocket closed for serial={serial}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_harmony_mjpeg_server(serial: str):
|
|
175
|
+
from hypium import UiDriver
|
|
176
|
+
|
|
177
|
+
from uiautodev.remote.harmony_mjpeg import HarmonyMjpegServer
|
|
178
|
+
|
|
179
|
+
driver = UiDriver.connect(device_sn=serial)
|
|
180
|
+
logger.info("create harmony mjpeg server for %s", serial)
|
|
181
|
+
logger.info(f"device wake_up_display: {driver.wake_up_display()}")
|
|
182
|
+
return HarmonyMjpegServer(driver)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.websocket("/ws/harmony/mjpeg/{serial}")
|
|
186
|
+
async def unified_harmony_ws(websocket: WebSocket, serial: str):
|
|
187
|
+
"""
|
|
188
|
+
Args:
|
|
189
|
+
serial: device serial
|
|
190
|
+
websocket: WebSocket
|
|
191
|
+
"""
|
|
192
|
+
await websocket.accept()
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
logger.info(f"WebSocket serial: {serial}")
|
|
196
|
+
|
|
197
|
+
# 获取 HarmonyScrcpyServer 实例
|
|
198
|
+
server = get_harmony_mjpeg_server(serial)
|
|
199
|
+
server.start()
|
|
200
|
+
await server.handle_ws(websocket)
|
|
201
|
+
except ImportError as e:
|
|
202
|
+
logger.error(f"missing library for harmony: {e}")
|
|
203
|
+
await websocket.close(
|
|
204
|
+
code=1000, reason='missing library, fix by "pip install uiautodev[harmony]"'
|
|
205
|
+
)
|
|
206
|
+
except WebSocketDisconnect:
|
|
207
|
+
logger.info(f"WebSocket disconnected by client.")
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.exception(f"WebSocket error for serial={serial}: {e}")
|
|
210
|
+
await websocket.close(code=1000, reason=str(e))
|
|
211
|
+
finally:
|
|
212
|
+
logger.info(f"WebSocket closed for serial={serial}")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
uvicorn.run("uiautodev.app:app", port=4000, reload=True, use_colors=True)
|
|
Binary file
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
|
+
import os
|
|
10
11
|
import platform
|
|
11
12
|
import subprocess
|
|
12
13
|
import sys
|
|
@@ -18,6 +19,8 @@ import click
|
|
|
18
19
|
import httpx
|
|
19
20
|
import pydantic
|
|
20
21
|
import uvicorn
|
|
22
|
+
from retry import retry
|
|
23
|
+
from rich.logging import RichHandler
|
|
21
24
|
|
|
22
25
|
from uiautodev import __version__, command_proxy
|
|
23
26
|
from uiautodev.command_types import Command
|
|
@@ -28,32 +31,29 @@ from uiautodev.utils.common import convert_params_to_model, print_json
|
|
|
28
31
|
logger = logging.getLogger(__name__)
|
|
29
32
|
|
|
30
33
|
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
|
34
|
+
HARMONY_PACKAGES = [
|
|
35
|
+
"setuptools",
|
|
36
|
+
"https://public.uiauto.devsleep.com/harmony/xdevice-5.0.7.200.tar.gz",
|
|
37
|
+
"https://public.uiauto.devsleep.com/harmony/xdevice-devicetest-5.0.7.200.tar.gz",
|
|
38
|
+
"https://public.uiauto.devsleep.com/harmony/xdevice-ohos-5.0.7.200.tar.gz",
|
|
39
|
+
"https://public.uiauto.devsleep.com/harmony/hypium-5.0.7.200.tar.gz",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def enable_logger_to_console(level):
|
|
44
|
+
_logger = logging.getLogger("uiautodev")
|
|
45
|
+
_logger.setLevel(level)
|
|
46
|
+
_logger.addHandler(RichHandler(enable_link_path=False))
|
|
47
|
+
|
|
31
48
|
|
|
32
49
|
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
33
50
|
@click.option("--verbose", "-v", is_flag=True, default=False, help="verbose mode")
|
|
34
51
|
def cli(verbose: bool):
|
|
35
52
|
if verbose:
|
|
36
|
-
|
|
37
|
-
# you have to setup logHandler(logFormatter) for the root logger
|
|
38
|
-
# and set all children logger to DEBUG
|
|
39
|
-
# that's why it is not easy to use it with logging
|
|
40
|
-
root_logger = logging.getLogger(__name__.split(".")[0])
|
|
41
|
-
root_logger.setLevel(logging.DEBUG)
|
|
42
|
-
|
|
43
|
-
console_handler = logging.StreamHandler()
|
|
44
|
-
console_handler.setLevel(logging.DEBUG)
|
|
45
|
-
|
|
46
|
-
formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
|
|
47
|
-
console_handler.setFormatter(formatter)
|
|
48
|
-
|
|
49
|
-
root_logger.addHandler(console_handler)
|
|
50
|
-
|
|
51
|
-
# set all children logger to DEBUG
|
|
52
|
-
for k in root_logger.manager.loggerDict.keys():
|
|
53
|
-
if k.startswith(root_logger.name+"."):
|
|
54
|
-
logging.getLogger(k).setLevel(logging.DEBUG)
|
|
55
|
-
|
|
53
|
+
enable_logger_to_console(level=logging.DEBUG)
|
|
56
54
|
logger.debug("Verbose mode enabled")
|
|
55
|
+
else:
|
|
56
|
+
enable_logger_to_console(level=logging.INFO)
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def run_driver_command(provider: BaseProvider, command: Command, params: list[str] = None):
|
|
@@ -113,7 +113,7 @@ def case():
|
|
|
113
113
|
def appium(command: Command, params: list[str] = None):
|
|
114
114
|
from uiautodev.driver.appium import AppiumProvider
|
|
115
115
|
from uiautodev.exceptions import AppiumDriverException
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
provider = AppiumProvider()
|
|
118
118
|
try:
|
|
119
119
|
run_driver_command(provider, command, params)
|
|
@@ -133,14 +133,29 @@ def self_update():
|
|
|
133
133
|
subprocess.run([sys.executable, '-m', "pip", "install", "--upgrade", "uiautodev"])
|
|
134
134
|
|
|
135
135
|
|
|
136
|
+
@cli.command('install-harmony')
|
|
137
|
+
def install_harmony():
|
|
138
|
+
for lib_url in HARMONY_PACKAGES:
|
|
139
|
+
click.echo(f"Installing {lib_url} ...")
|
|
140
|
+
pip_install(lib_url)
|
|
141
|
+
|
|
142
|
+
@retry(tries=2, delay=3, backoff=2)
|
|
143
|
+
def pip_install(package: str):
|
|
144
|
+
"""Install a package using pip."""
|
|
145
|
+
subprocess.run([sys.executable, '-m', "pip", "install", package], check=True)
|
|
146
|
+
click.echo(f"Successfully installed {package}")
|
|
147
|
+
|
|
148
|
+
|
|
136
149
|
@cli.command(help="start uiauto.dev local server [Default]")
|
|
137
150
|
@click.option("--port", default=20242, help="port number", show_default=True)
|
|
138
151
|
@click.option("--host", default="127.0.0.1", help="host", show_default=True)
|
|
139
152
|
@click.option("--reload", is_flag=True, default=False, help="auto reload, dev only")
|
|
140
|
-
@click.option("-f", "--force", is_flag=True, default=False, help="shutdown
|
|
153
|
+
@click.option("-f", "--force", is_flag=True, default=False, help="shutdown already running server")
|
|
141
154
|
@click.option("-s", "--no-browser", is_flag=True, default=False, help="silent mode, do not open browser")
|
|
142
|
-
|
|
143
|
-
|
|
155
|
+
@click.option("--offline", is_flag=True, default=False, help="offline mode, do not use internet")
|
|
156
|
+
@click.option("--server-url", default="https://uiauto.dev", help="uiauto.dev server url", show_default=True)
|
|
157
|
+
def server(port: int, host: str, reload: bool, force: bool, no_browser: bool, offline: bool, server_url: str):
|
|
158
|
+
click.echo(f"uiautodev version: {__version__}")
|
|
144
159
|
if force:
|
|
145
160
|
try:
|
|
146
161
|
httpx.get(f"http://{host}:{port}/shutdown", timeout=3)
|
|
@@ -150,32 +165,45 @@ def server(port: int, host: str, reload: bool, force: bool, no_browser: bool):
|
|
|
150
165
|
use_color = True
|
|
151
166
|
if platform.system() == 'Windows':
|
|
152
167
|
use_color = False
|
|
153
|
-
|
|
168
|
+
|
|
169
|
+
server_url = server_url.rstrip('/')
|
|
170
|
+
from uiautodev.router import proxy
|
|
171
|
+
proxy.base_url = server_url
|
|
172
|
+
|
|
173
|
+
if offline:
|
|
174
|
+
proxy.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
logger.info("offline mode enabled, cache dir: %s, server url: %s", proxy.cache_dir, proxy.base_url)
|
|
176
|
+
|
|
154
177
|
if not no_browser:
|
|
155
|
-
th = threading.Thread(target=open_browser_when_server_start, args=(f"http://{host}:{port}",))
|
|
178
|
+
th = threading.Thread(target=open_browser_when_server_start, args=(f"http://{host}:{port}", offline))
|
|
156
179
|
th.daemon = True
|
|
157
180
|
th.start()
|
|
158
181
|
uvicorn.run("uiautodev.app:app", host=host, port=port, reload=reload, use_colors=use_color)
|
|
159
182
|
|
|
183
|
+
@cli.command(help="shutdown uiauto.dev local server")
|
|
184
|
+
@click.option("--port", default=20242, help="port number", show_default=True)
|
|
185
|
+
def shutdown(port: int):
|
|
186
|
+
try:
|
|
187
|
+
httpx.get(f"http://127.0.0.1:{port}/shutdown", timeout=3)
|
|
188
|
+
except httpx.HTTPError:
|
|
189
|
+
pass
|
|
190
|
+
|
|
160
191
|
|
|
161
|
-
def open_browser_when_server_start(
|
|
192
|
+
def open_browser_when_server_start(local_server_url: str, offline: bool = False):
|
|
162
193
|
deadline = time.time() + 10
|
|
163
194
|
while time.time() < deadline:
|
|
164
195
|
try:
|
|
165
|
-
httpx.get(f"{
|
|
196
|
+
httpx.get(f"{local_server_url}/api/info", timeout=1)
|
|
166
197
|
break
|
|
167
198
|
except Exception as e:
|
|
168
199
|
time.sleep(0.5)
|
|
169
200
|
import webbrowser
|
|
170
|
-
web_url = get_webpage_url()
|
|
201
|
+
web_url = get_webpage_url(local_server_url if offline else None)
|
|
171
202
|
logger.info("open browser: %s", web_url)
|
|
172
203
|
webbrowser.open(web_url)
|
|
173
204
|
|
|
174
|
-
def main():
|
|
175
|
-
# set logger level to INFO
|
|
176
|
-
# logging.basicConfig(level=logging.INFO)
|
|
177
|
-
logger.setLevel(logging.INFO)
|
|
178
205
|
|
|
206
|
+
def main():
|
|
179
207
|
has_command = False
|
|
180
208
|
for name in sys.argv[1:]:
|
|
181
209
|
if not name.startswith("-"):
|
|
@@ -13,11 +13,11 @@ from typing import Callable, Dict, List, Optional, Union
|
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
15
15
|
from uiautodev.command_types import AppLaunchRequest, AppTerminateRequest, By, Command, CurrentAppResponse, \
|
|
16
|
-
DumpResponse, FindElementRequest, FindElementResponse, InstallAppRequest, InstallAppResponse,
|
|
17
|
-
WindowSizeResponse
|
|
16
|
+
DumpResponse, FindElementRequest, FindElementResponse, InstallAppRequest, InstallAppResponse, SendKeysRequest, \
|
|
17
|
+
TapRequest, WindowSizeResponse
|
|
18
18
|
from uiautodev.driver.base_driver import BaseDriver
|
|
19
19
|
from uiautodev.exceptions import ElementNotFoundError
|
|
20
|
-
from uiautodev.model import
|
|
20
|
+
from uiautodev.model import AppInfo, Node
|
|
21
21
|
from uiautodev.utils.common import node_travel
|
|
22
22
|
|
|
23
23
|
COMMANDS: Dict[Command, Callable] = {}
|
|
@@ -39,7 +39,7 @@ 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:
|
|
42
|
+
def send_command(driver: BaseDriver, command: Command, params=None):
|
|
43
43
|
if command not in COMMANDS:
|
|
44
44
|
raise NotImplementedError(f"command {command} not implemented")
|
|
45
45
|
func = COMMANDS[command]
|
|
@@ -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
|
|
@@ -144,6 +142,14 @@ def dump(driver: BaseDriver) -> DumpResponse:
|
|
|
144
142
|
def wake_up(driver: BaseDriver):
|
|
145
143
|
driver.wake_up()
|
|
146
144
|
|
|
145
|
+
@register(Command.SEND_KEYS)
|
|
146
|
+
def send_keys(driver: BaseDriver, params: SendKeysRequest):
|
|
147
|
+
driver.send_keys(params.text)
|
|
148
|
+
|
|
149
|
+
@register(Command.CLEAR_TEXT)
|
|
150
|
+
def clear_text(driver: BaseDriver):
|
|
151
|
+
driver.clear_text()
|
|
152
|
+
|
|
147
153
|
|
|
148
154
|
def node_match(node: Node, by: By, value: str) -> bool:
|
|
149
155
|
if by == By.ID:
|