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.
Files changed (98) hide show
  1. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/CHANGELOG.md +13 -0
  2. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/PKG-INFO +1 -1
  3. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/__init__.py +58 -11
  4. s_librarykit-0.2.0/librarykit/antibot/__init__.py +126 -0
  5. s_librarykit-0.2.0/librarykit/antibot/_helpers.py +69 -0
  6. s_librarykit-0.2.0/librarykit/antibot/cdp.py +272 -0
  7. s_librarykit-0.2.0/librarykit/antibot/curl_cffi.py +231 -0
  8. s_librarykit-0.2.0/librarykit/antibot/page_evaluate.py +301 -0
  9. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/auth.py +35 -19
  10. s_librarykit-0.2.0/librarykit/browser/__init__.py +835 -0
  11. s_librarykit-0.2.0/librarykit/browser/_engine.py +139 -0
  12. s_librarykit-0.2.0/librarykit/browser/camoufox.py +330 -0
  13. s_librarykit-0.2.0/librarykit/browser/geometry.py +114 -0
  14. s_librarykit-0.2.0/librarykit/browser/launchers.py +229 -0
  15. s_librarykit-0.2.0/librarykit/browser/state.py +308 -0
  16. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/checkpoint.py +7 -1
  17. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/contract.py +115 -2
  18. s_librarykit-0.2.0/librarykit/factory.py +390 -0
  19. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/limit.py +36 -3
  20. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/pool.py +29 -12
  21. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/refresh.py +18 -6
  22. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/retry.py +14 -6
  23. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/rpc.py +2 -1
  24. s_librarykit-0.1.22/librarykit/sessions.py → s_librarykit-0.2.0/librarykit/sessions/__init__.py +72 -160
  25. s_librarykit-0.2.0/librarykit/sessions/_const.py +89 -0
  26. s_librarykit-0.2.0/librarykit/sessions/_fsutil.py +83 -0
  27. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/stream.py +20 -0
  28. s_librarykit-0.2.0/librarykit/transport/__init__.py +88 -0
  29. s_librarykit-0.2.0/librarykit/transport/_helpers.py +43 -0
  30. s_librarykit-0.2.0/librarykit/transport/http_client.py +446 -0
  31. s_librarykit-0.2.0/librarykit/transport/permissive.py +147 -0
  32. s_librarykit-0.2.0/librarykit/transport/rest_client.py +382 -0
  33. s_librarykit-0.2.0/librarykit/transport/transport_http.py +355 -0
  34. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/pyproject.toml +1 -1
  35. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_antibot.py +42 -0
  36. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_auth.py +41 -2
  37. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_browser.py +154 -0
  38. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_checkpoint.py +10 -0
  39. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_errors.py +17 -0
  40. s_librarykit-0.2.0/tests/test_factory.py +265 -0
  41. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_limit.py +26 -0
  42. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_pool.py +34 -1
  43. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_refresh.py +37 -1
  44. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_retry.py +14 -0
  45. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_sessions.py +20 -2
  46. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_stream.py +10 -0
  47. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_transport_client.py +29 -0
  48. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/uv.lock +2 -2
  49. s_librarykit-0.1.22/librarykit/antibot.py +0 -880
  50. s_librarykit-0.1.22/librarykit/browser.py +0 -1673
  51. s_librarykit-0.1.22/librarykit/transport.py +0 -1347
  52. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/.gitignore +0 -0
  53. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/.gitlab-ci.yml +0 -0
  54. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/AGENTS.md +0 -0
  55. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/LICENSE +0 -0
  56. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/README.md +0 -0
  57. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/docs/superpowers/specs/2026-06-24-unified-sessions-design.md +0 -0
  58. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/blocks.py +0 -0
  59. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/bulk.py +0 -0
  60. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/config_util.py +0 -0
  61. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/entities.py +0 -0
  62. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/enums.py +0 -0
  63. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/errmap.py +0 -0
  64. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/errors.py +0 -0
  65. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/forms.py +0 -0
  66. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/graphql.py +0 -0
  67. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/orchestration/__init__.py +0 -0
  68. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/orchestration/health.py +0 -0
  69. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/orchestration/onboarding.py +0 -0
  70. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/orchestration/session_loader.py +0 -0
  71. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/pagination.py +0 -0
  72. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/ports.py +0 -0
  73. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/protocols.py +0 -0
  74. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/redaction.py +0 -0
  75. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/secret_store.py +0 -0
  76. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/session_bridge.py +0 -0
  77. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/session_paths.py +0 -0
  78. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/upload.py +0 -0
  79. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/librarykit/windows.py +0 -0
  80. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/conftest.py +0 -0
  81. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_blocks.py +0 -0
  82. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_bulk.py +0 -0
  83. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_config_util.py +0 -0
  84. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_errmap.py +0 -0
  85. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_forms.py +0 -0
  86. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_graphql.py +0 -0
  87. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_limit_contract.py +0 -0
  88. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_orchestration.py +0 -0
  89. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_pagination.py +0 -0
  90. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_permissive_http.py +0 -0
  91. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_redaction.py +0 -0
  92. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_rpc.py +0 -0
  93. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_session_bridge.py +0 -0
  94. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_session_paths.py +0 -0
  95. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_transport_rest.py +0 -0
  96. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_transport_sync.py +0 -0
  97. {s_librarykit-0.1.22 → s_librarykit-0.2.0}/tests/test_upload.py +0 -0
  98. {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.1.22
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 — жирный КОРЕНЬ китов (граф librarykit <- adapterkit <- clikit).
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.1.22"
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()