pantoqa-bridge 0.4.4__py3-none-any.whl
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.
- pantoqa_bridge/__init__.py +0 -0
- pantoqa_bridge/__main__.py +4 -0
- pantoqa_bridge/cli.py +58 -0
- pantoqa_bridge/config.py +35 -0
- pantoqa_bridge/lib/adb.py +112 -0
- pantoqa_bridge/lib/scrcpy.py +119 -0
- pantoqa_bridge/logger.py +39 -0
- pantoqa_bridge/models/code_execute.py +6 -0
- pantoqa_bridge/routes/action.py +114 -0
- pantoqa_bridge/routes/adb.py +16 -0
- pantoqa_bridge/routes/executor.py +117 -0
- pantoqa_bridge/routes/misc.py +74 -0
- pantoqa_bridge/routes/screen_mirror.py +75 -0
- pantoqa_bridge/scrcpy-server-v3.3.1 +0 -0
- pantoqa_bridge/server.py +178 -0
- pantoqa_bridge/service/uiautomator_action.py +102 -0
- pantoqa_bridge/tasks/executor.py +88 -0
- pantoqa_bridge/utils/misc.py +131 -0
- pantoqa_bridge/utils/pkg.py +45 -0
- pantoqa_bridge/utils/process.py +183 -0
- pantoqa_bridge-0.4.4.dist-info/METADATA +48 -0
- pantoqa_bridge-0.4.4.dist-info/RECORD +25 -0
- pantoqa_bridge-0.4.4.dist-info/WHEEL +5 -0
- pantoqa_bridge-0.4.4.dist-info/entry_points.txt +2 -0
- pantoqa_bridge-0.4.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import signal
|
|
4
|
+
import socket
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
8
|
+
|
|
9
|
+
from pantoqa_bridge.config import IS_WINDOWS
|
|
10
|
+
from pantoqa_bridge.logger import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_stream_pipe() -> tuple[
|
|
14
|
+
AsyncIterator[str],
|
|
15
|
+
Callable[[str], Awaitable[None]],
|
|
16
|
+
asyncio.Event,
|
|
17
|
+
]:
|
|
18
|
+
queue: asyncio.Queue[str] = asyncio.Queue()
|
|
19
|
+
done: asyncio.Event = asyncio.Event()
|
|
20
|
+
|
|
21
|
+
async def push_data_to_stream(message: str) -> None:
|
|
22
|
+
queue.put_nowait(message)
|
|
23
|
+
|
|
24
|
+
async def create_stream() -> AsyncIterator[str]:
|
|
25
|
+
try:
|
|
26
|
+
while not done.is_set():
|
|
27
|
+
yield await queue.get()
|
|
28
|
+
except asyncio.CancelledError:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
return create_stream(), push_data_to_stream, done
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def stream_process(
|
|
35
|
+
process: asyncio.subprocess.Process,
|
|
36
|
+
on_data: Callable[[str, str], Awaitable[None]],
|
|
37
|
+
) -> int:
|
|
38
|
+
queue: asyncio.Queue[tuple[str, str] | None] = asyncio.Queue()
|
|
39
|
+
|
|
40
|
+
async def stream_output(stream: asyncio.StreamReader | None, label: str) -> None:
|
|
41
|
+
if not stream:
|
|
42
|
+
queue.put_nowait(None)
|
|
43
|
+
return
|
|
44
|
+
while True:
|
|
45
|
+
line = await stream.readline()
|
|
46
|
+
if not line:
|
|
47
|
+
queue.put_nowait(None)
|
|
48
|
+
break
|
|
49
|
+
queue.put_nowait((label, line.decode().strip()))
|
|
50
|
+
|
|
51
|
+
tasks: list[asyncio.Task[None]] = []
|
|
52
|
+
tasks.append(asyncio.create_task(stream_output(process.stdout, "stdout")))
|
|
53
|
+
tasks.append(asyncio.create_task(stream_output(process.stderr, "stderr")))
|
|
54
|
+
|
|
55
|
+
# Stream output as it comes
|
|
56
|
+
completed_streams = 0
|
|
57
|
+
while completed_streams < len(tasks):
|
|
58
|
+
try:
|
|
59
|
+
item = await asyncio.wait_for(queue.get(), timeout=0.1)
|
|
60
|
+
if item is None:
|
|
61
|
+
completed_streams += 1
|
|
62
|
+
else:
|
|
63
|
+
stream_type, data = item
|
|
64
|
+
await on_data(stream_type, data)
|
|
65
|
+
except TimeoutError:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
# Wait for tasks to complete
|
|
69
|
+
await asyncio.gather(*tasks)
|
|
70
|
+
return await process.wait()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def wait_for_port_to_alive(port: int, host: str = "127.0.0.1", timeout=15):
|
|
74
|
+
start = time.time()
|
|
75
|
+
while time.time() - start < timeout:
|
|
76
|
+
try:
|
|
77
|
+
with socket.create_connection((host, port), timeout=1):
|
|
78
|
+
return True
|
|
79
|
+
except OSError:
|
|
80
|
+
await asyncio.sleep(0.5)
|
|
81
|
+
raise TimeoutError("Appium did not start in time")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def watch_process_bg(
|
|
85
|
+
pid: int,
|
|
86
|
+
on_exit: Callable[[int], Awaitable[None] | None],
|
|
87
|
+
*,
|
|
88
|
+
poll_interval: float = 2.0,
|
|
89
|
+
) -> None:
|
|
90
|
+
|
|
91
|
+
async def _watcher():
|
|
92
|
+
try:
|
|
93
|
+
while True:
|
|
94
|
+
if not is_process_alive_sync(pid):
|
|
95
|
+
result = on_exit(pid)
|
|
96
|
+
if asyncio.iscoroutine(result):
|
|
97
|
+
await result
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
await asyncio.sleep(poll_interval)
|
|
101
|
+
except asyncio.CancelledError:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
asyncio.create_task(_watcher())
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def is_process_alive_sync(pid: int) -> bool:
|
|
108
|
+
# TODO: Can we use psutil here?
|
|
109
|
+
try:
|
|
110
|
+
if IS_WINDOWS:
|
|
111
|
+
# Windows: use tasklist to check if PID exists
|
|
112
|
+
out = subprocess.check_output(
|
|
113
|
+
["tasklist", "/FI", f"PID eq {pid}", "/NH"],
|
|
114
|
+
stderr=subprocess.DEVNULL,
|
|
115
|
+
).decode().strip()
|
|
116
|
+
# tasklist returns "INFO: No tasks are running..." if PID doesn't exist
|
|
117
|
+
return str(pid) in out and "INFO:" not in out
|
|
118
|
+
else:
|
|
119
|
+
# Unix/Linux: use ps
|
|
120
|
+
out = subprocess.check_output(["ps", "-o", "state=", "-p", str(pid)]).decode().strip()
|
|
121
|
+
return "z" not in out.lower()
|
|
122
|
+
except subprocess.CalledProcessError:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def kill_by_pid(pid: int):
|
|
127
|
+
# TODO: Can we use psutil here?
|
|
128
|
+
if IS_WINDOWS:
|
|
129
|
+
subprocess.run(
|
|
130
|
+
["taskkill", "/PID", str(pid)],
|
|
131
|
+
stderr=subprocess.DEVNULL,
|
|
132
|
+
stdout=subprocess.DEVNULL,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
os.kill(pid, signal.SIGTERM)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def force_kill_by_pid(pid: int):
|
|
139
|
+
# TODO: Can we use psutil here?
|
|
140
|
+
if IS_WINDOWS:
|
|
141
|
+
subprocess.run(
|
|
142
|
+
["taskkill", "/F", "/PID", str(pid)],
|
|
143
|
+
stderr=subprocess.DEVNULL,
|
|
144
|
+
stdout=subprocess.DEVNULL,
|
|
145
|
+
)
|
|
146
|
+
else:
|
|
147
|
+
os.kill(pid, signal.SIGKILL)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def kill_self_process():
|
|
151
|
+
# DON'T WAIT HERE
|
|
152
|
+
self_pid = os.getpid()
|
|
153
|
+
logger.info(f"Killing self process {self_pid}...")
|
|
154
|
+
kill_by_pid(self_pid)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def kill_process_sync(pid: int, timeout: int = 10) -> None:
|
|
158
|
+
if pid <= 0:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
if not is_process_alive_sync(pid):
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Step 1: graceful terminate
|
|
165
|
+
logger.info(f"Terminating process {pid}...")
|
|
166
|
+
if kill_by_pid(pid):
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
if not is_process_alive_sync(pid):
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
# Step 2: wait
|
|
173
|
+
logger.info(f"Waiting for process {pid} to terminate...")
|
|
174
|
+
start = time.time()
|
|
175
|
+
while time.time() - start < timeout:
|
|
176
|
+
if not is_process_alive_sync(pid):
|
|
177
|
+
logger.info(f"Process {pid} terminated.")
|
|
178
|
+
return
|
|
179
|
+
time.sleep(0.2)
|
|
180
|
+
|
|
181
|
+
# Step 3: force kill
|
|
182
|
+
logger.info(f"Killing process {pid}...")
|
|
183
|
+
force_kill_by_pid(pid)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pantoqa_bridge
|
|
3
|
+
Version: 0.4.4
|
|
4
|
+
Summary: Panto QA Bridge
|
|
5
|
+
Author-email: Ritwick Dey <ritwick@getpanto.ai>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: fastapi>=0.110.0
|
|
9
|
+
Requires-Dist: uvicorn[standard]>=0.24.0
|
|
10
|
+
Requires-Dist: Appium-Python-Client>=4.0.0
|
|
11
|
+
Requires-Dist: click>=8.2.1
|
|
12
|
+
Requires-Dist: dotenv>=0.9.9
|
|
13
|
+
Requires-Dist: aiohttp>=3.13.2
|
|
14
|
+
Requires-Dist: rich>=14.2.0
|
|
15
|
+
Requires-Dist: uiautomator2>=3.5.0
|
|
16
|
+
Requires-Dist: adbutils>=2.12.0
|
|
17
|
+
Requires-Dist: appium-utility>=0.3.0
|
|
18
|
+
Requires-Dist: packaging>=25.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
21
|
+
Requires-Dist: build>=1.3.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pre_commit>=4.5.1; extra == "dev"
|
|
23
|
+
Requires-Dist: mypy>=1.19.1; extra == "dev"
|
|
24
|
+
|
|
25
|
+
# PantoAI QA Bridge
|
|
26
|
+
|
|
27
|
+
PantoAI QA Bridge connects the PantoAI dashboard to your local environment so you can execute mobile tests on real devices connected to your machine. Keep the bridge running to unlock local testing features from the dashboard.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pipx install pantoqa_bridge
|
|
33
|
+
pantoqa_bridge
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Prerequisites
|
|
37
|
+
|
|
38
|
+
- Python
|
|
39
|
+
- Node.js and npm
|
|
40
|
+
- Appium with the uiautomator2 driver
|
|
41
|
+
- Android Debug Bridge (adb)
|
|
42
|
+
|
|
43
|
+
## How It Fits Together
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
PantoAI Dashboard -> PantoAI QA Bridge -> Your Devices
|
|
47
|
+
(Browser) (this tool) (USB/Wi‑Fi)
|
|
48
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
pantoqa_bridge/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pantoqa_bridge/__main__.py,sha256=5m0xHXIz0lDmzAOmkxmaRSUUVKA6_CDisaFxfd9Xe08,57
|
|
3
|
+
pantoqa_bridge/cli.py,sha256=1EK3P3XcmFl4Il3-KNBNWddbL11ENR7Fi8zstXj1QIM,2016
|
|
4
|
+
pantoqa_bridge/config.py,sha256=yJ_860ih9BG5e6n70tN_3EkpxCLzjgrpvDx-QFo86Fc,852
|
|
5
|
+
pantoqa_bridge/logger.py,sha256=emPHG2YMPvHifHVbmzh3G2ZHDiX7tiS-iB4mUtSMfMY,899
|
|
6
|
+
pantoqa_bridge/scrcpy-server-v3.3.1,sha256=oPcLIKpJmPv2WMlBGM1sjatqu7Bkejvas0TXC8Hry7g,90788
|
|
7
|
+
pantoqa_bridge/server.py,sha256=tFvnQ9xbJQ14vuDEuJLuisxwdaXAIDhiBou8Yy59PGA,6509
|
|
8
|
+
pantoqa_bridge/lib/adb.py,sha256=_4Vd3VUfklRqHFLhoMQdbzH2ZnS-lhQcCwu-p3a-SLY,2532
|
|
9
|
+
pantoqa_bridge/lib/scrcpy.py,sha256=UNesa2XrV54r56MIm15X67HLka94I6mheNqKh4Bw54A,4023
|
|
10
|
+
pantoqa_bridge/models/code_execute.py,sha256=SsvzgBmP1FweXhRXIirmQ_R2EtFmf6Kox567mrGht9U,87
|
|
11
|
+
pantoqa_bridge/routes/action.py,sha256=pplP4eQI1qK7LIKxXb4I5j6tkcT6MowoIAtz-RMPwQI,3339
|
|
12
|
+
pantoqa_bridge/routes/adb.py,sha256=sDy3UjeB9bNZPn7w07avoLKJ1K3KCk0V5gSX3pxoDdE,369
|
|
13
|
+
pantoqa_bridge/routes/executor.py,sha256=gHRlNcncohPsD-VtaFTTBcu6JHrD4F-LvvETdoisuZI,3503
|
|
14
|
+
pantoqa_bridge/routes/misc.py,sha256=TWz9aErGGdg96K-wnxvCfh2vjmdZmJEy9Wmih0YsbWg,2122
|
|
15
|
+
pantoqa_bridge/routes/screen_mirror.py,sha256=KXpqKC0GzTW5JAoLZ0cKUY1RqZE3bNpk3zVVc5MReRE,2380
|
|
16
|
+
pantoqa_bridge/service/uiautomator_action.py,sha256=UE_5-oyE_z5SjGa6Mtioap5jEM5MR9leXTdRuDl-vDI,2766
|
|
17
|
+
pantoqa_bridge/tasks/executor.py,sha256=PWAS5hP79nYMLz4sNHLEYhF33YmslbcrjnqVr78rs2k,2683
|
|
18
|
+
pantoqa_bridge/utils/misc.py,sha256=8tFn8p2OhLNIdT31w0LczQ8hjkhkpx3fkaeeMgs71sM,3668
|
|
19
|
+
pantoqa_bridge/utils/pkg.py,sha256=difSA1MW6auFUNzOH7Emmukh5mqdsKb-Ow3jdiKKatg,1297
|
|
20
|
+
pantoqa_bridge/utils/process.py,sha256=O71jC8twabT2MfcowAlMTOFeCQRb17MGTc3BFG4E9Bs,4674
|
|
21
|
+
pantoqa_bridge-0.4.4.dist-info/METADATA,sha256=XuyB6iCj-9sH0RyU4h7Xzr2pZylaAkNJJi02z-PLXBE,1350
|
|
22
|
+
pantoqa_bridge-0.4.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
pantoqa_bridge-0.4.4.dist-info/entry_points.txt,sha256=GBytwhIuiZGX_cj_1JQpUteBsP51TSxTk4NxTp4bN_I,58
|
|
24
|
+
pantoqa_bridge-0.4.4.dist-info/top_level.txt,sha256=r03tgM1pQrHwfxF9gkvU94HgR_s8tqDvZVSAzq8qwrA,15
|
|
25
|
+
pantoqa_bridge-0.4.4.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pantoqa_bridge
|