Skip to main content

parsek-cdp

An async Chrome DevTools Protocol client for driving Chromium-based browsers from Python.

The protocol layer (cdp/) is fully generated from the official devtools-protocol JSON definitions — one typed module per domain. The runtime (core/) is hand-written: one websocket per target, typed command calls, event handlers and an opt-in feature system. No sessionId multiplexing — every target (browser, page, worker, OOPIF) gets its own connection.

Requires Python 3.12+ (the API uses PEP 695 generics) and a running Chromium/Chrome with remote debugging enabled.

pip install parsek-cdp

Документация на русском: README.ru.md.

Quickstart

Start a browser with remote debugging:

chromium --headless=new --remote-debugging-port=9222

Then drive it:

import asyncio
from parsek_cdp import Browser, Page, RequestListener


async def main():
    # `page_class` types every page in this browser as Page[RequestListener].
    browser = await Browser.connect_http(
        "http://127.0.0.1:9222", page_class=Page[RequestListener]
    )

    page = await browser.new_page()
    requests = page.get_feature(RequestListener)

    await page.navigate("https://example.com", wait_load=True)

    for r in requests.requests:
        status = r.response.status_code if r.response else "(pending)"
        print(r.method, r.url, "->", status)

    await browser.close()


asyncio.run(main())

Concepts

Targets and connections

A Target is a single CDP session on its own websocket. Browser, Page, workers and out-of-process iframes are all targets. The DevTools host is bound once (in a contextvar) when the browser is reached, so it is not threaded through every call.

The full command surface is reached through target.cdp, one attribute per CDP domain, with typed methods and results:

await page.cdp.Network.enable()
info = await page.cdp.Target.get_target_info()

Domains enable/disable themselves on use, and domain_enabled scopes a domain to a block:

async with page.domain_enabled(page.cdp.Page):
    await page.cdp.Page.navigate(url="https://example.com")

Events

Register handlers on a specific target, or globally on the event class:

from parsek_cdp.cdp import Network

# per-target
page.on(Network.RequestWillBeSent, lambda e: print(e.request.url))

# globally, for every target
@Network.RequestWillBeSent.add_handler
async def on_request(e):
    print(e.request.url)

# await a single event
loaded = await page.wait_for(Network.ResponseReceived, timeout=10)

Handlers may be sync or async; one failing handler never tears down the connection or stops the others.

Pages, frames and elements

A Page is its own main frame, so frame methods work on the top frame directly:

el = await page.select(selector="h1")      # query the main frame
print(el.text)
await el.fill("hello")
await el.mouse_click()

title = await page.evaluate("document.title")

The full frame tree (subframes, cross-origin OOPIFs — each transparently routed through its own session) lives on page.frames. page.with_same_navigation() guards a block against a main-frame navigation cutting it short.

Browser contexts

Pages are grouped into browser contexts (incognito-like profiles). The default context holds pages created without an explicit one:

context = await browser.create_context()        # isolated profile
page = await context.new_page("https://example.com")

Features

A Page attaches no behaviour by default. A feature is a unit of behaviour you opt into, either declaratively or imperatively:

from parsek_cdp import Page, RequestListener

Page[RequestListener]                 # declarative — typed page class
page.get_feature(RequestListener)     # imperative — attached on first use, typed

The key idea: a feature is written once and runs in three roles —

  • local — direct to the browser: the producers subscribe to raw CDP on the page and reduce it in-process (a plain local Page);
  • server — the proxy feeds raw CDP to the producers, whose output is emitted to the client as a Parsek.* event and reduced locally for snapshots;
  • client — only the reducers run, fed by those Parsek.* events off the wire.

Conceptually that is two sides — a producer of Parsek.* events and a view of them — but the producer side exists both in-process (local) and over the wire (server). The reducers run in every role, so the feature's public API is identical whether the browser is local or behind a parsek-cdp-server proxy.

Writing a custom feature — domain-like structure

A feature mirrors the layout of a generated CDP domain (cdp/<domain>/ = types · functions · events). Give it its own package with the same three files, so the feature class doubles as its own namespace:

features/<feature>/
  types.py       # wire dataclasses + view types  (@parsek_type)
  events.py      # aggregated Parsek.* events       (@register_event)
  __init__.py    # the Feature class itself: domains + @on/@emit handlers

The building blocks (all from parsek_cdp.core.feature):

