Browser#

Операция#

Browser.act(content, /, *, at=None, scope=None, frames=(), outcomes=None, errors=(),
            inherit_errors=True, handlers=(), requires=(), operation_id=None, tags=())
Browser.goto(url, /, *, wait="load", at=None, ...)            # переход без карты: TContent = None
Browser.goto(url, content, /, *, wait="load", at=None, ...)   # переход с картой для исходов
# остальные ключевые параметры goto совпадают с act

class BrowserOperation[TContent, TResult]:
    __browser__: ClassVar[_BrowserSpec[TContent]]
    async def act(self, content: TContent) -> None: ...

Операция оформляется как @dataclass(frozen=True, slots=True, kw_only=True). TContent задаёт карту элементов (None у перехода без карты), TResult задаёт объединение исходов или None.

Роутер и клиент#

class AsyncBrowserApi:
    errors: ClassVar[tuple[Failure, ...]] = ()
    handlers: ClassVar[tuple[Handle, ...]] = ()
    frames: ClassVar[tuple[str, ...]] = ()
    def __init__(self, client: AsyncBrowserClient, *, defaults=None) -> None: ...

AsyncBrowserClient(driver, *, base_url="", config=None, session=None, owns_driver=False)
AsyncBrowserClient.without_session() -> AsyncBrowserClient   # тот же драйвер, база, конфиг
BrowserClientConfig(timeout=5.0, element_timeout=5.0, response_timeout=5.0,
                    navigation_timeout=30.0, auth_retries=1)
BrowserCallOptions(timeout=None, element_timeout=None, response_timeout=None,
                   navigation_timeout=None)
await execute(operation, driver, *, base_url="", config=None, options=None)

op(Operation) из eazy_sdk публикует операцию на роутере: portal.name(**fields, options=...), portal.name.request(**fields), portal.name.send(request). Правила errors операции стоят выше правил роутера; фреймы роутера охватывают фреймы операции. Роутер ставится в AsyncRoot через api_group(...) и bind(Router, client=AsyncBrowserClient(...)).

Исходы#

outcomes(*cases: When, otherwise: Callable[[TContent], Awaitable[Z]]) -> Outcomes
when(sign, *, then: Callable[[TContent], Awaitable[T]]) -> When
when(sign, *, to: type[State]) -> When     # State(api: AsyncBrowserApi)

Карта элементов#

Маркер

Поле

Что даёт

css(selector, *, timeout=None, frames=())

Element

первый видимый узел

last(selector, ...)

Element

последний узел

any_of(*selectors, ...)

Element

первый найденный кандидат

each(locator)

Elements

count(), nth(i), texts(), async for

region(Card, root=locator)

Card

подкарта внутри корня

template(*selectors, ...)

Template

row(name=...), значение по CSS.escape

template_text(*selectors, ...)

Template

для text="…" и :has-text("…")

rich(locator)

RichText

элемент плюс set_html(html); нужен Capability.rich_text

ApiResponse(url_contains, decoder)

ApiValue[T]

разобранный ответ API; нужен Capability.network

Element: fill, click, hover, check, uncheck, select, press, text, value, attribute, visible.

Признаки#

visible(locator)
text.contains(fragment, *, ignore_case=True)
url.contains(marker)
url.matches(pattern)
response.arrived(url_contains)   # Capability.network
navigated()                      # Capability.navigation_events

Операторы: |, &, ~. У признака есть label и requires.

Отказы и помехи#

Failure(when: Sign, exception: type[Exception] | Callable[[Observation], Awaitable[Exception]])
text_of(locator) -> Callable[[Observation], Awaitable[str]]
Handle(when: Sign, do: Action, once: bool = True)
click(locator) -> Action

Возможности#

class Capability(StrEnum):
    network, session_state, page_requests, navigation_events, shadow_dom, rich_text

BrowserProfile(name, **axes: CapabilityLevel)
validate_profile(requires, profile)   # CapabilityMismatchError из eazy_sdk.handlers

Вход и сессия#

BrowserLogin(*, service, cookies=(), expired=(), leeway=timedelta(seconds=30), clock=...)
BrowserLogin.validate(state) -> bool
BrowserLogin.is_expired(error) -> bool
await BrowserLogin.sign_in(client, credentials) -> BrowserState
await BrowserLogin.refresh(client, credentials, state) -> BrowserState
BrowserLogin.session(credentials, *, store=None, identity="client") -> BrowserSession
BrowserLoginContext(client).export_state()

class SessionSource[TRevision](Protocol):          # что клиент знает о сессии
    def is_expired(self, error: BaseException) -> bool: ...
    async def ensure(self, client) -> TRevision: ...
    async def renew(self, client, rejected: TRevision) -> TRevision | None: ...

BrowserSession                                     # SessionSource без пула, один на контекст
await BrowserSession.state(client) -> BrowserState

Identity(cookies=CookieState(state.cookies))       # та же сессия для HTTP-клиента

