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)
Карта элементов#
Маркер |
Поле |
Что даёт |
|---|---|---|
|
|
первый видимый узел |
|
|
последний узел |
|
|
первый найденный кандидат |
|
|
|
|
|
подкарта внутри корня |
|
|
|
|
|
для |
|
|
элемент плюс |
|
|
разобранный ответ API; нужен |
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().
Ось |
Уровень |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Pydoll работает с Chromium-based браузерами. Запуск браузера, контексты, proxy, identity, lease и retry задачи остаются внешней оркестрацией.
Ошибки#
Класс |
База |
Когда |
|---|---|---|
|
|
объявление собрано неверно (D-B-xx) |
|
|
отказ во время сценария |
|
|
объявленный отказ страницы, база исключений SDK |
|
|
условие готовности не выполнилось, правила отказа не объяснили |
|
|
ни один кандидат не найден; |
|
|
ответ API из карты отсутствует либо его тело превышает лимит захвата ( |
|
|
в сессии нет нужной куки или она истекла; объекту сессии принесли клиента из другого контекста |
|
из |
драйвер отказал: закрытая страница, сеть, срок перехода |
Диагностики#
Код |
Что |
|---|---|
D-B-01 |
у операции нет |
D-B-02 |
карта спеки не совпадает с параметром класса |
D-B-03 |
поле карты без маркера |
D-B-04 |
возможность, нужная карте или признакам, не объявлена либо объявлена и не используется |
D-B-05 |
операция не frozen dataclass |
D-B-06 |
браузерная операция опубликована не на |
D-B-07 |
HTTP-операция опубликована на |
D-B-08 |
тип результата класса не совпадает с union исходов |
D-B-09 |
у операции-перехода есть |
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 обещают возможность в профиле без
метода и служат для проверки адаптеров.