s-librarykit 0.1.22__tar.gz → 0.2.0__tar.gz
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.
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/CHANGELOG.md +13 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/PKG-INFO +1 -1
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/__init__.py +58 -11
- s_librarykit-0.2.0/librarykit/antibot/__init__.py +126 -0
- s_librarykit-0.2.0/librarykit/antibot/_helpers.py +69 -0
- s_librarykit-0.2.0/librarykit/antibot/cdp.py +272 -0
- s_librarykit-0.2.0/librarykit/antibot/curl_cffi.py +231 -0
- s_librarykit-0.2.0/librarykit/antibot/page_evaluate.py +301 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/auth.py +35 -19
- s_librarykit-0.2.0/librarykit/browser/__init__.py +835 -0
- s_librarykit-0.2.0/librarykit/browser/_engine.py +139 -0
- s_librarykit-0.2.0/librarykit/browser/camoufox.py +330 -0
- s_librarykit-0.2.0/librarykit/browser/geometry.py +114 -0
- s_librarykit-0.2.0/librarykit/browser/launchers.py +229 -0
- s_librarykit-0.2.0/librarykit/browser/state.py +308 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/checkpoint.py +7 -1
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/contract.py +115 -2
- s_librarykit-0.2.0/librarykit/factory.py +390 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/limit.py +36 -3
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/pool.py +29 -12
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/refresh.py +18 -6
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/retry.py +14 -6
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/rpc.py +2 -1
- s_librarykit-0.1.22/librarykit/sessions.py → s_librarykit-0.2.0/librarykit/sessions/__init__.py +72 -160
- s_librarykit-0.2.0/librarykit/sessions/_const.py +89 -0
- s_librarykit-0.2.0/librarykit/sessions/_fsutil.py +83 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/stream.py +20 -0
- s_librarykit-0.2.0/librarykit/transport/__init__.py +88 -0
- s_librarykit-0.2.0/librarykit/transport/_helpers.py +43 -0
- s_librarykit-0.2.0/librarykit/transport/http_client.py +446 -0
- s_librarykit-0.2.0/librarykit/transport/permissive.py +147 -0
- s_librarykit-0.2.0/librarykit/transport/rest_client.py +382 -0
- s_librarykit-0.2.0/librarykit/transport/transport_http.py +355 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/pyproject.toml +1 -1
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_antibot.py +42 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_auth.py +41 -2
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_browser.py +154 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_checkpoint.py +10 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_errors.py +17 -0
- s_librarykit-0.2.0/tests/test_factory.py +265 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_limit.py +26 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_pool.py +34 -1
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_refresh.py +37 -1
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_retry.py +14 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_sessions.py +20 -2
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_stream.py +10 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_transport_client.py +29 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/uv.lock +2 -2
- s_librarykit-0.1.22/librarykit/antibot.py +0 -880
- s_librarykit-0.1.22/librarykit/browser.py +0 -1673
- s_librarykit-0.1.22/librarykit/transport.py +0 -1347
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/.gitignore +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/.gitlab-ci.yml +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/AGENTS.md +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/LICENSE +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/README.md +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/docs/superpowers/specs/2026-06-24-unified-sessions-design.md +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/blocks.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/bulk.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/config_util.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/entities.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/enums.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/errmap.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/errors.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/forms.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/graphql.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/orchestration/__init__.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/orchestration/health.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/orchestration/onboarding.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/orchestration/session_loader.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/pagination.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/ports.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/protocols.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/redaction.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/secret_store.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/session_bridge.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/session_paths.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/upload.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/windows.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/conftest.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_blocks.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_bulk.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_config_util.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_errmap.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_forms.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_graphql.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_limit_contract.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_orchestration.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_pagination.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_permissive_http.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_redaction.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_rpc.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_session_bridge.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_session_paths.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_transport_rest.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_transport_sync.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_upload.py +0 -0
- {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_windows.py +0 -0
|
@@ -5,6 +5,19 @@
|
|
|
5
5
|
[SemVer](https://semver.org/lang/ru/). Релизы помечаются git-тегами `vX.Y.Z`;
|
|
6
6
|
push тега запускает автопубликацию на PyPI (GitLab CI, OIDC Trusted Publishing).
|
|
7
7
|
|
|
8
|
+
## [0.1.23] — 2026-07-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`contract.QuotaInfo.limit_source`** (`str | None`, default `None`) — honesty-флаг
|
|
13
|
+
ИСТОЧНИКА потолка/процента окна лимита (хвост E-GW32 %-модели): ``"fact"`` (реальное
|
|
14
|
+
окно — probe/заголовки провайдера, provider ``percentage``), ``"spec"`` (generic
|
|
15
|
+
`LimitSpec.limit`), ``"estimate"`` (оценочный per-tier fallback `TIER_ESTIMATE`),
|
|
16
|
+
``None`` = неизвестен (back-compat: старые данные без пометки). Даёт UI отличить
|
|
17
|
+
реальную квоту (яркий бар) от прикидки («≈X% (оценка)», приглушённо). Проставляется
|
|
18
|
+
потребителем (`accountpoolkit.LimitAuditor.snapshot`) по приоритету fact → spec →
|
|
19
|
+
estimate; сам DTO неизменяемый (frozen dataclass), поле опционально.
|
|
20
|
+
|
|
8
21
|
## [0.1.22] — 2026-07-07
|
|
9
22
|
|
|
10
23
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: s-librarykit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Жирный КОРЕНЬ китов: единая error-иерархия, config/retry/paths-утили, checkpoint/resume, транспорт/auth/sessions — общий код для clikit, adapterkit и доменных пакетов.
|
|
5
5
|
Author: Dmitry
|
|
6
6
|
License: MIT
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""librarykit — жирный КОРЕНЬ китов (
|
|
1
|
+
"""librarykit — жирный КОРЕНЬ китов (fan-in граф: adapterkit → librarykit ← clikit).
|
|
2
2
|
|
|
3
3
|
Общий код вместо дублирования в clikit/adapterkit и доменных пакетах-потребителях:
|
|
4
4
|
единая error-иерархия (ERROR-HUB), config/paths-утили, политики повторов,
|
|
@@ -40,15 +40,6 @@ from librarykit.bulk import (
|
|
|
40
40
|
run_bulk_async,
|
|
41
41
|
)
|
|
42
42
|
from librarykit.checkpoint import Checkpoint, JsonlSink, RunMetrics
|
|
43
|
-
from librarykit.redaction import (
|
|
44
|
-
BODY_FIELD_PATTERNS,
|
|
45
|
-
HEADER_BLACKLIST,
|
|
46
|
-
SECRET_MASK,
|
|
47
|
-
excerpt_response_body,
|
|
48
|
-
sanitize_body,
|
|
49
|
-
sanitize_headers,
|
|
50
|
-
truncate_body,
|
|
51
|
-
)
|
|
52
43
|
from librarykit.config_util import (
|
|
53
44
|
AppPaths,
|
|
54
45
|
atomic_write_text,
|
|
@@ -62,9 +53,12 @@ from librarykit.config_util import (
|
|
|
62
53
|
from librarykit.contract import (
|
|
63
54
|
Auth,
|
|
64
55
|
AuthMode,
|
|
56
|
+
BrowserBackend,
|
|
57
|
+
BrowserSession,
|
|
65
58
|
Codec,
|
|
66
59
|
Creds,
|
|
67
60
|
ErrorMapper,
|
|
61
|
+
HttpRequestExecutor,
|
|
68
62
|
LimitSpec,
|
|
69
63
|
PaginationMode,
|
|
70
64
|
Paginator,
|
|
@@ -73,10 +67,14 @@ from librarykit.contract import (
|
|
|
73
67
|
QuotaScope,
|
|
74
68
|
Refreshable,
|
|
75
69
|
RequestBody,
|
|
70
|
+
RequestExecutionMode,
|
|
76
71
|
ResponseLike,
|
|
77
72
|
SessionRef,
|
|
78
73
|
SessionStoreProtocol,
|
|
79
74
|
StreamTransport,
|
|
75
|
+
SyncBrowserBackend,
|
|
76
|
+
SyncBrowserSession,
|
|
77
|
+
SyncHttpRequestExecutor,
|
|
80
78
|
SyncProactiveRefreshable,
|
|
81
79
|
SyncRefreshable,
|
|
82
80
|
SyncTransport,
|
|
@@ -115,6 +113,21 @@ from librarykit.errors import (
|
|
|
115
113
|
TransportError,
|
|
116
114
|
ValidationError,
|
|
117
115
|
)
|
|
116
|
+
from librarykit.factory import (
|
|
117
|
+
AutomationDriver,
|
|
118
|
+
BrowserEngine,
|
|
119
|
+
BrowserOptions,
|
|
120
|
+
BrowserProvider,
|
|
121
|
+
BrowserSessionSettings,
|
|
122
|
+
create_browser_session,
|
|
123
|
+
create_request_executor,
|
|
124
|
+
make_browser_backend,
|
|
125
|
+
make_transport,
|
|
126
|
+
register_browser_backend,
|
|
127
|
+
register_browser_provider,
|
|
128
|
+
register_request_provider,
|
|
129
|
+
register_transport,
|
|
130
|
+
)
|
|
118
131
|
|
|
119
132
|
# forms (W8): кодек form-urlencoded тел с nested-JSON полями (hidden-API write).
|
|
120
133
|
from librarykit.forms import FormCodec
|
|
@@ -154,6 +167,15 @@ from librarykit.protocols import (
|
|
|
154
167
|
LoginRequirements,
|
|
155
168
|
OnboardingProtocol,
|
|
156
169
|
)
|
|
170
|
+
from librarykit.redaction import (
|
|
171
|
+
BODY_FIELD_PATTERNS,
|
|
172
|
+
HEADER_BLACKLIST,
|
|
173
|
+
SECRET_MASK,
|
|
174
|
+
excerpt_response_body,
|
|
175
|
+
sanitize_body,
|
|
176
|
+
sanitize_headers,
|
|
177
|
+
truncate_body,
|
|
178
|
+
)
|
|
157
179
|
from librarykit.refresh import (
|
|
158
180
|
DEFAULT_ROTATE_INTERVAL,
|
|
159
181
|
Keepalive,
|
|
@@ -201,6 +223,8 @@ from librarykit.stream import (
|
|
|
201
223
|
# выведен наверх (нужен потребителям form-urlencoded write / chunked-аплоада).
|
|
202
224
|
from librarykit.transport import (
|
|
203
225
|
HttpClient,
|
|
226
|
+
HttpxRequestExecutor,
|
|
227
|
+
HttpxSyncRequestExecutor,
|
|
204
228
|
HttpxSyncTransport,
|
|
205
229
|
HttpxTransport,
|
|
206
230
|
RestHttpClient,
|
|
@@ -220,7 +244,7 @@ from librarykit.windows import (
|
|
|
220
244
|
parse_window,
|
|
221
245
|
)
|
|
222
246
|
|
|
223
|
-
__version__ = "0.
|
|
247
|
+
__version__ = "0.2.0"
|
|
224
248
|
|
|
225
249
|
__all__ = [
|
|
226
250
|
"__version__",
|
|
@@ -263,15 +287,36 @@ __all__ = [
|
|
|
263
287
|
"PaginationMode",
|
|
264
288
|
"AuthMode",
|
|
265
289
|
"TransportKind",
|
|
290
|
+
"RequestExecutionMode",
|
|
266
291
|
"SessionRef",
|
|
267
292
|
"Creds",
|
|
268
293
|
"RequestBody",
|
|
269
294
|
"ResponseLike",
|
|
270
295
|
"Transport",
|
|
271
296
|
"SyncTransport",
|
|
297
|
+
"HttpRequestExecutor",
|
|
298
|
+
"SyncHttpRequestExecutor",
|
|
272
299
|
"StreamTransport",
|
|
300
|
+
"BrowserBackend",
|
|
301
|
+
"SyncBrowserBackend",
|
|
302
|
+
"BrowserSession",
|
|
303
|
+
"SyncBrowserSession",
|
|
273
304
|
"Codec",
|
|
274
305
|
"Auth",
|
|
306
|
+
# factory — нейтральный выбор транспорта/браузера по роли (Enum → фабрика → реестр)
|
|
307
|
+
"BrowserEngine",
|
|
308
|
+
"BrowserProvider",
|
|
309
|
+
"AutomationDriver",
|
|
310
|
+
"BrowserOptions",
|
|
311
|
+
"BrowserSessionSettings",
|
|
312
|
+
"make_transport",
|
|
313
|
+
"register_transport",
|
|
314
|
+
"create_request_executor",
|
|
315
|
+
"register_request_provider",
|
|
316
|
+
"make_browser_backend",
|
|
317
|
+
"register_browser_backend",
|
|
318
|
+
"create_browser_session",
|
|
319
|
+
"register_browser_provider",
|
|
275
320
|
"Refreshable",
|
|
276
321
|
"ProactiveRefreshable",
|
|
277
322
|
"SyncRefreshable",
|
|
@@ -305,10 +350,12 @@ __all__ = [
|
|
|
305
350
|
"resolve_kek",
|
|
306
351
|
# transport (W4): конкретный httpx-транспорт + choke-point клиент
|
|
307
352
|
"HttpxTransport",
|
|
353
|
+
"HttpxRequestExecutor",
|
|
308
354
|
"HttpClient",
|
|
309
355
|
"RestHttpClient",
|
|
310
356
|
# transport sync-сиблинги (для навыков без event-loop)
|
|
311
357
|
"HttpxSyncTransport",
|
|
358
|
+
"HttpxSyncRequestExecutor",
|
|
312
359
|
"SyncHttpClient",
|
|
313
360
|
# forms (W8): кодек form-urlencoded тел с nested-JSON полями
|
|
314
361
|
"FormCodec",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Антибот-стратегия (КОРЕНЬ китов, W4) как ВЫБОР ТРАНСПОРТА, а не переписывание.
|
|
2
|
+
|
|
3
|
+
Перенесено из `adapterkit.antibot` в жирный КОРЕНЬ `librarykit` (граф
|
|
4
|
+
`librarykit <- adapterkit <- clikit`); `adapterkit.antibot` — тонкий реэкспорт.
|
|
5
|
+
curl-cffi — ОПЦИОНАЛЬНЫЙ extra `[antibot]` (ленивый импорт внутри транспорта),
|
|
6
|
+
librarykit-without-extras не падает на import пакета.
|
|
7
|
+
|
|
8
|
+
Идея (spec §2, recipes/antibot reverse-factory): антибот — это не «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
|
+
Каждый из них реализует ОДИН И ТОТ ЖЕ контракт `librarykit.contract.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
|
+
|
|
95
|
+
# Первичные ролевые имена исполнителей запроса (роль снаружи, вендор — внутри):
|
|
96
|
+
# ImpersonatedHttpExecutor — TLS/JA3-impersonate (curl-cffi) [было CurlCffiTransport];
|
|
97
|
+
# BrowserRequestExecutor — запрос «руками браузера» по CDP/CamouFox [было CdpTransport];
|
|
98
|
+
# BorrowedPageRequestExecutor — запрос в ЗАИМСТВОВАННОЙ странице [было PageEvaluateTransport];
|
|
99
|
+
# BrowserRequester — роль «умеет выполнить запрос в браузере» [было CdpBrowserBackend].
|
|
100
|
+
# Старые имена остаются deprecated-псевдонимами (ничего не удалено).
|
|
101
|
+
ImpersonatedHttpExecutor = CurlCffiTransport
|
|
102
|
+
BrowserRequestExecutor = CdpTransport
|
|
103
|
+
SyncBrowserRequestExecutor = SyncCdpTransport
|
|
104
|
+
BorrowedPageRequestExecutor = PageEvaluateTransport
|
|
105
|
+
BrowserRequester = CdpBrowserBackend
|
|
106
|
+
SyncBrowserRequester = SyncCdpBrowserBackend
|
|
107
|
+
|
|
108
|
+
__all__ = [
|
|
109
|
+
"CurlCffiTransport",
|
|
110
|
+
"CdpTransport",
|
|
111
|
+
"CdpBrowserBackend",
|
|
112
|
+
"SyncCdpTransport",
|
|
113
|
+
"SyncCdpBrowserBackend",
|
|
114
|
+
"PageEvaluateTransport",
|
|
115
|
+
"PageFetchResult",
|
|
116
|
+
"page_fetch",
|
|
117
|
+
"sync_page_fetch",
|
|
118
|
+
"pick_chrome_impersonate",
|
|
119
|
+
# первичные ролевые имена (псевдонимы вендор-классов выше)
|
|
120
|
+
"ImpersonatedHttpExecutor",
|
|
121
|
+
"BrowserRequestExecutor",
|
|
122
|
+
"SyncBrowserRequestExecutor",
|
|
123
|
+
"BorrowedPageRequestExecutor",
|
|
124
|
+
"BrowserRequester",
|
|
125
|
+
"SyncBrowserRequester",
|
|
126
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
|
|
14
|
+
from librarykit.errors import TransportError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextlib.contextmanager
|
|
18
|
+
def _as_transport_error(prefix: str) -> Iterator[None]:
|
|
19
|
+
"""Обернуть сбой backend'а транспорта в `TransportError` с единым префиксом.
|
|
20
|
+
|
|
21
|
+
Устраняет повтор ``except Exception → TransportError(f"<prefix>: ...")`` в
|
|
22
|
+
request-методах транспортов (curl_cffi / CDP / CDP(sync) / page.evaluate).
|
|
23
|
+
Уже-`TransportError` пробрасывается КАК ЕСТЬ — не заворачиваем дважды (префикс
|
|
24
|
+
и цепочка причин остаются исходными); прочее исключение backend'а
|
|
25
|
+
сворачивается в `TransportError` с типом и текстом.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
yield
|
|
29
|
+
except TransportError:
|
|
30
|
+
raise
|
|
31
|
+
except Exception as exc:
|
|
32
|
+
raise TransportError(f"{prefix}: {type(exc).__name__}: {exc}") from exc
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _to_httpx_response(
|
|
36
|
+
raw: Any, *, request: httpx.Request, default_encoding: str = "utf-8"
|
|
37
|
+
) -> httpx.Response:
|
|
38
|
+
"""Сконвертировать ответ стороннего backend (curl-cffi) в `httpx.Response`.
|
|
39
|
+
|
|
40
|
+
Нормализует разнотипный ответ к единому контракту `Transport`: вышестоящий код
|
|
41
|
+
(`ErrorMapper`/адаптер) работает только с `httpx.Response`. Тянем status/headers/
|
|
42
|
+
тело и привязываем реконструированный `httpx.Request` (нужен для url в ошибках).
|
|
43
|
+
"""
|
|
44
|
+
status = int(getattr(raw, "status_code", 0) or 0)
|
|
45
|
+
raw_headers = getattr(raw, "headers", None) or {}
|
|
46
|
+
headers = dict(raw_headers.items()) if hasattr(raw_headers, "items") else dict(raw_headers)
|
|
47
|
+
# curl-cffi (libcurl) уже РАСПАКОВАЛ тело (Accept-Encoding согласуется при
|
|
48
|
+
# impersonate), но оставил исходные Content-Encoding/Content-Length. Пробросив
|
|
49
|
+
# их в httpx.Response с уже-распакованным content, мы заставим httpx
|
|
50
|
+
# распаковать ПОВТОРНО на .text/.json/.content → `Error -3 while decompressing
|
|
51
|
+
# data: incorrect header check`. Тело здесь финальное → снимаем эти заголовки.
|
|
52
|
+
headers = {
|
|
53
|
+
name: value
|
|
54
|
+
for name, value in headers.items()
|
|
55
|
+
if name.lower() not in ("content-encoding", "content-length")
|
|
56
|
+
}
|
|
57
|
+
content = getattr(raw, "content", None)
|
|
58
|
+
if content is None:
|
|
59
|
+
text = getattr(raw, "text", "") or ""
|
|
60
|
+
content = text.encode(default_encoding)
|
|
61
|
+
elif isinstance(content, str):
|
|
62
|
+
content = content.encode(default_encoding)
|
|
63
|
+
return httpx.Response(
|
|
64
|
+
status,
|
|
65
|
+
headers=headers,
|
|
66
|
+
content=content,
|
|
67
|
+
request=request,
|
|
68
|
+
default_encoding=default_encoding,
|
|
69
|
+
)
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""CDP-транспорты: подключение к реальному системному Edge/Chrome (antibot Tier 0-1).
|
|
2
|
+
|
|
3
|
+
Выделено из god-модуля `antibot.py`. Async `CdpTransport` + sync `SyncCdpTransport`
|
|
4
|
+
и их backend-контракты. Имена реэкспортируются фасадом `librarykit.antibot`;
|
|
5
|
+
публичный API прежний.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import Any, Literal, Protocol, runtime_checkable
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from librarykit.contract import SyncTransport, Transport
|
|
15
|
+
from librarykit.errors import TransportError
|
|
16
|
+
|
|
17
|
+
from ._helpers import _as_transport_error, _to_httpx_response
|
|
18
|
+
|
|
19
|
+
# =========================================================================== #
|
|
20
|
+
# Tier 0-1: CDP-транспорт (подключение к реальному системному Edge/Chrome) #
|
|
21
|
+
# =========================================================================== #
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class CdpBrowserBackend(Protocol):
|
|
26
|
+
"""Контракт «движка реального браузера» под `CdpTransport` (точка DI).
|
|
27
|
+
|
|
28
|
+
Скрывает за собой запуск системного Edge/Chrome с `--remote-debugging-port`
|
|
29
|
+
и `connect_over_cdp` (см. recipes/antibot `cdp_real_browser.py`). Реализация —
|
|
30
|
+
в `librarykit.browser` (Playwright/patchright); здесь — только контракт, чтобы
|
|
31
|
+
`antibot` не тащил тяжёлый browser-стек и тестировался фейком той же формы.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
async def fetch(
|
|
35
|
+
self,
|
|
36
|
+
method: str,
|
|
37
|
+
url: str,
|
|
38
|
+
*,
|
|
39
|
+
params: Mapping[str, Any] | None = None,
|
|
40
|
+
json: Any | None = None,
|
|
41
|
+
data: Any | None = None,
|
|
42
|
+
headers: Mapping[str, str] | None = None,
|
|
43
|
+
cookies: Mapping[str, str] | None = None,
|
|
44
|
+
) -> Any:
|
|
45
|
+
"""Выполнить запрос внутри реального браузера; вернуть ответоподобный объект.
|
|
46
|
+
|
|
47
|
+
Объект нормализуется `_to_httpx_response` (нужны `status_code`/`headers`/
|
|
48
|
+
`content`|`text`). Браузер сам ставит нативные `sec-fetch-*` / Client-Hints
|
|
49
|
+
/ TLS-fingerprint — ради чего CDP-транспорт и существует.
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
async def aclose(self) -> None:
|
|
54
|
+
"""Отключить CDP и завершить процесс браузера (idempotent)."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CdpTransport(Transport):
|
|
59
|
+
"""Транспорт через РЕАЛЬНЫЙ системный Edge/Chrome по CDP (antibot Tier 0-1).
|
|
60
|
+
|
|
61
|
+
PRIMARY-путь для signing-heavy сервисов (TikTok / WB seller / Bitrix24-auth):
|
|
62
|
+
реальный браузер сам ставит нативный триплет `sec-fetch-{dest,mode,site}`,
|
|
63
|
+
корректные Client-Hints и TLS-fingerprint, которых нет у patchright (симптомы
|
|
64
|
+
`isTTwidDecryptedFail`, `signatures don't match`, «unusual activity»). `headless`
|
|
65
|
+
переключает Tier 0 (видимый, debug/bootstrap) ↔ Tier 1 (`--headless=new`, prod
|
|
66
|
+
1-100 calls/day).
|
|
67
|
+
|
|
68
|
+
Сам тяжёлый browser-движок инкапсулирован в `CdpBrowserBackend` (реализация —
|
|
69
|
+
`librarykit.browser`), который внедряется в конструктор. Так `antibot`:
|
|
70
|
+
1) не импортирует Playwright/patchright (опциональны → graceful);
|
|
71
|
+
2) реализует ровно `librarykit.contract.Transport` (взаимозаменяем с остальными);
|
|
72
|
+
3) детерминированно тестируется фейк-backend без реального браузера.
|
|
73
|
+
|
|
74
|
+
Параметры:
|
|
75
|
+
- `backend` — реализация `CdpBrowserBackend` (composition root внедряет реальную);
|
|
76
|
+
- `headless` — Tier 1 (`True`) против Tier 0 (`False`); пробрасывается в реализацию;
|
|
77
|
+
- `prefer` — предпочесть ``"edge"`` или ``"chrome"`` (Edge стабильнее на Windows);
|
|
78
|
+
- `headers` — базовые заголовки, доливаемые к каждому запросу.
|
|
79
|
+
|
|
80
|
+
Если `backend=None` и реальный движок не сконфигурирован — первый `request`
|
|
81
|
+
бьёт понятным `TransportError` (а не падает при импорте модуля).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
backend: CdpBrowserBackend | None = None,
|
|
88
|
+
headless: bool = True,
|
|
89
|
+
prefer: Literal["chrome", "edge"] = "edge",
|
|
90
|
+
headers: Mapping[str, str] | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
self._backend = backend
|
|
93
|
+
self.headless = headless
|
|
94
|
+
self.prefer: Literal["chrome", "edge"] = prefer
|
|
95
|
+
self._headers: dict[str, str] = dict(headers) if headers else {}
|
|
96
|
+
|
|
97
|
+
def _ensure_backend(self) -> CdpBrowserBackend:
|
|
98
|
+
"""Вернуть browser-backend или объяснить, что реальный движок не подключён.
|
|
99
|
+
|
|
100
|
+
В spine `antibot` НЕ создаёт Playwright-backend сам (он живёт в
|
|
101
|
+
`librarykit.browser`); composition root внедряет его через конструктор.
|
|
102
|
+
"""
|
|
103
|
+
if self._backend is None:
|
|
104
|
+
raise TransportError(
|
|
105
|
+
"CdpTransport требует CdpBrowserBackend (реальный Edge/Chrome через CDP). "
|
|
106
|
+
"Передайте backend=... из librarykit.browser или установите extra 'browser' "
|
|
107
|
+
"(uv sync --extra browser)."
|
|
108
|
+
)
|
|
109
|
+
return self._backend
|
|
110
|
+
|
|
111
|
+
async def request(
|
|
112
|
+
self,
|
|
113
|
+
method: str,
|
|
114
|
+
url: str,
|
|
115
|
+
*,
|
|
116
|
+
params: Mapping[str, Any] | None = None,
|
|
117
|
+
json: Any | None = None,
|
|
118
|
+
data: Any | None = None,
|
|
119
|
+
content: bytes | str | None = None,
|
|
120
|
+
headers: Mapping[str, str] | None = None,
|
|
121
|
+
files: Any | None = None,
|
|
122
|
+
cookies: Mapping[str, str] | None = None,
|
|
123
|
+
) -> httpx.Response:
|
|
124
|
+
"""Выполнить запрос внутри реального браузера и вернуть `httpx.Response`.
|
|
125
|
+
|
|
126
|
+
`files` контрактом принимается, но в браузерном fetch не применяется
|
|
127
|
+
(multipart внутри страницы — отдельный сценарий); передаётся `None`. Сбой
|
|
128
|
+
браузера/CDP → `TransportError`. Маппинг статусов — выше по стеку.
|
|
129
|
+
|
|
130
|
+
``content`` (сырое тело) сворачивается в ``data`` (у in-page fetch один слот
|
|
131
|
+
тела), если ``data`` не задан.
|
|
132
|
+
"""
|
|
133
|
+
backend = self._ensure_backend()
|
|
134
|
+
body = data if data is not None else content # один слот тела у in-page fetch
|
|
135
|
+
merged_headers = {**self._headers, **(dict(headers) if headers else {})}
|
|
136
|
+
req = httpx.Request(
|
|
137
|
+
method,
|
|
138
|
+
url,
|
|
139
|
+
params=params,
|
|
140
|
+
headers=merged_headers or None,
|
|
141
|
+
cookies=cookies,
|
|
142
|
+
json=json,
|
|
143
|
+
data=data,
|
|
144
|
+
content=content, # сырое тело через content= (без httpx-deprecation)
|
|
145
|
+
)
|
|
146
|
+
with _as_transport_error("CDP"):
|
|
147
|
+
raw = await backend.fetch(
|
|
148
|
+
method,
|
|
149
|
+
url,
|
|
150
|
+
params=dict(params) if params else None,
|
|
151
|
+
json=json,
|
|
152
|
+
data=body,
|
|
153
|
+
headers=merged_headers or None,
|
|
154
|
+
cookies=dict(cookies) if cookies else None,
|
|
155
|
+
)
|
|
156
|
+
return _to_httpx_response(raw, request=req)
|
|
157
|
+
|
|
158
|
+
async def aclose(self) -> None:
|
|
159
|
+
"""Закрыть browser-backend (отключить CDP, завершить процесс), если он есть."""
|
|
160
|
+
if self._backend is not None:
|
|
161
|
+
await self._backend.aclose()
|
|
162
|
+
|
|
163
|
+
async def __aenter__(self) -> CdpTransport:
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
167
|
+
await self.aclose()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# =========================================================================== #
|
|
171
|
+
# Sync-сиблинги: браузер-backend + транспорт БЕЗ event-loop (для sync-навыков) #
|
|
172
|
+
# =========================================================================== #
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@runtime_checkable
|
|
176
|
+
class SyncCdpBrowserBackend(Protocol):
|
|
177
|
+
"""Sync-сиблинг `CdpBrowserBackend`: движок реального/stealth-браузера БЕЗ await.
|
|
178
|
+
|
|
179
|
+
Та же роль (выполнить запрос внутри живого браузера, вернуть ответоподобный
|
|
180
|
+
объект под `_to_httpx_response`), но синхронная сигнатура — для навыков без
|
|
181
|
+
event-loop. Реализация — `librarykit.browser.SyncCamoufoxBackend`.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def fetch(
|
|
185
|
+
self,
|
|
186
|
+
method: str,
|
|
187
|
+
url: str,
|
|
188
|
+
*,
|
|
189
|
+
params: Mapping[str, Any] | None = None,
|
|
190
|
+
json: Any | None = None,
|
|
191
|
+
data: Any | None = None,
|
|
192
|
+
headers: Mapping[str, str] | None = None,
|
|
193
|
+
cookies: Mapping[str, str] | None = None,
|
|
194
|
+
) -> Any:
|
|
195
|
+
"""Выполнить запрос внутри браузера; вернуть ответоподобный объект (sync)."""
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
def close(self) -> None:
|
|
199
|
+
"""Завершить браузер/сессию (idempotent)."""
|
|
200
|
+
...
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class SyncCdpTransport(SyncTransport):
|
|
204
|
+
"""Sync-сиблинг `CdpTransport`: запрос через sync браузер-backend → `httpx.Response`.
|
|
205
|
+
|
|
206
|
+
Точная параллель `CdpTransport`, но синхронная: реализует `contract.SyncTransport`
|
|
207
|
+
(взаимозаменяем с `HttpxSyncTransport` в `SyncHttpClient`). Движок — внедряемый
|
|
208
|
+
`SyncCdpBrowserBackend` (напр. `SyncCamoufoxBackend`); маппинг кодов в доменные
|
|
209
|
+
ошибки — выше (`SyncHttpClient`/`ErrorMapper`).
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
*,
|
|
215
|
+
backend: SyncCdpBrowserBackend | None = None,
|
|
216
|
+
headers: Mapping[str, str] | None = None,
|
|
217
|
+
) -> None:
|
|
218
|
+
self._backend = backend
|
|
219
|
+
self._headers: dict[str, str] = dict(headers) if headers else {}
|
|
220
|
+
|
|
221
|
+
def _ensure_backend(self) -> SyncCdpBrowserBackend:
|
|
222
|
+
if self._backend is None:
|
|
223
|
+
raise TransportError(
|
|
224
|
+
"SyncCdpTransport требует SyncCdpBrowserBackend (напр. "
|
|
225
|
+
"librarykit.browser.SyncCamoufoxBackend)."
|
|
226
|
+
)
|
|
227
|
+
return self._backend
|
|
228
|
+
|
|
229
|
+
def request(
|
|
230
|
+
self,
|
|
231
|
+
method: str,
|
|
232
|
+
url: str,
|
|
233
|
+
*,
|
|
234
|
+
params: Mapping[str, Any] | None = None,
|
|
235
|
+
json: Any | None = None,
|
|
236
|
+
data: Any | None = None,
|
|
237
|
+
content: bytes | str | None = None,
|
|
238
|
+
headers: Mapping[str, str] | None = None,
|
|
239
|
+
files: Any | None = None,
|
|
240
|
+
cookies: Mapping[str, str] | None = None,
|
|
241
|
+
) -> httpx.Response:
|
|
242
|
+
"""Выполнить запрос в браузере (sync) и вернуть `httpx.Response`.
|
|
243
|
+
|
|
244
|
+
``content`` сворачивается в ``data`` (у in-page fetch один слот тела), если
|
|
245
|
+
``data`` не задан. ``files`` не применяется (multipart внутри страницы —
|
|
246
|
+
отдельный сценарий). Сбой браузера → `TransportError`.
|
|
247
|
+
"""
|
|
248
|
+
backend = self._ensure_backend()
|
|
249
|
+
body = data if data is not None else content # один слот тела у in-page fetch
|
|
250
|
+
merged_headers = {**self._headers, **(dict(headers) if headers else {})}
|
|
251
|
+
req = httpx.Request(
|
|
252
|
+
method, url, params=params, headers=merged_headers or None,
|
|
253
|
+
cookies=cookies, json=json, data=data, content=content,
|
|
254
|
+
)
|
|
255
|
+
with _as_transport_error("CDP(sync)"):
|
|
256
|
+
raw = backend.fetch(
|
|
257
|
+
method, url, params=dict(params) if params else None,
|
|
258
|
+
json=json, data=body, headers=merged_headers or None,
|
|
259
|
+
cookies=dict(cookies) if cookies else None,
|
|
260
|
+
)
|
|
261
|
+
return _to_httpx_response(raw, request=req)
|
|
262
|
+
|
|
263
|
+
def close(self) -> None:
|
|
264
|
+
"""Закрыть browser-backend (sync), если он есть."""
|
|
265
|
+
if self._backend is not None:
|
|
266
|
+
self._backend.close()
|
|
267
|
+
|
|
268
|
+
def __enter__(self) -> SyncCdpTransport:
|
|
269
|
+
return self
|
|
270
|
+
|
|
271
|
+
def __exit__(self, *exc: object) -> None:
|
|
272
|
+
self.close()
|