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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pantoqa_bridge = pantoqa_bridge.cli:cli
@@ -0,0 +1 @@
1
+ pantoqa_bridge