mywebscrcpy-sdk 1.20.1__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.
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ recursive-include examples *.py
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: mywebscrcpy-sdk
3
+ Version: 1.20.1
4
+ Summary: Python client for MyWebScrcpy device automation
5
+ Project-URL: Homepage, https://github.com/liuzhuogood/MyWebScrcpy
6
+ Project-URL: Documentation, https://github.com/liuzhuogood/MyWebScrcpy/blob/main/packages/python-sdk/README.md
7
+ Project-URL: Repository, https://github.com/liuzhuogood/MyWebScrcpy
8
+ Project-URL: Issues, https://github.com/liuzhuogood/MyWebScrcpy/issues
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: httpx<1,>=0.28
12
+
13
+ # MyWebScrcpy Python SDK
14
+
15
+ SDK 使用 HTTPX,支持 `Client`、绑定设备的 `Device.adb()`、`Device.page_info()` 和类型化结果。按键、点击、滑动及 ASCII 输入可通过明确的 ADB 参数执行。当前尚不提供视频解码、模板管理和匹配 API,也无需安装 OpenCV/PyAV。
16
+
17
+ 使用 Python 3.10+,从 PyPI 安装:
18
+
19
+ ```bash
20
+ python -m pip install mywebscrcpy-sdk
21
+ ```
22
+
23
+ 也可从仓库根目录安装源码并运行示例:
24
+
25
+ ```bash
26
+ python -m pip install ./packages/python-sdk
27
+ python packages/python-sdk/examples/device_query.py DEVICE_SERIAL --base-url http://HOST:PORT
28
+ ```
29
+
30
+ 也可构建并安装 wheel:
31
+
32
+ ```bash
33
+ python -m pip install build
34
+ python -m build packages/python-sdk
35
+ python -m pip install --find-links packages/python-sdk/dist mywebscrcpy-sdk
36
+ ```
37
+
38
+ ```python
39
+ from mywebscrcpy import Client
40
+
41
+ with Client("http://HOST:PORT") as client:
42
+ with client.device("DEVICE_SERIAL") as device:
43
+ result = device.adb(["shell", "getprop", "ro.product.model"], timeout=10)
44
+ print(result.stdout)
45
+ device.adb(["shell", "input", "keyevent", "3"], timeout=10)
46
+ size = device.page_info()
47
+ device.adb(["shell", "input", "tap", str(size.width // 2), str(size.height // 2)], timeout=10)
48
+ ```
49
+
50
+ `timeout` 为秒,SDK 转换为接口的 `timeout_ms`。ADB 参数不得包含可执行文件名、设备选择或宿主切换选项。SDK 检查 HTTP 响应及实际 `status/exit_code`;失败、截断输出或超时抛异常,不自动重试动作。HTTP 超时后的设备结果可能未知,不应直接重发。
51
+
52
+ `CommandResult` 包含 `status/exit_code/stdout/stderr/truncated`;`PageInfo` 包含正整数 `width/height`。必需字段缺失或类型错误抛 `ProtocolError`,不猜替代字段。`CommandError.result` 保留命令结果,`HTTPError` 保留 `status/code/detail`;连接错误使用 `ConnectionError`。
53
+
54
+ `Client(headers=...)` 接收显式请求头,默认验证 TLS,可传 CA 路径。不会读取未知凭据、自动启动主服务或修改 root logger。Client/Device 可重复关闭,关闭后的对象拒绝操作。
55
+
56
+ 应用任务会释放同一套 SDK 源文件,并提供连接环境变量;配置的 Python 环境仍需安装 `httpx`。导出的脚本可在其他目录通过安装本 SDK 独立运行,远程操作要求服务与设备可达。
57
+
58
+ 接口依据 `web/openapi.json` 的 `/api/adb/commands` 和 `/api/ui/page-info`。安装版与应用内释放版共享源码,避免两套运行库。仅本机和受控接口已验证,真机及 Windows/Linux 原生链路仍待验收。
59
+
60
+ SDK 版本与仓库根目录 `VERSION` 一致,Python 包版本去掉 `v` 前缀。修改 `VERSION` 后运行 `node scripts/sync-version.mjs`,同时更新主程序与 SDK 版本。
61
+
62
+ GitHub Actions 的 `Publish Python SDK` 工作流在主分支的版本、SDK 或发布配置变更时运行,也支持 `v*` 标签和手动触发;构建检查及安装测试通过后发布未发布的版本,已存在的完整版本会跳过。仓库需在 Settings → Secrets and variables → Actions 配置 `PYPI_API_TOKEN`,值为有此项目发布权限的 PyPI API Token。只修改 SDK 而不更新版本号不会覆盖 PyPI 上已有的版本。
@@ -0,0 +1,50 @@
1
+ # MyWebScrcpy Python SDK
2
+
3
+ SDK 使用 HTTPX,支持 `Client`、绑定设备的 `Device.adb()`、`Device.page_info()` 和类型化结果。按键、点击、滑动及 ASCII 输入可通过明确的 ADB 参数执行。当前尚不提供视频解码、模板管理和匹配 API,也无需安装 OpenCV/PyAV。
4
+
5
+ 使用 Python 3.10+,从 PyPI 安装:
6
+
7
+ ```bash
8
+ python -m pip install mywebscrcpy-sdk
9
+ ```
10
+
11
+ 也可从仓库根目录安装源码并运行示例:
12
+
13
+ ```bash
14
+ python -m pip install ./packages/python-sdk
15
+ python packages/python-sdk/examples/device_query.py DEVICE_SERIAL --base-url http://HOST:PORT
16
+ ```
17
+
18
+ 也可构建并安装 wheel:
19
+
20
+ ```bash
21
+ python -m pip install build
22
+ python -m build packages/python-sdk
23
+ python -m pip install --find-links packages/python-sdk/dist mywebscrcpy-sdk
24
+ ```
25
+
26
+ ```python
27
+ from mywebscrcpy import Client
28
+
29
+ with Client("http://HOST:PORT") as client:
30
+ with client.device("DEVICE_SERIAL") as device:
31
+ result = device.adb(["shell", "getprop", "ro.product.model"], timeout=10)
32
+ print(result.stdout)
33
+ device.adb(["shell", "input", "keyevent", "3"], timeout=10)
34
+ size = device.page_info()
35
+ device.adb(["shell", "input", "tap", str(size.width // 2), str(size.height // 2)], timeout=10)
36
+ ```
37
+
38
+ `timeout` 为秒,SDK 转换为接口的 `timeout_ms`。ADB 参数不得包含可执行文件名、设备选择或宿主切换选项。SDK 检查 HTTP 响应及实际 `status/exit_code`;失败、截断输出或超时抛异常,不自动重试动作。HTTP 超时后的设备结果可能未知,不应直接重发。
39
+
40
+ `CommandResult` 包含 `status/exit_code/stdout/stderr/truncated`;`PageInfo` 包含正整数 `width/height`。必需字段缺失或类型错误抛 `ProtocolError`,不猜替代字段。`CommandError.result` 保留命令结果,`HTTPError` 保留 `status/code/detail`;连接错误使用 `ConnectionError`。
41
+
42
+ `Client(headers=...)` 接收显式请求头,默认验证 TLS,可传 CA 路径。不会读取未知凭据、自动启动主服务或修改 root logger。Client/Device 可重复关闭,关闭后的对象拒绝操作。
43
+
44
+ 应用任务会释放同一套 SDK 源文件,并提供连接环境变量;配置的 Python 环境仍需安装 `httpx`。导出的脚本可在其他目录通过安装本 SDK 独立运行,远程操作要求服务与设备可达。
45
+
46
+ 接口依据 `web/openapi.json` 的 `/api/adb/commands` 和 `/api/ui/page-info`。安装版与应用内释放版共享源码,避免两套运行库。仅本机和受控接口已验证,真机及 Windows/Linux 原生链路仍待验收。
47
+
48
+ SDK 版本与仓库根目录 `VERSION` 一致,Python 包版本去掉 `v` 前缀。修改 `VERSION` 后运行 `node scripts/sync-version.mjs`,同时更新主程序与 SDK 版本。
49
+
50
+ GitHub Actions 的 `Publish Python SDK` 工作流在主分支的版本、SDK 或发布配置变更时运行,也支持 `v*` 标签和手动触发;构建检查及安装测试通过后发布未发布的版本,已存在的完整版本会跳过。仓库需在 Settings → Secrets and variables → Actions 配置 `PYPI_API_TOKEN`,值为有此项目发布权限的 PyPI API Token。只修改 SDK 而不更新版本号不会覆盖 PyPI 上已有的版本。
@@ -0,0 +1,26 @@
1
+ import argparse
2
+ import os
3
+ from mywebscrcpy import Client
4
+
5
+
6
+ def main():
7
+ parser = argparse.ArgumentParser(description="查询设备型号")
8
+ parser.add_argument("serial", help="ADB 设备序列号")
9
+ parser.add_argument("--base-url", default=os.environ.get("MYWEBSCRCPY_API_URL", ""))
10
+ parser.add_argument("--token", default=os.environ.get("MYWEBSCRCPY_API_TOKEN", ""))
11
+ options = parser.parse_args()
12
+ if not options.base_url:
13
+ raise SystemExit("请通过 --base-url 指定 MyWebScrcpy 服务地址")
14
+ headers = {}
15
+ if options.token:
16
+ headers["Authorization"] = "Bearer " + options.token
17
+
18
+ with Client(options.base_url, headers=headers) as client:
19
+ with client.device(options.serial) as device:
20
+ print("步骤 1:读取设备型号", flush=True)
21
+ result = device.adb(["shell", "getprop", "ro.product.model"], timeout=10)
22
+ print(result.stdout, end="", flush=True)
23
+
24
+
25
+ if __name__ == "__main__":
26
+ main()
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mywebscrcpy-sdk"
7
+ version = "1.20.1"
8
+ description = "Python client for MyWebScrcpy device automation"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = ["httpx>=0.28,<1"]
12
+
13
+ [project.urls]
14
+ Homepage = "https://github.com/liuzhuogood/MyWebScrcpy"
15
+ Documentation = "https://github.com/liuzhuogood/MyWebScrcpy/blob/main/packages/python-sdk/README.md"
16
+ Repository = "https://github.com/liuzhuogood/MyWebScrcpy"
17
+ Issues = "https://github.com/liuzhuogood/MyWebScrcpy/issues"
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """Installable MyWebScrcpy SDK. Basic imports do not load vision dependencies."""
2
+ from .client import Client
3
+ from .device import Device
4
+ from .errors import CommandError, ConnectionError, HTTPError, ProtocolError, SDKError
5
+ from .models import CommandResult, PageInfo
6
+
7
+ __version__ = "1.20.1"
8
+ __all__ = ["Client", "Device", "CommandResult", "PageInfo", "SDKError", "CommandError",
9
+ "ConnectionError", "HTTPError", "ProtocolError"]
@@ -0,0 +1,73 @@
1
+ import logging
2
+ from urllib.parse import urlsplit
3
+
4
+ try:
5
+ import httpx
6
+ except ImportError as error:
7
+ raise ImportError("请在当前 Python 环境安装 mywebscrcpy-sdk,或安装 httpx") from error
8
+
9
+ from .errors import ConnectionError, HTTPError, ProtocolError, SDKError
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class Client:
15
+ """HTTP client. No device process, service or Vision session starts implicitly."""
16
+
17
+ def __init__(self, base_url: str, *, headers: dict[str, str] | None = None,
18
+ timeout: float = 30, verify: bool | str = True):
19
+ parsed = urlsplit(base_url)
20
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc or parsed.username or parsed.password or parsed.query or parsed.fragment:
21
+ raise ValueError("base_url 必须是无凭据的 HTTP(S) 服务地址")
22
+ if parsed.path not in {"", "/"}:
23
+ raise ValueError("base_url 不能包含接口路径")
24
+ if timeout <= 0:
25
+ raise ValueError("timeout 必须为正数秒")
26
+ if verify is False:
27
+ logger.warning("TLS certificate verification explicitly disabled")
28
+ self._http = httpx.Client(base_url=base_url.rstrip("/"), headers=headers,
29
+ timeout=timeout, verify=verify, trust_env=False)
30
+ self._closed = False
31
+
32
+ def device(self, serial: str):
33
+ from .device import Device
34
+ self._check_open()
35
+ return Device(self, serial)
36
+
37
+ def _check_open(self):
38
+ if self._closed:
39
+ raise SDKError("Client 已关闭")
40
+
41
+ def _request(self, method: str, path: str, *, serial: str, timeout: float, body=None):
42
+ self._check_open()
43
+ try:
44
+ response = self._http.request(method, path, params={"serial": serial}, json=body, timeout=timeout)
45
+ except httpx.RequestError as error:
46
+ logger.warning("HTTP connection failed: method=%s path=%s type=%s", method, path, type(error).__name__)
47
+ raise ConnectionError(f"{method} {path}: {type(error).__name__}") from error
48
+ try:
49
+ value = response.json()
50
+ except ValueError as error:
51
+ raise ProtocolError(f"{method} {path}: HTTP {response.status_code} 返回非 JSON") from error
52
+ if not isinstance(value, dict):
53
+ raise ProtocolError("API 响应必须是 JSON 对象")
54
+ if response.is_error:
55
+ # ErrorResponse's message is optional; code is always taken from error.
56
+ code = value.get("error", "")
57
+ detail = value.get("message", "")
58
+ if not isinstance(code, str) or not isinstance(detail, str):
59
+ raise ProtocolError("错误响应字段类型不合法")
60
+ raise HTTPError(response.status_code, code, detail)
61
+ return value
62
+
63
+ def close(self):
64
+ if not self._closed:
65
+ self._closed = True
66
+ self._http.close()
67
+
68
+ def __enter__(self):
69
+ self._check_open()
70
+ return self
71
+
72
+ def __exit__(self, exc_type, exc_value, traceback):
73
+ self.close()
@@ -0,0 +1,84 @@
1
+ import logging
2
+ import math
3
+
4
+ from .errors import CommandError, ProtocolError, SDKError
5
+ from .models import CommandResult, PageInfo
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def _seconds(value):
11
+ if type(value) not in {int, float} or not math.isfinite(value) or not 0 < value <= 600:
12
+ raise ValueError("timeout 必须在 0 到 600 秒之间")
13
+ return value
14
+
15
+
16
+ class Device:
17
+ """Basic HTTP operations. Vision and matching APIs are added separately."""
18
+
19
+ def __init__(self, client, serial: str):
20
+ if not isinstance(serial, str) or not serial.strip():
21
+ raise ValueError("serial 必须为非空字符串")
22
+ self._client = client
23
+ self.serial = serial
24
+ self._closed = False
25
+
26
+ def _check_open(self):
27
+ if self._closed:
28
+ raise SDKError("Device 已关闭")
29
+ self._client._check_open()
30
+
31
+ def adb(self, args: list[str], timeout: float = 30) -> CommandResult:
32
+ self._check_open()
33
+ seconds = _seconds(timeout)
34
+ if not isinstance(args, list) or not 1 <= len(args) <= 128 or any(not isinstance(v, str) or not v or "\x00" in v for v in args):
35
+ raise ValueError("args 必须包含 1 到 128 个非空字符串")
36
+ forbidden = {"adb", "-s", "--serial", "-H", "-P", "-L"}
37
+ if any(v in forbidden or v.startswith(("-H", "-P", "-L")) for v in args):
38
+ raise ValueError("args 不得改变 ADB 宿主或目标设备")
39
+ logger.debug("ADB command started: serial=%s timeout=%s", self.serial, seconds)
40
+ value = self._client._request("POST", "/api/adb/commands", serial=self.serial,
41
+ timeout=seconds + 10,
42
+ body={"args": args, "timeout_ms": max(1, round(seconds * 1000))})
43
+ try:
44
+ status = value["status"]
45
+ if status not in {"queued", "running", "completed", "failed", "timed_out"}:
46
+ raise ValueError("status")
47
+ # A successful synchronous result must contain its actual exit code.
48
+ exit_code = value["exit_code"] if status == "completed" else value.get("exit_code")
49
+ if (status == "completed" and type(exit_code) is not int) or (exit_code is not None and type(exit_code) is not int):
50
+ raise ValueError("exit_code")
51
+ stdout, stderr = value.get("stdout", ""), value.get("stderr", "")
52
+ truncated = value.get("truncated", False)
53
+ if not isinstance(stdout, str) or not isinstance(stderr, str) or type(truncated) is not bool:
54
+ raise ValueError("output")
55
+ except (KeyError, ValueError, TypeError) as error:
56
+ raise ProtocolError("ADB 响应缺少字段或字段类型不合法") from error
57
+ result = CommandResult(status, exit_code, stdout, stderr, truncated)
58
+ if status != "completed" or exit_code != 0 or truncated:
59
+ logger.warning("ADB command failed: serial=%s status=%s exit_code=%s truncated=%s", self.serial, status, exit_code, truncated)
60
+ raise CommandError(result)
61
+ logger.debug("ADB command completed: serial=%s", self.serial)
62
+ return result
63
+
64
+ def page_info(self, timeout: float = 10) -> PageInfo:
65
+ self._check_open()
66
+ value = self._client._request("GET", "/api/ui/page-info", serial=self.serial, timeout=_seconds(timeout))
67
+ try:
68
+ display = value["display"]
69
+ width, height = display["width"], display["height"]
70
+ if type(width) is not int or type(height) is not int or width <= 0 or height <= 0:
71
+ raise ValueError("display dimensions")
72
+ except (KeyError, ValueError, TypeError) as error:
73
+ raise ProtocolError("响应缺少有效 display.width/height") from error
74
+ return PageInfo(width, height)
75
+
76
+ def close(self):
77
+ self._closed = True
78
+
79
+ def __enter__(self):
80
+ self._check_open()
81
+ return self
82
+
83
+ def __exit__(self, exc_type, exc_value, traceback):
84
+ self.close()
@@ -0,0 +1,27 @@
1
+ """Explicit SDK errors; device actions are never automatically retried."""
2
+
3
+
4
+ class SDKError(RuntimeError):
5
+ pass
6
+
7
+
8
+ class ConnectionError(SDKError):
9
+ pass
10
+
11
+
12
+ class ProtocolError(SDKError):
13
+ pass
14
+
15
+
16
+ class HTTPError(SDKError):
17
+ def __init__(self, status: int, code: str, detail: str):
18
+ self.status = status
19
+ self.code = code
20
+ self.detail = detail
21
+ super().__init__(f"HTTP {status}: {code}: {detail}")
22
+
23
+
24
+ class CommandError(SDKError):
25
+ def __init__(self, result):
26
+ self.result = result
27
+ super().__init__(f"ADB command {result.status}, exit_code={result.exit_code}")
@@ -0,0 +1,16 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class CommandResult:
6
+ status: str
7
+ exit_code: int | None
8
+ stdout: str
9
+ stderr: str
10
+ truncated: bool
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class PageInfo:
15
+ width: int
16
+ height: int
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: mywebscrcpy-sdk
3
+ Version: 1.20.1
4
+ Summary: Python client for MyWebScrcpy device automation
5
+ Project-URL: Homepage, https://github.com/liuzhuogood/MyWebScrcpy
6
+ Project-URL: Documentation, https://github.com/liuzhuogood/MyWebScrcpy/blob/main/packages/python-sdk/README.md
7
+ Project-URL: Repository, https://github.com/liuzhuogood/MyWebScrcpy
8
+ Project-URL: Issues, https://github.com/liuzhuogood/MyWebScrcpy/issues
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: httpx<1,>=0.28
12
+
13
+ # MyWebScrcpy Python SDK
14
+
15
+ SDK 使用 HTTPX,支持 `Client`、绑定设备的 `Device.adb()`、`Device.page_info()` 和类型化结果。按键、点击、滑动及 ASCII 输入可通过明确的 ADB 参数执行。当前尚不提供视频解码、模板管理和匹配 API,也无需安装 OpenCV/PyAV。
16
+
17
+ 使用 Python 3.10+,从 PyPI 安装:
18
+
19
+ ```bash
20
+ python -m pip install mywebscrcpy-sdk
21
+ ```
22
+
23
+ 也可从仓库根目录安装源码并运行示例:
24
+
25
+ ```bash
26
+ python -m pip install ./packages/python-sdk
27
+ python packages/python-sdk/examples/device_query.py DEVICE_SERIAL --base-url http://HOST:PORT
28
+ ```
29
+
30
+ 也可构建并安装 wheel:
31
+
32
+ ```bash
33
+ python -m pip install build
34
+ python -m build packages/python-sdk
35
+ python -m pip install --find-links packages/python-sdk/dist mywebscrcpy-sdk
36
+ ```
37
+
38
+ ```python
39
+ from mywebscrcpy import Client
40
+
41
+ with Client("http://HOST:PORT") as client:
42
+ with client.device("DEVICE_SERIAL") as device:
43
+ result = device.adb(["shell", "getprop", "ro.product.model"], timeout=10)
44
+ print(result.stdout)
45
+ device.adb(["shell", "input", "keyevent", "3"], timeout=10)
46
+ size = device.page_info()
47
+ device.adb(["shell", "input", "tap", str(size.width // 2), str(size.height // 2)], timeout=10)
48
+ ```
49
+
50
+ `timeout` 为秒,SDK 转换为接口的 `timeout_ms`。ADB 参数不得包含可执行文件名、设备选择或宿主切换选项。SDK 检查 HTTP 响应及实际 `status/exit_code`;失败、截断输出或超时抛异常,不自动重试动作。HTTP 超时后的设备结果可能未知,不应直接重发。
51
+
52
+ `CommandResult` 包含 `status/exit_code/stdout/stderr/truncated`;`PageInfo` 包含正整数 `width/height`。必需字段缺失或类型错误抛 `ProtocolError`,不猜替代字段。`CommandError.result` 保留命令结果,`HTTPError` 保留 `status/code/detail`;连接错误使用 `ConnectionError`。
53
+
54
+ `Client(headers=...)` 接收显式请求头,默认验证 TLS,可传 CA 路径。不会读取未知凭据、自动启动主服务或修改 root logger。Client/Device 可重复关闭,关闭后的对象拒绝操作。
55
+
56
+ 应用任务会释放同一套 SDK 源文件,并提供连接环境变量;配置的 Python 环境仍需安装 `httpx`。导出的脚本可在其他目录通过安装本 SDK 独立运行,远程操作要求服务与设备可达。
57
+
58
+ 接口依据 `web/openapi.json` 的 `/api/adb/commands` 和 `/api/ui/page-info`。安装版与应用内释放版共享源码,避免两套运行库。仅本机和受控接口已验证,真机及 Windows/Linux 原生链路仍待验收。
59
+
60
+ SDK 版本与仓库根目录 `VERSION` 一致,Python 包版本去掉 `v` 前缀。修改 `VERSION` 后运行 `node scripts/sync-version.mjs`,同时更新主程序与 SDK 版本。
61
+
62
+ GitHub Actions 的 `Publish Python SDK` 工作流在主分支的版本、SDK 或发布配置变更时运行,也支持 `v*` 标签和手动触发;构建检查及安装测试通过后发布未发布的版本,已存在的完整版本会跳过。仓库需在 Settings → Secrets and variables → Actions 配置 `PYPI_API_TOKEN`,值为有此项目发布权限的 PyPI API Token。只修改 SDK 而不更新版本号不会覆盖 PyPI 上已有的版本。
@@ -0,0 +1,15 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ examples/device_query.py
5
+ src/mywebscrcpy/__init__.py
6
+ src/mywebscrcpy/client.py
7
+ src/mywebscrcpy/device.py
8
+ src/mywebscrcpy/errors.py
9
+ src/mywebscrcpy/models.py
10
+ src/mywebscrcpy_sdk.egg-info/PKG-INFO
11
+ src/mywebscrcpy_sdk.egg-info/SOURCES.txt
12
+ src/mywebscrcpy_sdk.egg-info/dependency_links.txt
13
+ src/mywebscrcpy_sdk.egg-info/requires.txt
14
+ src/mywebscrcpy_sdk.egg-info/top_level.txt
15
+ tests/test_basic.py
@@ -0,0 +1 @@
1
+ httpx<1,>=0.28
@@ -0,0 +1,116 @@
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import sys
5
+ import tempfile
6
+ import threading
7
+ import unittest
8
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
9
+ from pathlib import Path
10
+
11
+ import httpx
12
+ from mywebscrcpy import Client, CommandError, HTTPError, ProtocolError, SDKError
13
+
14
+
15
+ class BasicContractTests(unittest.TestCase):
16
+ def fixture(self, response):
17
+ seen = []
18
+ client = Client("http://test.invalid")
19
+ client._http.close()
20
+ def handle(request):
21
+ seen.append(request)
22
+ return httpx.Response(200, json=response)
23
+ client._http = httpx.Client(base_url="http://test.invalid", transport=httpx.MockTransport(handle))
24
+ self.addCleanup(client.close)
25
+ return client, seen
26
+
27
+ def test_binding_units_optional_output_and_close(self):
28
+ client, seen = self.fixture({"status": "completed", "exit_code": 0})
29
+ with client.device("phone-A") as device:
30
+ result = device.adb(["shell", "getprop", "ro.product.model"], timeout=1.25)
31
+ self.assertEqual(result.stdout, "")
32
+ request = seen[0]
33
+ self.assertEqual(request.url.params["serial"], "phone-A")
34
+ self.assertEqual(json.loads(request.content), {"args": ["shell", "getprop", "ro.product.model"], "timeout_ms": 1250})
35
+ with self.assertRaises(SDKError):
36
+ device.adb(["shell"])
37
+ client.close()
38
+ client.close()
39
+ with self.assertRaises(SDKError):
40
+ client.device("phone-A")
41
+
42
+ def test_command_failure_truncation_and_missing_fields(self):
43
+ for response in [{"status": "failed", "exit_code": 1}, {"status": "timed_out"}, {"status": "completed", "exit_code": 0, "truncated": True}]:
44
+ client, seen = self.fixture(response)
45
+ with self.assertRaises(CommandError):
46
+ client.device("phone").adb(["shell"])
47
+ self.assertEqual(len(seen), 1, "actions must never retry")
48
+ for response in [{}, {"state": "completed", "exit_code": 0}, {"status": "completed"}, {"status": "completed", "exit_code": "0"}, {"status": "completed", "exit_code": 0, "stdout": None}]:
49
+ client, _ = self.fixture(response)
50
+ with self.assertRaises(ProtocolError):
51
+ client.device("phone").adb(["shell"])
52
+
53
+ def test_target_override_and_timeouts_reject_before_request(self):
54
+ client, seen = self.fixture({"status": "completed", "exit_code": 0})
55
+ device = client.device("phone")
56
+ for args in [["-s", "other"], ["adb", "devices"], ["shell", "-Hhost"], [], ["shell", ""]]:
57
+ with self.assertRaises(ValueError):
58
+ device.adb(args)
59
+ for timeout in [0, -1, 601, float("nan"), True]:
60
+ with self.assertRaises(ValueError):
61
+ device.adb(["shell"], timeout=timeout)
62
+ self.assertEqual(seen, [])
63
+
64
+ def test_exact_page_info_fields(self):
65
+ client, _ = self.fixture({"display": {"width": 1080, "height": 2400}})
66
+ self.assertEqual(client.device("phone").page_info().height, 2400)
67
+ for response in [{"screen": {"width": 1080, "height": 2400}}, {"display": {"width": 0, "height": 2400}}]:
68
+ client, _ = self.fixture(response)
69
+ with self.assertRaises(ProtocolError):
70
+ client.device("phone").page_info()
71
+
72
+ def test_http_error_retains_exact_error_fields(self):
73
+ client = Client("http://test.invalid")
74
+ client._http.close()
75
+ client._http = httpx.Client(base_url="http://test.invalid", transport=httpx.MockTransport(lambda r: httpx.Response(400, json={"error": "invalid_request", "message": "bad args"})))
76
+ with client:
77
+ with self.assertRaises(HTTPError) as caught:
78
+ client.device("phone").adb(["shell"])
79
+ self.assertEqual((caught.exception.status, caught.exception.code, caught.exception.detail), (400, "invalid_request", "bad args"))
80
+
81
+ def test_portable_example_runs_outside_application_without_vision(self):
82
+ requests = []
83
+ class Handler(BaseHTTPRequestHandler):
84
+ def do_POST(self):
85
+ requests.append((self.path, json.loads(self.rfile.read(int(self.headers["Content-Length"])))))
86
+ value = json.dumps({"status": "completed", "exit_code": 0, "stdout": "Portable Phone\n"}).encode()
87
+ self.send_response(200)
88
+ self.send_header("Content-Type", "application/json")
89
+ self.send_header("Content-Length", str(len(value)))
90
+ self.end_headers()
91
+ self.wfile.write(value)
92
+ def log_message(self, *args):
93
+ pass
94
+ server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
95
+ thread = threading.Thread(target=server.serve_forever)
96
+ thread.start()
97
+ try:
98
+ sample = Path(__file__).resolve().parents[1] / "examples/device_query.py"
99
+ with tempfile.TemporaryDirectory() as directory:
100
+ copied = Path(directory) / "workflow.py"
101
+ copied.write_text(sample.read_text(), encoding="utf-8")
102
+ result = subprocess.run([sys.executable, str(copied), "portable-phone", "--base-url", f"http://127.0.0.1:{server.server_port}"], cwd=directory, capture_output=True, text=True, timeout=10)
103
+ self.assertEqual(result.returncode, 0, result.stderr)
104
+ self.assertIn("Portable Phone", result.stdout)
105
+ self.assertEqual(requests[0][1]["timeout_ms"], 10000)
106
+ self.assertIn("serial=portable-phone", requests[0][0])
107
+ self.assertNotIn("cv2", sys.modules)
108
+ self.assertNotIn("av", sys.modules)
109
+ finally:
110
+ server.shutdown()
111
+ server.server_close()
112
+ thread.join()
113
+
114
+
115
+ if __name__ == "__main__":
116
+ unittest.main()