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.
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.pyc
7
+ .pytest_cache/
8
+ .mypy_cache/
@@ -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"]