BrowserLogin.session(...) требует непустой cookies. Клиент с session= вызывает ensure вызывается перед операцией. После отказа, для которого is_expired истинно, выполняются renew и повтор, но не больше auth_retries раз. Если renew вернул None, наружу уходит исходный отказ.

Состояние сессии#

class StateAware(Protocol):                        # драйвер с Capability.session_state
    async def export_state(self) -> BrowserState: ...
    async def add_cookies(self, cookies: tuple[StoredCookie, ...]) -> None: ...
    def context_key(self) -> object: ...

BrowserState(cookies=(), origins=())
StoredCookie(name, value, domain, path="/", host_only=True, expires_at=None,
             secure=False, http_only=False, same_site="")   # из eazy_sdk.cookies
Origin(origin, items=())

# eazy_sdk_browser.handlers.playwright
to_storage_state(state) -> StorageState            # для browser.new_context(storage_state=…)
from_storage_state(raw) -> BrowserState

# eazy_sdk_browser.integrations.accounts
BrowserSessions(workspace).load(account) -> BrowserState | None
await BrowserSessions.save(account, state, *, expires_at=None)
BrowserSessions.store(account) -> SessionStore[BrowserState]

localStorage в живой контекст не пишется: его задаёт storage_state при создании контекста. Куки в контекст кладёт только BrowserSession.

Драйвер playwright#

async with PlaywrightDriver(page, *, capture=DEFAULT_CAPTURE) as driver: ...

# eazy_sdk_browser.handlers
CapturePolicy(*, resource_types=frozenset({"xhr", "fetch", "document"}),
              max_body_bytes=2 * 1024 * 1024, max_total_bytes=32 * 1024 * 1024)

Один драйвер соответствует одной странице; он отписывается при её закрытии. capture=None сеть не слушается, и профиль не обещает Capability.network. Тело больше max_body_bytes не читается: ответ лежит в буфере с ResponseView.body_dropped=True, а карта, которая его ждёт, бросает ResponseMissingError с причиной.

Драйвер Pydoll 3#

from eazy_sdk_browser.handlers.pydoll import PYDOLL_PROFILE, PydollDriver

async with PydollDriver(tab, context_key=context, capture=DEFAULT_CAPTURE) as driver: ...

Адаптер принимает готовый нативный pydoll.browser.tab.Tab из pydoll-python>=3,<4 и не закрывает его. context_key одинаков у вкладок одного контекста; без него создаётся безопасный page-local ключ. capture=None понижает только network до UNSUPPORTED. Один Tab не оборачивают двумя живыми адаптерами одновременно: перед повторным созданием вызывают aclose(), а в пуле используют один именованный lease.attachment().

Ось

Уровень

network

CAPTURE_VERIFIED

session_state

BEST_EFFORT: cookies контекста и localStorage текущего origin

page_requests

CAPTURE_VERIFIED

navigation_events

CAPTURE_VERIFIED

shadow_dom

UNSUPPORTED

rich_text

CAPTURE_VERIFIED

Pydoll работает с Chromium-based браузерами. Запуск браузера, контексты, proxy, identity, lease и retry задачи остаются внешней оркестрацией.

Ошибки#

Класс

База

Когда

BrowserDeclarationError

PlanError

объявление собрано неверно (D-B-xx)

BrowserError

EazySdkError

отказ во время сценария

PageError

BrowserError

объявленный отказ страницы, база исключений SDK

NotReadyError

BrowserError

условие готовности не выполнилось, правила отказа не объяснили

ElementNotFoundError

BrowserError

ни один кандидат не найден; index содержит номер узла коллекции

ResponseMissingError

BrowserError

ответ API из карты отсутствует либо его тело превышает лимит захвата (reason)

BrowserSessionError

BrowserError

в сессии нет нужной куки или она истекла; объекту сессии принесли клиента из другого контекста

TransportError

из eazy_sdk.handlers

драйвер отказал: закрытая страница, сеть, срок перехода

Диагностики#

Код

Что

D-B-01

у операции нет __browser__

D-B-02

карта спеки не совпадает с параметром класса

D-B-03

поле карты без маркера

D-B-04

возможность, нужная карте или признакам, не объявлена либо объявлена и не используется

D-B-05

операция не frozen dataclass

D-B-06

браузерная операция опубликована не на AsyncBrowserApi

D-B-07

HTTP-операция опубликована на AsyncBrowserApi

D-B-08

тип результата класса не совпадает с union исходов

D-B-09

у операции-перехода есть act

D-B-10

{имя} в адресе перехода не является полем операции

Тестовые драйверы#

eazy_sdk_browser.testing: FakeDriver(present=, url=, text=, latency=, redirects=, pages=, counts=, values=, attributes=) пишет действия в log; NetworkFakeDriver, NavigatingFakeDriver, StatefulFakeDriver, FetchFakeDriver, RichTextFakeDriver добавляют по одной возможности. Несколько StatefulFakeDriver(context=FakeContext(...)) с общим FakeContext моделирует вкладки одного контекста с общими cookies и журналом cookie_writes. LyingDriver, StatelessLiar, FetchLiar, RichTextLiar обещают возможность в профиле без метода и служат для проверки адаптеров.