scalebrowser 0.2.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.
- scalebrowser-0.2.0/.gitignore +8 -0
- scalebrowser-0.2.0/LICENSE +21 -0
- scalebrowser-0.2.0/PKG-INFO +170 -0
- scalebrowser-0.2.0/README.md +141 -0
- scalebrowser-0.2.0/examples/quickstart.py +45 -0
- scalebrowser-0.2.0/examples/quickstart_sync.py +35 -0
- scalebrowser-0.2.0/pyproject.toml +50 -0
- scalebrowser-0.2.0/src/scalebrowser/__init__.py +234 -0
- scalebrowser-0.2.0/src/scalebrowser/_http.py +180 -0
- scalebrowser-0.2.0/src/scalebrowser/_sync.py +604 -0
- scalebrowser-0.2.0/src/scalebrowser/_version.py +3 -0
- scalebrowser-0.2.0/src/scalebrowser/cdp.py +429 -0
- scalebrowser-0.2.0/src/scalebrowser/client.py +825 -0
- scalebrowser-0.2.0/src/scalebrowser/errors.py +97 -0
- scalebrowser-0.2.0/src/scalebrowser/events.py +52 -0
- scalebrowser-0.2.0/src/scalebrowser/models.py +861 -0
- scalebrowser-0.2.0/src/scalebrowser/models_control.py +128 -0
- scalebrowser-0.2.0/src/scalebrowser/models_identity.py +130 -0
- scalebrowser-0.2.0/src/scalebrowser/models_runs.py +56 -0
- scalebrowser-0.2.0/tests/__init__.py +0 -0
- scalebrowser-0.2.0/tests/conftest.py +225 -0
- scalebrowser-0.2.0/tests/test_cdp.py +123 -0
- scalebrowser-0.2.0/tests/test_client.py +435 -0
- scalebrowser-0.2.0/tests/test_coverage.py +299 -0
- scalebrowser-0.2.0/tests/test_e2e.py +47 -0
- scalebrowser-0.2.0/tests/test_errors.py +49 -0
- scalebrowser-0.2.0/tests/test_events.py +141 -0
- scalebrowser-0.2.0/tests/test_sync.py +83 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Scalebrowser
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: scalebrowser
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Official Python SDK for the Scalebrowser daemon — typed REST client + direct-CDP driver (nodriver-style).
|
|
5
|
+
Project-URL: Homepage, https://scalebrowser.net
|
|
6
|
+
Project-URL: Documentation, https://scalebrowser.net
|
|
7
|
+
Author: Scalebrowser
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent-browser,ai-agents,browser,browser-automation,cdp,mcp,scalebrowser
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx>=0.27
|
|
22
|
+
Requires-Dist: pydantic>=2.7
|
|
23
|
+
Requires-Dist: websockets>=13
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# scalebrowser — Python SDK
|
|
31
|
+
|
|
32
|
+
Official Python SDK for the [Scalebrowser](https://scalebrowser.net) daemon: a
|
|
33
|
+
typed REST client **plus a direct-CDP driver** (nodriver-style) for the
|
|
34
|
+
self-hosted browser infrastructure that gives each AI agent its own browser.
|
|
35
|
+
|
|
36
|
+
The driver plane is **direct-CDP, not** Playwright/Puppeteer: anti-bot stacks
|
|
37
|
+
block the Playwright control plane regardless of how good the browser patches
|
|
38
|
+
are. `start_profile` returns a `cdp_ws` endpoint and this SDK speaks the Chrome
|
|
39
|
+
DevTools Protocol over it directly. Credentials never leave the daemon and are
|
|
40
|
+
never logged by the SDK.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install scalebrowser
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires Python ≥ 3.10 and depends on `httpx`, `websockets`, `pydantic` v2. The
|
|
49
|
+
SDK is MIT-licensed; the daemon it talks to is a separate, licensed product.
|
|
50
|
+
|
|
51
|
+
## Quickstart (sync)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from scalebrowser import ScalebrowserClient, CreateProfileBody
|
|
55
|
+
|
|
56
|
+
sb = ScalebrowserClient(base_url="http://127.0.0.1:8787", token="…")
|
|
57
|
+
|
|
58
|
+
profile = sb.create_profile(CreateProfileBody(name="acct-01"))
|
|
59
|
+
|
|
60
|
+
# start → direct-CDP connect → navigate → humanized click → stop
|
|
61
|
+
with sb.launch(profile.id, headless=True) as page:
|
|
62
|
+
page.navigate("https://example.com")
|
|
63
|
+
print(page.evaluate("document.title"))
|
|
64
|
+
page.humanize_click(120, 240) # routed through the daemon trusted-input (G8)
|
|
65
|
+
|
|
66
|
+
sb.close()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quickstart (async)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import asyncio
|
|
73
|
+
from scalebrowser import AsyncScalebrowserClient
|
|
74
|
+
|
|
75
|
+
async def main():
|
|
76
|
+
async with AsyncScalebrowserClient(token="…") as sb:
|
|
77
|
+
started = await sb.start_profile(profile_id, headless=True) # StartProfileResult
|
|
78
|
+
async with await sb.connect_cdp(started, profile_id) as page:
|
|
79
|
+
await page.navigate("https://example.com")
|
|
80
|
+
title = await page.evaluate("document.title")
|
|
81
|
+
await page.humanize_click(120, 240)
|
|
82
|
+
await sb.stop_profile(profile_id)
|
|
83
|
+
|
|
84
|
+
asyncio.run(main())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## REST surface
|
|
88
|
+
|
|
89
|
+
Every `/v1` endpoint is a typed method on the client, under the same name in
|
|
90
|
+
both the sync and the async client:
|
|
91
|
+
|
|
92
|
+
- **Profiles** — `list_profiles`, `get_profile`, `create_profile`,
|
|
93
|
+
`update_profile`, `delete_profile`, `start_profile`, `stop_profile`
|
|
94
|
+
- **Bulk** — `bulk_create_profiles`, `bulk_start`, `bulk_stop`, `bulk_delete`,
|
|
95
|
+
`bulk_assign_proxy`
|
|
96
|
+
- **Groups / Presets** — `list_groups`/`create_group`/`get_group`/`update_group`/`delete_group`,
|
|
97
|
+
`list_presets`/`create_preset`/`get_preset`/`update_preset`/`delete_preset`,
|
|
98
|
+
`get_persona_constraints`. A preset is `config` (what the profiles do:
|
|
99
|
+
`geo_mode`, `proxy_id`, …) plus `constraints` (what they are: `country`, which
|
|
100
|
+
pins the persona's language, timezone and voices). Both are typed
|
|
101
|
+
(`PresetConfig` / `PresetConstraints`) and the daemon refuses an unknown key
|
|
102
|
+
with a 400 — read the valid regions from `get_persona_constraints()` rather than
|
|
103
|
+
hardcoding them.
|
|
104
|
+
- **Proxies** — `list_proxies`/`create_proxy`/`get_proxy`/`update_proxy`/`delete_proxy`/`check_proxy`,
|
|
105
|
+
plus `check_proxy_config` (probe a config before saving it; pass `id` to reuse
|
|
106
|
+
an existing proxy's stored credentials)
|
|
107
|
+
- **Extensions** — `list_extensions`, `attach_extension`, `detach_extension`,
|
|
108
|
+
plus the daemon-wide library (`upload_extension`, `get_library_extension`,
|
|
109
|
+
`delete_library_extension`). An attached package IS loaded into the browser at
|
|
110
|
+
launch, under the canonical Web-Store id its own key derives
|
|
111
|
+
- **Credentials** — `list_credentials`, `put_credential`, `reveal_credential`
|
|
112
|
+
(needs the vault password), `export_credentials`, `import_credentials`
|
|
113
|
+
- **Cookies** — `reveal_cookies`, the one route a cookie VALUE leaves through,
|
|
114
|
+
behind the same vault password
|
|
115
|
+
- **Sessions** — `export_session`, `import_session`
|
|
116
|
+
- **Mailboxes** — `list_inboxes`, `create_inbox`, `update_inbox`, `delete_inbox`,
|
|
117
|
+
`get_inbox_bindings`, `bind_inbox`, `unbind_inbox` — where a profile's
|
|
118
|
+
confirmation codes arrive
|
|
119
|
+
- **Passkeys** — `list_passkeys`, `delete_passkey`. Metadata only: the private
|
|
120
|
+
key has no field and no endpoint
|
|
121
|
+
- **Agent runs** — `list_runs`, `get_run`, `list_run_steps`, `get_run_shot`,
|
|
122
|
+
`get_activity`. Read-only, all of it
|
|
123
|
+
- **Interruptions** — `list_interruption_locks`, `set_interruption_lock`,
|
|
124
|
+
`list_interruption_rules`, `set_interruption_rule`,
|
|
125
|
+
`delete_interruption_rule` — who may answer when the browser asks something
|
|
126
|
+
- **Artifacts** — `put_artifact` (hand the daemon a file to upload later),
|
|
127
|
+
`get_artifact` (fetch a screenshot, download or saved PDF as bytes)
|
|
128
|
+
- **Input / Metrics / Account / Events** — `send_input`, `get_metrics`,
|
|
129
|
+
`get_account`, `health`, `ready`, `events()`
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
async for event in sb_async.events(): # SSE lifecycle stream (Bearer-authenticated)
|
|
133
|
+
print(event.type) # typed: profile_started / profile_crashed / …
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Errors map the daemon contract: `ApiError(status, code, message)` with codes
|
|
137
|
+
`4001–4010` (`ApiError.is_auth_error` for 401 / 4010); `NetworkError` when the
|
|
138
|
+
daemon is unreachable; `CdpError` for protocol-level failures.
|
|
139
|
+
|
|
140
|
+
## Direct-CDP driver
|
|
141
|
+
|
|
142
|
+
`CdpSession` (async) / `SyncCdpSession` give you:
|
|
143
|
+
|
|
144
|
+
- `send(method, params)` — any CDP command, awaited by `id`
|
|
145
|
+
- `navigate(url)`, `evaluate(expr, isolated=False)` — **never** calls
|
|
146
|
+
`Runtime.enable` (a detection leak); isolated worlds via
|
|
147
|
+
`create_isolated_world()`
|
|
148
|
+
- `on(method, cb)` / `events()` — subscribe to CDP events
|
|
149
|
+
- `humanize_move/click/type/scroll` — humanized OS-level input via the daemon
|
|
150
|
+
|
|
151
|
+
## Tests
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
pip install -e ".[dev]"
|
|
155
|
+
pytest # unit tests (mock REST + a real fake-CDP ws server)
|
|
156
|
+
SCALEBROWSER_E2E=1 pytest tests/test_e2e.py # against a real daemon
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Contract assumptions
|
|
160
|
+
|
|
161
|
+
- Default base URL `http://127.0.0.1:8787`; Bearer token always.
|
|
162
|
+
- The trusted-input body beyond `{action, humanize}` (coordinates, `button`,
|
|
163
|
+
`delta_x/y`, `text`) is an SDK convention — see `cdp.py`.
|
|
164
|
+
- Two endpoints are optional and answer 404 on a daemon without them, which the
|
|
165
|
+
SDK treats as information rather than as an error: `get_metrics()` then derives
|
|
166
|
+
running counts from profile state, and `get_account()` returns
|
|
167
|
+
`licensed=False`, which is what "self-hosted, no control plane" means.
|
|
168
|
+
- Every method is present on BOTH clients under the same name. The sync client
|
|
169
|
+
is a hand-written mirror over one background event loop; there is no duplicated
|
|
170
|
+
endpoint logic.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# scalebrowser — Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [Scalebrowser](https://scalebrowser.net) daemon: a
|
|
4
|
+
typed REST client **plus a direct-CDP driver** (nodriver-style) for the
|
|
5
|
+
self-hosted browser infrastructure that gives each AI agent its own browser.
|
|
6
|
+
|
|
7
|
+
The driver plane is **direct-CDP, not** Playwright/Puppeteer: anti-bot stacks
|
|
8
|
+
block the Playwright control plane regardless of how good the browser patches
|
|
9
|
+
are. `start_profile` returns a `cdp_ws` endpoint and this SDK speaks the Chrome
|
|
10
|
+
DevTools Protocol over it directly. Credentials never leave the daemon and are
|
|
11
|
+
never logged by the SDK.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install scalebrowser
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Python ≥ 3.10 and depends on `httpx`, `websockets`, `pydantic` v2. The
|
|
20
|
+
SDK is MIT-licensed; the daemon it talks to is a separate, licensed product.
|
|
21
|
+
|
|
22
|
+
## Quickstart (sync)
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from scalebrowser import ScalebrowserClient, CreateProfileBody
|
|
26
|
+
|
|
27
|
+
sb = ScalebrowserClient(base_url="http://127.0.0.1:8787", token="…")
|
|
28
|
+
|
|
29
|
+
profile = sb.create_profile(CreateProfileBody(name="acct-01"))
|
|
30
|
+
|
|
31
|
+
# start → direct-CDP connect → navigate → humanized click → stop
|
|
32
|
+
with sb.launch(profile.id, headless=True) as page:
|
|
33
|
+
page.navigate("https://example.com")
|
|
34
|
+
print(page.evaluate("document.title"))
|
|
35
|
+
page.humanize_click(120, 240) # routed through the daemon trusted-input (G8)
|
|
36
|
+
|
|
37
|
+
sb.close()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quickstart (async)
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from scalebrowser import AsyncScalebrowserClient
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
async with AsyncScalebrowserClient(token="…") as sb:
|
|
48
|
+
started = await sb.start_profile(profile_id, headless=True) # StartProfileResult
|
|
49
|
+
async with await sb.connect_cdp(started, profile_id) as page:
|
|
50
|
+
await page.navigate("https://example.com")
|
|
51
|
+
title = await page.evaluate("document.title")
|
|
52
|
+
await page.humanize_click(120, 240)
|
|
53
|
+
await sb.stop_profile(profile_id)
|
|
54
|
+
|
|
55
|
+
asyncio.run(main())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## REST surface
|
|
59
|
+
|
|
60
|
+
Every `/v1` endpoint is a typed method on the client, under the same name in
|
|
61
|
+
both the sync and the async client:
|
|
62
|
+
|
|
63
|
+
- **Profiles** — `list_profiles`, `get_profile`, `create_profile`,
|
|
64
|
+
`update_profile`, `delete_profile`, `start_profile`, `stop_profile`
|
|
65
|
+
- **Bulk** — `bulk_create_profiles`, `bulk_start`, `bulk_stop`, `bulk_delete`,
|
|
66
|
+
`bulk_assign_proxy`
|
|
67
|
+
- **Groups / Presets** — `list_groups`/`create_group`/`get_group`/`update_group`/`delete_group`,
|
|
68
|
+
`list_presets`/`create_preset`/`get_preset`/`update_preset`/`delete_preset`,
|
|
69
|
+
`get_persona_constraints`. A preset is `config` (what the profiles do:
|
|
70
|
+
`geo_mode`, `proxy_id`, …) plus `constraints` (what they are: `country`, which
|
|
71
|
+
pins the persona's language, timezone and voices). Both are typed
|
|
72
|
+
(`PresetConfig` / `PresetConstraints`) and the daemon refuses an unknown key
|
|
73
|
+
with a 400 — read the valid regions from `get_persona_constraints()` rather than
|
|
74
|
+
hardcoding them.
|
|
75
|
+
- **Proxies** — `list_proxies`/`create_proxy`/`get_proxy`/`update_proxy`/`delete_proxy`/`check_proxy`,
|
|
76
|
+
plus `check_proxy_config` (probe a config before saving it; pass `id` to reuse
|
|
77
|
+
an existing proxy's stored credentials)
|
|
78
|
+
- **Extensions** — `list_extensions`, `attach_extension`, `detach_extension`,
|
|
79
|
+
plus the daemon-wide library (`upload_extension`, `get_library_extension`,
|
|
80
|
+
`delete_library_extension`). An attached package IS loaded into the browser at
|
|
81
|
+
launch, under the canonical Web-Store id its own key derives
|
|
82
|
+
- **Credentials** — `list_credentials`, `put_credential`, `reveal_credential`
|
|
83
|
+
(needs the vault password), `export_credentials`, `import_credentials`
|
|
84
|
+
- **Cookies** — `reveal_cookies`, the one route a cookie VALUE leaves through,
|
|
85
|
+
behind the same vault password
|
|
86
|
+
- **Sessions** — `export_session`, `import_session`
|
|
87
|
+
- **Mailboxes** — `list_inboxes`, `create_inbox`, `update_inbox`, `delete_inbox`,
|
|
88
|
+
`get_inbox_bindings`, `bind_inbox`, `unbind_inbox` — where a profile's
|
|
89
|
+
confirmation codes arrive
|
|
90
|
+
- **Passkeys** — `list_passkeys`, `delete_passkey`. Metadata only: the private
|
|
91
|
+
key has no field and no endpoint
|
|
92
|
+
- **Agent runs** — `list_runs`, `get_run`, `list_run_steps`, `get_run_shot`,
|
|
93
|
+
`get_activity`. Read-only, all of it
|
|
94
|
+
- **Interruptions** — `list_interruption_locks`, `set_interruption_lock`,
|
|
95
|
+
`list_interruption_rules`, `set_interruption_rule`,
|
|
96
|
+
`delete_interruption_rule` — who may answer when the browser asks something
|
|
97
|
+
- **Artifacts** — `put_artifact` (hand the daemon a file to upload later),
|
|
98
|
+
`get_artifact` (fetch a screenshot, download or saved PDF as bytes)
|
|
99
|
+
- **Input / Metrics / Account / Events** — `send_input`, `get_metrics`,
|
|
100
|
+
`get_account`, `health`, `ready`, `events()`
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
async for event in sb_async.events(): # SSE lifecycle stream (Bearer-authenticated)
|
|
104
|
+
print(event.type) # typed: profile_started / profile_crashed / …
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Errors map the daemon contract: `ApiError(status, code, message)` with codes
|
|
108
|
+
`4001–4010` (`ApiError.is_auth_error` for 401 / 4010); `NetworkError` when the
|
|
109
|
+
daemon is unreachable; `CdpError` for protocol-level failures.
|
|
110
|
+
|
|
111
|
+
## Direct-CDP driver
|
|
112
|
+
|
|
113
|
+
`CdpSession` (async) / `SyncCdpSession` give you:
|
|
114
|
+
|
|
115
|
+
- `send(method, params)` — any CDP command, awaited by `id`
|
|
116
|
+
- `navigate(url)`, `evaluate(expr, isolated=False)` — **never** calls
|
|
117
|
+
`Runtime.enable` (a detection leak); isolated worlds via
|
|
118
|
+
`create_isolated_world()`
|
|
119
|
+
- `on(method, cb)` / `events()` — subscribe to CDP events
|
|
120
|
+
- `humanize_move/click/type/scroll` — humanized OS-level input via the daemon
|
|
121
|
+
|
|
122
|
+
## Tests
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pip install -e ".[dev]"
|
|
126
|
+
pytest # unit tests (mock REST + a real fake-CDP ws server)
|
|
127
|
+
SCALEBROWSER_E2E=1 pytest tests/test_e2e.py # against a real daemon
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Contract assumptions
|
|
131
|
+
|
|
132
|
+
- Default base URL `http://127.0.0.1:8787`; Bearer token always.
|
|
133
|
+
- The trusted-input body beyond `{action, humanize}` (coordinates, `button`,
|
|
134
|
+
`delta_x/y`, `text`) is an SDK convention — see `cdp.py`.
|
|
135
|
+
- Two endpoints are optional and answer 404 on a daemon without them, which the
|
|
136
|
+
SDK treats as information rather than as an error: `get_metrics()` then derives
|
|
137
|
+
running counts from profile state, and `get_account()` returns
|
|
138
|
+
`licensed=False`, which is what "self-hosted, no control plane" means.
|
|
139
|
+
- Every method is present on BOTH clients under the same name. The sync client
|
|
140
|
+
is a hand-written mirror over one background event loop; there is no duplicated
|
|
141
|
+
endpoint logic.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Async quickstart: start a profile, drive it over direct-CDP, humanize a click,
|
|
2
|
+
stop it.
|
|
3
|
+
|
|
4
|
+
SCALEBROWSER_TOKEN=… SCALEBROWSER_PROFILE_ID=… python examples/quickstart.py
|
|
5
|
+
|
|
6
|
+
Requires a running Scalebrowser daemon (the driver plane is direct-CDP, not
|
|
7
|
+
Playwright).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from scalebrowser import AsyncScalebrowserClient, CreateProfileBody
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def main() -> None:
|
|
19
|
+
base_url = os.environ.get("SCALEBROWSER_BASE_URL", "http://127.0.0.1:8787")
|
|
20
|
+
token = os.environ.get("SCALEBROWSER_TOKEN")
|
|
21
|
+
|
|
22
|
+
async with AsyncScalebrowserClient(base_url, token=token) as sb:
|
|
23
|
+
# Reuse a profile if given, otherwise create one.
|
|
24
|
+
profile_id = os.environ.get("SCALEBROWSER_PROFILE_ID")
|
|
25
|
+
if profile_id is None:
|
|
26
|
+
profile = await sb.create_profile(CreateProfileBody(name="quickstart"))
|
|
27
|
+
profile_id = profile.id
|
|
28
|
+
print(f"created profile {profile_id}")
|
|
29
|
+
|
|
30
|
+
# start → direct-CDP connect → navigate → humanized click → stop
|
|
31
|
+
async with sb.launch(profile_id, headless=True) as page:
|
|
32
|
+
await page.navigate("https://example.com")
|
|
33
|
+
title = await page.evaluate("document.title")
|
|
34
|
+
print(f"page title: {title!r}")
|
|
35
|
+
|
|
36
|
+
# Humanized trusted input goes through the daemon (G8), not raw CDP.
|
|
37
|
+
await page.humanize_move(120, 240)
|
|
38
|
+
await page.humanize_click(120, 240)
|
|
39
|
+
print("humanized click sent")
|
|
40
|
+
|
|
41
|
+
print("profile stopped")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Synchronous quickstart — same flow as ``quickstart.py`` without async/await.
|
|
2
|
+
|
|
3
|
+
SCALEBROWSER_TOKEN=… SCALEBROWSER_PROFILE_ID=… python examples/quickstart_sync.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from scalebrowser import CreateProfileBody, ScalebrowserClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
base_url = os.environ.get("SCALEBROWSER_BASE_URL", "http://127.0.0.1:8787")
|
|
15
|
+
token = os.environ.get("SCALEBROWSER_TOKEN")
|
|
16
|
+
|
|
17
|
+
with ScalebrowserClient(base_url, token=token) as sb:
|
|
18
|
+
profile_id = os.environ.get("SCALEBROWSER_PROFILE_ID")
|
|
19
|
+
if profile_id is None:
|
|
20
|
+
profile = sb.create_profile(CreateProfileBody(name="quickstart-sync"))
|
|
21
|
+
profile_id = profile.id
|
|
22
|
+
print(f"created profile {profile_id}")
|
|
23
|
+
|
|
24
|
+
with sb.launch(profile_id, headless=True) as page:
|
|
25
|
+
page.navigate("https://example.com")
|
|
26
|
+
print(f"page title: {page.evaluate('document.title')!r}")
|
|
27
|
+
page.humanize_move(120, 240)
|
|
28
|
+
page.humanize_click(120, 240)
|
|
29
|
+
print("humanized click sent")
|
|
30
|
+
|
|
31
|
+
print("profile stopped")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
main()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scalebrowser"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Official Python SDK for the Scalebrowser daemon — typed REST client + direct-CDP driver (nodriver-style)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Scalebrowser" }]
|
|
14
|
+
keywords = ["scalebrowser", "ai-agents", "agent-browser", "browser", "cdp", "mcp", "browser-automation"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.27",
|
|
28
|
+
"websockets>=13",
|
|
29
|
+
"pydantic>=2.7",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://scalebrowser.net"
|
|
34
|
+
Documentation = "https://scalebrowser.net"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=8",
|
|
39
|
+
"pytest-asyncio>=0.23",
|
|
40
|
+
"build>=1.2",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/scalebrowser"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
asyncio_mode = "auto"
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
addopts = "-q"
|
|
50
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|