surfsky 0.0.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.
- surfsky-0.0.1/.gitignore +24 -0
- surfsky-0.0.1/LICENSE +21 -0
- surfsky-0.0.1/PKG-INFO +424 -0
- surfsky-0.0.1/README.md +397 -0
- surfsky-0.0.1/examples/basic_scrape.py +39 -0
- surfsky-0.0.1/examples/duckduckgo.py +53 -0
- surfsky-0.0.1/examples/fingerprint.py +53 -0
- surfsky-0.0.1/examples/form.py +54 -0
- surfsky-0.0.1/examples/login.py +44 -0
- surfsky-0.0.1/examples/multi_account.py +89 -0
- surfsky-0.0.1/examples/one_time.py +23 -0
- surfsky-0.0.1/examples/parallel_urls.py +51 -0
- surfsky-0.0.1/examples/persistent.py +34 -0
- surfsky-0.0.1/examples/playwright_connect.py +30 -0
- surfsky-0.0.1/examples/popup.py +32 -0
- surfsky-0.0.1/examples/premium_proxy.py +38 -0
- surfsky-0.0.1/examples/selenium_connect.py +29 -0
- surfsky-0.0.1/examples/tabs.py +47 -0
- surfsky-0.0.1/pyproject.toml +88 -0
- surfsky-0.0.1/src/surfsky/__init__.py +91 -0
- surfsky-0.0.1/src/surfsky/browser/__init__.py +5 -0
- surfsky-0.0.1/src/surfsky/browser/actions.py +199 -0
- surfsky-0.0.1/src/surfsky/browser/browser.py +346 -0
- surfsky-0.0.1/src/surfsky/browser/cdp.py +153 -0
- surfsky-0.0.1/src/surfsky/browser/page.py +669 -0
- surfsky-0.0.1/src/surfsky/browser/pool.py +248 -0
- surfsky-0.0.1/src/surfsky/client.py +321 -0
- surfsky-0.0.1/src/surfsky/errors.py +97 -0
- surfsky-0.0.1/src/surfsky/proxy.py +106 -0
- surfsky-0.0.1/src/surfsky/py.typed +0 -0
- surfsky-0.0.1/src/surfsky/resources/__init__.py +5 -0
- surfsky-0.0.1/src/surfsky/resources/account.py +68 -0
- surfsky-0.0.1/src/surfsky/resources/extensions.py +100 -0
- surfsky-0.0.1/src/surfsky/resources/fingerprints.py +77 -0
- surfsky-0.0.1/src/surfsky/resources/profiles.py +424 -0
- surfsky-0.0.1/src/surfsky/resources/proxies.py +110 -0
- surfsky-0.0.1/src/surfsky/transport.py +312 -0
- surfsky-0.0.1/src/surfsky/types.py +430 -0
- surfsky-0.0.1/tests/conftest.py +18 -0
- surfsky-0.0.1/tests/test_actions.py +81 -0
- surfsky-0.0.1/tests/test_browser_session.py +1297 -0
- surfsky-0.0.1/tests/test_cdp.py +299 -0
- surfsky-0.0.1/tests/test_client.py +292 -0
- surfsky-0.0.1/tests/test_extensions.py +77 -0
- surfsky-0.0.1/tests/test_fingerprints.py +62 -0
- surfsky-0.0.1/tests/test_http.py +244 -0
- surfsky-0.0.1/tests/test_integration.py +157 -0
- surfsky-0.0.1/tests/test_live_concurrency.py +88 -0
- surfsky-0.0.1/tests/test_pages.py +486 -0
- surfsky-0.0.1/tests/test_pool.py +750 -0
- surfsky-0.0.1/tests/test_profiles.py +445 -0
- surfsky-0.0.1/tests/test_proxy.py +351 -0
- surfsky-0.0.1/tests/test_types.py +70 -0
surfsky-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# Tool caches
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
|
|
16
|
+
# OS junk
|
|
17
|
+
.DS_Store
|
|
18
|
+
|
|
19
|
+
# Local scrape input/output (examples)
|
|
20
|
+
results.jsonl
|
|
21
|
+
urls.txt
|
|
22
|
+
|
|
23
|
+
# Local reference checkouts, not part of the project
|
|
24
|
+
vendor/
|
surfsky-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Surfsky SDK contributors
|
|
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.
|
surfsky-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: surfsky
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python SDK for the Surfsky antidetect browser cloud.
|
|
5
|
+
Project-URL: Homepage, https://surfsky.io
|
|
6
|
+
Project-URL: Documentation, https://docs.surfsky.io
|
|
7
|
+
Project-URL: Repository, https://github.com/surfskyio/surfsky-py
|
|
8
|
+
Project-URL: Issues, https://github.com/surfskyio/surfsky-py/issues
|
|
9
|
+
Author: Surfsky SDK contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: antidetect,automation,browser,cdp,devtools-protocol,scraping,surfsky
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: anyio>=4.4
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: pydantic>=2.8
|
|
24
|
+
Requires-Dist: tenacity>=9.0
|
|
25
|
+
Requires-Dist: websockets>=13
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# surfsky
|
|
29
|
+
|
|
30
|
+
Python SDK for [Surfsky](https://surfsky.io), a cloud-based antidetect browser.
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
- Talks plain CDP, with none of the traces Playwright, Puppeteer or Selenium
|
|
36
|
+
leave behind: no `Runtime.enable`, no injected scripts, no extra globals.
|
|
37
|
+
- Clicks, scrolls and keystrokes go through Surfsky's server-side
|
|
38
|
+
[human emulation](https://docs.surfsky.io/human_emulation), with real mouse
|
|
39
|
+
paths and per-key timing. The page gets trusted events.
|
|
40
|
+
- Residential and mobile proxies, 100M+ IPs, picked by country, region or city.
|
|
41
|
+
Or bring your own.
|
|
42
|
+
- Fingerprints from a pool of 2.5M+ real devices. Persistent profiles keep the
|
|
43
|
+
same identity between runs.
|
|
44
|
+
- A browser pool that keeps every slot in your plan busy.
|
|
45
|
+
- Nothing to install. Browsers run in Surfsky's cloud. Playwright-style API,
|
|
46
|
+
sync and async clients, fully typed.
|
|
47
|
+
|
|
48
|
+
API reference for the service itself: https://docs.surfsky.io/api-reference
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
uv add surfsky
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
or, with pip:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
pip install surfsky
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires Python 3.12 or newer.
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
Sign up at [surfsky.io](https://surfsky.io). Once you're logged in, the
|
|
67
|
+
[dashboard](https://app.surfsky.io) shows your API token and base URL. Put them
|
|
68
|
+
in the environment:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
export SURFSKY_API_TOKEN=...
|
|
72
|
+
export SURFSKY_API_BASE_URL=...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then start a browser and use it:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import asyncio
|
|
79
|
+
|
|
80
|
+
from surfsky import AsyncSurfsky, SharedProxy
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def main():
|
|
84
|
+
# or AsyncSurfsky(api_token="...", base_url="...") instead of env vars
|
|
85
|
+
async with AsyncSurfsky() as client:
|
|
86
|
+
# starts a session, stops it on exit so it doesn't keep billing
|
|
87
|
+
async with client.browser(proxy=SharedProxy(country="us")) as browser:
|
|
88
|
+
await browser.goto("https://duckduckgo.com", wait_until="domcontentloaded")
|
|
89
|
+
await browser.type('[name="q"]', "surfsky cloud browser")
|
|
90
|
+
await browser.click("#searchbox_homepage button[type=submit]")
|
|
91
|
+
print(await browser.wait_for_url("?q="))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
asyncio.run(main())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
More examples in [`examples/`](examples/).
|
|
98
|
+
|
|
99
|
+
## Browser automation
|
|
100
|
+
|
|
101
|
+
The API sticks to Playwright's names and semantics where it can: `click`,
|
|
102
|
+
`fill`, `hover`, `wait_for_selector`, `inner_text`, `select_option`,
|
|
103
|
+
`keyboard.press`, `mouse.move` and so on. Input goes through Surfsky's
|
|
104
|
+
[human emulation](https://docs.surfsky.io/human_emulation). Reads use plain CDP
|
|
105
|
+
and run no JavaScript in the page.
|
|
106
|
+
|
|
107
|
+
When you need JavaScript:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
await browser.evaluate("document.title")
|
|
111
|
+
await browser.evaluate("(a, b) => a + b", 1, 2)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Scripts run in an isolated world the page can't see. Pass `isolated=False` to
|
|
115
|
+
use the page's own context.
|
|
116
|
+
|
|
117
|
+
Also useful:
|
|
118
|
+
|
|
119
|
+
- `client.browser(block_resources={"image", "font", "media"})` skips those
|
|
120
|
+
downloads and saves proxy traffic.
|
|
121
|
+
- Grab the JSON a page fetches instead of parsing its HTML:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
await browser.capture_responses("/api/search")
|
|
125
|
+
await browser.goto(url)
|
|
126
|
+
data = (await browser.wait_for_response("/api/search")).json()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- Work with several pages at once. `browser.pages` has every open tab, popups
|
|
130
|
+
included, and `new_page()` adds one:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
await browser.new_page()
|
|
134
|
+
await browser.pages[1].goto("https://example.com")
|
|
135
|
+
print(await browser.pages[0].title(), await browser.pages[1].title())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Every method is listed in the [API reference](#api-reference).
|
|
139
|
+
|
|
140
|
+
## Running multiple browsers
|
|
141
|
+
|
|
142
|
+
`client.map` runs a function over a list of items in parallel, one browser per
|
|
143
|
+
item, and collects the results:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
async def title(browser, url):
|
|
147
|
+
await browser.goto(url)
|
|
148
|
+
return await browser.title()
|
|
149
|
+
|
|
150
|
+
for o in await client.map(title, urls):
|
|
151
|
+
print(o.item, o.value if o.ok else o.error)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Errors land in `o.error` instead of raising, so one bad page doesn't stop the
|
|
155
|
+
run. By default the pool uses every browser your plan allows
|
|
156
|
+
(`concurrency="auto"`). Pass `concurrency=5` to cap it.
|
|
157
|
+
|
|
158
|
+
If you need more control, write your own loop on top of the pool:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
async with client.browsers() as pool:
|
|
162
|
+
async with pool.lease() as browser: # waits for a free browser
|
|
163
|
+
if not browser.data.get("logged_in"):
|
|
164
|
+
await log_in(browser)
|
|
165
|
+
browser.data["logged_in"] = True
|
|
166
|
+
...
|
|
167
|
+
if looks_blocked:
|
|
168
|
+
browser.retire() # next lease gets a fresh identity
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
A browser keeps its fingerprint, proxy and cookies between leases until you
|
|
172
|
+
retire it.
|
|
173
|
+
|
|
174
|
+
## Using Playwright or Puppeteer
|
|
175
|
+
|
|
176
|
+
You don't have to use the SDK's browser API. `client.session` starts a browser
|
|
177
|
+
and gives you its WebSocket URL, so Playwright, Puppeteer or any other CDP client
|
|
178
|
+
can drive it. The SDK then handles profiles, proxies and session lifecycle:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from playwright.sync_api import sync_playwright
|
|
182
|
+
from surfsky import Surfsky
|
|
183
|
+
|
|
184
|
+
with Surfsky() as client, client.session() as session, sync_playwright() as pw:
|
|
185
|
+
browser = pw.chromium.connect_over_cdp(session.connect_url)
|
|
186
|
+
page = browser.contexts[0].pages[0]
|
|
187
|
+
page.goto("https://example.com")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Don't use stock Playwright or Puppeteer. Both leave automation traces the SDK
|
|
191
|
+
goes out of its way to avoid, `Runtime.enable` above all. Use one of the
|
|
192
|
+
patched forks that strip those traces. They're drop-in replacements.
|
|
193
|
+
|
|
194
|
+
Selenium works too (`enable_chromedriver=True`, see
|
|
195
|
+
[`examples/selenium_connect.py`](examples/selenium_connect.py)) but isn't
|
|
196
|
+
recommended: chromedriver is easy to detect.
|
|
197
|
+
|
|
198
|
+
## Proxies
|
|
199
|
+
|
|
200
|
+
Every session start accepts `proxy=`. Bring your own, or use Surfsky's built-in
|
|
201
|
+
pools:
|
|
202
|
+
|
|
203
|
+
- Premium: clean residential and mobile IPs, targeted by country, region or
|
|
204
|
+
city. Use it for production.
|
|
205
|
+
- Shared: a pool for testing. Don't rely on it against sites that matter.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from surfsky import PremiumProxy, ProxyCycle, ProxyGeo, ProxyTemplate, SharedProxy
|
|
209
|
+
|
|
210
|
+
proxy = PremiumProxy(country="us", region="ny", type="mobile") # Surfsky premium
|
|
211
|
+
proxy = SharedProxy(country="us") # Surfsky shared, for tests
|
|
212
|
+
proxy = ProxyGeo(country="de") # premium if set up, else shared
|
|
213
|
+
proxy = "socks5://user:pass@host:1080" # your own
|
|
214
|
+
proxy = ProxyCycle(my_proxies) # round-robin over your own list
|
|
215
|
+
proxy = ProxyTemplate("http://user-sessid-{session}:pw@gate.example.com:7000")
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`client.proxies` lists the available countries, regions and cities, and your quota.
|
|
219
|
+
|
|
220
|
+
## Profiles and the REST API
|
|
221
|
+
|
|
222
|
+
Profiles, proxies, fingerprints, extensions and account limits are all typed
|
|
223
|
+
calls on the client. Profiles are the ones you'll use most: a profile is a saved
|
|
224
|
+
identity, and every session started on it gets the same fingerprint, proxy and
|
|
225
|
+
cookies.
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from surfsky import AsyncSurfsky, Fingerprint, PremiumProxy
|
|
229
|
+
|
|
230
|
+
async with AsyncSurfsky() as client:
|
|
231
|
+
profile = await client.profiles.create(
|
|
232
|
+
title="account-1",
|
|
233
|
+
fingerprint=Fingerprint(os="win", os_arch="x86", os_version="11"),
|
|
234
|
+
proxy=PremiumProxy(country="us"),
|
|
235
|
+
)
|
|
236
|
+
async with client.browser(profile_uuid=profile.uuid) as browser:
|
|
237
|
+
await browser.goto("https://example.com/login")
|
|
238
|
+
... # log in once, the cookies stay with the profile
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Next time, `client.browser(profile_uuid=...)` brings back the same identity,
|
|
242
|
+
still logged in. `client.profiles.iter_all()` lists your profiles and
|
|
243
|
+
`delete(uuid)` removes one.
|
|
244
|
+
|
|
245
|
+
The sync client, `Surfsky`, has the same REST calls without the browser API.
|
|
246
|
+
For endpoints the SDK doesn't cover, `client.request(method, path, ...)` sends
|
|
247
|
+
the request and returns the raw `httpx.Response`.
|
|
248
|
+
|
|
249
|
+
## API reference
|
|
250
|
+
|
|
251
|
+
Every browser method is async. `Browser` is a `Page` plus the connection: page
|
|
252
|
+
methods on it act on the session's first tab. Waits take `timeout` in seconds,
|
|
253
|
+
default 30, and raise `BrowserTimeoutError`.
|
|
254
|
+
|
|
255
|
+
### Client
|
|
256
|
+
|
|
257
|
+
`AsyncSurfsky(api_token=None, base_url=None, timeout=30, max_retries=3, backoff_factor=0.5)`.
|
|
258
|
+
`Surfsky` is the sync client, REST only.
|
|
259
|
+
|
|
260
|
+
| Method | Description |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `session(profile_uuid=None, **options)` | Start a session, stop it on exit. Yields `Session` with `internal_uuid` and `connect_url`. |
|
|
263
|
+
| `browser(profile_uuid=None, block_resources=None, block_urls=None, **options)` | Start a session and connect a `Browser`. Stops on exit. |
|
|
264
|
+
| `browsers(concurrency="auto", block_resources=None, block_urls=None, **options)` | A `BrowserPool`. Exiting it stops every browser. |
|
|
265
|
+
| `map(handler, items, **pool_options)` | `browsers()` and `pool.map()` in one call. |
|
|
266
|
+
| `with_options(timeout=None, max_retries=None, headers=None)` | Copy with overrides. Same connection pool. |
|
|
267
|
+
| `request(method, path, json=None, params=None, ...)` | Raw call. Returns `httpx.Response`, never raises on status. |
|
|
268
|
+
|
|
269
|
+
Session options: `fingerprint`, `proxy`, `browser_settings`
|
|
270
|
+
(`inactive_kill_timeout`, `cache_enabled`, `cache_key`), `enable_chromedriver`,
|
|
271
|
+
`extensions` (up to 5 uuids), `proxy_blacklist`, `domain_routes`, `cookies`.
|
|
272
|
+
`fingerprint` and `cookies` apply to one-time sessions only.
|
|
273
|
+
|
|
274
|
+
### Pool
|
|
275
|
+
|
|
276
|
+
| Member | Description |
|
|
277
|
+
|---|---|
|
|
278
|
+
| `pool.lease()` | Async context manager. Yields a live browser and hands it back on exit. Waits while all are busy. |
|
|
279
|
+
| `pool.map(handler, items)` | `handler(browser, item)` per item, `capacity` at a time. Returns `PoolOutcome` list in input order: `item`, `index`, `value`, `error`, `ok`. Raise `StopRun` to end early. |
|
|
280
|
+
| `pool.capacity` | Max live browsers. `"auto"` is the plan's limit, `SURFSKY_MAX_BROWSERS` overrides it. |
|
|
281
|
+
| `browser.data` | Per-browser dict. Survives leases. |
|
|
282
|
+
| `browser.use_count` | Leases so far, current included. |
|
|
283
|
+
| `browser.retire()` | Replace this browser with a fresh identity after the lease. |
|
|
284
|
+
| `browser.internal_uuid` | Session id. |
|
|
285
|
+
| `browser.connected` | Socket is up. |
|
|
286
|
+
|
|
287
|
+
The plan limit counts browsers started elsewhere with the same token. `lease()`
|
|
288
|
+
waits for one of its own and raises `RateLimitError` only if it has none.
|
|
289
|
+
|
|
290
|
+
### Navigation
|
|
291
|
+
|
|
292
|
+
| Method | Description |
|
|
293
|
+
|---|---|
|
|
294
|
+
| `goto(url, wait_until="load", timeout=30)` | Navigate. `wait_until`: `commit`, `domcontentloaded`, `load`, `networkidle`. Follows redirects. |
|
|
295
|
+
| `reload(wait_until="load", timeout=30)` | Reload. |
|
|
296
|
+
| `go_back(timeout=30)`, `go_forward(timeout=30)` | Returns the new URL, `None` at the end of history. |
|
|
297
|
+
| `wait_for_load_state(state="load", timeout=30)` | Wait for the current document to reach `state`. |
|
|
298
|
+
| `wait_for_url(fragment, timeout=30)` | Wait until the URL contains `fragment`. Returns the URL. |
|
|
299
|
+
| `status` | HTTP status of the current document. Set even when `goto` raises. |
|
|
300
|
+
|
|
301
|
+
### Reading
|
|
302
|
+
|
|
303
|
+
| Method | Description |
|
|
304
|
+
|---|---|
|
|
305
|
+
| `url()`, `title()` | Current URL and title. |
|
|
306
|
+
| `content()` | Full HTML. |
|
|
307
|
+
| `outer_html(selector)` | HTML of the first match, `None` if none. |
|
|
308
|
+
| `inner_text(selector)`, `all_inner_texts(selector)` | Rendered text of the first match, or of every match. Runs script in the isolated world. |
|
|
309
|
+
| `get_attribute(selector, name)` | `None` if missing. |
|
|
310
|
+
| `count(selector)` | Number of matches. |
|
|
311
|
+
| `is_visible(selector)` | First match has a bounding box. |
|
|
312
|
+
| `wait_for_selector(selector, visible=True, timeout=30)` | Wait for the element, visible by default. |
|
|
313
|
+
| `screenshot(selector=None, full_page=False, format="png", quality=None)` | Bytes. Viewport, one element or the full page. `format`: `png`, `jpeg`, `webp`. |
|
|
314
|
+
|
|
315
|
+
### Input
|
|
316
|
+
|
|
317
|
+
Server-side human emulation. The first CSS match is used. `click`, `dblclick`
|
|
318
|
+
and `hover` also take `wait_for_visible`, `scroll_into_view`, `pre_delay`,
|
|
319
|
+
`post_delay`, `timeout`.
|
|
320
|
+
|
|
321
|
+
| Method | Description |
|
|
322
|
+
|---|---|
|
|
323
|
+
| `click(selector, button=None, click_count=None, modifiers=None)` | `button`: `left`, `right`, `middle`. `modifiers`: `Alt`, `Control`, `Meta`, `Shift`. Waits up to 30s for the element. |
|
|
324
|
+
| `dblclick(selector, ...)` | Double-click. |
|
|
325
|
+
| `hover(selector)` | Move the mouse over it. |
|
|
326
|
+
| `type(selector, text)` | Click, then type after the existing text. |
|
|
327
|
+
| `fill(selector, text)` | Select the existing text, then type over it. |
|
|
328
|
+
| `select_option(selector, value=None, label=None)` | Pick an `<option>` by value or label. Returns the value. |
|
|
329
|
+
| `scroll(delta_x=None, delta_y=None, duration=None)` | Animated scroll. |
|
|
330
|
+
| `scroll_into_view(selector, behavior=None)`, `scroll_to(x=None, y=None, behavior=None)` | `behavior`: `smooth`, `instant`. |
|
|
331
|
+
| `keyboard.type(text)`, `keyboard.press(key, modifiers=None, delay=None)` | Keys to the focused element. `press("Enter")` doesn't submit forms, click the button. |
|
|
332
|
+
| `mouse.move(x, y)`, `mouse.click(x, y)`, `mouse.down(x, y)`, `mouse.up(x, y)`, `mouse.wheel(delta_x, delta_y)`, `mouse.drag(start_x=, start_y=, end_x=, end_y=)` | Viewport coordinates. |
|
|
333
|
+
|
|
334
|
+
### Script
|
|
335
|
+
|
|
336
|
+
| Method | Description |
|
|
337
|
+
|---|---|
|
|
338
|
+
| `evaluate(expression, *args, isolated=True, await_promise=True)` | Run JS. A function is called with `args` as JSON, anything else is an expression. Isolated world by default. |
|
|
339
|
+
| `wait_for_function(expression, *args, isolated=True, timeout=30)` | Poll until truthy. Returns the value. |
|
|
340
|
+
| `send(method, params=None)` | Raw page-level CDP command. |
|
|
341
|
+
| `browser.cdp` | Raw browser-level client: `send`, `post`, `on`. |
|
|
342
|
+
|
|
343
|
+
### Cookies and storage
|
|
344
|
+
|
|
345
|
+
| Method | Description |
|
|
346
|
+
|---|---|
|
|
347
|
+
| `cookies()` | All cookies, `httpOnly` included, as `Cookie` models. |
|
|
348
|
+
| `set_cookies(cookies)` | `Cookie` models or dicts. |
|
|
349
|
+
| `clear_cookies()` | Remove every cookie. |
|
|
350
|
+
| `local_storage()`, `set_local_storage(values)` | Current origin, as a dict. |
|
|
351
|
+
| `session_storage()`, `set_session_storage(values)` | Same for sessionStorage. |
|
|
352
|
+
|
|
353
|
+
### Network
|
|
354
|
+
|
|
355
|
+
| Method | Description |
|
|
356
|
+
|---|---|
|
|
357
|
+
| `capture_responses(*fragments)` | Record responses whose URL contains a fragment. Call before navigating. |
|
|
358
|
+
| `wait_for_response(fragment, timeout=30)` | First captured match. `CapturedResponse`: `url`, `status`, `headers`, `body`, `text`, `json()`. |
|
|
359
|
+
| `responses` | Everything captured, oldest first. |
|
|
360
|
+
| `stop_capturing()` | Drop captures, stop recording. |
|
|
361
|
+
|
|
362
|
+
### Dialogs
|
|
363
|
+
|
|
364
|
+
`page.on_dialog = handler(kind, message)`. `kind`: `alert`, `confirm`, `prompt`,
|
|
365
|
+
`beforeunload`. Return `True` to accept, `False` to dismiss, a string to answer
|
|
366
|
+
a prompt, `None` for the default. Default: dismiss, except `beforeunload` is
|
|
367
|
+
accepted.
|
|
368
|
+
|
|
369
|
+
### Pages
|
|
370
|
+
|
|
371
|
+
| Member | Description |
|
|
372
|
+
|---|---|
|
|
373
|
+
| `browser.pages` | Every open page. The browser's own first, newest last. |
|
|
374
|
+
| `browser.new_page()` | Blank page in a new window. |
|
|
375
|
+
| `browser.wait_for_page(action, timeout=30)` | Await `action` (a click) and return the page it opened. |
|
|
376
|
+
| `page.close()` | Close the tab. On the browser itself: close the connection. |
|
|
377
|
+
| `page.closed` | `True` once gone. Commands then raise `PageClosedError`. |
|
|
378
|
+
| `page.bring_to_front()` | Make it the visible tab. Screenshots of hidden tabs hang. |
|
|
379
|
+
| `page.target_id` | CDP target id. |
|
|
380
|
+
|
|
381
|
+
### REST
|
|
382
|
+
|
|
383
|
+
Same on both clients.
|
|
384
|
+
|
|
385
|
+
| Namespace | Methods |
|
|
386
|
+
|---|---|
|
|
387
|
+
| `client.profiles` | `start_one_time(**options)`, `start(uuid, **options)`, `stop(session)`, `stop_all()`, `list_active()`, `create(title=, fingerprint=, description=, proxy=, cookies=, storage_options=)`, `get(uuid)`, `update(uuid, **fields)`, `delete(uuid)`, `delete_many(uuids)`, `list_page(page=, page_len=, ordering=)`, `iter_all(page_len=100, ordering="created")`, `export_cookies(uuid, export_format="json")`, `import_cookies(uuid, cookies)`, `scrape(session, url, screenshot=, wait=, wait_until=, wait_for=, human_actions=)` |
|
|
388
|
+
| `client.proxies` | `countries()`, `regions(country)`, `cities(country, region)`, `quota()`, `premium_stats()`, `shared_countries()`, `shared_quota()`, `shared_stats()`. The first four need a premium provider on the account. |
|
|
389
|
+
| `client.fingerprints` | `renderers(os, os_arch)`, `screens(os, os_arch)`, `device_models(os=, os_arch=, os_version=, device_type=)` |
|
|
390
|
+
| `client.extensions` | `upload(file, name)` (path, bytes or stream, zip up to 100 MB), `list_all()`, `get(uuid)`, `update(uuid, name=)`, `delete(uuid)` |
|
|
391
|
+
| `client.account` | `session_limits()`, `browser_limits()`, `max_browsers()` |
|
|
392
|
+
|
|
393
|
+
### Errors
|
|
394
|
+
|
|
395
|
+
All subclasses of `SurfskyError`. HTTP: `APIError` subclasses named after the
|
|
396
|
+
status (`NotFoundError`, `RateLimitError`, ...). Browser: `CDPError`,
|
|
397
|
+
`BrowserTimeoutError`, `PageClosedError`. Idempotent requests retry on 429, 5xx
|
|
398
|
+
and connection errors. POST and PATCH retry on 429 only, so a lost reply can't
|
|
399
|
+
start a second billed session.
|
|
400
|
+
|
|
401
|
+
## Examples
|
|
402
|
+
|
|
403
|
+
More examples in [`examples/`](examples/). Install the extras first:
|
|
404
|
+
|
|
405
|
+
```sh
|
|
406
|
+
uv sync --group examples
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Development
|
|
410
|
+
|
|
411
|
+
```sh
|
|
412
|
+
uv sync --all-extras
|
|
413
|
+
uv run ruff check . && uv run ty check && uv run pytest
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Live tests start real sessions and bill your account:
|
|
417
|
+
|
|
418
|
+
```sh
|
|
419
|
+
SURFSKY_LIVE_TESTS=1 SURFSKY_API_TOKEN=... uv run pytest tests/test_live_concurrency.py
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## License
|
|
423
|
+
|
|
424
|
+
MIT
|