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.
@@ -0,0 +1,5 @@
1
+ __pycache__/
2
+ *.pyc
3
+ target/
4
+ node_modules/
5
+ dist/
@@ -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()