ceki-sdk 2.15.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.
- ceki_sdk-2.15.1/.github/workflows/ci.yml +31 -0
- ceki_sdk-2.15.1/.github/workflows/publish.yml +28 -0
- ceki_sdk-2.15.1/.gitignore +17 -0
- ceki_sdk-2.15.1/LICENSE +21 -0
- ceki_sdk-2.15.1/PKG-INFO +294 -0
- ceki_sdk-2.15.1/README.md +265 -0
- ceki_sdk-2.15.1/SMOKE.md +102 -0
- ceki_sdk-2.15.1/ceki_sdk/__init__.py +52 -0
- ceki_sdk-2.15.1/ceki_sdk/_browser.py +646 -0
- ceki_sdk-2.15.1/ceki_sdk/_captcha.py +88 -0
- ceki_sdk-2.15.1/ceki_sdk/_chat.py +203 -0
- ceki_sdk-2.15.1/ceki_sdk/_client.py +501 -0
- ceki_sdk-2.15.1/ceki_sdk/_config.py +19 -0
- ceki_sdk-2.15.1/ceki_sdk/_connect.py +32 -0
- ceki_sdk-2.15.1/ceki_sdk/_exceptions.py +70 -0
- ceki_sdk-2.15.1/ceki_sdk/_models.py +88 -0
- ceki_sdk-2.15.1/ceki_sdk/_profile.py +137 -0
- ceki_sdk-2.15.1/ceki_sdk/_state.py +44 -0
- ceki_sdk-2.15.1/ceki_sdk/cli.py +583 -0
- ceki_sdk-2.15.1/ceki_sdk/humanize/__init__.py +4 -0
- ceki_sdk-2.15.1/ceki_sdk/humanize/humanizer.py +56 -0
- ceki_sdk-2.15.1/ceki_sdk/humanize/keymap.py +70 -0
- ceki_sdk-2.15.1/ceki_sdk/humanize/profile.py +96 -0
- ceki_sdk-2.15.1/ceki_sdk/humanize/profiles/careful.json +30 -0
- ceki_sdk-2.15.1/ceki_sdk/humanize/profiles/natural.json +30 -0
- ceki_sdk-2.15.1/examples/SMOKE.md +61 -0
- ceki_sdk-2.15.1/examples/__init__.py +0 -0
- ceki_sdk-2.15.1/examples/captcha_helper.py +41 -0
- ceki_sdk-2.15.1/examples/github_signup.py +203 -0
- ceki_sdk-2.15.1/examples/hello.py +22 -0
- ceki_sdk-2.15.1/examples/imap_helper.py +69 -0
- ceki_sdk-2.15.1/examples/navigate.py +41 -0
- ceki_sdk-2.15.1/examples/reddit_signup.py +205 -0
- ceki_sdk-2.15.1/examples/scraping.py +21 -0
- ceki_sdk-2.15.1/examples/smoke/README.md +74 -0
- ceki_sdk-2.15.1/examples/smoke/__init__.py +0 -0
- ceki_sdk-2.15.1/examples/smoke/mvp_smoke_v2.py +466 -0
- ceki_sdk-2.15.1/pyproject.toml +52 -0
- ceki_sdk-2.15.1/tests/__init__.py +0 -0
- ceki_sdk-2.15.1/tests/conftest.py +66 -0
- ceki_sdk-2.15.1/tests/e2e/README.md +43 -0
- ceki_sdk-2.15.1/tests/e2e/test_fingerprint_persistence.py +188 -0
- ceki_sdk-2.15.1/tests/test_browser_cdp.py +122 -0
- ceki_sdk-2.15.1/tests/test_browser_errors.py +138 -0
- ceki_sdk-2.15.1/tests/test_browser_release_alias.py +41 -0
- ceki_sdk-2.15.1/tests/test_browser_screenshot_format.py +108 -0
- ceki_sdk-2.15.1/tests/test_captcha.py +369 -0
- ceki_sdk-2.15.1/tests/test_chat.py +246 -0
- ceki_sdk-2.15.1/tests/test_chat_history.py +136 -0
- ceki_sdk-2.15.1/tests/test_chat_image.py +135 -0
- ceki_sdk-2.15.1/tests/test_cli.py +436 -0
- ceki_sdk-2.15.1/tests/test_connect.py +48 -0
- ceki_sdk-2.15.1/tests/test_error_mapping.py +164 -0
- ceki_sdk-2.15.1/tests/test_examples_signature.py +25 -0
- ceki_sdk-2.15.1/tests/test_humanize_browser.py +127 -0
- ceki_sdk-2.15.1/tests/test_multi_session.py +129 -0
- ceki_sdk-2.15.1/tests/test_profile.py +277 -0
- ceki_sdk-2.15.1/tests/test_provider_disconnect.py +117 -0
- ceki_sdk-2.15.1/tests/test_provider_offline.py +59 -0
- ceki_sdk-2.15.1/tests/test_reconnect.py +57 -0
- ceki_sdk-2.15.1/tests/test_rent_flow.py +183 -0
- ceki_sdk-2.15.1/tests/test_search.py +157 -0
- ceki_sdk-2.15.1/tests/test_sessions.py +106 -0
- ceki_sdk-2.15.1/tests/test_state_persistence.py +112 -0
- ceki_sdk-2.15.1/tests/test_switch_tab.py +65 -0
- ceki_sdk-2.15.1/tests/test_type_keyboard_events.py +95 -0
- ceki_sdk-2.15.1/tests/test_type_with_pointer.py +144 -0
- ceki_sdk-2.15.1/tests/test_upload.py +282 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, "feature/**"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: pip install -e ".[dev]"
|
|
26
|
+
|
|
27
|
+
- name: Lint
|
|
28
|
+
run: ruff check .
|
|
29
|
+
|
|
30
|
+
- name: Test
|
|
31
|
+
run: pytest -v
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write
|
|
12
|
+
contents: read
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
|
|
20
|
+
- name: Build
|
|
21
|
+
run: |
|
|
22
|
+
pip install build
|
|
23
|
+
python -m build
|
|
24
|
+
|
|
25
|
+
- name: Publish to PyPI
|
|
26
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
27
|
+
with:
|
|
28
|
+
attestations: false
|
ceki_sdk-2.15.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ceki.me
|
|
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.
|
ceki_sdk-2.15.1/PKG-INFO
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ceki-sdk
|
|
3
|
+
Version: 2.15.1
|
|
4
|
+
Summary: Python SDK for browser.ceki.me — rent real browsers from real people
|
|
5
|
+
Project-URL: Homepage, https://ceki.me
|
|
6
|
+
Project-URL: Repository, https://github.com/Ceki-me/python-sdk
|
|
7
|
+
Author-email: "Ceki.me" <hello@ceki.me>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai-agent,automation,browser,ceki,websocket
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx>=0.27
|
|
21
|
+
Requires-Dist: pydantic>=2
|
|
22
|
+
Requires-Dist: websockets>=12
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# ceki-sdk
|
|
31
|
+
|
|
32
|
+
Python SDK for [browser.ceki.me](https://browser.ceki.me) — rent real browsers from real people for AI agent automation.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install ceki-sdk
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
import os
|
|
45
|
+
from ceki_sdk import connect, ConnectOptions
|
|
46
|
+
|
|
47
|
+
async def main():
|
|
48
|
+
client = await connect(os.environ["CEKI_API_KEY"])
|
|
49
|
+
options = await client.search({"geo": "US", "language": "en"})
|
|
50
|
+
browser = await client.rent(options[0].schedule_id)
|
|
51
|
+
# ... CDP calls (see docs)
|
|
52
|
+
await browser.close()
|
|
53
|
+
await client.close()
|
|
54
|
+
|
|
55
|
+
asyncio.run(main())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**BREAKING in 2.2.0:** `connect()` no longer accepts `relay_url=` or `reconnect=` kwargs — pass a `ConnectOptions` object instead.
|
|
59
|
+
|
|
60
|
+
## Environment Variables
|
|
61
|
+
|
|
62
|
+
| Variable | Description |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `CEKI_API_KEY` | Your API key (required) |
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
### `connect(api_key, options: ConnectOptions | None = None) -> Client`
|
|
69
|
+
|
|
70
|
+
Establish a WebSocket connection to the relay. Returns a `Client` instance.
|
|
71
|
+
|
|
72
|
+
### `ConnectOptions`
|
|
73
|
+
|
|
74
|
+
| Field | Default | Description |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `reconnect` | `True` | Auto-reconnect on disconnect |
|
|
77
|
+
|
|
78
|
+
### `client.search(filters=None, limit=20) -> list[BrowserOption]`
|
|
79
|
+
|
|
80
|
+
Search for available browsers. Filters: `geo`, `language`, etc.
|
|
81
|
+
|
|
82
|
+
### `client.rent(schedule_id) -> Browser`
|
|
83
|
+
|
|
84
|
+
Rent a browser by schedule ID. Waits up to 60s for a match.
|
|
85
|
+
|
|
86
|
+
### `client.close()`
|
|
87
|
+
|
|
88
|
+
Close all sessions and the connection.
|
|
89
|
+
|
|
90
|
+
## Error Codes
|
|
91
|
+
|
|
92
|
+
| Exception | Cause |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `AuthFailed` | Invalid API key or token revoked |
|
|
95
|
+
| `RateLimitExceeded` | Too many requests. Has `.retry_after` (seconds) |
|
|
96
|
+
| `InsufficientFunds` | Account balance too low |
|
|
97
|
+
| `SessionEnded` | Provider ended the session. Has `.reason` |
|
|
98
|
+
| `CdpUnrecoverable` | CDP connection lost permanently |
|
|
99
|
+
| `ConnectionLost` | Relay connection lost after max reconnects |
|
|
100
|
+
|
|
101
|
+
## Session profile (cookies + storage)
|
|
102
|
+
|
|
103
|
+
`browser.profile` lets you snapshot and restore cookies, `localStorage`, and `sessionStorage` between sessions — without involving the relay or backend. The blob stays in your own storage.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import json
|
|
107
|
+
|
|
108
|
+
# First session — sign up, then export profile
|
|
109
|
+
async with await client.rent(schedule_id) as browser:
|
|
110
|
+
await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com/login"}})
|
|
111
|
+
# ... perform signup, 2FA ...
|
|
112
|
+
profile = await browser.profile.export(domains=[".reddit.com", "reddit.com"])
|
|
113
|
+
|
|
114
|
+
with open("reddit_profile.json", "w") as f:
|
|
115
|
+
json.dump(profile, f)
|
|
116
|
+
|
|
117
|
+
# Next session — restore profile (navigate first, then import storage)
|
|
118
|
+
with open("reddit_profile.json") as f:
|
|
119
|
+
profile = json.load(f)
|
|
120
|
+
|
|
121
|
+
async with await client.rent(schedule_id) as browser:
|
|
122
|
+
# Cookies are domain-scoped — set them before navigation
|
|
123
|
+
await browser.profile.import_(profile)
|
|
124
|
+
await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com"}})
|
|
125
|
+
# already logged in
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Notes:**
|
|
129
|
+
- `localStorage`/`sessionStorage` require a document context — navigate to the target origin before calling `import_()`, or call it right after navigation.
|
|
130
|
+
- Cookies (`Network.setCookies`) work before any navigation.
|
|
131
|
+
- Use `domains` to export only relevant cookies and avoid importing third-party trackers.
|
|
132
|
+
- Encrypt the blob before writing to disk if it contains sensitive credentials.
|
|
133
|
+
- `import_()` raises `ValueError` on `schema_version` mismatch (future-proofing).
|
|
134
|
+
|
|
135
|
+
## CDP Lifecycle
|
|
136
|
+
|
|
137
|
+
The relay maintains the CDP connection to the incognito browser tab. If the connection drops, it automatically reattaches with 1s/2s/4s exponential backoff. Commands during reattach are buffered (FIFO, max 50). If 3 reattach attempts fail, a new fallback tab is created. If that also fails, `cdp_unrecoverable` error is sent.
|
|
138
|
+
|
|
139
|
+
## Real-signup examples
|
|
140
|
+
|
|
141
|
+
See `examples/SMOKE.md` for full runbook.
|
|
142
|
+
|
|
143
|
+
Quick:
|
|
144
|
+
```bash
|
|
145
|
+
pip install -e ".[dev]"
|
|
146
|
+
export CEKI_API_KEY=...
|
|
147
|
+
export SCHEDULE_ID=...
|
|
148
|
+
python examples/reddit_signup.py
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
These are NOT automated tests — they require a live relay, an online provider, and a real IMAP mailbox. Run manually as part of Phase 2 acceptance.
|
|
152
|
+
|
|
153
|
+
## Human Mode
|
|
154
|
+
|
|
155
|
+
Browser actions can optionally include human-like timing — delays before/after actions and per-character typing with jitter.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# Default: natural profile (enabled by default)
|
|
159
|
+
browser = await client.rent(schedule_id)
|
|
160
|
+
|
|
161
|
+
# Explicit profile
|
|
162
|
+
browser = await client.rent(schedule_id, human="careful")
|
|
163
|
+
|
|
164
|
+
# Disable humanization
|
|
165
|
+
browser = await client.rent(schedule_id, human=None)
|
|
166
|
+
|
|
167
|
+
# Custom profile dict
|
|
168
|
+
browser = await client.rent(schedule_id, human={"typing": {"wpm": 130}})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### High-level methods
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
await browser.navigate("https://example.com")
|
|
175
|
+
await browser.click(100, 200)
|
|
176
|
+
await browser.type("Hello, world!") # Per-char with jitter when human mode on
|
|
177
|
+
await browser.scroll(delta_y=-300)
|
|
178
|
+
img_bytes = await browser.screenshot()
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Runtime control
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
prev = browser.set_human("careful") # Switch profile, returns previous
|
|
185
|
+
browser.set_human(None) # Disable mid-session
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Environment variables
|
|
189
|
+
|
|
190
|
+
- `CEKI_HUMAN_PROFILE` — Override default profile name (e.g., `careful`)
|
|
191
|
+
- `CEKI_HUMAN_PROFILE_PATH` — Path to custom JSON profile file
|
|
192
|
+
- `CEKI_HUMAN_DISABLE=1` — Disable humanization entirely
|
|
193
|
+
|
|
194
|
+
## CLI
|
|
195
|
+
|
|
196
|
+
The SDK installs a `ceki` CLI binary on your PATH.
|
|
197
|
+
|
|
198
|
+
### Install
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
pip install ceki-sdk
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Environment variables
|
|
205
|
+
|
|
206
|
+
| Variable | Required | Purpose |
|
|
207
|
+
|---|---|---|
|
|
208
|
+
| `CEKI_API_KEY` | yes | Agent token (`ag_...`) |
|
|
209
|
+
|
|
210
|
+
### Quick start
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
export CEKI_API_KEY=ag_...
|
|
214
|
+
|
|
215
|
+
SCHEDULE=$(ceki search --limit 1 | jq -r '.[0].schedule_id')
|
|
216
|
+
SID=$(ceki rent --schedule $SCHEDULE | jq -r .session_id)
|
|
217
|
+
ceki navigate $SID https://example.com
|
|
218
|
+
ceki snapshot $SID -o snap.png
|
|
219
|
+
ceki stop $SID
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The CLI persists session state locally — after `rent` it saves the session ID so subsequent commands resume it by SID without re-renting.
|
|
223
|
+
|
|
224
|
+
### Commands
|
|
225
|
+
|
|
226
|
+
#### Discovery and lifecycle
|
|
227
|
+
|
|
228
|
+
| Command | Description |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `search [--limit N] [--filter K=V]…` | List available browsers |
|
|
231
|
+
| `my-browsers` | List browsers with pre-arranged rent contracts |
|
|
232
|
+
| `rent --schedule ID [--mode incognito\|main] [--fingerprint-from FILE]` | Rent a browser |
|
|
233
|
+
| `sessions [--all] [--limit N] [--json]` | List your sessions |
|
|
234
|
+
| `stop SID` | End a session |
|
|
235
|
+
| `wait SID` | Block until the session ends |
|
|
236
|
+
|
|
237
|
+
#### Browser control
|
|
238
|
+
|
|
239
|
+
| Command | Description |
|
|
240
|
+
|---|---|
|
|
241
|
+
| `navigate SID URL` | Open URL |
|
|
242
|
+
| `click SID X Y` | Click at viewport coordinates |
|
|
243
|
+
| `type SID TEXT [--natural]` | Type text into focused element |
|
|
244
|
+
| `scroll SID X Y DY` | Scroll from (X, Y) by `DY` pixels |
|
|
245
|
+
| `screenshot SID -o FILE [--format png\|jpeg] [--full]` | Save screenshot |
|
|
246
|
+
| `snapshot SID -o FILE` | Screenshot + new chat messages |
|
|
247
|
+
| `switch-tab SID` | Switch active tab |
|
|
248
|
+
| `upload SID --selector CSS --file PATH [--filename NAME]` | Attach file to `<input type="file">` |
|
|
249
|
+
|
|
250
|
+
#### Chat with host
|
|
251
|
+
|
|
252
|
+
| Command | Description |
|
|
253
|
+
|---|---|
|
|
254
|
+
| `chat SID send TEXT` | Send message to host |
|
|
255
|
+
| `chat SID next [--timeout SEC]` | Wait for next host message |
|
|
256
|
+
| `chat SID history [--since TS] [--limit N]` | Fetch chat history |
|
|
257
|
+
| `chat SID send-image --image PATH [--text MSG]` | Send image to host |
|
|
258
|
+
|
|
259
|
+
#### Advanced
|
|
260
|
+
|
|
261
|
+
| Command | Description |
|
|
262
|
+
|---|---|
|
|
263
|
+
| `profile SID export -o FILE [--domains CSV] [--no-session-storage]` | Export cookies / localStorage |
|
|
264
|
+
| `profile SID import -i FILE` | Import previously exported profile |
|
|
265
|
+
| `request-captcha SID [--acceptance SEC] [--completion SEC] [--manual]` | Ask host to solve CAPTCHA |
|
|
266
|
+
| `configure SID [--masking-mode VAL] [--fingerprint VAL]` | Toggle masking / fingerprint |
|
|
267
|
+
| `cdp SID --method METHOD [--params JSON]` | Raw CDP command |
|
|
268
|
+
|
|
269
|
+
### Output and errors
|
|
270
|
+
|
|
271
|
+
Successful commands write a single JSON line to stdout. Errors go to stderr as `{"error": "...", "code": "..."}`. Pipe stdout through `jq` to chain commands.
|
|
272
|
+
|
|
273
|
+
### Exit codes
|
|
274
|
+
|
|
275
|
+
| Code | Meaning |
|
|
276
|
+
|---|---|
|
|
277
|
+
| `0` | success |
|
|
278
|
+
| `1` | generic error |
|
|
279
|
+
| `2` | `CEKI_API_KEY` not set |
|
|
280
|
+
| `3` | session not found or not owner |
|
|
281
|
+
| `4` | timeout |
|
|
282
|
+
| `5` | network / connection error |
|
|
283
|
+
| `130` | interrupted (Ctrl-C) |
|
|
284
|
+
|
|
285
|
+
Full reference (with EN+RU): https://browser.ceki.me/docs#cli
|
|
286
|
+
|
|
287
|
+
## Development
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
pip install -e ".[dev]"
|
|
291
|
+
pytest
|
|
292
|
+
ruff check ceki_sdk/
|
|
293
|
+
mypy ceki_sdk/
|
|
294
|
+
```
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# ceki-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for [browser.ceki.me](https://browser.ceki.me) — rent real browsers from real people for AI agent automation.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ceki-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import asyncio
|
|
15
|
+
import os
|
|
16
|
+
from ceki_sdk import connect, ConnectOptions
|
|
17
|
+
|
|
18
|
+
async def main():
|
|
19
|
+
client = await connect(os.environ["CEKI_API_KEY"])
|
|
20
|
+
options = await client.search({"geo": "US", "language": "en"})
|
|
21
|
+
browser = await client.rent(options[0].schedule_id)
|
|
22
|
+
# ... CDP calls (see docs)
|
|
23
|
+
await browser.close()
|
|
24
|
+
await client.close()
|
|
25
|
+
|
|
26
|
+
asyncio.run(main())
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**BREAKING in 2.2.0:** `connect()` no longer accepts `relay_url=` or `reconnect=` kwargs — pass a `ConnectOptions` object instead.
|
|
30
|
+
|
|
31
|
+
## Environment Variables
|
|
32
|
+
|
|
33
|
+
| Variable | Description |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `CEKI_API_KEY` | Your API key (required) |
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### `connect(api_key, options: ConnectOptions | None = None) -> Client`
|
|
40
|
+
|
|
41
|
+
Establish a WebSocket connection to the relay. Returns a `Client` instance.
|
|
42
|
+
|
|
43
|
+
### `ConnectOptions`
|
|
44
|
+
|
|
45
|
+
| Field | Default | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `reconnect` | `True` | Auto-reconnect on disconnect |
|
|
48
|
+
|
|
49
|
+
### `client.search(filters=None, limit=20) -> list[BrowserOption]`
|
|
50
|
+
|
|
51
|
+
Search for available browsers. Filters: `geo`, `language`, etc.
|
|
52
|
+
|
|
53
|
+
### `client.rent(schedule_id) -> Browser`
|
|
54
|
+
|
|
55
|
+
Rent a browser by schedule ID. Waits up to 60s for a match.
|
|
56
|
+
|
|
57
|
+
### `client.close()`
|
|
58
|
+
|
|
59
|
+
Close all sessions and the connection.
|
|
60
|
+
|
|
61
|
+
## Error Codes
|
|
62
|
+
|
|
63
|
+
| Exception | Cause |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `AuthFailed` | Invalid API key or token revoked |
|
|
66
|
+
| `RateLimitExceeded` | Too many requests. Has `.retry_after` (seconds) |
|
|
67
|
+
| `InsufficientFunds` | Account balance too low |
|
|
68
|
+
| `SessionEnded` | Provider ended the session. Has `.reason` |
|
|
69
|
+
| `CdpUnrecoverable` | CDP connection lost permanently |
|
|
70
|
+
| `ConnectionLost` | Relay connection lost after max reconnects |
|
|
71
|
+
|
|
72
|
+
## Session profile (cookies + storage)
|
|
73
|
+
|
|
74
|
+
`browser.profile` lets you snapshot and restore cookies, `localStorage`, and `sessionStorage` between sessions — without involving the relay or backend. The blob stays in your own storage.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import json
|
|
78
|
+
|
|
79
|
+
# First session — sign up, then export profile
|
|
80
|
+
async with await client.rent(schedule_id) as browser:
|
|
81
|
+
await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com/login"}})
|
|
82
|
+
# ... perform signup, 2FA ...
|
|
83
|
+
profile = await browser.profile.export(domains=[".reddit.com", "reddit.com"])
|
|
84
|
+
|
|
85
|
+
with open("reddit_profile.json", "w") as f:
|
|
86
|
+
json.dump(profile, f)
|
|
87
|
+
|
|
88
|
+
# Next session — restore profile (navigate first, then import storage)
|
|
89
|
+
with open("reddit_profile.json") as f:
|
|
90
|
+
profile = json.load(f)
|
|
91
|
+
|
|
92
|
+
async with await client.rent(schedule_id) as browser:
|
|
93
|
+
# Cookies are domain-scoped — set them before navigation
|
|
94
|
+
await browser.profile.import_(profile)
|
|
95
|
+
await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com"}})
|
|
96
|
+
# already logged in
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Notes:**
|
|
100
|
+
- `localStorage`/`sessionStorage` require a document context — navigate to the target origin before calling `import_()`, or call it right after navigation.
|
|
101
|
+
- Cookies (`Network.setCookies`) work before any navigation.
|
|
102
|
+
- Use `domains` to export only relevant cookies and avoid importing third-party trackers.
|
|
103
|
+
- Encrypt the blob before writing to disk if it contains sensitive credentials.
|
|
104
|
+
- `import_()` raises `ValueError` on `schema_version` mismatch (future-proofing).
|
|
105
|
+
|
|
106
|
+
## CDP Lifecycle
|
|
107
|
+
|
|
108
|
+
The relay maintains the CDP connection to the incognito browser tab. If the connection drops, it automatically reattaches with 1s/2s/4s exponential backoff. Commands during reattach are buffered (FIFO, max 50). If 3 reattach attempts fail, a new fallback tab is created. If that also fails, `cdp_unrecoverable` error is sent.
|
|
109
|
+
|
|
110
|
+
## Real-signup examples
|
|
111
|
+
|
|
112
|
+
See `examples/SMOKE.md` for full runbook.
|
|
113
|
+
|
|
114
|
+
Quick:
|
|
115
|
+
```bash
|
|
116
|
+
pip install -e ".[dev]"
|
|
117
|
+
export CEKI_API_KEY=...
|
|
118
|
+
export SCHEDULE_ID=...
|
|
119
|
+
python examples/reddit_signup.py
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
These are NOT automated tests — they require a live relay, an online provider, and a real IMAP mailbox. Run manually as part of Phase 2 acceptance.
|
|
123
|
+
|
|
124
|
+
## Human Mode
|
|
125
|
+
|
|
126
|
+
Browser actions can optionally include human-like timing — delays before/after actions and per-character typing with jitter.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# Default: natural profile (enabled by default)
|
|
130
|
+
browser = await client.rent(schedule_id)
|
|
131
|
+
|
|
132
|
+
# Explicit profile
|
|
133
|
+
browser = await client.rent(schedule_id, human="careful")
|
|
134
|
+
|
|
135
|
+
# Disable humanization
|
|
136
|
+
browser = await client.rent(schedule_id, human=None)
|
|
137
|
+
|
|
138
|
+
# Custom profile dict
|
|
139
|
+
browser = await client.rent(schedule_id, human={"typing": {"wpm": 130}})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### High-level methods
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
await browser.navigate("https://example.com")
|
|
146
|
+
await browser.click(100, 200)
|
|
147
|
+
await browser.type("Hello, world!") # Per-char with jitter when human mode on
|
|
148
|
+
await browser.scroll(delta_y=-300)
|
|
149
|
+
img_bytes = await browser.screenshot()
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Runtime control
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
prev = browser.set_human("careful") # Switch profile, returns previous
|
|
156
|
+
browser.set_human(None) # Disable mid-session
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Environment variables
|
|
160
|
+
|
|
161
|
+
- `CEKI_HUMAN_PROFILE` — Override default profile name (e.g., `careful`)
|
|
162
|
+
- `CEKI_HUMAN_PROFILE_PATH` — Path to custom JSON profile file
|
|
163
|
+
- `CEKI_HUMAN_DISABLE=1` — Disable humanization entirely
|
|
164
|
+
|
|
165
|
+
## CLI
|
|
166
|
+
|
|
167
|
+
The SDK installs a `ceki` CLI binary on your PATH.
|
|
168
|
+
|
|
169
|
+
### Install
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
pip install ceki-sdk
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Environment variables
|
|
176
|
+
|
|
177
|
+
| Variable | Required | Purpose |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| `CEKI_API_KEY` | yes | Agent token (`ag_...`) |
|
|
180
|
+
|
|
181
|
+
### Quick start
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
export CEKI_API_KEY=ag_...
|
|
185
|
+
|
|
186
|
+
SCHEDULE=$(ceki search --limit 1 | jq -r '.[0].schedule_id')
|
|
187
|
+
SID=$(ceki rent --schedule $SCHEDULE | jq -r .session_id)
|
|
188
|
+
ceki navigate $SID https://example.com
|
|
189
|
+
ceki snapshot $SID -o snap.png
|
|
190
|
+
ceki stop $SID
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The CLI persists session state locally — after `rent` it saves the session ID so subsequent commands resume it by SID without re-renting.
|
|
194
|
+
|
|
195
|
+
### Commands
|
|
196
|
+
|
|
197
|
+
#### Discovery and lifecycle
|
|
198
|
+
|
|
199
|
+
| Command | Description |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `search [--limit N] [--filter K=V]…` | List available browsers |
|
|
202
|
+
| `my-browsers` | List browsers with pre-arranged rent contracts |
|
|
203
|
+
| `rent --schedule ID [--mode incognito\|main] [--fingerprint-from FILE]` | Rent a browser |
|
|
204
|
+
| `sessions [--all] [--limit N] [--json]` | List your sessions |
|
|
205
|
+
| `stop SID` | End a session |
|
|
206
|
+
| `wait SID` | Block until the session ends |
|
|
207
|
+
|
|
208
|
+
#### Browser control
|
|
209
|
+
|
|
210
|
+
| Command | Description |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `navigate SID URL` | Open URL |
|
|
213
|
+
| `click SID X Y` | Click at viewport coordinates |
|
|
214
|
+
| `type SID TEXT [--natural]` | Type text into focused element |
|
|
215
|
+
| `scroll SID X Y DY` | Scroll from (X, Y) by `DY` pixels |
|
|
216
|
+
| `screenshot SID -o FILE [--format png\|jpeg] [--full]` | Save screenshot |
|
|
217
|
+
| `snapshot SID -o FILE` | Screenshot + new chat messages |
|
|
218
|
+
| `switch-tab SID` | Switch active tab |
|
|
219
|
+
| `upload SID --selector CSS --file PATH [--filename NAME]` | Attach file to `<input type="file">` |
|
|
220
|
+
|
|
221
|
+
#### Chat with host
|
|
222
|
+
|
|
223
|
+
| Command | Description |
|
|
224
|
+
|---|---|
|
|
225
|
+
| `chat SID send TEXT` | Send message to host |
|
|
226
|
+
| `chat SID next [--timeout SEC]` | Wait for next host message |
|
|
227
|
+
| `chat SID history [--since TS] [--limit N]` | Fetch chat history |
|
|
228
|
+
| `chat SID send-image --image PATH [--text MSG]` | Send image to host |
|
|
229
|
+
|
|
230
|
+
#### Advanced
|
|
231
|
+
|
|
232
|
+
| Command | Description |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `profile SID export -o FILE [--domains CSV] [--no-session-storage]` | Export cookies / localStorage |
|
|
235
|
+
| `profile SID import -i FILE` | Import previously exported profile |
|
|
236
|
+
| `request-captcha SID [--acceptance SEC] [--completion SEC] [--manual]` | Ask host to solve CAPTCHA |
|
|
237
|
+
| `configure SID [--masking-mode VAL] [--fingerprint VAL]` | Toggle masking / fingerprint |
|
|
238
|
+
| `cdp SID --method METHOD [--params JSON]` | Raw CDP command |
|
|
239
|
+
|
|
240
|
+
### Output and errors
|
|
241
|
+
|
|
242
|
+
Successful commands write a single JSON line to stdout. Errors go to stderr as `{"error": "...", "code": "..."}`. Pipe stdout through `jq` to chain commands.
|
|
243
|
+
|
|
244
|
+
### Exit codes
|
|
245
|
+
|
|
246
|
+
| Code | Meaning |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `0` | success |
|
|
249
|
+
| `1` | generic error |
|
|
250
|
+
| `2` | `CEKI_API_KEY` not set |
|
|
251
|
+
| `3` | session not found or not owner |
|
|
252
|
+
| `4` | timeout |
|
|
253
|
+
| `5` | network / connection error |
|
|
254
|
+
| `130` | interrupted (Ctrl-C) |
|
|
255
|
+
|
|
256
|
+
Full reference (with EN+RU): https://browser.ceki.me/docs#cli
|
|
257
|
+
|
|
258
|
+
## Development
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
pip install -e ".[dev]"
|
|
262
|
+
pytest
|
|
263
|
+
ruff check ceki_sdk/
|
|
264
|
+
mypy ceki_sdk/
|
|
265
|
+
```
|