hyperbrowser-lite 1.0.0__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.
- hyperbrowser_lite-1.0.0/.gitignore +1 -0
- hyperbrowser_lite-1.0.0/PKG-INFO +14 -0
- hyperbrowser_lite-1.0.0/README.md +234 -0
- hyperbrowser_lite-1.0.0/pyproject.toml +32 -0
- hyperbrowser_lite-1.0.0/src/hyperbrowser_lite/__init__.py +182 -0
- hyperbrowser_lite-1.0.0/src/hyperbrowser_lite/_http.py +209 -0
- hyperbrowser_lite-1.0.0/src/hyperbrowser_lite/client.py +128 -0
- hyperbrowser_lite-1.0.0/src/hyperbrowser_lite/exceptions.py +55 -0
- hyperbrowser_lite-1.0.0/src/hyperbrowser_lite/models.py +126 -0
- hyperbrowser_lite-1.0.0/src/hyperbrowser_lite/sessions.py +298 -0
- hyperbrowser_lite-1.0.0/tests/__init__.py +0 -0
- hyperbrowser_lite-1.0.0/tests/conftest.py +98 -0
- hyperbrowser_lite-1.0.0/tests/test_client.py +88 -0
- hyperbrowser_lite-1.0.0/tests/test_exceptions.py +61 -0
- hyperbrowser_lite-1.0.0/tests/test_http.py +226 -0
- hyperbrowser_lite-1.0.0/tests/test_models.py +137 -0
- hyperbrowser_lite-1.0.0/tests/test_sessions.py +275 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/.pypirc
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hyperbrowser-lite
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Minimal cloud browser SDK for Hyperbrowser — session lifecycle management via CDP.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ayanami-browser-pilot/hyperbrowser-lite
|
|
6
|
+
Project-URL: Repository, https://github.com/ayanami-browser-pilot/hyperbrowser-lite
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: httpx>=0.27.0
|
|
9
|
+
Requires-Dist: pydantic<3,>=2.5.2
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# hyperbrowser-lite
|
|
2
|
+
|
|
3
|
+
Minimal Python SDK for [Hyperbrowser](https://www.hyperbrowser.ai/) cloud browser sessions. Only does one thing: **create a cloud browser, return a CDP URL, clean up when done**.
|
|
4
|
+
|
|
5
|
+
Part of the [cloud-browser-sdk-spec](https://github.com/ayanami-browser-pilot) unified interface for cloud browser providers.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install hyperbrowser-lite
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from hyperbrowser_lite import HyperbrowserCloud
|
|
17
|
+
|
|
18
|
+
client = HyperbrowserCloud(api_key="hb-...") # or set HYPERBROWSER_API_KEY env var
|
|
19
|
+
|
|
20
|
+
# Create a cloud browser session
|
|
21
|
+
session = client.sessions.create()
|
|
22
|
+
print(session.cdp_url) # wss://connect-us-west-2.hyperbrowser.ai/?token=...
|
|
23
|
+
print(session.session_id) # UUID
|
|
24
|
+
|
|
25
|
+
# Connect with Playwright
|
|
26
|
+
# Hyperbrowser CDP uses token-based URLs — no auth headers needed
|
|
27
|
+
from playwright.sync_api import sync_playwright
|
|
28
|
+
|
|
29
|
+
with sync_playwright() as pw:
|
|
30
|
+
browser = pw.chromium.connect_over_cdp(session.cdp_url)
|
|
31
|
+
page = browser.contexts[0].new_page()
|
|
32
|
+
page.goto("https://example.com")
|
|
33
|
+
print(page.title())
|
|
34
|
+
|
|
35
|
+
# Clean up
|
|
36
|
+
client.sessions.delete(session.session_id)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Context Manager (auto-cleanup)
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
with client.sessions.create() as session:
|
|
43
|
+
# use session.cdp_url ...
|
|
44
|
+
pass
|
|
45
|
+
# session automatically deleted on exit
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Async
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from hyperbrowser_lite import AsyncHyperbrowserCloud
|
|
52
|
+
|
|
53
|
+
async with AsyncHyperbrowserCloud(api_key="hb-...") as client:
|
|
54
|
+
session = await client.sessions.create()
|
|
55
|
+
print(session.cdp_url)
|
|
56
|
+
await client.sessions.delete(session.session_id)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
### Client
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
HyperbrowserCloud(api_key=None, *, base_url=None, timeout=60.0, max_retries=2)
|
|
65
|
+
AsyncHyperbrowserCloud(api_key=None, *, base_url=None, timeout=60.0, max_retries=2)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `api_key` — defaults to `HYPERBROWSER_API_KEY` env var
|
|
69
|
+
- `base_url` — defaults to `https://api.hyperbrowser.ai`
|
|
70
|
+
|
|
71
|
+
### Sessions (`client.sessions`)
|
|
72
|
+
|
|
73
|
+
| Method | Description |
|
|
74
|
+
|--------|-------------|
|
|
75
|
+
| `create(**kwargs) -> SessionInfo` | Create cloud browser, returns CDP URL |
|
|
76
|
+
| `get(session_id) -> SessionInfo` | Get session status |
|
|
77
|
+
| `list(**filters) -> list[SessionInfo]` | List sessions |
|
|
78
|
+
| `delete(session_id) -> None` | Stop session (idempotent) |
|
|
79
|
+
|
|
80
|
+
### `create()` Parameters
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
session = client.sessions.create(
|
|
84
|
+
# Browser mode — controls anti-detection
|
|
85
|
+
browser_mode="stealth", # or "ultra_stealth" (enterprise), default "normal"
|
|
86
|
+
|
|
87
|
+
# Proxy — managed or custom
|
|
88
|
+
proxy=ManagedProxyConfig(country="US"),
|
|
89
|
+
# proxy=ProxyConfig(server="http://proxy:8080", username="u", password="p"),
|
|
90
|
+
|
|
91
|
+
# Recording — ON by default, explicitly enable/disable
|
|
92
|
+
recording=RecordingConfig(enabled=True),
|
|
93
|
+
|
|
94
|
+
# Fingerprint — locale and viewport
|
|
95
|
+
fingerprint=FingerprintConfig(locale="en-US", viewport=ViewportConfig(1920, 1080)),
|
|
96
|
+
|
|
97
|
+
# Vendor params
|
|
98
|
+
solve_captchas=True, # paid plan required
|
|
99
|
+
adblock=True, # block ads, faster page loads
|
|
100
|
+
trackers=True, # block tracking scripts
|
|
101
|
+
annoyances=True, # block cookie banners, newsletter popups
|
|
102
|
+
timeout_minutes=30,
|
|
103
|
+
extension_ids=["ext-123"],
|
|
104
|
+
region="us-central",
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### SessionInfo Fields
|
|
109
|
+
|
|
110
|
+
| Field | Type | Description |
|
|
111
|
+
|-------|------|-------------|
|
|
112
|
+
| `session_id` | `str` | Unique identifier (UUID) |
|
|
113
|
+
| `cdp_url` | `str \| None` | CDP WebSocket URL (`wss://connect-{region}.hyperbrowser.ai/?token=...`) |
|
|
114
|
+
| `status` | `str` | `"active"`, `"closed"`, or `"error"` |
|
|
115
|
+
| `created_at` | `datetime \| None` | Creation timestamp |
|
|
116
|
+
| `inspect_url` | `str \| None` | Live view URL — open in browser to watch session in real-time |
|
|
117
|
+
| `metadata` | `dict` | Vendor-specific data (see below) |
|
|
118
|
+
|
|
119
|
+
#### Metadata Contents
|
|
120
|
+
|
|
121
|
+
The `metadata` dict contains Hyperbrowser-specific fields from the API response:
|
|
122
|
+
|
|
123
|
+
| Key | Description |
|
|
124
|
+
|-----|-------------|
|
|
125
|
+
| `teamId` | Team identifier |
|
|
126
|
+
| `token` | JWT auth token (embedded in `cdp_url`) |
|
|
127
|
+
| `sessionUrl` | Dashboard URL for this session |
|
|
128
|
+
| `launchState` | Full launch configuration (see Feature Details below) |
|
|
129
|
+
| `proxyDataConsumed` | Proxy bandwidth consumed |
|
|
130
|
+
| `liveDomain` | WebSocket server domain |
|
|
131
|
+
| `webdriverEndpoint` | WebDriver protocol endpoint |
|
|
132
|
+
| `computerActionEndpoint` | Computer action API endpoint |
|
|
133
|
+
|
|
134
|
+
### CDP WebSocket Authentication
|
|
135
|
+
|
|
136
|
+
Hyperbrowser's CDP WebSocket endpoint uses **token-based URLs** — the `wsEndpoint` contains an embedded JWT token. **No additional headers are needed**:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Playwright — just connect directly
|
|
140
|
+
browser = pw.chromium.connect_over_cdp(session.cdp_url)
|
|
141
|
+
|
|
142
|
+
# Works with any CDP client (Playwright, Puppeteer, Selenium, raw websockets)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This is different from providers like Skyvern which require `x-api-key` headers on the WebSocket handshake.
|
|
146
|
+
|
|
147
|
+
## Feature Details
|
|
148
|
+
|
|
149
|
+
All features are verified against the live Hyperbrowser API. The `launchState` in `metadata` confirms which features are active.
|
|
150
|
+
|
|
151
|
+
### Stealth Mode (`browser_mode="stealth"`)
|
|
152
|
+
|
|
153
|
+
Hides browser automation fingerprints so anti-bot systems cannot easily detect the session:
|
|
154
|
+
- `navigator.webdriver` returns `false`
|
|
155
|
+
- Chrome DevTools Protocol detection signatures are masked
|
|
156
|
+
- Other automation hints are suppressed
|
|
157
|
+
|
|
158
|
+
**Verified**: `launchState.useStealth` changes from `false` to `true`. Available on free plan.
|
|
159
|
+
|
|
160
|
+
### Ultra Stealth Mode (`browser_mode="ultra_stealth"`)
|
|
161
|
+
|
|
162
|
+
Enhanced stealth with additional anti-detection measures beyond standard stealth.
|
|
163
|
+
|
|
164
|
+
**Requires enterprise plan** — returns HTTP 402 on free plan.
|
|
165
|
+
|
|
166
|
+
### Recording (`RecordingConfig`)
|
|
167
|
+
|
|
168
|
+
Records DOM mutations during the session (similar to [rrweb](https://github.com/rrweb-io/rrweb)). After the session ends, the recording can be retrieved via the Hyperbrowser dashboard or API for playback.
|
|
169
|
+
|
|
170
|
+
**Important**: Recording is **enabled by default** (`enableWebRecording: true`). Passing `RecordingConfig(enabled=True)` is redundant unless you previously disabled it. Video recording (`enableVideoWebRecording`) is a separate feature and is off by default.
|
|
171
|
+
|
|
172
|
+
### Ad Blocking (`adblock=True`)
|
|
173
|
+
|
|
174
|
+
Built-in ad blocker that removes ads (banners, popups, video ads) from pages:
|
|
175
|
+
- Faster page loads — fewer network requests
|
|
176
|
+
- Cleaner pages — no ad popups interfering with automation
|
|
177
|
+
- Less bandwidth — reduces `proxyDataConsumed`
|
|
178
|
+
|
|
179
|
+
Related options: `trackers=True` (blocks tracking scripts), `annoyances=True` (blocks cookie banners, newsletter popups).
|
|
180
|
+
|
|
181
|
+
**Verified**: `launchState.adblock` changes to `true`. Available on free plan.
|
|
182
|
+
|
|
183
|
+
### Captcha Solving (`solve_captchas=True`)
|
|
184
|
+
|
|
185
|
+
Automatically solves CAPTCHAs encountered during browsing.
|
|
186
|
+
|
|
187
|
+
**Requires paid plan** — returns HTTP 402 on free plan.
|
|
188
|
+
|
|
189
|
+
### Feature Support Matrix
|
|
190
|
+
|
|
191
|
+
| Feature | Free Plan | Paid Plan | Enterprise | Usage |
|
|
192
|
+
|---------|-----------|-----------|------------|-------|
|
|
193
|
+
| Custom proxy server | ? | ? | ? | `ProxyConfig(server=...)` |
|
|
194
|
+
| Managed proxy (200+ countries) | ? | ? | ? | `ManagedProxyConfig(country="US")` |
|
|
195
|
+
| Stealth mode | Yes | Yes | Yes | `browser_mode="stealth"` |
|
|
196
|
+
| Ultra stealth mode | No | No | Yes | `browser_mode="ultra_stealth"` |
|
|
197
|
+
| Browser fingerprint | Yes | Yes | Yes | `FingerprintConfig(locale=..., viewport=...)` |
|
|
198
|
+
| Session recording (DOM) | Yes (default ON) | Yes | Yes | `RecordingConfig(enabled=True)` |
|
|
199
|
+
| Video recording | ? | ? | ? | Vendor param: `enable_video_web_recording=True` |
|
|
200
|
+
| Captcha solving | No | Yes | Yes | `solve_captchas=True` |
|
|
201
|
+
| Ad blocking | Yes | Yes | Yes | `adblock=True` |
|
|
202
|
+
| Tracker blocking | Yes | Yes | Yes | `trackers=True` |
|
|
203
|
+
| Annoyance blocking | Yes | Yes | Yes | `annoyances=True` |
|
|
204
|
+
| Extensions | Yes | Yes | Yes | `extension_ids=[...]` |
|
|
205
|
+
| Region selection | Yes | Yes | Yes | `region="us-central"` |
|
|
206
|
+
| Browser profiles | Yes | Yes | Yes | `profile={"id": "..."}` |
|
|
207
|
+
| Context persistence | N/A | N/A | N/A | Use profiles instead |
|
|
208
|
+
|
|
209
|
+
> `?` = not tested (requires proxy configuration / paid features)
|
|
210
|
+
|
|
211
|
+
### Exceptions
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
CloudBrowserError
|
|
215
|
+
├── AuthenticationError # 401/403 — invalid or expired API key
|
|
216
|
+
├── QuotaExceededError # 429 — rate limit (has .retry_after)
|
|
217
|
+
├── SessionNotFoundError # 404 — session not found
|
|
218
|
+
├── ProviderError # 5xx — server error (has .status_code, .request_id)
|
|
219
|
+
├── TimeoutError # Operation timeout
|
|
220
|
+
└── NetworkError # Connection failure
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Note: Plan-restricted features return **HTTP 402** (mapped to `CloudBrowserError`), not 403.
|
|
224
|
+
|
|
225
|
+
### Backward Compatibility
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from hyperbrowser_lite import Hyperbrowser # alias for HyperbrowserCloud
|
|
229
|
+
from hyperbrowser_lite import AsyncHyperbrowser # alias for AsyncHyperbrowserCloud
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hyperbrowser-lite"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Minimal cloud browser SDK for Hyperbrowser — session lifecycle management via CDP."
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx>=0.27.0",
|
|
12
|
+
"pydantic>=2.5.2,<3",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest>=8.0",
|
|
18
|
+
"pytest-asyncio>=0.24",
|
|
19
|
+
"pytest-cov>=5.0",
|
|
20
|
+
"respx>=0.21",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/ayanami-browser-pilot/hyperbrowser-lite"
|
|
25
|
+
Repository = "https://github.com/ayanami-browser-pilot/hyperbrowser-lite"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/hyperbrowser_lite"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
asyncio_mode = "auto"
|
|
32
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Hyperbrowser Cloud Browser SDK — minimal interface for browser session lifecycle.
|
|
2
|
+
|
|
3
|
+
Quick Start
|
|
4
|
+
-----------
|
|
5
|
+
::
|
|
6
|
+
|
|
7
|
+
from hyperbrowser_lite import HyperbrowserCloud
|
|
8
|
+
|
|
9
|
+
client = HyperbrowserCloud(api_key="hb-...") # or set HYPERBROWSER_API_KEY env var
|
|
10
|
+
|
|
11
|
+
# Create a cloud browser session
|
|
12
|
+
session = client.sessions.create()
|
|
13
|
+
print(session.cdp_url) # wss://...
|
|
14
|
+
print(session.session_id)
|
|
15
|
+
|
|
16
|
+
# Use with Playwright (or any CDP client)
|
|
17
|
+
# NOTE: Hyperbrowser CDP uses token-based wsEndpoint URLs — no auth headers needed.
|
|
18
|
+
from playwright.sync_api import sync_playwright
|
|
19
|
+
with sync_playwright() as pw:
|
|
20
|
+
browser = pw.chromium.connect_over_cdp(session.cdp_url)
|
|
21
|
+
page = browser.contexts[0].new_page()
|
|
22
|
+
page.goto("https://example.com")
|
|
23
|
+
|
|
24
|
+
# Cleanup
|
|
25
|
+
client.sessions.delete(session.session_id)
|
|
26
|
+
|
|
27
|
+
# Or use context manager for auto-cleanup:
|
|
28
|
+
with client.sessions.create() as session:
|
|
29
|
+
... # session auto-deleted on exit
|
|
30
|
+
|
|
31
|
+
API Reference
|
|
32
|
+
-------------
|
|
33
|
+
|
|
34
|
+
Client Classes
|
|
35
|
+
~~~~~~~~~~~~~~
|
|
36
|
+
- ``HyperbrowserCloud(api_key, *, base_url, timeout, max_retries)`` — Sync client
|
|
37
|
+
- ``AsyncHyperbrowserCloud(api_key, *, base_url, timeout, max_retries)`` — Async client
|
|
38
|
+
- ``Hyperbrowser`` / ``AsyncHyperbrowser`` — Backward-compatible aliases
|
|
39
|
+
|
|
40
|
+
Client Properties
|
|
41
|
+
~~~~~~~~~~~~~~~~~
|
|
42
|
+
- ``client.sessions`` — SessionsResource for CRUD operations
|
|
43
|
+
- ``client.contexts`` — Always None (use profiles for persistence)
|
|
44
|
+
- ``client.capabilities`` — Returns ``["proxy", "fingerprint", "recording"]``
|
|
45
|
+
|
|
46
|
+
Session CRUD (``client.sessions``)
|
|
47
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
48
|
+
- ``create(*, browser_mode, proxy, recording, fingerprint, context, **vendor_params) -> SessionInfo``
|
|
49
|
+
- ``get(session_id) -> SessionInfo``
|
|
50
|
+
- ``list(**filters) -> list[SessionInfo]``
|
|
51
|
+
- ``delete(session_id) -> None`` (idempotent, safe to call multiple times)
|
|
52
|
+
|
|
53
|
+
Vendor Parameters (pass via ``**vendor_params`` in ``create()``)
|
|
54
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
55
|
+
- ``timeout_minutes: int`` — Session timeout in minutes
|
|
56
|
+
- ``solve_captchas: bool`` — Enable captcha solving
|
|
57
|
+
- ``adblock: bool`` — Enable ad blocking
|
|
58
|
+
- ``trackers: bool`` — Block trackers
|
|
59
|
+
- ``annoyances: bool`` — Block annoyances
|
|
60
|
+
- ``profile: dict`` — Session profile (``{"id": "..."}`` for persistence)
|
|
61
|
+
- ``extension_ids: list[str]`` — Browser extension IDs
|
|
62
|
+
- ``region: str`` — Deploy region (e.g. ``"us-central"``)
|
|
63
|
+
- ``accept_cookies: bool`` — Auto-accept cookie banners
|
|
64
|
+
- ``browser_args: list[str]`` — Extra browser CLI arguments
|
|
65
|
+
- ``url_blocklist: list[str]`` — URLs to block
|
|
66
|
+
- ``save_downloads: bool`` — Persist downloaded files
|
|
67
|
+
|
|
68
|
+
Example with features::
|
|
69
|
+
|
|
70
|
+
session = client.sessions.create(
|
|
71
|
+
browser_mode="stealth",
|
|
72
|
+
proxy=ManagedProxyConfig(country="US"),
|
|
73
|
+
recording=RecordingConfig(enabled=True),
|
|
74
|
+
fingerprint=FingerprintConfig(locale="en-US"),
|
|
75
|
+
solve_captchas=True,
|
|
76
|
+
adblock=True,
|
|
77
|
+
timeout_minutes=30,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
Feature Support Matrix
|
|
81
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
82
|
+
========================= ========= ==================================================
|
|
83
|
+
Feature Supported Notes
|
|
84
|
+
========================= ========= ==================================================
|
|
85
|
+
Custom proxy server Yes ``ProxyConfig(server, username, password)``
|
|
86
|
+
Managed proxy (200+ ctry) Yes ``ManagedProxyConfig(country="US")``
|
|
87
|
+
Stealth mode Yes ``browser_mode="stealth"``
|
|
88
|
+
Ultra stealth mode Yes ``browser_mode="ultra_stealth"``
|
|
89
|
+
Browser fingerprint Yes ``FingerprintConfig(locale=..., viewport=...)``
|
|
90
|
+
Session recording Yes ``RecordingConfig(enabled=True)``
|
|
91
|
+
Captcha solving Yes ``solve_captchas=True``
|
|
92
|
+
Ad blocking Yes ``adblock=True``
|
|
93
|
+
Extensions Yes ``extension_ids=[...]``
|
|
94
|
+
Region selection Yes ``region="us-central"``
|
|
95
|
+
Browser profiles Yes ``profile={"id": "..."}`` (vendor param)
|
|
96
|
+
Context persistence No Use profiles instead
|
|
97
|
+
========================= ========= ==================================================
|
|
98
|
+
|
|
99
|
+
SessionInfo Fields
|
|
100
|
+
~~~~~~~~~~~~~~~~~~
|
|
101
|
+
- ``session_id: str`` — Unique session identifier
|
|
102
|
+
- ``cdp_url: str | None`` — CDP WebSocket URL (wss://)
|
|
103
|
+
- ``status: str`` — "active", "closed", or "error"
|
|
104
|
+
- ``created_at: datetime | None``
|
|
105
|
+
- ``inspect_url: str | None`` — Live view URL for debugging
|
|
106
|
+
- ``metadata: dict`` — Vendor-specific data (teamId, token, proxyDataConsumed, etc.)
|
|
107
|
+
|
|
108
|
+
Proxy Configuration
|
|
109
|
+
~~~~~~~~~~~~~~~~~~~
|
|
110
|
+
- ``ManagedProxyConfig(country="US")`` — Managed proxy with country
|
|
111
|
+
- ``ManagedProxyConfig(country="US", city="New York")`` — Managed proxy with city
|
|
112
|
+
- ``ProxyConfig(server="http://proxy:8080")`` — Custom proxy server
|
|
113
|
+
- ``ProxyConfig(server, username="u", password="p")`` — Custom proxy with auth
|
|
114
|
+
|
|
115
|
+
CDP Authentication
|
|
116
|
+
~~~~~~~~~~~~~~~~~~
|
|
117
|
+
Hyperbrowser's CDP WebSocket endpoint uses token-based URLs (the ``wsEndpoint``
|
|
118
|
+
contains an embedded auth token). **No additional headers are needed** for the
|
|
119
|
+
WebSocket connection::
|
|
120
|
+
|
|
121
|
+
browser = pw.chromium.connect_over_cdp(session.cdp_url) # No headers needed
|
|
122
|
+
|
|
123
|
+
Exception Hierarchy
|
|
124
|
+
~~~~~~~~~~~~~~~~~~~
|
|
125
|
+
::
|
|
126
|
+
|
|
127
|
+
CloudBrowserError # Base exception
|
|
128
|
+
├── AuthenticationError # 401/403 — invalid or expired API key
|
|
129
|
+
├── QuotaExceededError # 429 — rate limit (has .retry_after attribute)
|
|
130
|
+
├── SessionNotFoundError # 404 — session doesn't exist
|
|
131
|
+
├── ProviderError # 5xx — server error (has .status_code, .request_id)
|
|
132
|
+
├── TimeoutError # Operation timed out
|
|
133
|
+
└── NetworkError # Connection failure
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
from .client import AsyncHyperbrowserCloud, HyperbrowserCloud
|
|
137
|
+
from .exceptions import (
|
|
138
|
+
AuthenticationError,
|
|
139
|
+
CloudBrowserError,
|
|
140
|
+
NetworkError,
|
|
141
|
+
ProviderError,
|
|
142
|
+
QuotaExceededError,
|
|
143
|
+
SessionNotFoundError,
|
|
144
|
+
TimeoutError,
|
|
145
|
+
)
|
|
146
|
+
from .models import (
|
|
147
|
+
ContextAttach,
|
|
148
|
+
FingerprintConfig,
|
|
149
|
+
ManagedProxyConfig,
|
|
150
|
+
ProxyConfig,
|
|
151
|
+
RecordingConfig,
|
|
152
|
+
SessionInfo,
|
|
153
|
+
ViewportConfig,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Backward compatibility aliases
|
|
157
|
+
Hyperbrowser = HyperbrowserCloud
|
|
158
|
+
AsyncHyperbrowser = AsyncHyperbrowserCloud
|
|
159
|
+
|
|
160
|
+
__all__ = [
|
|
161
|
+
# Clients
|
|
162
|
+
"HyperbrowserCloud",
|
|
163
|
+
"AsyncHyperbrowserCloud",
|
|
164
|
+
"Hyperbrowser",
|
|
165
|
+
"AsyncHyperbrowser",
|
|
166
|
+
# Models
|
|
167
|
+
"SessionInfo",
|
|
168
|
+
"ContextAttach",
|
|
169
|
+
"FingerprintConfig",
|
|
170
|
+
"ViewportConfig",
|
|
171
|
+
"ProxyConfig",
|
|
172
|
+
"ManagedProxyConfig",
|
|
173
|
+
"RecordingConfig",
|
|
174
|
+
# Exceptions
|
|
175
|
+
"CloudBrowserError",
|
|
176
|
+
"AuthenticationError",
|
|
177
|
+
"QuotaExceededError",
|
|
178
|
+
"SessionNotFoundError",
|
|
179
|
+
"ProviderError",
|
|
180
|
+
"TimeoutError",
|
|
181
|
+
"NetworkError",
|
|
182
|
+
]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Internal HTTP client wrappers with retry and exception mapping."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
CloudBrowserError,
|
|
13
|
+
NetworkError,
|
|
14
|
+
ProviderError,
|
|
15
|
+
QuotaExceededError,
|
|
16
|
+
SessionNotFoundError,
|
|
17
|
+
TimeoutError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
|
|
21
|
+
_DEFAULT_BACKOFF_BASE = 0.5
|
|
22
|
+
_DEFAULT_BACKOFF_MAX = 3.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_retry_after(response: httpx.Response) -> float | None:
|
|
26
|
+
"""Parse Retry-After header value in seconds."""
|
|
27
|
+
value = response.headers.get("retry-after")
|
|
28
|
+
if value is None:
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
return float(value)
|
|
32
|
+
except (ValueError, TypeError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
37
|
+
"""Map HTTP status codes to SDK exceptions."""
|
|
38
|
+
status = response.status_code
|
|
39
|
+
if 200 <= status < 300:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
body = response.json()
|
|
44
|
+
except Exception:
|
|
45
|
+
body = {}
|
|
46
|
+
|
|
47
|
+
message = body.get("detail") or body.get("message") or response.text or f"HTTP {status}"
|
|
48
|
+
request_id = response.headers.get("x-request-id")
|
|
49
|
+
|
|
50
|
+
if status in (401, 403):
|
|
51
|
+
raise AuthenticationError(str(message))
|
|
52
|
+
if status == 404:
|
|
53
|
+
raise SessionNotFoundError(str(message))
|
|
54
|
+
if status == 429:
|
|
55
|
+
retry_after_val = _parse_retry_after(response)
|
|
56
|
+
retry_after_int = int(retry_after_val) if retry_after_val is not None else None
|
|
57
|
+
raise QuotaExceededError(str(message), retry_after=retry_after_int)
|
|
58
|
+
if status >= 500:
|
|
59
|
+
raise ProviderError(
|
|
60
|
+
str(message),
|
|
61
|
+
status_code=status,
|
|
62
|
+
request_id=request_id,
|
|
63
|
+
)
|
|
64
|
+
raise CloudBrowserError(f"HTTP {status}: {message}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SyncHttpClient:
|
|
68
|
+
"""Synchronous HTTP client with retry logic."""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
base_url: str,
|
|
73
|
+
api_key: str,
|
|
74
|
+
timeout: float = 60.0,
|
|
75
|
+
max_retries: int = 2,
|
|
76
|
+
):
|
|
77
|
+
self._client = httpx.Client(
|
|
78
|
+
base_url=base_url,
|
|
79
|
+
headers={"x-api-key": api_key},
|
|
80
|
+
timeout=timeout,
|
|
81
|
+
)
|
|
82
|
+
self._max_retries = max_retries
|
|
83
|
+
|
|
84
|
+
def request(
|
|
85
|
+
self,
|
|
86
|
+
method: str,
|
|
87
|
+
path: str,
|
|
88
|
+
*,
|
|
89
|
+
json: Any = None,
|
|
90
|
+
params: dict[str, Any] | None = None,
|
|
91
|
+
) -> dict[str, Any]:
|
|
92
|
+
"""Send an HTTP request with retry on transient errors."""
|
|
93
|
+
last_exc: Exception | None = None
|
|
94
|
+
|
|
95
|
+
for attempt in range(self._max_retries + 1):
|
|
96
|
+
try:
|
|
97
|
+
response = self._client.request(
|
|
98
|
+
method, path, json=json, params=params
|
|
99
|
+
)
|
|
100
|
+
except httpx.TimeoutException as exc:
|
|
101
|
+
last_exc = TimeoutError(str(exc))
|
|
102
|
+
if attempt < self._max_retries:
|
|
103
|
+
self._backoff(attempt)
|
|
104
|
+
continue
|
|
105
|
+
raise last_exc from exc
|
|
106
|
+
except httpx.ConnectError as exc:
|
|
107
|
+
last_exc = NetworkError(str(exc))
|
|
108
|
+
if attempt < self._max_retries:
|
|
109
|
+
self._backoff(attempt)
|
|
110
|
+
continue
|
|
111
|
+
raise last_exc from exc
|
|
112
|
+
|
|
113
|
+
if response.status_code in _RETRYABLE_STATUS_CODES and attempt < self._max_retries:
|
|
114
|
+
wait = _parse_retry_after(response) or self._backoff_delay(attempt)
|
|
115
|
+
time.sleep(wait)
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
_raise_for_status(response)
|
|
119
|
+
|
|
120
|
+
if response.status_code == 204 or not response.content:
|
|
121
|
+
return {}
|
|
122
|
+
return response.json() # type: ignore[no-any-return]
|
|
123
|
+
|
|
124
|
+
# Should not reach here, but just in case
|
|
125
|
+
if last_exc is not None:
|
|
126
|
+
raise last_exc
|
|
127
|
+
raise CloudBrowserError("Request failed after retries")
|
|
128
|
+
|
|
129
|
+
def close(self) -> None:
|
|
130
|
+
self._client.close()
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _backoff_delay(attempt: int) -> float:
|
|
134
|
+
return min(_DEFAULT_BACKOFF_BASE * (2**attempt), _DEFAULT_BACKOFF_MAX)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _backoff(attempt: int) -> None:
|
|
138
|
+
time.sleep(min(_DEFAULT_BACKOFF_BASE * (2**attempt), _DEFAULT_BACKOFF_MAX))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class AsyncHttpClient:
|
|
142
|
+
"""Asynchronous HTTP client with retry logic."""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
base_url: str,
|
|
147
|
+
api_key: str,
|
|
148
|
+
timeout: float = 60.0,
|
|
149
|
+
max_retries: int = 2,
|
|
150
|
+
):
|
|
151
|
+
self._client = httpx.AsyncClient(
|
|
152
|
+
base_url=base_url,
|
|
153
|
+
headers={"x-api-key": api_key},
|
|
154
|
+
timeout=timeout,
|
|
155
|
+
)
|
|
156
|
+
self._max_retries = max_retries
|
|
157
|
+
|
|
158
|
+
async def request(
|
|
159
|
+
self,
|
|
160
|
+
method: str,
|
|
161
|
+
path: str,
|
|
162
|
+
*,
|
|
163
|
+
json: Any = None,
|
|
164
|
+
params: dict[str, Any] | None = None,
|
|
165
|
+
) -> dict[str, Any]:
|
|
166
|
+
"""Send an async HTTP request with retry on transient errors."""
|
|
167
|
+
import asyncio
|
|
168
|
+
|
|
169
|
+
last_exc: Exception | None = None
|
|
170
|
+
|
|
171
|
+
for attempt in range(self._max_retries + 1):
|
|
172
|
+
try:
|
|
173
|
+
response = await self._client.request(
|
|
174
|
+
method, path, json=json, params=params
|
|
175
|
+
)
|
|
176
|
+
except httpx.TimeoutException as exc:
|
|
177
|
+
last_exc = TimeoutError(str(exc))
|
|
178
|
+
if attempt < self._max_retries:
|
|
179
|
+
await asyncio.sleep(self._backoff_delay(attempt))
|
|
180
|
+
continue
|
|
181
|
+
raise last_exc from exc
|
|
182
|
+
except httpx.ConnectError as exc:
|
|
183
|
+
last_exc = NetworkError(str(exc))
|
|
184
|
+
if attempt < self._max_retries:
|
|
185
|
+
await asyncio.sleep(self._backoff_delay(attempt))
|
|
186
|
+
continue
|
|
187
|
+
raise last_exc from exc
|
|
188
|
+
|
|
189
|
+
if response.status_code in _RETRYABLE_STATUS_CODES and attempt < self._max_retries:
|
|
190
|
+
wait = _parse_retry_after(response) or self._backoff_delay(attempt)
|
|
191
|
+
await asyncio.sleep(wait)
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
_raise_for_status(response)
|
|
195
|
+
|
|
196
|
+
if response.status_code == 204 or not response.content:
|
|
197
|
+
return {}
|
|
198
|
+
return response.json() # type: ignore[no-any-return]
|
|
199
|
+
|
|
200
|
+
if last_exc is not None:
|
|
201
|
+
raise last_exc
|
|
202
|
+
raise CloudBrowserError("Request failed after retries")
|
|
203
|
+
|
|
204
|
+
async def close(self) -> None:
|
|
205
|
+
await self._client.aclose()
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def _backoff_delay(attempt: int) -> float:
|
|
209
|
+
return min(_DEFAULT_BACKOFF_BASE * (2**attempt), _DEFAULT_BACKOFF_MAX)
|