Block Where What
domains = (Network, ...) feature class CDP domains the feature owns — auto-enabled, and their raw events are suppressed from client passthrough
@on(Event) method register a handler. A CDP event makes it a producer (runs server/local); a Parsek.* event makes it a reducer (runs in every role)
@emit(ParsekEvent) producer method its return value is published to the client and reduced locally — so state is built by the same code path everywhere
@parsek_type("Parsek.<Feature>.<Type>") types.py declare a wire dataclass; __FIELDS__ are derived from annotations (snake_case ↔ camelCase)
@register_event("Parsek.<Feature>.<event>") events.py declare an aggregated event class
snapshot() feature class state handed to a late-joining client (the server may replay it on connect)
attach_namespace(Feat, types, events) end of __init__.py expose the types/events as attributes on the feature class (Feat.RequestData, Feat.RequestSent, ...)

Sketch — a producer that folds a raw CDP burst into one Parsek.* event, plus the reducer that rebuilds state from it:

# events.py
@register_event("Parsek.MyFeature.thing")
@dataclass
class ThingHappened(Event):
    payload: ThingData
    __FIELDS__ = (FieldMeta("payload", "payload", False, "object",
                            ref="Parsek.MyFeature.ThingData"),)

# __init__.py
class MyFeature(Feature):
    domains = (SomeDomain,)                 # owned + auto-enabled

    @on(SomeDomain.SomethingRaw)            # CDP event -> producer
    @emit(ThingHappened)                    # return is sent to client + reduced
    def _produce(self, e) -> ThingHappened:
        return ThingHappened(payload=ThingData(...))

    @on(ThingHappened)                      # Parsek event -> reducer (both roles)
    def _reduce(self, e: ThingHappened) -> None:
        self._things.append(e.payload)

    @property
    def things(self):                       # the ergonomic public API
        return list(self._things)

attach_namespace(MyFeature, _types, _events)

RequestListener (below) is the reference implementation of exactly this shape.

RequestListener

The built-in feature: aggregated network observation. It records every request of the current document with its response, headers and timing:

requests = page.get_feature(RequestListener)
await page.navigate("https://example.com", wait_load=True)

# all requests of the current document
for r in requests.requests:
    print(r.method, r.url, r.response.status_code)

# find / await a specific one (substring or compiled regex against the URL)
r = await requests.wait_for_response("/api/orders", timeout=30, load_body=True)
print(r.response.status_code, await r.response.body())

By default the log is cleared on main-frame navigation (like DevTools with Preserve log off); pass preserve_log=True to keep it across documents.

Remote browsers (with parsek-cdp-server)

Install the optional server distribution to launch and supervise browsers behind a proxy:

pip install parsek-cdp[server]

Then connect to a running server instead of a local Chrome — the API is identical, and the declared features are aggregated server-side:

browser = await Browser[Page[RequestListener]].get_distant_browser(
    "http://127.0.0.1:9333", headless=True
)
page = await browser.new_page("https://example.com")

See the parsek-cdp-server package for running the server, lifecycle supervision, metrics and the zombie reaper.

License

Apache-2.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

parsek_cdp-0.1.7.tar.gz (266.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

parsek_cdp-0.1.7-py3-none-any.whl (366.2 kB view details)

Uploaded Python 3

File details

Details for the file parsek_cdp-0.1.7.tar.gz.

File metadata

  • Download URL: parsek_cdp-0.1.7.tar.gz
  • Upload date:
  • Size: 266.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for parsek_cdp-0.1.7.tar.gz
Algorithm Hash digest
SHA256 698d739134db7d30d3bce579793632b00aed3bcd82c87c294756c55a2852a0d4
MD5 dd1b49e554e8087b071aa95c25e61ce7
BLAKE2b-256 3842ed2dc4d5810dd3cb4a2be9cea503168aa802e6c45153f5239a4036c7145e

See more details on using hashes here.

Provenance

The following attestation bundles were made for parsek_cdp-0.1.7.tar.gz:

Publisher: publish.yml on xa1era/parsek-cdp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file parsek_cdp-0.1.7-py3-none-any.whl.

File metadata

  • Download URL: parsek_cdp-0.1.7-py3-none-any.whl
  • Upload date:
  • Size: 366.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for parsek_cdp-0.1.7-py3-none-any.whl
Algorithm Hash digest
SHA256 60e70ca0928c3b59c731c658fa8723c3decb5c12872648e2d4d183791a8e1540
MD5 710ad164d605996ae574491a143565c1
BLAKE2b-256 c29bcc1f8311dd5f3b64f3f214e8b271c25f3a0147169b0834841916e1948526

See more details on using hashes here.

Provenance

The following attestation bundles were made for parsek_cdp-0.1.7-py3-none-any.whl:

Publisher: publish.yml on xa1era/parsek-cdp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.1.9

2 files

0.1.8

2 files

This release

0.1.7 This release

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page