Files
KingOfTheNOPs-CDP-Toolkit/cdptoolkit/client.py
T
2026-05-14 12:37:51 -04:00

176 lines
7.2 KiB
Python

from __future__ import annotations
import base64
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from .discovery import BrowserEndpoint, get_version
from .log import info
from .transport import CDPConnection
@dataclass
class TargetSession:
client: "CDPClient"
target_id: str
session_id: str
async def send(self, method: str, params: dict | None = None) -> dict:
return await self.client.send(method, params, session_id=self.session_id)
async def evaluate(self, expression: str, *, return_by_value: bool = True, await_promise: bool = True) -> dict:
return await self.send(
"Runtime.evaluate",
{
"expression": expression,
"returnByValue": return_by_value,
"awaitPromise": await_promise,
"userGesture": True,
},
)
class CDPClient:
def __init__(self, endpoint: BrowserEndpoint, connection: CDPConnection):
self.endpoint = endpoint
self.connection = connection
@classmethod
async def connect(cls, cdp_endpoint: str, socks: str | None = None) -> "CDPClient":
info("connecting to CDP endpoint {}", cdp_endpoint)
endpoint = await get_version(cdp_endpoint, proxy=socks)
conn = CDPConnection(endpoint.web_socket_debugger_url, proxy=socks)
await conn.connect()
info("connected to {}", endpoint.product)
return cls(endpoint, conn)
async def __aenter__(self) -> "CDPClient":
return self
async def __aexit__(self, exc_type, exc, tb) -> None:
await self.close()
async def close(self) -> None:
info("closing browser client")
await self.connection.close()
async def send(self, method: str, params: dict | None = None, session_id: str | None = None) -> dict:
return await self.connection.send(method, params, session_id=session_id)
async def send_no_wait(self, method: str, params: dict | None = None, session_id: str | None = None) -> int:
return await self.connection.send_no_wait(method, params, session_id=session_id)
async def targets(self, include_non_pages: bool = False) -> list[dict]:
result = await self.send("Target.getTargets")
targets = result.get("targetInfos", [])
info("received {} targets", len(targets))
if include_non_pages:
return targets
pages = [target for target in targets if target.get("type") == "page"]
info("filtered to {} page targets", len(pages))
return pages
async def attach(self, target_id: str) -> TargetSession:
info("attaching to target {}", target_id)
result = await self.send("Target.attachToTarget", {"targetId": target_id, "flatten": True})
info("attached session {}", result["sessionId"])
return TargetSession(self, target_id, result["sessionId"])
async def attach_browser_target(self) -> TargetSession:
info("attaching to browser target")
result = await self.send("Target.attachToBrowserTarget")
info("attached browser session {}", result["sessionId"])
return TargetSession(self, "browser", result["sessionId"])
async def detach(self, session: TargetSession) -> None:
info("detaching session {}", session.session_id)
await self.send("Target.detachFromTarget", {"sessionId": session.session_id})
async def create_browser_context(self, *, proxy_server: str | None = None, dispose_on_detach: bool = True) -> str:
params: dict[str, Any] = {"disposeOnDetach": dispose_on_detach}
if proxy_server:
params["proxyServer"] = proxy_server
info("creating browser context{} disposeOnDetach={}", f" proxy={proxy_server}" if proxy_server else "", dispose_on_detach)
result = await self.send("Target.createBrowserContext", params)
info("created browser context {}", result["browserContextId"])
return result["browserContextId"]
async def browser_contexts(self) -> list[str]:
result = await self.send("Target.getBrowserContexts")
contexts = result.get("browserContextIds", [])
info("received {} non-default browser contexts", len(contexts))
return contexts
async def dispose_browser_context(self, browser_context_id: str) -> None:
info("disposing browser context {}", browser_context_id)
await self.send("Target.disposeBrowserContext", {"browserContextId": browser_context_id})
async def create_target(
self,
url: str,
*,
mode: str = "visible",
browser_context_id: str | None = None,
focus: bool | None = None,
) -> str:
params: dict[str, Any] = {"url": url}
if browser_context_id:
params["browserContextId"] = browser_context_id
if mode == "hidden":
params["hidden"] = True
params["background"] = True
elif mode == "background":
params["background"] = True
if focus is not None:
params["focus"] = focus
info("creating target url={} mode={} context={}", url, mode, browser_context_id or "default")
result = await self.send("Target.createTarget", params)
info("created target {}", result["targetId"])
return result["targetId"]
async def close_target(self, target_id: str) -> bool:
info("closing target {}", target_id)
result = await self.send("Target.closeTarget", {"targetId": target_id})
return bool(result.get("success", True))
async def get_cookies(self, browser_context_id: str | None = None) -> list[dict]:
params = {}
if browser_context_id:
params["browserContextId"] = browser_context_id
result = await self.send("Storage.getCookies", params)
cookies = result.get("cookies", [])
info("received {} cookies", len(cookies))
return cookies
async def set_cookies(self, cookies: list[dict], browser_context_id: str | None = None) -> None:
params: dict[str, Any] = {"cookies": cookies}
if browser_context_id:
params["browserContextId"] = browser_context_id
info("setting {} cookies{}", len(cookies), f" in context {browser_context_id}" if browser_context_id else "")
await self.send("Storage.setCookies", params)
async def load_unpacked_extension(self, path: str, *, enable_in_incognito: bool = False) -> dict:
session = await self.attach_browser_target()
try:
info("loading unpacked extension from {}", path)
return await session.send(
"Extensions.loadUnpacked",
{"path": path, "enableInIncognito": enable_in_incognito},
)
finally:
await self.detach(session)
async def screenshot(self, target_id: str, out: Path, *, full: bool = False) -> Path:
session = await self.attach(target_id)
try:
params: dict[str, Any] = {"format": "png"}
if full:
params["captureBeyondViewport"] = True
result = await session.send("Page.captureScreenshot", params)
out.write_bytes(base64.b64decode(result["data"]))
info("wrote screenshot {}", out)
return out
finally:
await self.detach(session)