Python-клиент robotex: sync/async, сессии, задачи v2, ошибки

Пакет в client/ (зависимость — httpx), установка через pip из git или
wheel (make client-build). Тесты против api и воркера из harness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
protokeyandClaude Opus 5.5 committed 2026-10-13 22:18:40 +04:00
1 parent 11ba83140d
commit cb85e5f3ab
16 files changed
+1004 -1

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
# robotex — Python-клиент Robotex API
Обвязка над HTTP API Robotex (docs/API.md): задачи v2 через очередь, синхронный `/v1/solve`, сессии с удержанием окна
браузера. Синхронный `Robotex` и асинхронный `AsyncRobotex` с одинаковым интерфейсом. Зависимость одна — `httpx`.
## Установка
```bash
pip install "robotex @ git+https://git.sokolab.xyz/s0k0l/robotex.git@v0.1.0#subdirectory=client"
```
или из wheel (`make client-build` в репозитории кладёт его в `client/dist/`):
```bash
pip install robotex-0.1.0-py3-none-any.whl
```
## Быстрый старт
```python
from robotex import Robotex, Action
with Robotex("https://robotex.example.com", api_key="rbx_...") as rx:
result = rx.run(
"https://example.com",
[Action.wait_for("h1"), Action.inner_text("h1")],
html=True,
).raise_for_status()
print(result.results[0], result.user_agent, result.cookie("cf_clearance"))
```
`base_url` и `api_key` можно не передавать — тогда берутся из `ROBOTEX_URL` и `ROBOTEX_API_KEY`.
Остальные параметры запроса передаются именованными аргументами как есть: `proxy`, `cookies`, `html`, `screenshot`,
`timeout`, `captcha`, `captcha_type`, `disable_resources`, `blocked_domains`, `retries`, `wait`, `google_search` и т. д.
## Методы
| Метод | Что делает |
|---|---|
| `run(url, actions, **opts)` | задача v2: поставить в очередь и дождаться результата (`wait=` секунд, по умолчанию 600) |
| `submit(url, actions, **opts)` | только поставить; вернёт `TaskRef` с `.status()` и `.wait()` |
| `solve(url, actions, **opts)` | синхронный `/v1/solve`; `NoSlotsError`, если свободных браузеров нет |
| `session(session_id=None, idle_timeout=5)` | сессия: один профиль и окно, которое держится `idle_timeout` минут |
| `close_session(session_id)` | закрыть окно сессии и освободить слот |
| `health()` | слоты и воркеры |
Вызовы без сессии каждый раз получают новый профиль браузера.
## Сессии и удержание окна
```python
with rx.session(idle_timeout=10) as s:
s.run("https://site/login", [
Action.typing("#login", "me"),
Action.typing("#password", "secret"),
Action.submit("button[type=submit]"),
]).raise_for_status()
# без url — на текущей странице того же окна, без повторного запуска Chrome и антибота
balance = s.run(actions=[Action.inner_text(".balance")]).results[0]
saved = s.id
# на выходе из with окно закрывается (слот освобождается через секунду-две), профиль остаётся
again = rx.session(saved).run("https://site/account") # продолжить сессию позже
```
## Асинхронно
```python
import asyncio
from robotex import AsyncRobotex, Action
async def main():
async with AsyncRobotex() as rx:
results = await asyncio.gather(*(rx.run(url, [Action.inner_text("h1")]) for url in urls))
async with rx.session(idle_timeout=5) as s:
await s.run("https://example.com")
```
## Результат и ошибки
`Result`: `status` (`ok`/`err`), `code`, `error`, `user_agent`, `cookies`, `actions` (`ActionResult`), `results`
(тексты действий по порядку), `html`, `screenshot` (base64, `screenshot_bytes()`), `duration_ms`, `session_id`.
Ошибка выполнения (`challenge_failed`, `action_failed`, `request_failed`) — это результат со `status="err"`, исключения
нет; `raise_for_status()` превращает её в `ResultError` (в `e.result` всё собранное: cookies, html, скриншот).
Исключения (все от `RobotexError`):
| Исключение | Когда |
|---|---|
| `AuthError` | 401 — ключ не передан, неверен или отозван |
| `InvalidRequest` | 422 — неверное действие, нет `url` без открытого окна и т. п. |
| `NoSlotsError` | 503 у `solve()`; `retry_after` — через сколько секунд повторить |
| `NotFound` | 404 — задача истекла или из другой сессии |
| `TaskFailed` | задача `failed`/`cancelled` (например, упал воркер) |
| `TaskTimeout` | `wait` истёк; задача продолжает выполняться, `TaskRef.wait()` можно повторить |
| `ResultError` | `raise_for_status()` на результате со `status="err"` |
## Действия
`Action.fill`, `typing`, `click`, `submit`, `mouse_move`, `delay(seconds)`, `wait_for(selector, timeout=None)`,
`inner_html`, `inner_text`, `captcha(selector, kind)`, `pass_challenge()`. Вместо `Action` можно передать dict в формате
API: `{"type": "click", "selector": "#a"}`.