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.
Files changed (53) hide show
  1. surfsky-0.0.1/.gitignore +24 -0
  2. surfsky-0.0.1/LICENSE +21 -0
  3. surfsky-0.0.1/PKG-INFO +424 -0
  4. surfsky-0.0.1/README.md +397 -0
  5. surfsky-0.0.1/examples/basic_scrape.py +39 -0
  6. surfsky-0.0.1/examples/duckduckgo.py +53 -0
  7. surfsky-0.0.1/examples/fingerprint.py +53 -0
  8. surfsky-0.0.1/examples/form.py +54 -0
  9. surfsky-0.0.1/examples/login.py +44 -0
  10. surfsky-0.0.1/examples/multi_account.py +89 -0
  11. surfsky-0.0.1/examples/one_time.py +23 -0
  12. surfsky-0.0.1/examples/parallel_urls.py +51 -0
  13. surfsky-0.0.1/examples/persistent.py +34 -0
  14. surfsky-0.0.1/examples/playwright_connect.py +30 -0
  15. surfsky-0.0.1/examples/popup.py +32 -0
  16. surfsky-0.0.1/examples/premium_proxy.py +38 -0
  17. surfsky-0.0.1/examples/selenium_connect.py +29 -0
  18. surfsky-0.0.1/examples/tabs.py +47 -0
  19. surfsky-0.0.1/pyproject.toml +88 -0
  20. surfsky-0.0.1/src/surfsky/__init__.py +91 -0
  21. surfsky-0.0.1/src/surfsky/browser/__init__.py +5 -0
  22. surfsky-0.0.1/src/surfsky/browser/actions.py +199 -0
  23. surfsky-0.0.1/src/surfsky/browser/browser.py +346 -0
  24. surfsky-0.0.1/src/surfsky/browser/cdp.py +153 -0
  25. surfsky-0.0.1/src/surfsky/browser/page.py +669 -0
  26. surfsky-0.0.1/src/surfsky/browser/pool.py +248 -0
  27. surfsky-0.0.1/src/surfsky/client.py +321 -0
  28. surfsky-0.0.1/src/surfsky/errors.py +97 -0
  29. surfsky-0.0.1/src/surfsky/proxy.py +106 -0
  30. surfsky-0.0.1/src/surfsky/py.typed +0 -0
  31. surfsky-0.0.1/src/surfsky/resources/__init__.py +5 -0
  32. surfsky-0.0.1/src/surfsky/resources/account.py +68 -0
  33. surfsky-0.0.1/src/surfsky/resources/extensions.py +100 -0
  34. surfsky-0.0.1/src/surfsky/resources/fingerprints.py +77 -0
  35. surfsky-0.0.1/src/surfsky/resources/profiles.py +424 -0
  36. surfsky-0.0.1/src/surfsky/resources/proxies.py +110 -0
  37. surfsky-0.0.1/src/surfsky/transport.py +312 -0
  38. surfsky-0.0.1/src/surfsky/types.py +430 -0
  39. surfsky-0.0.1/tests/conftest.py +18 -0
  40. surfsky-0.0.1/tests/test_actions.py +81 -0
  41. surfsky-0.0.1/tests/test_browser_session.py +1297 -0
  42. surfsky-0.0.1/tests/test_cdp.py +299 -0
  43. surfsky-0.0.1/tests/test_client.py +292 -0
  44. surfsky-0.0.1/tests/test_extensions.py +77 -0
  45. surfsky-0.0.1/tests/test_fingerprints.py +62 -0
  46. surfsky-0.0.1/tests/test_http.py +244 -0
  47. surfsky-0.0.1/tests/test_integration.py +157 -0
  48. surfsky-0.0.1/tests/test_live_concurrency.py +88 -0
  49. surfsky-0.0.1/tests/test_pages.py +486 -0
  50. surfsky-0.0.1/tests/test_pool.py +750 -0
  51. surfsky-0.0.1/tests/test_profiles.py +445 -0
  52. surfsky-0.0.1/tests/test_proxy.py +351 -0
  53. surfsky-0.0.1/tests/test_types.py +70 -0
@@ -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
+ ![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue)
33
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
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