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.
- pybroswer-0.3.1/PKG-INFO +117 -0
- pybroswer-0.3.1/README.md +95 -0
- pybroswer-0.3.1/pyproject.toml +48 -0
- pybroswer-0.3.1/setup.cfg +4 -0
- pybroswer-0.3.1/src/pybroswer/__init__.py +40 -0
- pybroswer-0.3.1/src/pybroswer/__main__.py +23 -0
- pybroswer-0.3.1/src/pybroswer/_http.py +89 -0
- pybroswer-0.3.1/src/pybroswer/client.py +155 -0
- pybroswer-0.3.1/src/pybroswer/dom.py +351 -0
- pybroswer-0.3.1/src/pybroswer/engine.py +615 -0
- pybroswer-0.3.1/src/pybroswer/exceptions.py +21 -0
- pybroswer-0.3.1/src/pybroswer/frames.py +88 -0
- pybroswer-0.3.1/src/pybroswer/http_engine.py +579 -0
- pybroswer-0.3.1/src/pybroswer/models.py +246 -0
- pybroswer-0.3.1/src/pybroswer/py.typed +0 -0
- pybroswer-0.3.1/src/pybroswer/server.py +146 -0
- pybroswer-0.3.1/src/pybroswer/session.py +281 -0
- pybroswer-0.3.1/src/pybroswer/ua.py +110 -0
- pybroswer-0.3.1/src/pybroswer.egg-info/PKG-INFO +117 -0
- pybroswer-0.3.1/src/pybroswer.egg-info/SOURCES.txt +29 -0
- pybroswer-0.3.1/src/pybroswer.egg-info/dependency_links.txt +1 -0
- pybroswer-0.3.1/src/pybroswer.egg-info/entry_points.txt +2 -0
- pybroswer-0.3.1/src/pybroswer.egg-info/requires.txt +7 -0
- pybroswer-0.3.1/src/pybroswer.egg-info/top_level.txt +2 -0
- pybroswer-0.3.1/src/pybrowser/__init__.py +23 -0
- pybroswer-0.3.1/src/pybrowser/py.typed +0 -0
- pybroswer-0.3.1/tests/test_client.py +150 -0
- pybroswer-0.3.1/tests/test_engine.py +60 -0
- pybroswer-0.3.1/tests/test_http_engine.py +94 -0
- pybroswer-0.3.1/tests/test_local_api.py +58 -0
- pybroswer-0.3.1/tests/test_models.py +26 -0
pybroswer-0.3.1/PKG-INFO
ADDED
|
@@ -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,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)
|