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 +185 -0
- browserkit/antibot/__init__.py +131 -0
- browserkit/antibot/_helpers.py +68 -0
- browserkit/antibot/cdp.py +294 -0
- browserkit/antibot/curl_cffi.py +231 -0
- browserkit/antibot/page_evaluate.py +301 -0
- browserkit/antibot/warmup_minter.py +255 -0
- browserkit/browser/__init__.py +1002 -0
- browserkit/browser/_cdp_state.py +43 -0
- browserkit/browser/_engine.py +150 -0
- browserkit/browser/camoufox.py +330 -0
- browserkit/browser/capture_tap.py +719 -0
- browserkit/browser/geometry.py +114 -0
- browserkit/browser/launchers.py +229 -0
- browserkit/browser/nodriver.py +284 -0
- browserkit/browser/persistent.py +185 -0
- browserkit/browser/state.py +308 -0
- browserkit/capabilities.py +308 -0
- browserkit/ports.py +105 -0
- browserkit/proc.py +313 -0
- browserkit/profile_health.py +340 -0
- s_browserkit-0.0.1.dist-info/METADATA +149 -0
- s_browserkit-0.0.1.dist-info/RECORD +25 -0
- s_browserkit-0.0.1.dist-info/WHEEL +4 -0
- s_browserkit-0.0.1.dist-info/licenses/LICENSE +21 -0
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
|
+
)
|