pybroswer 0.3.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.
Files changed (31) hide show
  1. pybroswer-0.3.1/PKG-INFO +117 -0
  2. pybroswer-0.3.1/README.md +95 -0
  3. pybroswer-0.3.1/pyproject.toml +48 -0
  4. pybroswer-0.3.1/setup.cfg +4 -0
  5. pybroswer-0.3.1/src/pybroswer/__init__.py +40 -0
  6. pybroswer-0.3.1/src/pybroswer/__main__.py +23 -0
  7. pybroswer-0.3.1/src/pybroswer/_http.py +89 -0
  8. pybroswer-0.3.1/src/pybroswer/client.py +155 -0
  9. pybroswer-0.3.1/src/pybroswer/dom.py +351 -0
  10. pybroswer-0.3.1/src/pybroswer/engine.py +615 -0
  11. pybroswer-0.3.1/src/pybroswer/exceptions.py +21 -0
  12. pybroswer-0.3.1/src/pybroswer/frames.py +88 -0
  13. pybroswer-0.3.1/src/pybroswer/http_engine.py +579 -0
  14. pybroswer-0.3.1/src/pybroswer/models.py +246 -0
  15. pybroswer-0.3.1/src/pybroswer/py.typed +0 -0
  16. pybroswer-0.3.1/src/pybroswer/server.py +146 -0
  17. pybroswer-0.3.1/src/pybroswer/session.py +281 -0
  18. pybroswer-0.3.1/src/pybroswer/ua.py +110 -0
  19. pybroswer-0.3.1/src/pybroswer.egg-info/PKG-INFO +117 -0
  20. pybroswer-0.3.1/src/pybroswer.egg-info/SOURCES.txt +29 -0
  21. pybroswer-0.3.1/src/pybroswer.egg-info/dependency_links.txt +1 -0
  22. pybroswer-0.3.1/src/pybroswer.egg-info/entry_points.txt +2 -0
  23. pybroswer-0.3.1/src/pybroswer.egg-info/requires.txt +7 -0
  24. pybroswer-0.3.1/src/pybroswer.egg-info/top_level.txt +2 -0
  25. pybroswer-0.3.1/src/pybrowser/__init__.py +23 -0
  26. pybroswer-0.3.1/src/pybrowser/py.typed +0 -0
  27. pybroswer-0.3.1/tests/test_client.py +150 -0
  28. pybroswer-0.3.1/tests/test_engine.py +60 -0
  29. pybroswer-0.3.1/tests/test_http_engine.py +94 -0
  30. pybroswer-0.3.1/tests/test_local_api.py +58 -0
  31. pybroswer-0.3.1/tests/test_models.py +26 -0
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.4
2
+ Name: pybroswer
3
+ Version: 0.3.1
4
+ Summary: Python browser for PyDroid. Works on phone without Chromium or a PC host.
5
+ Author: Hacker78
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/prateeknishad305/Pybrowser
8
+ Keywords: playwright,chromium,browser,pydroid,remote,automation
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: httpx>=0.27
18
+ Provides-Extra: chromium
19
+ Requires-Dist: playwright>=1.40; extra == "chromium"
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=8.0; extra == "dev"
22
+
23
+ # pybroswer
24
+
25
+ Python browser for **PyDroid**. No Chromium, Playwright, or PC host is required on the phone.
26
+
27
+ `Browser.launch()` fetches pages with httpx and parses HTML. The same API also works with remote Chromium.
28
+
29
+ ## Install (PyDroid)
30
+
31
+ ```bash
32
+ pip install httpx
33
+ pip install https://github.com/prateeknishad305/Pybrowser/archive/refs/heads/master.zip
34
+ ```
35
+
36
+ If that fails, install httpx first, then:
37
+
38
+ ```bash
39
+ pip install git+https://github.com/prateeknishad305/Pybrowser.git
40
+ ```
41
+
42
+ Import name:
43
+
44
+ ```python
45
+ from pybroswer import Browser
46
+ ```
47
+
48
+ `from pybrowser import Browser` also works.
49
+
50
+ Check:
51
+
52
+ ```python
53
+ import pybroswer
54
+ print(pybroswer.__version__, pybroswer.__file__)
55
+ ```
56
+
57
+ ## PyDroid usage
58
+
59
+ ```python
60
+ from pybroswer import Browser
61
+
62
+ with Browser.launch() as browser:
63
+ shot = browser.browse("https://example.com")
64
+ print(shot.title, shot.url, shot.text)
65
+
66
+ with browser.create_session(device="mobile") as page:
67
+ page.goto("https://example.com")
68
+ print(page.evaluate("document.title"))
69
+ page.click("a")
70
+ page.type("hello", selector="input[name=q]")
71
+ page.press("Enter")
72
+ print(page.html()[:500])
73
+ ```
74
+
75
+ `Browser()` with no URL also starts the local HTTP engine.
76
+
77
+ ## Optional: PC Chromium
78
+
79
+ If you want real Chromium on a PC:
80
+
81
+ ```bash
82
+ pip install "pybroswer[chromium]"
83
+ playwright install chromium
84
+ python -m pybroswer serve --port 5173
85
+ ```
86
+
87
+ From PyDroid:
88
+
89
+ ```python
90
+ from pybroswer import Browser
91
+ with Browser.connect("http://192.168.1.10:5173") as browser:
92
+ ...
93
+ ```
94
+
95
+ In-process on PC: `Browser.launch(engine="chromium")`.
96
+
97
+ ## Limits (phone HTTP engine)
98
+
99
+ - JS-heavy SPAs are not rendered (you get the HTML the server sent)
100
+ - Screenshot is a 1x1 placeholder PNG
101
+ - `evaluate` only supports simple `document.title` / `querySelector` expressions
102
+ - Iframe contents are not fetched separately
103
+ - Click coordinates (`x, y`) require Chromium
104
+
105
+ Forms, links, type/press/select, cookies, and redirects work.
106
+
107
+ ## API
108
+
109
+ | Method | |
110
+ |--------|--|
111
+ | `Browser.launch()` | local HTTP engine (PyDroid default) |
112
+ | `Browser.launch(engine="chromium")` | Playwright Chromium (PC) |
113
+ | `Browser.connect(url)` | remote REST |
114
+ | `browser.browse(url)` | one-shot open |
115
+ | `page.goto` / `click` / `type` / `press` / `select` | session actions |
116
+ | `page.evaluate("document.title")` | read page |
117
+ | `page.html()` / `page.inspect(selector)` | DOM |
@@ -0,0 +1,95 @@
1
+ # pybroswer
2
+
3
+ Python browser for **PyDroid**. No Chromium, Playwright, or PC host is required on the phone.
4
+
5
+ `Browser.launch()` fetches pages with httpx and parses HTML. The same API also works with remote Chromium.
6
+
7
+ ## Install (PyDroid)
8
+
9
+ ```bash
10
+ pip install httpx
11
+ pip install https://github.com/prateeknishad305/Pybrowser/archive/refs/heads/master.zip
12
+ ```
13
+
14
+ If that fails, install httpx first, then:
15
+
16
+ ```bash
17
+ pip install git+https://github.com/prateeknishad305/Pybrowser.git
18
+ ```
19
+
20
+ Import name:
21
+
22
+ ```python
23
+ from pybroswer import Browser
24
+ ```
25
+
26
+ `from pybrowser import Browser` also works.
27
+
28
+ Check:
29
+
30
+ ```python
31
+ import pybroswer
32
+ print(pybroswer.__version__, pybroswer.__file__)
33
+ ```
34
+
35
+ ## PyDroid usage
36
+
37
+ ```python
38
+ from pybroswer import Browser
39
+
40
+ with Browser.launch() as browser:
41
+ shot = browser.browse("https://example.com")
42
+ print(shot.title, shot.url, shot.text)
43
+
44
+ with browser.create_session(device="mobile") as page:
45
+ page.goto("https://example.com")
46
+ print(page.evaluate("document.title"))
47
+ page.click("a")
48
+ page.type("hello", selector="input[name=q]")
49
+ page.press("Enter")
50
+ print(page.html()[:500])
51
+ ```
52
+
53
+ `Browser()` with no URL also starts the local HTTP engine.
54
+
55
+ ## Optional: PC Chromium
56
+
57
+ If you want real Chromium on a PC:
58
+
59
+ ```bash
60
+ pip install "pybroswer[chromium]"
61
+ playwright install chromium
62
+ python -m pybroswer serve --port 5173
63
+ ```
64
+
65
+ From PyDroid:
66
+
67
+ ```python
68
+ from pybroswer import Browser
69
+ with Browser.connect("http://192.168.1.10:5173") as browser:
70
+ ...
71
+ ```
72
+
73
+ In-process on PC: `Browser.launch(engine="chromium")`.
74
+
75
+ ## Limits (phone HTTP engine)
76
+
77
+ - JS-heavy SPAs are not rendered (you get the HTML the server sent)
78
+ - Screenshot is a 1x1 placeholder PNG
79
+ - `evaluate` only supports simple `document.title` / `querySelector` expressions
80
+ - Iframe contents are not fetched separately
81
+ - Click coordinates (`x, y`) require Chromium
82
+
83
+ Forms, links, type/press/select, cookies, and redirects work.
84
+
85
+ ## API
86
+
87
+ | Method | |
88
+ |--------|--|
89
+ | `Browser.launch()` | local HTTP engine (PyDroid default) |
90
+ | `Browser.launch(engine="chromium")` | Playwright Chromium (PC) |
91
+ | `Browser.connect(url)` | remote REST |
92
+ | `browser.browse(url)` | one-shot open |
93
+ | `page.goto` / `click` / `type` / `press` / `select` | session actions |
94
+ | `page.evaluate("document.title")` | read page |
95
+ | `page.html()` / `page.inspect(selector)` | DOM |
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pybroswer"
7
+ version = "0.3.1"
8
+ description = "Python browser for PyDroid. Works on phone without Chromium or a PC host."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Hacker78" }]
13
+ keywords = ["playwright", "chromium", "browser", "pydroid", "remote", "automation"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = [
23
+ "httpx>=0.27",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ chromium = [
28
+ "playwright>=1.40",
29
+ ]
30
+ dev = [
31
+ "pytest>=8.0",
32
+ ]
33
+
34
+ [project.scripts]
35
+ pybroswer = "pybroswer.__main__:main"
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/prateeknishad305/Pybrowser"
39
+
40
+ [tool.setuptools.package-dir]
41
+ "" = "src"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,40 @@
1
+ """Python browser for PyDroid. HTTP engine in-app; optional remote Chromium."""
2
+
3
+ from pybroswer.client import Browser
4
+ from pybroswer.exceptions import ActionFailed, ApiError, EngineError, PyBroswerError, SessionNotFound
5
+ from pybroswer.models import (
6
+ DevicePreset,
7
+ FrameInfo,
8
+ FrameTarget,
9
+ Health,
10
+ InspectResult,
11
+ LogEntry,
12
+ SessionInfo,
13
+ Snapshot,
14
+ UploadFile,
15
+ Viewport,
16
+ )
17
+ from pybroswer.session import Session
18
+
19
+ __all__ = [
20
+ "ActionFailed",
21
+ "ApiError",
22
+ "Browser",
23
+ "DevicePreset",
24
+ "EngineError",
25
+ "FrameInfo",
26
+ "FrameTarget",
27
+ "Health",
28
+ "InspectResult",
29
+ "LogEntry",
30
+ "PyBroswerError",
31
+ "Session",
32
+ "SessionInfo",
33
+ "SessionNotFound",
34
+ "Snapshot",
35
+ "UploadFile",
36
+ "Viewport",
37
+ "__version__",
38
+ ]
39
+
40
+ __version__ = "0.3.1"
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from pybroswer.server import serve
6
+
7
+
8
+ def main() -> None:
9
+ parser = argparse.ArgumentParser(prog="pybroswer", description="Local Chromium REST for PyDroid")
10
+ sub = parser.add_subparsers(dest="cmd", required=True)
11
+
12
+ serve_p = sub.add_parser("serve", help="Start Chromium on this PC and expose REST for phones")
13
+ serve_p.add_argument("--host", default="0.0.0.0")
14
+ serve_p.add_argument("--port", type=int, default=5173)
15
+ serve_p.add_argument("--headed", action="store_true", help="Show the browser window")
16
+
17
+ args = parser.parse_args()
18
+ if args.cmd == "serve":
19
+ serve(host=args.host, port=args.port, headless=not args.headed)
20
+
21
+
22
+ if __name__ == "__main__":
23
+ main()
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol
4
+
5
+ import httpx
6
+
7
+ from pybroswer.exceptions import ActionFailed, ApiError, SessionNotFound
8
+
9
+
10
+ class Transport(Protocol):
11
+ base_url: str
12
+
13
+ def close(self) -> None: ...
14
+
15
+ def request(
16
+ self,
17
+ method: str,
18
+ path: str,
19
+ *,
20
+ json: dict[str, Any] | None = None,
21
+ params: dict[str, Any] | None = None,
22
+ expect_json: bool = True,
23
+ ) -> Any: ...
24
+
25
+
26
+ class HttpTransport:
27
+ def __init__(
28
+ self,
29
+ base_url: str,
30
+ *,
31
+ timeout: float = 60.0,
32
+ headers: dict[str, str] | None = None,
33
+ client: httpx.Client | None = None,
34
+ ) -> None:
35
+ self.base_url = base_url.rstrip("/")
36
+ self._owns = client is None
37
+ self._client = client or httpx.Client(
38
+ base_url=self.base_url,
39
+ timeout=timeout,
40
+ headers={"Accept": "application/json", **(headers or {})},
41
+ follow_redirects=True,
42
+ )
43
+
44
+ def close(self) -> None:
45
+ if self._owns:
46
+ self._client.close()
47
+
48
+ def request(
49
+ self,
50
+ method: str,
51
+ path: str,
52
+ *,
53
+ json: dict[str, Any] | None = None,
54
+ params: dict[str, Any] | None = None,
55
+ expect_json: bool = True,
56
+ ) -> Any:
57
+ response = self._client.request(method, path, json=json, params=params)
58
+ if response.status_code == 404:
59
+ payload = _safe_json(response)
60
+ message = _error_message(payload, response)
61
+ if "session" in message.lower():
62
+ raise SessionNotFound(message, status_code=404, payload=payload)
63
+ raise ApiError(message, status_code=404, payload=payload)
64
+ if response.status_code >= 400:
65
+ payload = _safe_json(response)
66
+ raise ApiError(_error_message(payload, response), status_code=response.status_code, payload=payload)
67
+ if not expect_json:
68
+ return response.content
69
+ if not response.content:
70
+ return {}
71
+ ctype = response.headers.get("content-type", "")
72
+ if "application/json" not in ctype:
73
+ return response.content
74
+ data = response.json()
75
+ if isinstance(data, dict) and data.get("ok") is False and data.get("error"):
76
+ raise ActionFailed(str(data["error"]), status_code=response.status_code, payload=data)
77
+ return data
78
+
79
+
80
+ def _safe_json(response: httpx.Response) -> dict[str, Any]:
81
+ try:
82
+ data = response.json()
83
+ except Exception:
84
+ return {"error": response.text}
85
+ return data if isinstance(data, dict) else {"error": data}
86
+
87
+
88
+ def _error_message(payload: dict[str, Any], response: httpx.Response) -> str:
89
+ return str(payload.get("error") or response.reason_phrase or f"HTTP {response.status_code}")
@@ -0,0 +1,155 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pybroswer._http import HttpTransport, Transport
6
+ from pybroswer.models import (
7
+ DeviceName,
8
+ DevicePreset,
9
+ Health,
10
+ SessionInfo,
11
+ Snapshot,
12
+ presets_from,
13
+ session_info_from,
14
+ snapshot_from,
15
+ )
16
+ from pybroswer.session import Session
17
+
18
+
19
+ class Browser:
20
+ def __init__(
21
+ self,
22
+ base_url: str | None = None,
23
+ *,
24
+ timeout: float = 60.0,
25
+ headers: dict[str, str] | None = None,
26
+ transport: Transport | None = None,
27
+ engine: Any | None = None,
28
+ ) -> None:
29
+ if transport is not None:
30
+ self._http: Transport = transport
31
+ elif engine is not None:
32
+ from pybroswer.engine import EngineTransport
33
+
34
+ self._http = EngineTransport(engine, owns=False)
35
+ elif base_url:
36
+ self._http = HttpTransport(base_url, timeout=timeout, headers=headers)
37
+ else:
38
+ self._http = _http_engine_transport(max_sessions=5, idle_ms=10 * 60 * 1000, timeout=timeout)
39
+
40
+ @classmethod
41
+ def launch(
42
+ cls,
43
+ *,
44
+ engine: str = "http",
45
+ headless: bool = True,
46
+ max_sessions: int = 5,
47
+ idle_ms: int = 10 * 60 * 1000,
48
+ timeout: float = 30.0,
49
+ executable_path: str | None = None,
50
+ channel: str | None = None,
51
+ ) -> Browser:
52
+ from pybroswer.engine import EngineTransport
53
+
54
+ if engine in {"chromium", "playwright"}:
55
+ from pybroswer.engine import ChromiumEngine
56
+
57
+ chromium = ChromiumEngine(
58
+ headless=headless,
59
+ max_sessions=max_sessions,
60
+ idle_ms=idle_ms,
61
+ executable_path=executable_path,
62
+ channel=channel,
63
+ )
64
+ chromium.start()
65
+ return cls(transport=EngineTransport(chromium, owns=True))
66
+ return cls(transport=_http_engine_transport(max_sessions=max_sessions, idle_ms=idle_ms, timeout=timeout))
67
+
68
+ @classmethod
69
+ def connect(
70
+ cls,
71
+ url: str = "http://127.0.0.1:5173",
72
+ *,
73
+ timeout: float = 60.0,
74
+ headers: dict[str, str] | None = None,
75
+ ) -> Browser:
76
+ return cls(url, timeout=timeout, headers=headers)
77
+
78
+ def close(self) -> None:
79
+ self._http.close()
80
+
81
+ def health(self) -> Health:
82
+ data = self._http.request("GET", "/api/v1/health")
83
+ return Health(
84
+ ok=bool(data.get("ok")),
85
+ service=data.get("service"),
86
+ engine=data.get("engine"),
87
+ sessions=int(data.get("sessions") or 0),
88
+ max_sessions=int(data.get("maxSessions") or 0),
89
+ error=data.get("error"),
90
+ )
91
+
92
+ def ua_presets(self) -> list[DevicePreset]:
93
+ data = self._http.request("GET", "/api/v1/ua")
94
+ return presets_from(data.get("presets"))
95
+
96
+ def sessions(self) -> list[SessionInfo]:
97
+ data = self._http.request("GET", "/api/v1/sessions")
98
+ return [session_info_from(item) for item in data.get("sessions") or []]
99
+
100
+ def create_session(
101
+ self,
102
+ *,
103
+ device: DeviceName | str = "mobile",
104
+ url: str | None = None,
105
+ user_agent: str | None = None,
106
+ width: int | None = None,
107
+ height: int | None = None,
108
+ ) -> Session:
109
+ body: dict[str, Any] = {"device": device}
110
+ if url:
111
+ body["url"] = url
112
+ if user_agent:
113
+ body["userAgent"] = user_agent
114
+ if width is not None:
115
+ body["width"] = width
116
+ if height is not None:
117
+ body["height"] = height
118
+ data = self._http.request("POST", "/api/v1/sessions", json=body)
119
+ return Session(self._http, str(data["id"]))
120
+
121
+ def session(self, session_id: str) -> Session:
122
+ return Session(self._http, session_id)
123
+
124
+ def browse(
125
+ self,
126
+ url: str,
127
+ *,
128
+ device: DeviceName | str = "desktop",
129
+ user_agent: str | None = None,
130
+ width: int | None = None,
131
+ height: int | None = None,
132
+ ) -> Snapshot:
133
+ body: dict[str, Any] = {"url": url, "device": device}
134
+ if user_agent:
135
+ body["userAgent"] = user_agent
136
+ if width is not None:
137
+ body["width"] = width
138
+ if height is not None:
139
+ body["height"] = height
140
+ return snapshot_from(self._http.request("POST", "/api/v1/browse", json=body))
141
+
142
+ def __enter__(self) -> Browser:
143
+ return self
144
+
145
+ def __exit__(self, *args: object) -> None:
146
+ self.close()
147
+
148
+
149
+ def _http_engine_transport(*, max_sessions: int, idle_ms: int, timeout: float) -> Any:
150
+ from pybroswer.engine import EngineTransport
151
+ from pybroswer.http_engine import HttpEngine
152
+
153
+ engine = HttpEngine(max_sessions=max_sessions, idle_ms=idle_ms, timeout=timeout)
154
+ engine.start()
155
+ return EngineTransport(engine, owns=True)