solari-browser 0.1.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.
- solari_browser-0.1.1/.gitignore +5 -0
- solari_browser-0.1.1/PKG-INFO +66 -0
- solari_browser-0.1.1/README.md +50 -0
- solari_browser-0.1.1/pyproject.toml +45 -0
- solari_browser-0.1.1/solari_browser/__init__.py +60 -0
- solari_browser-0.1.1/solari_browser/browser_session.py +94 -0
- solari_browser-0.1.1/solari_browser/client.py +521 -0
- solari_browser-0.1.1/solari_browser/errors.py +68 -0
- solari_browser-0.1.1/solari_browser/types.py +141 -0
- solari_browser-0.1.1/tests/test_client.py +388 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: solari-browser
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python SDK for Solari Browser — managed, stealthy remote Chromium over the Playwright wire protocol / raw CDP.
|
|
5
|
+
Project-URL: Homepage, https://getsolari.com
|
|
6
|
+
Author: Solari
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
Keywords: automation,browser,cdp,playwright,scraping,solari,stealth
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Requires-Dist: httpx>=0.24
|
|
11
|
+
Requires-Dist: patchright<1.60,>=1.59
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
14
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# solari-browser (Python)
|
|
18
|
+
|
|
19
|
+
Python SDK for **Solari Browser** — managed, stealth-hardened remote Chromium as
|
|
20
|
+
an API, driven over the Playwright wire protocol (or raw CDP). A faithful port of
|
|
21
|
+
the TypeScript `@solarisdk/browser` package; class, method, and field names match
|
|
22
|
+
it one-for-one (snake_case where TS uses camelCase).
|
|
23
|
+
|
|
24
|
+
The browser runs remotely on Solari's pool — `pip install` pulls the driver only;
|
|
25
|
+
you do **not** run `patchright install chromium` locally.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install solari-browser
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quickstart
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio, os
|
|
37
|
+
from solari_browser import Solari
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
solari = Solari(api_key=os.environ["SOLARI_API_KEY"])
|
|
41
|
+
browser = await solari.launch(stealth=True) # managed, stealth-hardened Chromium
|
|
42
|
+
page = await browser.new_page()
|
|
43
|
+
await page.goto("https://example.com")
|
|
44
|
+
print(await page.title())
|
|
45
|
+
await browser.close()
|
|
46
|
+
|
|
47
|
+
asyncio.run(main())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Sessions
|
|
51
|
+
|
|
52
|
+
Drive a session yourself instead of `launch()`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
solari = Solari(api_key=os.environ["SOLARI_API_KEY"])
|
|
56
|
+
session = await solari.sessions.create(stealth=True, proxy="smart")
|
|
57
|
+
# ... connect over the session's CDP/WS endpoint ...
|
|
58
|
+
await solari.close()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`Solari(...)` accepts `api_key`, `region` (default `us-west`), and `base_url`
|
|
62
|
+
(default `https://api.getsolari.com`).
|
|
63
|
+
|
|
64
|
+
## Docs
|
|
65
|
+
|
|
66
|
+
Full reference and language guides: <https://getsolari.com>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# solari-browser (Python)
|
|
2
|
+
|
|
3
|
+
Python SDK for **Solari Browser** — managed, stealth-hardened remote Chromium as
|
|
4
|
+
an API, driven over the Playwright wire protocol (or raw CDP). A faithful port of
|
|
5
|
+
the TypeScript `@solarisdk/browser` package; class, method, and field names match
|
|
6
|
+
it one-for-one (snake_case where TS uses camelCase).
|
|
7
|
+
|
|
8
|
+
The browser runs remotely on Solari's pool — `pip install` pulls the driver only;
|
|
9
|
+
you do **not** run `patchright install chromium` locally.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pip install solari-browser
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quickstart
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import asyncio, os
|
|
21
|
+
from solari_browser import Solari
|
|
22
|
+
|
|
23
|
+
async def main():
|
|
24
|
+
solari = Solari(api_key=os.environ["SOLARI_API_KEY"])
|
|
25
|
+
browser = await solari.launch(stealth=True) # managed, stealth-hardened Chromium
|
|
26
|
+
page = await browser.new_page()
|
|
27
|
+
await page.goto("https://example.com")
|
|
28
|
+
print(await page.title())
|
|
29
|
+
await browser.close()
|
|
30
|
+
|
|
31
|
+
asyncio.run(main())
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Sessions
|
|
35
|
+
|
|
36
|
+
Drive a session yourself instead of `launch()`:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
solari = Solari(api_key=os.environ["SOLARI_API_KEY"])
|
|
40
|
+
session = await solari.sessions.create(stealth=True, proxy="smart")
|
|
41
|
+
# ... connect over the session's CDP/WS endpoint ...
|
|
42
|
+
await solari.close()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`Solari(...)` accepts `api_key`, `region` (default `us-west`), and `base_url`
|
|
46
|
+
(default `https://api.getsolari.com`).
|
|
47
|
+
|
|
48
|
+
## Docs
|
|
49
|
+
|
|
50
|
+
Full reference and language guides: <https://getsolari.com>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "solari-browser"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Python SDK for Solari Browser — managed, stealthy remote Chromium over the Playwright wire protocol / raw CDP."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Solari" }]
|
|
13
|
+
keywords = ["solari", "browser", "playwright", "cdp", "stealth", "automation", "scraping"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"httpx>=0.24",
|
|
16
|
+
# patchright is Playwright-with-stealth-patches. The POOL runs patchright and
|
|
17
|
+
# the Playwright wire protocol embeds the client version in the User-Agent;
|
|
18
|
+
# the server rejects a mismatch with HTTP 428.
|
|
19
|
+
#
|
|
20
|
+
# The gate compares MAJOR.MINOR ONLY — patchright-core's
|
|
21
|
+
# lib/remote/playwrightServer.js does:
|
|
22
|
+
# received = clientVersion.split(".").slice(0, 2).join(".")
|
|
23
|
+
# expected = getPlaywrightVersion(/* majorMinorOnly */ true)
|
|
24
|
+
# so any 1.59.x client talks to the pool's 1.59.3 (node) server. This range is
|
|
25
|
+
# therefore pinned to the pool's major.minor, NOT to an exact version — PyPI
|
|
26
|
+
# has no 1.59.3 (the node and python patchright release trains differ; PyPI's
|
|
27
|
+
# 1.59 line tops out at 1.59.1).
|
|
28
|
+
#
|
|
29
|
+
# If the pool's `patchright` pin moves to 1.60/1.61, this MUST move with it or
|
|
30
|
+
# every launch() 428s. Asserted by scripts/check-sdk-version-pin.mjs.
|
|
31
|
+
"patchright>=1.59,<1.60",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = ["pytest>=7", "ruff>=0.1"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://getsolari.com"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["solari_browser"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 100
|
|
45
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Solari Browser — Python SDK.
|
|
2
|
+
|
|
3
|
+
Managed, stealthy remote Chromium over the Playwright wire protocol / raw CDP.
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from solari_browser import Solari
|
|
7
|
+
|
|
8
|
+
async def main():
|
|
9
|
+
async with Solari(api_key="slr_live_...") as solari:
|
|
10
|
+
async with await solari.launch(stealth=True) as browser:
|
|
11
|
+
page = await browser.new_page()
|
|
12
|
+
await page.goto("https://example.com")
|
|
13
|
+
print(await page.title())
|
|
14
|
+
|
|
15
|
+
asyncio.run(main())
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .browser_session import BrowserSession
|
|
19
|
+
from .client import Solari, derive_cdp_from_ws
|
|
20
|
+
from .errors import (
|
|
21
|
+
BROWSER_UNHEALTHY,
|
|
22
|
+
CONCURRENCY_LIMIT_EXCEEDED,
|
|
23
|
+
FEATURE_REQUIRES_PLAN,
|
|
24
|
+
PLAN_LIMIT_EXCEEDED,
|
|
25
|
+
SolariError,
|
|
26
|
+
)
|
|
27
|
+
from .types import (
|
|
28
|
+
DEFAULT_REGION,
|
|
29
|
+
REGION_URLS,
|
|
30
|
+
UNSET,
|
|
31
|
+
Profile,
|
|
32
|
+
ProxyRequest,
|
|
33
|
+
ReplayUrl,
|
|
34
|
+
ResolvedProxyConfig,
|
|
35
|
+
SaveResult,
|
|
36
|
+
Session,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__version__ = "0.1.0"
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"Solari",
|
|
43
|
+
"BrowserSession",
|
|
44
|
+
"SolariError",
|
|
45
|
+
"Session",
|
|
46
|
+
"Profile",
|
|
47
|
+
"ProxyRequest",
|
|
48
|
+
"ResolvedProxyConfig",
|
|
49
|
+
"ReplayUrl",
|
|
50
|
+
"SaveResult",
|
|
51
|
+
"UNSET",
|
|
52
|
+
"REGION_URLS",
|
|
53
|
+
"DEFAULT_REGION",
|
|
54
|
+
"derive_cdp_from_ws",
|
|
55
|
+
"FEATURE_REQUIRES_PLAN",
|
|
56
|
+
"CONCURRENCY_LIMIT_EXCEEDED",
|
|
57
|
+
"PLAN_LIMIT_EXCEEDED",
|
|
58
|
+
"BROWSER_UNHEALTHY",
|
|
59
|
+
"__version__",
|
|
60
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""BrowserSession — a connected browser + its Solari session. Mirrors sdk/src/browser-session.ts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, List, Optional
|
|
6
|
+
|
|
7
|
+
from .types import ResolvedProxyConfig, Session
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
10
|
+
from .client import Solari
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BrowserSession:
|
|
14
|
+
"""Wraps a patchright `Browser` and the Solari session backing it.
|
|
15
|
+
|
|
16
|
+
`close()` closes the browser AND releases the session — a browser close alone
|
|
17
|
+
would leave the slot held until its plan deadline.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, client: "Solari", session: Session, browser: Any) -> None:
|
|
21
|
+
self._client = client
|
|
22
|
+
self._session = session
|
|
23
|
+
self._browser = browser
|
|
24
|
+
self._closed = False
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def id(self) -> str:
|
|
28
|
+
return self._session.id
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def expires_at(self) -> str:
|
|
32
|
+
"""ISO 8601 UTC deadline; the session auto-releases at this point."""
|
|
33
|
+
return self._session.expires_at
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def proxy(self) -> Optional[ResolvedProxyConfig]:
|
|
37
|
+
return self._session.proxy
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def ws_endpoint(self) -> str:
|
|
41
|
+
"""Upstream Playwright wire-protocol endpoint."""
|
|
42
|
+
return self._session.ws_endpoint
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def cdp_endpoint(self) -> str:
|
|
46
|
+
"""Upstream raw-CDP endpoint.
|
|
47
|
+
|
|
48
|
+
NOTE: driving the browser over raw CDP bypasses the pool's Playwright-path
|
|
49
|
+
input humanization (see CLAUDE.md) — relevant if you care about stealth.
|
|
50
|
+
"""
|
|
51
|
+
return self._session.cdp_endpoint
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def session(self) -> Session:
|
|
55
|
+
return self._session
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def raw(self) -> Any:
|
|
59
|
+
"""The underlying patchright `Browser`."""
|
|
60
|
+
return self._browser
|
|
61
|
+
|
|
62
|
+
def is_connected(self) -> bool:
|
|
63
|
+
return bool(self._browser.is_connected())
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def version(self) -> str:
|
|
67
|
+
return str(self._browser.version)
|
|
68
|
+
|
|
69
|
+
def contexts(self) -> List[Any]:
|
|
70
|
+
"""Existing browser contexts. The default context is `contexts()[0]`."""
|
|
71
|
+
return list(self._browser.contexts)
|
|
72
|
+
|
|
73
|
+
async def new_context(self, **kwargs: Any) -> Any:
|
|
74
|
+
return await self._browser.new_context(**kwargs)
|
|
75
|
+
|
|
76
|
+
async def new_page(self) -> Any:
|
|
77
|
+
return await self._browser.new_page()
|
|
78
|
+
|
|
79
|
+
async def close(self) -> None:
|
|
80
|
+
"""Close the browser and release the session. Idempotent."""
|
|
81
|
+
if self._closed:
|
|
82
|
+
return
|
|
83
|
+
self._closed = True
|
|
84
|
+
try:
|
|
85
|
+
await self._browser.close()
|
|
86
|
+
except Exception: # noqa: BLE001 - releasing the slot matters more
|
|
87
|
+
pass
|
|
88
|
+
await self._client.sessions.release_and_wait(self._session.id)
|
|
89
|
+
|
|
90
|
+
async def __aenter__(self) -> "BrowserSession":
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
async def __aexit__(self, *_exc: Any) -> None:
|
|
94
|
+
await self.close()
|