# pypecdp

Fully async Chrome DevTools Protocol over POSIX pipes with a high-level Browser/Tab/Elem API for Python 3.12+ on Linux.

Chrome automation using `--remote-debugging-pipe` (no websockets, no ports, just pipes) with bundled CDP protocol classes.

Inspired by [playwright-python](https://github.com/microsoft/playwright-python), [python-cdp](https://github.com/HMaker/python-cdp) and [nodriver](https://github.com/ultrafunkamsterdam/nodriver).

## Features

- **Fully Async**: Built from ground up with asyncio for concurrent operations
- **Fast**: Direct pipe communication via file descriptors - no websockets, no network overhead
- **Minimal dependencies**: Only one dependency (`deprecated`) - lightweight and easy to install
- **Secure**: Browser only communicates over local pipes, no open ports accessible to other processes
- **No zombies**: No risk of orphaned Chrome processes if code crashes - automatic lifecycle management
- **Linux focused**: Leverages POSIX pipes and process management

## Install

```bash
pip install pypecdp
```

Install Chromium if needed:

```bash
# Ubuntu/Debian
sudo apt-get install chromium-browser

# Fedora
sudo dnf install chromium

# Arch
sudo pacman -S chromium
```

## Quick Start

```python
import asyncio
from pypecdp import Browser

async def main():
    # Launch browser
    browser = await Browser.start(
        chrome_path="chromium",
        headless=True
    )
    
    # Navigate to a URL (creates/reuses a tab)
    tab = await browser.navigate("https://example.com")
    
    # Select and interact with elements
    h1 = await tab.wait_for_elem("h1")
    if h1:
        text = await h1.text()
        print(f"Page heading: {text}")
    
    # Evaluate JavaScript
    result = await tab.eval("document.title")
    print(f"Title: {result.value}")
    
    # Close browser
    await browser.close()

asyncio.run(main())
```

## Usage Guide

### Browser Management

```python
from pypecdp import Browser, Config

# Simple start
browser = await Browser.start(chrome_path="chromium", headless=True)

# Advanced configuration
config = Config(
    chrome_path="/usr/bin/google-chrome",
    user_data_dir="/tmp/chrome-profile",
    clean_data_dir=False,  # Preserve existing profile data
    headless=True,
    extra_args=["--no-sandbox", "--disable-gpu"],
    env={"LANG": "en_US.UTF-8"}
)
browser = await Browser.start(config=config)

# Close browser
await browser.close()
```

**Note**: By default, `clean_data_dir=True` which removes any existing user data directory before starting. Set it to `False` to preserve cookies, cache, and other browser state between runs.

### Element Interactions

```python
# Finding and clicking elements
button = await tab.wait_for_elem("button.submit")
if button:
    await button.click()

# Clicking elements that cause navigation
link = await tab.wait_for_elem('a[href="/next-page"]')
if link:
    # click() returns the top-level Tab after navigation
    current_tab = await link.click()
    if current_tab:
        # Wait for the new page to load
        await current_tab.wait_for_event(cdp.page.LoadEventFired, timeout=10.0)
        print(f"Navigated to: {current_tab.url}")
```

### Event Handlers

```python
from pypecdp import cdp

# Tab-level events (requires domain enable!)
await tab.send(cdp.runtime.enable())  # Required for runtime events!

async def on_console(event):
    print(f"Console {event.type_}: {event.args}")

tab.on(cdp.runtime.ConsoleAPICalled, on_console)

# Browser-level events
async def on_target_created(event):
    info = event.target_info
    print(f"Target created: {info.type_} - {info.url}")

browser.on(cdp.target.TargetCreated, on_target_created)
```

### Logging

pypecdp uses Python's standard logging module. Configure via environment variables:

```bash
# Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
export PYPECDP_LOGLEVEL=DEBUG

# Set custom logger name
export PYPECDP_LOGGER=myapp.browser
```

Or configure the logger directly in Python:

```python
from pypecdp import logger
import logging

# Set log level
logger.setLevel(logging.DEBUG)

# Add custom handler
handler = logging.FileHandler("pypecdp.log")
logger.addHandler(handler)
```

## Error Handling

```python
try:
    browser = await Browser.start()
    tab = await browser.navigate("https://example.com")
    
    # Your automation code
    elem = await tab.wait_for_elem("button")
    if elem:
        await elem.click()
    
    result = await tab.eval("document.title")
    
except ReferenceError as e:
    # Element's tab is no longer available (closed/detached)
    print(f"Target Error: {e}")
except RuntimeError as e:
    # CDP protocol errors
    print(f"CDP Error: {e}")
except ConnectionError as e:
    # Connection lost
    print(f"Connection Error: {e}")
except Exception as e:
    # Other errors
    print(f"Error: {e}")
finally:
    # Always cleanup
    await browser.close()
```

## Requirements

- Python 3.12+
- Linux (uses POSIX pipes and `preexec_fn`)
- Chromium or Google Chrome

## Links

- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)

## License

MIT License - See LICENSE file for details.

## Contributing

Contributions welcome! This project aims to provide a clean, type-safe interface to Chrome automation on Linux.
