s-browserkit 0.0.1__py3-none-any.whl

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.
browserkit/__init__.py ADDED
@@ -0,0 +1,185 @@
1
+ """browserkit — БРАУЗЕРНЫЙ СЛОЙ экосистемы китов: живой браузер, антибот, стелс.
2
+
3
+ ФОРМА ГРАФА::
4
+
5
+ clikit adapterkit <- ветки-оболочки
6
+ \\ /
7
+ \\ /
8
+ librarykit <- ОРКЕСТРАТОР (сессии, лимиты, фабрики)
9
+ / \\
10
+ browserkit … <- СЛОЙ-КИТ (браузер и антибот)
11
+ \\ /
12
+ corekit <- ОСНОВАНИЕ (значения и чистые правила)
13
+
14
+ Что сюда переехало из `librarykit` (реальный код, не копия):
15
+
16
+ * :mod:`browserkit.browser` — лаунчеры (системный Chromium по CDP, персистентный
17
+ контекст), anti-detect и undetected-бэкенды, `capture_tap` (нормализованный
18
+ event-tap над любым движком), `state` (storage-state, тёплый вход, детекторы
19
+ протухания), геометрия окна и брони портов;
20
+ * :mod:`browserkit.antibot` — исполнители запроса, которые ходят «как браузер»:
21
+ TLS/JA3-импersonate без браузера, запрос через CDP-бэкенд, in-page fetch,
22
+ warmup-минтер одноразовых токенов;
23
+ * :mod:`browserkit.profile_health` — предстартовая проверка браузерного профиля
24
+ (битые Cookies/Preferences, распухший кэш) и карантин;
25
+ * :mod:`browserkit.proc` — запуск подпроцессов без всплывающих консолей;
26
+ * :mod:`browserkit.ports` — Protocol'ы, которых киту не хватает снаружи.
27
+
28
+ ★ ГЛАВНОЕ — ФАСАД ВОЗМОЖНОСТЕЙ (:mod:`browserkit.capabilities`). Потребитель
29
+ просит ЦЕЛЬ, а не движок::
30
+
31
+ import browserkit
32
+ backend = browserkit.open_browser(browserkit.Capability.STEALTH_SESSION,
33
+ profile_dir=profile)
34
+ http = browserkit.make_requester(browserkit.Capability.STEALTH_REQUEST)
35
+
36
+ Замена движка (anti-detect Firefox начал палиться) — правка ОДНОЙ таблицы внутри
37
+ кита; ни один навык не меняется, потому что ни один навык не назвал движок.
38
+
39
+ ЛЕНИВОСТЬ. `import browserkit` НЕ поднимает браузер и НЕ импортирует ни одного
40
+ тяжёлого движка: `browser`/`antibot` — реальные подпакеты, но резолвятся через
41
+ PEP 562 по факту обращения; сами вендоры (playwright/camoufox/nodriver/curl_cffi)
42
+ тянутся ещё позже — внутри бэкендов, где уже бросается внятное
43
+ `BrowserUnavailable` с инструкцией по установке.
44
+
45
+ СОВМЕСТИМОСТЬ. Прежние пути импорта (`librarykit.browser.*`,
46
+ `librarykit.antibot.*`, `librarykit.profile_health`) продолжают работать: там
47
+ оставлены АЛИАСЫ НА ТЕ ЖЕ модули (`sys.modules`), а не копии, поэтому
48
+ monkeypatch, `isinstance` и `is`-сравнения ведут себя как до переноса.
49
+ """
50
+ from __future__ import annotations
51
+
52
+ from importlib import import_module as _import_module
53
+ from importlib.util import find_spec as _find_spec
54
+ from typing import TYPE_CHECKING
55
+
56
+ from browserkit.capabilities import (
57
+ Capability,
58
+ CapabilityUnavailable,
59
+ capability_plan,
60
+ describe_capabilities,
61
+ is_capability_available,
62
+ make_requester,
63
+ open_browser,
64
+ set_capability_plan,
65
+ )
66
+ from browserkit.ports import (
67
+ BrowserEngineRole,
68
+ HttpRequestExecutor,
69
+ SyncHttpRequestExecutor,
70
+ SyncTransport,
71
+ Transport,
72
+ )
73
+
74
+ __version__ = "0.0.1"
75
+
76
+ if TYPE_CHECKING: # pragma: no cover - только статическая видимость для IDE/mypy
77
+ from typing import Any
78
+
79
+ from browserkit.antibot import (
80
+ CurlCffiTransport as CurlCffiTransport,
81
+ )
82
+ from browserkit.antibot import (
83
+ TokenHarvest as TokenHarvest,
84
+ )
85
+ from browserkit.antibot import (
86
+ WarmupTokenMinter as WarmupTokenMinter,
87
+ )
88
+ from browserkit.browser import (
89
+ BrowserUnavailable as BrowserUnavailable,
90
+ )
91
+ from browserkit.profile_health import (
92
+ ProfileHealth as ProfileHealth,
93
+ )
94
+ from browserkit.profile_health import (
95
+ check_profile as check_profile,
96
+ )
97
+ from browserkit.profile_health import (
98
+ ensure_healthy_profile as ensure_healthy_profile,
99
+ )
100
+
101
+ #: Ленивые ИМЕНА корня: имя → модуль-источник. Держим их ленивыми по той же
102
+ #: причине, что и в `librarykit`: `import browserkit` обязан стоить дёшево, чтобы
103
+ #: фасад возможностей можно было спрашивать («умеешь ли ты стелс?»), не поднимая
104
+ #: ни одного движка.
105
+ _LAZY_NAMES: dict[str, str] = {
106
+ # browser
107
+ "BrowserUnavailable": "browserkit.browser",
108
+ "build_capture_backend": "browserkit.browser",
109
+ "attach_capture_tap": "browserkit.browser",
110
+ "camoufox_available": "browserkit.browser",
111
+ "nodriver_available": "browserkit.browser",
112
+ "playwright_available": "browserkit.browser",
113
+ # antibot
114
+ "CurlCffiTransport": "browserkit.antibot",
115
+ "TokenHarvest": "browserkit.antibot",
116
+ "WarmupTokenMinter": "browserkit.antibot",
117
+ "page_fetch": "browserkit.antibot",
118
+ # profile_health
119
+ "ProfileHealth": "browserkit.profile_health",
120
+ "ProfileProblem": "browserkit.profile_health",
121
+ "check_profile": "browserkit.profile_health",
122
+ "ensure_healthy_profile": "browserkit.profile_health",
123
+ "quarantine_profile": "browserkit.profile_health",
124
+ # proc
125
+ "ProcResult": "browserkit.proc",
126
+ }
127
+
128
+ __all__ = [
129
+ # ★ фасад возможностей — рекомендуемый вход
130
+ "Capability",
131
+ "CapabilityUnavailable",
132
+ "open_browser",
133
+ "make_requester",
134
+ "is_capability_available",
135
+ "describe_capabilities",
136
+ "capability_plan",
137
+ "set_capability_plan",
138
+ # порты (контракты)
139
+ "Transport",
140
+ "SyncTransport",
141
+ "HttpRequestExecutor",
142
+ "SyncHttpRequestExecutor",
143
+ "BrowserEngineRole",
144
+ # ленивые имена реализаций (совместимость с прежним корнем librarykit)
145
+ "BrowserUnavailable",
146
+ "build_capture_backend",
147
+ "attach_capture_tap",
148
+ "camoufox_available",
149
+ "nodriver_available",
150
+ "playwright_available",
151
+ "CurlCffiTransport",
152
+ "TokenHarvest",
153
+ "WarmupTokenMinter",
154
+ "page_fetch",
155
+ "ProfileHealth",
156
+ "ProfileProblem",
157
+ "check_profile",
158
+ "ensure_healthy_profile",
159
+ "quarantine_profile",
160
+ "ProcResult",
161
+ ]
162
+
163
+
164
+ def __getattr__(name: str) -> Any: # `Any` — только под TYPE_CHECKING
165
+ """Ленивый резолв (PEP 562): сперва карта имён, затем РЕАЛЬНЫЙ подмодуль."""
166
+ target = _LAZY_NAMES.get(name)
167
+ if target is None:
168
+ if not name.startswith("_"):
169
+ try:
170
+ found = _find_spec(f"{__name__}.{name}") is not None
171
+ except (ImportError, ValueError):
172
+ found = False
173
+ if found:
174
+ module = _import_module(f"{__name__}.{name}")
175
+ globals()[name] = module
176
+ return module
177
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
178
+ value = getattr(_import_module(target), name)
179
+ globals()[name] = value
180
+ return value
181
+
182
+
183
+ def __dir__() -> list[str]:
184
+ """Полная поверхность корня: жадные атрибуты + `__all__` + ленивые имена."""
185
+ return sorted(set(globals()) | set(__all__) | set(_LAZY_NAMES))
@@ -0,0 +1,131 @@
1
+ """Антибот-стратегия как ВЫБОР ТРАНСПОРТА, а не переписывание кода вызова.
2
+
3
+ ДОМ ПАКЕТА — БРАУЗЕРНЫЙ КИТ. Раньше код жил в `librarykit`
4
+ (а до того в `adapterkit.antibot`); по обоим старым путям остались АЛИАСЫ на эти
5
+ же модули. curl-cffi — ОПЦИОНАЛЬНЫЙ extra `[antibot]` (ленивый импорт внутри
6
+ транспорта), кит без extras не падает на import пакета.
7
+
8
+ Идея: антибот — это не «headless vs
9
+ residential proxy», а пять уровней архитектуры (Tier 0-4). Сервис «опт-инится» в
10
+ нужный уровень, просто выбрав транспорт у эндпоинта (`Endpoint.transport`):
11
+
12
+ - `TransportKind.HTTP` → `HttpxTransport` (Tier 0, обычный httpx) — другой модуль;
13
+ - `TransportKind.CURL_CFFI` → `CurlCffiTransport` (Tier 1-3, JA3-имитация Chrome);
14
+ - `TransportKind.BROWSER` → `CdpTransport` (Tier 0-1, реальный системный Edge/Chrome).
15
+
16
+ Каждый из них реализует ОДИН И ТОТ ЖЕ контракт `ports.Transport`
17
+ (`async request(method, url, *, ...) -> httpx.Response`), поэтому вышестоящий код
18
+ (`HttpClient`/`ErrorMapper`/адаптер) не различает транспорты — меняется только
19
+ TLS-fingerprint и источник заголовков, но НЕ форма вызова. Маппинг кодов в
20
+ доменные ошибки — НЕ здесь (это `HttpClient`/`ErrorMapper`); транспорт бросает
21
+ `TransportError` только на сетевых сбоях и возвращает сырой `httpx.Response`.
22
+
23
+ Этот пакет — ТОНКИЙ ФАСАД: механически разнесён по ответственности на подмодули
24
+ (`._helpers` — обёртка ошибок + нормализация ответа; `.curl_cffi` —
25
+ `CurlCffiTransport`/`pick_chrome_impersonate`; `.cdp` — `CdpTransport`/
26
+ `SyncCdpTransport` + backend-контракты; `.page_evaluate` — `PageEvaluateTransport`/
27
+ `page_fetch`/`_build_fetch_spec`). Публичный API прежний — все прежние имена
28
+ реэкспортированы.
29
+
30
+ Опциональные зависимости (`curl_cffi`, `playwright`/`patchright`) импортируются
31
+ ЛЕНИВО — модуль импортируется без них (graceful), а понятная ошибка о
32
+ недостающем пакете возникает только при реальной попытке использовать транспорт.
33
+
34
+ ────────────────────────────────────────────────────────────────────────────────
35
+ Tier 0-4 — какой транспорт выбрать под нагрузку и характер защиты
36
+ ────────────────────────────────────────────────────────────────────────────────
37
+ Антибот эскалируется уровнями; не «беги за прокси первым», а подбирай по симптому.
38
+
39
+ - **Tier 0** — видимый браузер (headed) / обычный httpx. RAM ~800 МБ. dev / debug /
40
+ первый bootstrap-логин. Здесь: `CdpTransport(headless=False)` / `HttpxTransport`.
41
+ - **Tier 1** — headless реальный браузер (CDP→Edge `--headless=new`), ~700 мс,
42
+ 500-800 МБ; prod 1-100 calls/day, антибот средний. Здесь: `CdpTransport(headless=True)`.
43
+ Вариант того же уровня — curl-cffi (JA3-имитация Chrome), ~10 МБ: JA3/TLS-детект
44
+ (Akamai, Google) БЕЗ signing. Здесь: `CurlCffiTransport`.
45
+ - **Tier 2** — jsdom one-shot subprocess, 3-5 с, 30-50 МБ; init-флоу известен,
46
+ sign-only без рендера. Вне этого модуля.
47
+ - **Tier 3** — jsdom persistent daemon (RPC), ~50 мс, ~80 МБ; prod sweet-spot
48
+ 100-10K calls/day. Вне этого модуля.
49
+ - **Tier 4** — pure native (Python/Go/Rust), ~1 мс, ~10 МБ; bundle pinned forever
50
+ + есть команда сопровождения. Вне этого модуля.
51
+
52
+ Дерево решений:
53
+ - read-only без подписи запроса → Tier 0/1 (или официальный REST, если есть);
54
+ - мутирующий вызов с подписью (X-Sign/X-Bogus/_signature/msToken):
55
+ * bootstrap-логин → `CdpTransport` (реальный Edge ставит нативные
56
+ `sec-fetch-*` / Client-Hints / TLS — patchright их НЕ шлёт);
57
+ * массовая prod-нагрузка → Tier 3 jsdom-daemon (вне adapterkit), а Python-сторона
58
+ шлёт уже подписанный URL через `CurlCffiTransport` (JA3 совпадает).
59
+
60
+ Симптом → направление (три ОРТОГОНАЛЬНЫХ оси, выбирай по симптому, не «по простоте»):
61
+ - `403`/`Access denied` на любом IP, RU-регион заблокирован → IP/proxy (дорого, last resort);
62
+ - видимый captcha-виджет (hCaptcha/Turnstile) → внешний solver (reCAPTCHA v3 — score-based,
63
+ solver НЕ работает, только реальный браузер `CdpTransport`);
64
+ - `signatures don't match` / `isTTwidDecryptedFail` / «unusual activity» / нет
65
+ `sec-fetch-*` → архитектура: `CdpTransport` (реальный Edge);
66
+ - `ECONNRESET` / другой контент на JA3-detection endpoint → архитектура: `CurlCffiTransport`.
67
+
68
+ Железные правила (анти-паттерны):
69
+ - версию Chrome НЕ пинить: захардкоженный `chrome131` через 4+ версии — сигнал «бот»
70
+ (`CurlCffiTransport` подбирает свежайший доступный профиль автоматически);
71
+ - UA major == sec-ch-ua major == curl-cffi `impersonate` major — рассинхрон палится;
72
+ - `CdpTransport` берёт версию браузера сам — держи системный Edge/Chrome обновлённым.
73
+ """
74
+ from __future__ import annotations
75
+
76
+ from ._helpers import ( # noqa: F401 # seam: тесты/потребители импортируют приватные
77
+ _as_transport_error,
78
+ _to_httpx_response,
79
+ )
80
+ from .cdp import (
81
+ CdpBrowserBackend,
82
+ CdpTransport,
83
+ SyncCdpBrowserBackend,
84
+ SyncCdpTransport,
85
+ )
86
+ from .curl_cffi import CurlCffiTransport, pick_chrome_impersonate
87
+ from .page_evaluate import (
88
+ PageEvaluateTransport,
89
+ PageFetchResult,
90
+ _build_fetch_spec, # noqa: F401 # seam: browser+тесты импортируют
91
+ page_fetch,
92
+ sync_page_fetch,
93
+ )
94
+ from .warmup_minter import TokenHarvest, WarmupTokenMinter
95
+
96
+ # Первичные ролевые имена исполнителей запроса (роль снаружи, вендор — внутри):
97
+ # ImpersonatedHttpExecutor — TLS/JA3-impersonate (curl-cffi) [было CurlCffiTransport];
98
+ # BrowserRequestExecutor — запрос «руками браузера» по CDP/CamouFox [было CdpTransport];
99
+ # BorrowedPageRequestExecutor — запрос в ЗАИМСТВОВАННОЙ странице [было PageEvaluateTransport];
100
+ # BrowserRequester — роль «умеет выполнить запрос в браузере» ПОВЕРХ живой
101
+ # contract.BrowserSession (fetch, не ЖЦ) [было CdpBrowserBackend].
102
+ # Старые имена остаются deprecated-псевдонимами (ничего не удалено).
103
+ ImpersonatedHttpExecutor = CurlCffiTransport
104
+ BrowserRequestExecutor = CdpTransport
105
+ SyncBrowserRequestExecutor = SyncCdpTransport
106
+ BorrowedPageRequestExecutor = PageEvaluateTransport
107
+ BrowserRequester = CdpBrowserBackend
108
+ SyncBrowserRequester = SyncCdpBrowserBackend
109
+
110
+ __all__ = [
111
+ "CurlCffiTransport",
112
+ "CdpTransport",
113
+ "CdpBrowserBackend",
114
+ "SyncCdpTransport",
115
+ "SyncCdpBrowserBackend",
116
+ "PageEvaluateTransport",
117
+ "PageFetchResult",
118
+ "page_fetch",
119
+ "sync_page_fetch",
120
+ "pick_chrome_impersonate",
121
+ # warmup-минт одноразовых токенов тёплой страницей (генерик chatgpt-паттерна)
122
+ "WarmupTokenMinter",
123
+ "TokenHarvest",
124
+ # первичные ролевые имена (псевдонимы вендор-классов выше)
125
+ "ImpersonatedHttpExecutor",
126
+ "BrowserRequestExecutor",
127
+ "SyncBrowserRequestExecutor",
128
+ "BorrowedPageRequestExecutor",
129
+ "BrowserRequester",
130
+ "SyncBrowserRequester",
131
+ ]
@@ -0,0 +1,68 @@
1
+ """Общие хелперы антибот-транспортов: обёртка ошибок + нормализация ответа.
2
+
3
+ Выделено из god-модуля `antibot.py`. Имена реэкспортируются фасадом
4
+ `librarykit.antibot`; публичный API прежний.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import contextlib
9
+ from collections.abc import Iterator
10
+ from typing import Any
11
+
12
+ import httpx
13
+ from corekit.errors import TransportError
14
+
15
+
16
+ @contextlib.contextmanager
17
+ def _as_transport_error(prefix: str) -> Iterator[None]:
18
+ """Обернуть сбой backend'а транспорта в `TransportError` с единым префиксом.
19
+
20
+ Устраняет повтор ``except Exception → TransportError(f"<prefix>: ...")`` в
21
+ request-методах транспортов (curl_cffi / CDP / CDP(sync) / page.evaluate).
22
+ Уже-`TransportError` пробрасывается КАК ЕСТЬ — не заворачиваем дважды (префикс
23
+ и цепочка причин остаются исходными); прочее исключение backend'а
24
+ сворачивается в `TransportError` с типом и текстом.
25
+ """
26
+ try:
27
+ yield
28
+ except TransportError:
29
+ raise
30
+ except Exception as exc:
31
+ raise TransportError(f"{prefix}: {type(exc).__name__}: {exc}") from exc
32
+
33
+
34
+ def _to_httpx_response(
35
+ raw: Any, *, request: httpx.Request, default_encoding: str = "utf-8"
36
+ ) -> httpx.Response:
37
+ """Сконвертировать ответ стороннего backend (curl-cffi) в `httpx.Response`.
38
+
39
+ Нормализует разнотипный ответ к единому контракту `Transport`: вышестоящий код
40
+ (`ErrorMapper`/адаптер) работает только с `httpx.Response`. Тянем status/headers/
41
+ тело и привязываем реконструированный `httpx.Request` (нужен для url в ошибках).
42
+ """
43
+ status = int(getattr(raw, "status_code", 0) or 0)
44
+ raw_headers = getattr(raw, "headers", None) or {}
45
+ headers = dict(raw_headers.items()) if hasattr(raw_headers, "items") else dict(raw_headers)
46
+ # curl-cffi (libcurl) уже РАСПАКОВАЛ тело (Accept-Encoding согласуется при
47
+ # impersonate), но оставил исходные Content-Encoding/Content-Length. Пробросив
48
+ # их в httpx.Response с уже-распакованным content, мы заставим httpx
49
+ # распаковать ПОВТОРНО на .text/.json/.content → `Error -3 while decompressing
50
+ # data: incorrect header check`. Тело здесь финальное → снимаем эти заголовки.
51
+ headers = {
52
+ name: value
53
+ for name, value in headers.items()
54
+ if name.lower() not in ("content-encoding", "content-length")
55
+ }
56
+ content = getattr(raw, "content", None)
57
+ if content is None:
58
+ text = getattr(raw, "text", "") or ""
59
+ content = text.encode(default_encoding)
60
+ elif isinstance(content, str):
61
+ content = content.encode(default_encoding)
62
+ return httpx.Response(
63
+ status,
64
+ headers=headers,
65
+ content=content,
66
+ request=request,
67
+ default_encoding=default_encoding,
68
+ )