Files
robotex/client
protokeyandClaude Opus 5.5 cb85e5f3ab 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>
2026-10-13 22:18:40 +04:00
..

robotex — Python-клиент Robotex API

Обвязка над HTTP API Robotex (docs/API.md): задачи v2 через очередь, синхронный /v1/solve, сессии с удержанием окна браузера. Синхронный Robotex и асинхронный AsyncRobotex с одинаковым интерфейсом. Зависимость одна — httpx.

Установка

pip install "robotex @ git+https://git.sokolab.xyz/s0k0l/robotex.git@v0.1.0#subdirectory=client"

или из wheel (make client-build в репозитории кладёт его в client/dist/):

pip install robotex-0.1.0-py3-none-any.whl

Быстрый старт

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() слоты и воркеры

Вызовы без сессии каждый раз получают новый профиль браузера.

Сессии и удержание окна

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")   # продолжить сессию позже

Асинхронно

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"}.