s-librarykit 0.5.0__tar.gz → 0.5.2__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.5.0 → s_librarykit-0.5.2}/PKG-INFO +1 -1
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/browser/capture_tap.py +91 -5
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/pyproject.toml +1 -1
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_capture_tap.py +24 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/.gitignore +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/.gitlab-ci.yml +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/AGENTS.md +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/CHANGELOG.md +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/LICENSE +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/README.md +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/docs/superpowers/specs/2026-06-24-unified-sessions-design.md +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/__init__.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/antibot/__init__.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/antibot/_helpers.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/antibot/cdp.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/antibot/curl_cffi.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/antibot/page_evaluate.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/antibot/warmup_minter.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/auth.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/blocks.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/browser/__init__.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/browser/_engine.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/browser/camoufox.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/browser/geometry.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/browser/launchers.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/browser/nodriver.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/browser/persistent.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/browser/state.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/bulk.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/checkpoint.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/config_util.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/contract.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/cookie_refresh.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/cookies.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/entities.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/enums.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/errmap.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/errors.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/factory.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/forms.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/graphql.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/limit.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/orchestration/__init__.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/orchestration/health.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/orchestration/onboarding.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/orchestration/session_loader.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/pagination.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/pool.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/ports.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/protocols.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/redaction.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/refresh.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/retry.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/rpc.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/secret_store.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/session_bridge.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/session_paths.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/sessions/__init__.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/sessions/_const.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/sessions/_fsutil.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/stream.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/transport/__init__.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/transport/_helpers.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/transport/http_client.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/transport/permissive.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/transport/rest_client.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/transport/transport_http.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/upload.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/librarykit/windows.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/conftest.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_antibot.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_auth.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_blocks.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_browser.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_bulk.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_checkpoint.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_config_util.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_cookie_refresh.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_cookies.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_errmap.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_errors.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_factory.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_forms.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_graphql.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_limit.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_limit_contract.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_nodriver_backend.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_orchestration.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_pagination.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_permissive_http.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_pool.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_redaction.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_refresh.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_retry.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_rpc.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_session_bridge.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_session_paths.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_sessions.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_stream.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_transport_client.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_transport_rest.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_transport_sync.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_upload.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_warmup_minter.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/tests/test_windows.py +0 -0
- {s_librarykit-0.5.0 → s_librarykit-0.5.2}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: s-librarykit
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Жирный КОРЕНЬ китов: единая error-иерархия, config/retry/paths-утили, checkpoint/resume, транспорт/auth/sessions — общий код для clikit, adapterkit и доменных пакетов.
|
|
5
5
|
Author: Dmitry
|
|
6
6
|
License: MIT
|
|
@@ -206,6 +206,8 @@ class _PlaywrightAdapter(_BaseAdapter):
|
|
|
206
206
|
self._context = page.context
|
|
207
207
|
# Request-объект → стабильный request_id (для сшивки request/response/failed).
|
|
208
208
|
self._ids: "weakref.WeakKeyDictionary[Any, str]" = weakref.WeakKeyDictionary()
|
|
209
|
+
self._drain_stop: asyncio.Event | None = None
|
|
210
|
+
self._drain_task: Any = None
|
|
209
211
|
|
|
210
212
|
def _id_for(self, request: Any) -> str:
|
|
211
213
|
rid = self._ids.get(request)
|
|
@@ -222,6 +224,10 @@ class _PlaywrightAdapter(_BaseAdapter):
|
|
|
222
224
|
await ctx.expose_binding(self._binding, self._on_binding)
|
|
223
225
|
with contextlib.suppress(Exception):
|
|
224
226
|
await ctx.add_init_script(self._init_script)
|
|
227
|
+
# Дренаж очереди — fallback для движков, где binding не виден из мира init-script
|
|
228
|
+
# (CamouFox/Firefox гонят его в изолированном мире). На chromium очередь пуста.
|
|
229
|
+
self._drain_stop = asyncio.Event()
|
|
230
|
+
self._drain_task = asyncio.ensure_future(self._drain_queue_loop())
|
|
225
231
|
if self._h.on_request is not None:
|
|
226
232
|
ctx.on("request", lambda r: self._spawn(self._handle_request(r)))
|
|
227
233
|
if self._h.on_response is not None:
|
|
@@ -282,7 +288,11 @@ class _PlaywrightAdapter(_BaseAdapter):
|
|
|
282
288
|
if self._body_filter(response.url, rtype, mime):
|
|
283
289
|
try:
|
|
284
290
|
raw = await response.body()
|
|
285
|
-
|
|
291
|
+
# Дропаем СЛИШКОМ большие тела (не пишем битый обрезок как «captured»):
|
|
292
|
+
if raw is not None and len(raw) > self._max_body_bytes:
|
|
293
|
+
body_error = f"body too large: {len(raw)} > {self._max_body_bytes}"
|
|
294
|
+
else:
|
|
295
|
+
body = raw
|
|
286
296
|
except Exception as err: # noqa: BLE001
|
|
287
297
|
body_error = f"{type(err).__name__}: {err}"[:160]
|
|
288
298
|
await self._emit(self._h.on_response, NormalizedResponse(
|
|
@@ -303,7 +313,42 @@ class _PlaywrightAdapter(_BaseAdapter):
|
|
|
303
313
|
return await self._context.storage_state()
|
|
304
314
|
return {"cookies": [], "origins": []}
|
|
305
315
|
|
|
316
|
+
async def _drain_queue_loop(self) -> None:
|
|
317
|
+
"""Периодически дренит globalThis.__captureTraceQueue → on_action (fallback CamouFox)."""
|
|
318
|
+
assert self._drain_stop is not None
|
|
319
|
+
while not self._drain_stop.is_set():
|
|
320
|
+
try:
|
|
321
|
+
await asyncio.wait_for(self._drain_stop.wait(), timeout=0.3)
|
|
322
|
+
break
|
|
323
|
+
except asyncio.TimeoutError:
|
|
324
|
+
pass
|
|
325
|
+
await self._drain_queue_once()
|
|
326
|
+
|
|
327
|
+
async def _drain_queue_once(self) -> None:
|
|
328
|
+
try:
|
|
329
|
+
items = await self._page.evaluate(
|
|
330
|
+
"() => { const q = globalThis.__captureTraceQueue || [];"
|
|
331
|
+
" globalThis.__captureTraceQueue = []; return q; }"
|
|
332
|
+
)
|
|
333
|
+
except Exception:
|
|
334
|
+
return
|
|
335
|
+
for s in items or []:
|
|
336
|
+
try:
|
|
337
|
+
payload = json.loads(s) if isinstance(s, str) else s
|
|
338
|
+
except Exception:
|
|
339
|
+
continue
|
|
340
|
+
await self._emit(self._h.on_action, NormalizedAction(raw=payload))
|
|
341
|
+
|
|
306
342
|
async def detach(self) -> None:
|
|
343
|
+
# Остановить дренаж очереди trace (CamouFox-fallback) + финальный дренаж.
|
|
344
|
+
if self._drain_stop is not None:
|
|
345
|
+
self._drain_stop.set()
|
|
346
|
+
with contextlib.suppress(Exception):
|
|
347
|
+
await self._drain_queue_once()
|
|
348
|
+
if self._drain_task is not None:
|
|
349
|
+
self._drain_task.cancel()
|
|
350
|
+
with contextlib.suppress(Exception, asyncio.CancelledError):
|
|
351
|
+
await self._drain_task
|
|
307
352
|
# Слушатели не снимаем поимённо (Playwright требует ссылку на сам хендлер):
|
|
308
353
|
# контекст всё равно закрывает backend.aclose() сразу после detach. Здесь —
|
|
309
354
|
# только дренаж фоновых задач нормализации, чтобы не потерять последние тела.
|
|
@@ -315,11 +360,23 @@ class _CdpAdapter(_BaseAdapter):
|
|
|
315
360
|
|
|
316
361
|
family = "cdp"
|
|
317
362
|
|
|
363
|
+
# Cap на незакрытые rid: SSE/стриминг/зависшие соединения не эмитят
|
|
364
|
+
# LoadingFinished/Failed, иначе _pending/_resp_meta растут всю сессию.
|
|
365
|
+
_MAX_INFLIGHT = 4096
|
|
366
|
+
|
|
318
367
|
def __init__(self, tab: Any, handlers: _Handlers, **kw: Any) -> None:
|
|
319
368
|
super().__init__(tab, handlers, **kw)
|
|
320
369
|
self._tab = tab
|
|
321
370
|
self._pending: dict[str, dict[str, Any]] = {}
|
|
322
371
|
self._resp_meta: dict[str, dict[str, Any]] = {}
|
|
372
|
+
self._ws_urls: dict[str, str] = {} # rid → реальный wss:// URL (из WebSocketCreated)
|
|
373
|
+
|
|
374
|
+
def _evict_stale(self) -> None:
|
|
375
|
+
"""FIFO-эвикция самых старых висящих rid при переполнении (dict хранит порядок)."""
|
|
376
|
+
while len(self._pending) > self._MAX_INFLIGHT:
|
|
377
|
+
old = next(iter(self._pending))
|
|
378
|
+
self._pending.pop(old, None)
|
|
379
|
+
self._resp_meta.pop(old, None)
|
|
323
380
|
|
|
324
381
|
async def attach(self) -> None:
|
|
325
382
|
from nodriver import cdp # type: ignore[import-not-found]
|
|
@@ -332,6 +389,8 @@ class _CdpAdapter(_BaseAdapter):
|
|
|
332
389
|
self._tab.add_handler(cdp.network.LoadingFinished, self._on_fin)
|
|
333
390
|
self._tab.add_handler(cdp.network.LoadingFailed, self._on_failed)
|
|
334
391
|
if self._capture_ws and self._h.on_ws is not None:
|
|
392
|
+
# WebSocketCreated несёт реальный URL (Frame*-события — только request_id).
|
|
393
|
+
self._tab.add_handler(cdp.network.WebSocketCreated, self._on_ws_created)
|
|
335
394
|
self._tab.add_handler(cdp.network.WebSocketFrameSent, self._on_ws_sent)
|
|
336
395
|
self._tab.add_handler(cdp.network.WebSocketFrameReceived, self._on_ws_recv)
|
|
337
396
|
if self._h.on_action is not None and self._init_script:
|
|
@@ -348,7 +407,24 @@ class _CdpAdapter(_BaseAdapter):
|
|
|
348
407
|
|
|
349
408
|
async def _on_req(self, ev: Any, *_: Any) -> None:
|
|
350
409
|
rid = str(ev.request_id)
|
|
351
|
-
|
|
410
|
+
# Chrome переиспользует тот же CDP requestId для цепочки редиректов и отдаёт
|
|
411
|
+
# промежуточный 3xx в ev.redirect_response (отдельного ResponseReceived НЕТ).
|
|
412
|
+
# Флашим завершённый хоп отдельной записью под его id — паритет с Playwright.
|
|
413
|
+
redirect = getattr(ev, "redirect_response", None)
|
|
414
|
+
if redirect is not None:
|
|
415
|
+
prev = self._pending.pop(rid, None)
|
|
416
|
+
self._resp_meta.pop(rid, None)
|
|
417
|
+
if prev is not None:
|
|
418
|
+
await self._emit(self._h.on_response, NormalizedResponse(
|
|
419
|
+
request_id=prev["id"], status=int(getattr(redirect, "status", 0) or 0),
|
|
420
|
+
status_text=getattr(redirect, "status_text", "") or "",
|
|
421
|
+
headers=_lower_headers(getattr(redirect, "headers", {}) or {}),
|
|
422
|
+
mime_type=getattr(redirect, "mime_type", "") or "", body=None, body_error=None,
|
|
423
|
+
))
|
|
424
|
+
entry = self._pending.get(rid)
|
|
425
|
+
if entry is None:
|
|
426
|
+
entry = {"id": self._next_id()}
|
|
427
|
+
self._pending[rid] = entry
|
|
352
428
|
req = ev.request
|
|
353
429
|
post = None
|
|
354
430
|
pd = getattr(req, "post_data", None)
|
|
@@ -360,6 +436,7 @@ class _CdpAdapter(_BaseAdapter):
|
|
|
360
436
|
frame_url=ev.document_url or "", post_data=post,
|
|
361
437
|
)
|
|
362
438
|
await self._emit(self._h.on_request, entry["norm"])
|
|
439
|
+
self._evict_stale()
|
|
363
440
|
|
|
364
441
|
async def _on_resp(self, ev: Any, *_: Any) -> None:
|
|
365
442
|
rid = str(ev.request_id)
|
|
@@ -372,6 +449,7 @@ class _CdpAdapter(_BaseAdapter):
|
|
|
372
449
|
"mime": getattr(resp, "mime_type", "") or "",
|
|
373
450
|
"rtype": self._rtype(ev.type_),
|
|
374
451
|
}
|
|
452
|
+
self._evict_stale()
|
|
375
453
|
|
|
376
454
|
async def _on_fin(self, ev: Any, *_: Any) -> None:
|
|
377
455
|
rid = str(ev.request_id)
|
|
@@ -392,7 +470,10 @@ class _CdpAdapter(_BaseAdapter):
|
|
|
392
470
|
try:
|
|
393
471
|
raw, b64 = await self._tab.send(self._cdp.network.get_response_body(ev.request_id))
|
|
394
472
|
data = _b64.b64decode(raw) if b64 else (raw or "").encode("utf-8", "replace")
|
|
395
|
-
|
|
473
|
+
if len(data) > self._max_body_bytes:
|
|
474
|
+
body_error = f"body too large: {len(data)} > {self._max_body_bytes}"
|
|
475
|
+
else:
|
|
476
|
+
body = data
|
|
396
477
|
last_err = None
|
|
397
478
|
break
|
|
398
479
|
except Exception as err: # noqa: BLE001
|
|
@@ -414,6 +495,10 @@ class _CdpAdapter(_BaseAdapter):
|
|
|
414
495
|
err = getattr(ev, "error_text", "") or ("canceled" if getattr(ev, "canceled", False) else "failed")
|
|
415
496
|
await self._emit(self._h.on_failure, NormalizedFailure(request_id=entry["id"], error_text=str(err)))
|
|
416
497
|
|
|
498
|
+
async def _on_ws_created(self, ev: Any, *_: Any) -> None:
|
|
499
|
+
# Единственное CDP-событие WS с реальным URL — запоминаем rid→url.
|
|
500
|
+
self._ws_urls[str(getattr(ev, "request_id", ""))] = getattr(ev, "url", "") or ""
|
|
501
|
+
|
|
417
502
|
async def _on_ws_sent(self, ev: Any, *_: Any) -> None:
|
|
418
503
|
await self._ws(ev, "sent")
|
|
419
504
|
|
|
@@ -424,8 +509,9 @@ class _CdpAdapter(_BaseAdapter):
|
|
|
424
509
|
resp = getattr(ev, "response", None)
|
|
425
510
|
payload = getattr(resp, "payload_data", "") if resp is not None else ""
|
|
426
511
|
opcode = getattr(resp, "opcode", None) if resp is not None else None
|
|
512
|
+
rid = str(getattr(ev, "request_id", ""))
|
|
427
513
|
await self._emit(self._h.on_ws, NormalizedWsFrame(
|
|
428
|
-
url=
|
|
514
|
+
url=self._ws_urls.get(rid, ""), direction=direction,
|
|
429
515
|
payload=payload, opcode=int(opcode) if opcode is not None else None,
|
|
430
516
|
))
|
|
431
517
|
|
|
@@ -450,7 +536,7 @@ class _CdpAdapter(_BaseAdapter):
|
|
|
450
536
|
"path": getattr(c, "path", "/"), "expires": float(getattr(c, "expires", -1) or -1),
|
|
451
537
|
"httpOnly": bool(getattr(c, "http_only", False)),
|
|
452
538
|
"secure": bool(getattr(c, "secure", False)),
|
|
453
|
-
"sameSite": (
|
|
539
|
+
"sameSite": (getattr(getattr(c, "same_site", None), "value", None) or "Lax"),
|
|
454
540
|
})
|
|
455
541
|
return {"cookies": cookies, "origins": []}
|
|
456
542
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "s-librarykit"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.2"
|
|
4
4
|
description = "Жирный КОРЕНЬ китов: единая error-иерархия, config/retry/paths-утили, checkpoint/resume, транспорт/auth/sessions — общий код для clikit, adapterkit и доменных пакетов."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -106,10 +106,15 @@ class _MockContext:
|
|
|
106
106
|
class _MockPage:
|
|
107
107
|
def __init__(self, context):
|
|
108
108
|
self.context = context
|
|
109
|
+
self.queue_items: list[str] = []
|
|
109
110
|
|
|
110
111
|
def on(self, *_a):
|
|
111
112
|
pass
|
|
112
113
|
|
|
114
|
+
async def evaluate(self, _script):
|
|
115
|
+
items, self.queue_items = self.queue_items, []
|
|
116
|
+
return items
|
|
117
|
+
|
|
113
118
|
|
|
114
119
|
class _MockBackend:
|
|
115
120
|
def __init__(self, page):
|
|
@@ -183,3 +188,22 @@ def test_attach_tap_playwright_flow():
|
|
|
183
188
|
assert got_resp[0].status == 200 and got_resp[0].body == b'{"ok":true}'
|
|
184
189
|
# request_id сшит между request и response
|
|
185
190
|
assert got_req[0].request_id == got_resp[0].request_id
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_attach_tap_drain_queue_fallback():
|
|
194
|
+
"""CamouFox-fallback: событие из globalThis.__captureTraceQueue дренится page.evaluate → on_action."""
|
|
195
|
+
ctx = _MockContext()
|
|
196
|
+
page = _MockPage(ctx)
|
|
197
|
+
page.queue_items = ['{"kind":"action","type":"click"}', '{"kind":"mutation"}']
|
|
198
|
+
backend = _MockBackend(page)
|
|
199
|
+
got: list = []
|
|
200
|
+
|
|
201
|
+
async def run():
|
|
202
|
+
handle = await attach_capture_tap(
|
|
203
|
+
backend, on_action=got.append, init_script="/*trace*/", action_binding="__t",
|
|
204
|
+
)
|
|
205
|
+
await handle.detach() # финальный дренаж соберёт очередь
|
|
206
|
+
|
|
207
|
+
asyncio.run(run())
|
|
208
|
+
kinds = {a.raw.get("kind") for a in got}
|
|
209
|
+
assert kinds == {"action", "mutation"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|