uiautodev 0.3.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.

Potentially problematic release.


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

@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 codeskyblue
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.1
2
+ Name: uiautodev
3
+ Version: 0.3.3
4
+ Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
5
+ Home-page: https://uiauto.dev
6
+ License: MIT
7
+ Author: codeskyblue
8
+ Author-email: codeskyblue@gmail.com
9
+ Requires-Python: >=3.8,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Provides-Extra: appium
18
+ Requires-Dist: adbutils (>=2.6.0,<3.0.0)
19
+ Requires-Dist: appium-python-client (>=4.0.0,<5.0.0) ; extra == "appium"
20
+ Requires-Dist: click (>=8.1.7,<9.0.0)
21
+ Requires-Dist: construct
22
+ Requires-Dist: fastapi[all] (>=0.109.2,<0.110.0)
23
+ Requires-Dist: httpretty (>=1.1.4,<2.0.0)
24
+ Requires-Dist: lxml
25
+ Requires-Dist: pillow
26
+ Requires-Dist: pygments (>=2)
27
+ Requires-Dist: uiautomator2 (>=3.0.11,<4.0.0)
28
+ Requires-Dist: uvicorn[standard] (>=0.27.1,<0.28.0)
29
+ Description-Content-Type: text/markdown
30
+
31
+ # uiautodev
32
+ [![codecov](https://codecov.io/gh/codeskyblue/appinspector/graph/badge.svg?token=aLTg4VOyQH)](https://codecov.io/gh/codeskyblue/appinspector)
33
+ [![PyPI version](https://badge.fury.io/py/uiautodev.svg)](https://badge.fury.io/py/uiautodev)
34
+
35
+ https://uiauto.dev
36
+
37
+ UI Inspector for Android and iOS, help inspector element properties, and auto generate XPath, script.
38
+
39
+
40
+ # DEVELOP
41
+ ```bash
42
+ # install poetry (python package manager)
43
+ pip install poetry # pipx install poetry
44
+
45
+ # install deps
46
+ poetry install
47
+
48
+ # format import
49
+ make format
50
+
51
+ # run server
52
+ make dev
53
+ ```
54
+
55
+ # LICENSE
56
+ [MIT](LICENSE)
@@ -0,0 +1,26 @@
1
+ # uiautodev
2
+ [![codecov](https://codecov.io/gh/codeskyblue/appinspector/graph/badge.svg?token=aLTg4VOyQH)](https://codecov.io/gh/codeskyblue/appinspector)
3
+ [![PyPI version](https://badge.fury.io/py/uiautodev.svg)](https://badge.fury.io/py/uiautodev)
4
+
5
+ https://uiauto.dev
6
+
7
+ UI Inspector for Android and iOS, help inspector element properties, and auto generate XPath, script.
8
+
9
+
10
+ # DEVELOP
11
+ ```bash
12
+ # install poetry (python package manager)
13
+ pip install poetry # pipx install poetry
14
+
15
+ # install deps
16
+ poetry install
17
+
18
+ # format import
19
+ make format
20
+
21
+ # run server
22
+ make dev
23
+ ```
24
+
25
+ # LICENSE
26
+ [MIT](LICENSE)
@@ -0,0 +1,41 @@
1
+ [tool.poetry]
2
+ name = "uiautodev"
3
+ version = "0.3.3"
4
+ description = "Mobile UI Automation, include UI hierarchy inspector, script recorder"
5
+ homepage = "https://uiauto.dev"
6
+ authors = ["codeskyblue <codeskyblue@gmail.com>"]
7
+ license = "MIT"
8
+ readme = "README.md"
9
+
10
+ [tool.poetry.dependencies]
11
+ python = "^3.8"
12
+ fastapi = {extras = ["all"], version = "^0.109.2"}
13
+ uvicorn = {extras = ["standard"], version = "^0.27.1"}
14
+ pillow = "*"
15
+ adbutils = "^2.6.0"
16
+ construct = "*"
17
+ lxml = "*"
18
+ click = "^8.1.7"
19
+ pygments = ">=2"
20
+ httpretty = {version = "^1.1.4", optional = true}
21
+ appium-python-client = {version = "^4.0.0", optional = true}
22
+ uiautomator2 = "^3.0.11"
23
+
24
+ [tool.poetry.extras]
25
+ appium = ["appium-python-client", "httppretty"]
26
+
27
+ [tool.poetry.scripts]
28
+ "uiauto.dev" = "uiautodev.__main__:main"
29
+ "uiautodev" = "uiautodev.__main__:main"
30
+
31
+ [tool.poetry.group.dev.dependencies]
32
+ pytest = "^8.0.1"
33
+ isort = "^5.13.2"
34
+ pytest-cov = "^4.1.0"
35
+
36
+ [tool.poetry-dynamic-versioning] # 根据tag来动态配置版本号
37
+ enable = false
38
+
39
+ [build-system]
40
+ requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
41
+ build-backend = "poetry_dynamic_versioning.backend"
@@ -0,0 +1,12 @@
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"
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """Created on Sun Feb 18 2024 14:20:15 by codeskyblue
5
+ """
6
+
7
+ from uiautodev.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ main()
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """Created on Sun Feb 18 2024 13:48:55 by codeskyblue
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ import platform
10
+ import signal
11
+ from pathlib import Path
12
+ from typing import List
13
+
14
+ from fastapi import FastAPI
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from fastapi.responses import FileResponse, RedirectResponse
17
+ from pydantic import BaseModel
18
+
19
+ from uiautodev import __version__
20
+ from uiautodev.provider import AndroidProvider, IOSProvider, MockProvider
21
+ from uiautodev.router.device import make_router
22
+ from uiautodev.router.xml import router as xml_router
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ app = FastAPI()
27
+
28
+ app.add_middleware(
29
+ CORSMiddleware,
30
+ allow_origins=["*"],
31
+ allow_credentials=True,
32
+ allow_methods=["GET", "POST"],
33
+ allow_headers=["*"],
34
+ )
35
+
36
+ android_router = make_router(AndroidProvider())
37
+ ios_router = make_router(IOSProvider())
38
+ mock_router = make_router(MockProvider())
39
+
40
+ app.include_router(mock_router, prefix="/api/mock", tags=["mock"])
41
+
42
+ if os.environ.get("UIAUTODEV_MOCK"):
43
+ app.include_router(mock_router, prefix="/api/android", tags=["mock"])
44
+ app.include_router(mock_router, prefix="/api/ios", tags=["mock"])
45
+ else:
46
+ app.include_router(android_router, prefix="/api/android", tags=["android"])
47
+ app.include_router(ios_router, prefix="/api/ios", tags=["ios"])
48
+
49
+ app.include_router(xml_router, prefix="/api/xml", tags=["xml"])
50
+
51
+
52
+ class InfoResponse(BaseModel):
53
+ version: str
54
+ description: str
55
+ platform: str
56
+ code_language: str
57
+ cwd: str
58
+ drivers: List[str]
59
+
60
+
61
+ @app.get("/api/info")
62
+ def info() -> InfoResponse:
63
+ """Information about the application"""
64
+ return InfoResponse(
65
+ version=__version__,
66
+ description="client for https://uiauto.dev",
67
+ platform=platform.system(), # Linux | Darwin | Windows
68
+ code_language="Python",
69
+ cwd=os.getcwd(),
70
+ drivers=["android"],
71
+ )
72
+
73
+
74
+ @app.get("/shutdown")
75
+ def shutdown() -> str:
76
+ """Shutdown the server"""
77
+ os.kill(os.getpid(), signal.SIGINT)
78
+ return "Server shutting down..."
79
+
80
+
81
+ @app.get("/demo")
82
+ def demo() -> str:
83
+ """Demo endpoint"""
84
+ static_dir = Path(__file__).parent / "static"
85
+ print(static_dir / "demo.html")
86
+ return FileResponse(static_dir / "demo.html")
87
+
88
+
89
+ @app.get("/")
90
+ def index_redirect():
91
+ """ redirect to official homepage """
92
+ return RedirectResponse("https://uiauto.dev")
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """Created on Tue Mar 19 2024 22:23:37 by codeskyblue
5
+ """
6
+
7
+ import sys
8
+
9
+ import httpx
10
+ from fastapi import FastAPI, Request, Response
11
+
12
+ app = FastAPI()
13
+
14
+
15
+ # Retrieve the target URL from the command line arguments
16
+ try:
17
+ TARGET_URL = sys.argv[1]
18
+ except IndexError:
19
+ print("Usage: python proxy_server.py <target_url>")
20
+ sys.exit(1)
21
+
22
+ @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"])
23
+ async def proxy(request: Request, path: str):
24
+ # Construct the full URL to forward the request to
25
+ if path.endswith('/execute/sync'):
26
+ # 旧版appium处理不好这个请求,直接返回404, unknown command
27
+ # 目前browserstack也不支持这个请求
28
+ return Response(content=b'{"value": {"error": "unknown command", "message": "unknown command", "stacktrace": "UnknownCommandError"}}', status_code=404)
29
+ full_url = f"{TARGET_URL}/{path}"
30
+ body = await request.body()
31
+ print("Forwarding to", request.method, full_url)
32
+ print("==> BODY <==")
33
+ print(body)
34
+ # Include original headers in the request
35
+ headers = {k: v for k, v in request.headers.items() if k != 'host'}
36
+
37
+ # Forward the request to the target server
38
+ async with httpx.AsyncClient(timeout=120) as client:
39
+ resp = await client.request(
40
+ method=request.method,
41
+ url=full_url,
42
+ headers=headers,
43
+ data=body,
44
+ follow_redirects=True,
45
+ )
46
+
47
+ # Return the response received from the target server
48
+ return Response(content=resp.content, status_code=resp.status_code, headers=dict(resp.headers))
49
+
50
+
51
+ if __name__ == "__main__":
52
+ import uvicorn
53
+ uvicorn.run(app, host="0.0.0.0", port=8000)
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """Created on Sat Apr 13 2024 22:35:03 by codeskyblue
5
+ """
6
+
7
+ import enum
8
+ import logging
9
+ from typing import Dict, Union
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from uiautodev import command_proxy
14
+ from uiautodev.command_types import Command
15
+ from uiautodev.driver.base_driver import BaseDriver
16
+ from uiautodev.provider import AndroidProvider
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ class CommandStep(BaseModel):
21
+ method: Union[str, Command]
22
+ params: Dict[str, str]
23
+ skip: bool = False
24
+ ignore_error: bool = False
25
+
26
+
27
+ class CompareEnum(str, enum.Enum):
28
+ EQUAL = "equal"
29
+ CONTAINS = "contains"
30
+ NOT_EQUAL = "not_equal"
31
+ NOT_CONTAINS = "not_contains"
32
+
33
+
34
+ class CompareCheckStep(BaseModel):
35
+ method: CompareEnum
36
+ value_a: str
37
+ value_b: str
38
+ skip: bool = False
39
+
40
+
41
+ def run_driver_command(driver: BaseDriver, command: Command, params: dict):
42
+ model = command_proxy.get_command_params_type(command)
43
+ params_obj = model.model_validate(params) if params else None
44
+ # print("Params:", params, params_obj)
45
+ result = command_proxy.send_command(driver, command, params_obj)
46
+ return result
47
+
48
+ def run():
49
+ # all params key and value should be string
50
+ # Step中
51
+ # 入参类型在为前端保存一份,后端需要同步兼容
52
+ # params所有的key和value都是string类型
53
+ # 出参类型支持重命名 result
54
+ # - key: string, old_key: string, desc: string
55
+ # eg. "WIDTH", "width", "屏幕宽度"
56
+ steps = [
57
+ CommandStep(
58
+ method=Command.APP_LAUNCH,
59
+ params= {
60
+ "package": "com.saucelabs.mydemoapp.android",
61
+ "stop": "true" # bool
62
+ }
63
+ ),
64
+ CommandStep(
65
+ method=Command.GET_WINDOW_SIZE,
66
+ result_trans=[
67
+ dict(key="WIDTH", result_key="width", desc="屏幕宽度"),
68
+ ]
69
+ ),
70
+ CommandStep(
71
+ method=Command.ECHO,
72
+ params={
73
+ "message": "WindowWidth is {{WIDTH}}",
74
+ }
75
+ ),
76
+ CommandStep(
77
+ method=Command.CLICK_ELEMENT,
78
+ params={
79
+ "by": "id",
80
+ "value": "com.saucelabs.mydemoapp.android:id/productIV",
81
+ }
82
+ ),
83
+ CommandStep(
84
+ method=Command.CLICK_ELEMENT,
85
+ params={
86
+ "by": "id",
87
+ "value": "com.saucelabs.mydemoapp.android:id/plusIV",
88
+ }
89
+ ),
90
+ CommandStep(
91
+ method=Command.CLICK_ELEMENT,
92
+ params={
93
+ "by": "id",
94
+ "value": "com.saucelabs.mydemoapp.android:id/cartBt",
95
+ }
96
+ ),
97
+ CommandStep(
98
+ method=Command.CLICK_ELEMENT,
99
+ params={
100
+ "by": "id",
101
+ "value": "com.saucelabs.mydemoapp.android:id/cartIV",
102
+ }
103
+ ),
104
+ CommandStep(
105
+ method=Command.FIND_ELEMENT,
106
+ params={
107
+ "by": "text",
108
+ "value": "Proceed To Checkout",
109
+ },
110
+ skip=True,
111
+ ),
112
+ CompareCheckStep(
113
+ method=CompareEnum.EQUAL,
114
+ value_a="$.name",
115
+ value_b="com.saucelabs.mydemoapp.android:id/cartIV",
116
+ ),
117
+ CommandStep(
118
+ method=Command.CLICK_ELEMENT,
119
+ params={
120
+ "by": "text",
121
+ "value": "Proceed To Checkout",
122
+ }
123
+ ),
124
+ ]
125
+ provider = AndroidProvider()
126
+ driver = provider.get_single_device_driver()
127
+ local_vars: Dict[str, str] = {}
128
+ for step in steps:
129
+ if not isinstance(step, CommandStep):
130
+ continue
131
+ command = Command(step.method)
132
+ params = step.params
133
+ print(step.method, params)
134
+ if step.skip:
135
+ logger.debug("Skip step: %s", step.method)
136
+ continue
137
+ run_driver_command(driver, command, params)
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """Created on Tue Mar 19 2024 10:53:03 by codeskyblue
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import platform
11
+ import sys
12
+ import threading
13
+ import time
14
+ from pprint import pprint
15
+
16
+ import click
17
+ import httpx
18
+ import pydantic
19
+ import uvicorn
20
+
21
+ from uiautodev import __version__, command_proxy
22
+ from uiautodev.command_types import Command
23
+ from uiautodev.provider import AndroidProvider, BaseProvider, IOSProvider
24
+ from uiautodev.utils.common import convert_params_to_model, print_json
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @click.group()
30
+ @click.option("--verbose", "-v", is_flag=True, default=False, help="verbose mode")
31
+ def cli(verbose: bool):
32
+ if verbose:
33
+ root_logger = logging.getLogger(__name__.split(".")[0])
34
+ root_logger.setLevel(logging.DEBUG)
35
+
36
+ console_handler = logging.StreamHandler()
37
+ console_handler.setLevel(logging.DEBUG)
38
+
39
+ formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
40
+ console_handler.setFormatter(formatter)
41
+
42
+ root_logger.addHandler(console_handler)
43
+ logger.debug("Verbose mode enabled")
44
+
45
+
46
+ def run_driver_command(provider: BaseProvider, command: Command, params: list[str] = None):
47
+ if command == Command.LIST:
48
+ devices = provider.list_devices()
49
+ print("==> Devices <==")
50
+ pprint(devices)
51
+ return
52
+ driver = provider.get_single_device_driver()
53
+ params_obj = None
54
+ model = command_proxy.get_command_params_type(command)
55
+ if model:
56
+ if not params:
57
+ print(f"params is required for {command}")
58
+ pprint(model.model_json_schema())
59
+ return
60
+ params_obj = convert_params_to_model(params, model)
61
+
62
+ try:
63
+ print("Command:", command.value)
64
+ print("Params ↓")
65
+ print_json(params_obj)
66
+ result = command_proxy.send_command(driver, command, params_obj)
67
+ print("Result ↓")
68
+ print_json(result)
69
+ except pydantic.ValidationError as e:
70
+ print(f"params error: {e}")
71
+ print(f"\n--- params should be match schema ---")
72
+ pprint(model.model_json_schema()["properties"])
73
+
74
+
75
+ @cli.command(help="COMMAND: " + ", ".join(c.value for c in Command))
76
+ @click.argument("command", type=Command, required=True)
77
+ @click.argument("params", required=False, nargs=-1)
78
+ def android(command: Command, params: list[str] = None):
79
+ provider = AndroidProvider()
80
+ run_driver_command(provider, command, params)
81
+
82
+
83
+ @cli.command(help="COMMAND: " + ", ".join(c.value for c in Command))
84
+ @click.argument("command", type=Command, required=True)
85
+ @click.argument("params", required=False, nargs=-1)
86
+ def ios(command: Command, params: list[str] = None):
87
+ provider = IOSProvider()
88
+ run_driver_command(provider, command, params)
89
+
90
+
91
+ @cli.command(help="run case (beta)")
92
+ def case():
93
+ from uiautodev.case import run
94
+ run()
95
+
96
+
97
+ @cli.command(help="COMMAND: " + ", ".join(c.value for c in Command))
98
+ @click.argument("command", type=Command, required=True)
99
+ @click.argument("params", required=False, nargs=-1)
100
+ def appium(command: Command, params: list[str] = None):
101
+ from uiautodev.driver.appium import AppiumProvider
102
+ from uiautodev.exceptions import AppiumDriverException
103
+
104
+ provider = AppiumProvider()
105
+ try:
106
+ run_driver_command(provider, command, params)
107
+ except AppiumDriverException as e:
108
+ print(f"Error: {e}")
109
+
110
+
111
+ @cli.command('version')
112
+ def print_version():
113
+ print(__version__)
114
+
115
+
116
+ @cli.command(help="start uiauto.dev local server [default]")
117
+ @click.option("--port", default=20242, help="port number", show_default=True)
118
+ @click.option("--host", default="127.0.0.1", help="host", show_default=True)
119
+ @click.option("--reload", is_flag=True, default=False, help="auto reload, dev only")
120
+ @click.option("-f", "--force", is_flag=True, default=False, help="shutdown alrealy runningserver")
121
+ @click.option("--no-browser", is_flag=True, default=False, help="do not open browser")
122
+ def server(port: int, host: str, reload: bool, force: bool, no_browser: bool):
123
+ logger.info("version: %s", __version__)
124
+ if force:
125
+ try:
126
+ httpx.get(f"http://{host}:{port}/shutdown", timeout=3)
127
+ except httpx.HTTPError:
128
+ pass
129
+
130
+ use_color = True
131
+ if platform.system() == 'Windows':
132
+ use_color = False
133
+
134
+ if not no_browser:
135
+ th = threading.Thread(target=open_browser_when_server_start, args=(f"http://{host}:{port}",))
136
+ th.daemon = True
137
+ th.start()
138
+ uvicorn.run("uiautodev.app:app", host=host, port=port, reload=reload, use_colors=use_color)
139
+
140
+
141
+ def open_browser_when_server_start(server_url: str):
142
+ deadline = time.time() + 10
143
+ while time.time() < deadline:
144
+ try:
145
+ httpx.get(f"{server_url}/api/info", timeout=1)
146
+ break
147
+ except Exception as e:
148
+ time.sleep(0.5)
149
+ import webbrowser
150
+ web_url = "https://uiauto.dev"
151
+ logger.info("open browser: %s", web_url)
152
+ webbrowser.open(web_url)
153
+
154
+ def main():
155
+ # set logger level to INFO
156
+ # logging.basicConfig(level=logging.INFO)
157
+ logger.setLevel(logging.INFO)
158
+
159
+ has_command = False
160
+ for name in sys.argv[1:]:
161
+ if not name.startswith("-"):
162
+ has_command = True
163
+
164
+ if not has_command:
165
+ cli.main(args=sys.argv[1:] + ["server"], prog_name="uiauto.dev")
166
+ else:
167
+ cli()
168
+
169
+
170
+ if __name__ == "__main__":
171
+ main()