pypecdp 0.3.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.
- pypecdp-0.3.0/.github/workflows/publish-to-pypi.yml +70 -0
- pypecdp-0.3.0/.gitignore +56 -0
- pypecdp-0.3.0/LICENSE +21 -0
- pypecdp-0.3.0/MANIFEST.in +3 -0
- pypecdp-0.3.0/PKG-INFO +203 -0
- pypecdp-0.3.0/README.md +181 -0
- pypecdp-0.3.0/example/console_logger.py +165 -0
- pypecdp-0.3.0/example/custom_events.py +191 -0
- pypecdp-0.3.0/example/dom_selection.py +240 -0
- pypecdp-0.3.0/example/form_interaction.py +89 -0
- pypecdp-0.3.0/example/multi_tab.py +148 -0
- pypecdp-0.3.0/example/network_intercept.py +203 -0
- pypecdp-0.3.0/example/pdf_generation.py +130 -0
- pypecdp-0.3.0/example/quickstart.py +34 -0
- pypecdp-0.3.0/example/screenshot.py +121 -0
- pypecdp-0.3.0/pyproject.toml +53 -0
- pypecdp-0.3.0/src/pypecdp/__init__.py +26 -0
- pypecdp-0.3.0/src/pypecdp/browser.py +530 -0
- pypecdp-0.3.0/src/pypecdp/cdp/README.md +5 -0
- pypecdp-0.3.0/src/pypecdp/cdp/__init__.py +6 -0
- pypecdp-0.3.0/src/pypecdp/cdp/accessibility.py +668 -0
- pypecdp-0.3.0/src/pypecdp/cdp/animation.py +494 -0
- pypecdp-0.3.0/src/pypecdp/cdp/audits.py +1941 -0
- pypecdp-0.3.0/src/pypecdp/cdp/autofill.py +292 -0
- pypecdp-0.3.0/src/pypecdp/cdp/background_service.py +215 -0
- pypecdp-0.3.0/src/pypecdp/cdp/bluetooth_emulation.py +626 -0
- pypecdp-0.3.0/src/pypecdp/cdp/browser.py +821 -0
- pypecdp-0.3.0/src/pypecdp/cdp/cache_storage.py +311 -0
- pypecdp-0.3.0/src/pypecdp/cdp/cast.py +172 -0
- pypecdp-0.3.0/src/pypecdp/cdp/console.py +107 -0
- pypecdp-0.3.0/src/pypecdp/cdp/css.py +2622 -0
- pypecdp-0.3.0/src/pypecdp/cdp/debugger.py +1405 -0
- pypecdp-0.3.0/src/pypecdp/cdp/device_access.py +141 -0
- pypecdp-0.3.0/src/pypecdp/cdp/device_orientation.py +45 -0
- pypecdp-0.3.0/src/pypecdp/cdp/dom.py +2229 -0
- pypecdp-0.3.0/src/pypecdp/cdp/dom_debugger.py +321 -0
- pypecdp-0.3.0/src/pypecdp/cdp/dom_snapshot.py +876 -0
- pypecdp-0.3.0/src/pypecdp/cdp/dom_storage.py +222 -0
- pypecdp-0.3.0/src/pypecdp/cdp/emulation.py +1663 -0
- pypecdp-0.3.0/src/pypecdp/cdp/event_breakpoints.py +56 -0
- pypecdp-0.3.0/src/pypecdp/cdp/extensions.py +165 -0
- pypecdp-0.3.0/src/pypecdp/cdp/fed_cm.py +283 -0
- pypecdp-0.3.0/src/pypecdp/cdp/fetch.py +507 -0
- pypecdp-0.3.0/src/pypecdp/cdp/file_system.py +115 -0
- pypecdp-0.3.0/src/pypecdp/cdp/headless_experimental.py +115 -0
- pypecdp-0.3.0/src/pypecdp/cdp/heap_profiler.py +401 -0
- pypecdp-0.3.0/src/pypecdp/cdp/indexed_db.py +528 -0
- pypecdp-0.3.0/src/pypecdp/cdp/input_.py +701 -0
- pypecdp-0.3.0/src/pypecdp/cdp/inspector.py +95 -0
- pypecdp-0.3.0/src/pypecdp/cdp/io.py +101 -0
- pypecdp-0.3.0/src/pypecdp/cdp/layer_tree.py +464 -0
- pypecdp-0.3.0/src/pypecdp/cdp/log.py +190 -0
- pypecdp-0.3.0/src/pypecdp/cdp/media.py +313 -0
- pypecdp-0.3.0/src/pypecdp/cdp/memory.py +305 -0
- pypecdp-0.3.0/src/pypecdp/cdp/network.py +4673 -0
- pypecdp-0.3.0/src/pypecdp/cdp/overlay.py +1397 -0
- pypecdp-0.3.0/src/pypecdp/cdp/page.py +4029 -0
- pypecdp-0.3.0/src/pypecdp/cdp/performance.py +124 -0
- pypecdp-0.3.0/src/pypecdp/cdp/performance_timeline.py +200 -0
- pypecdp-0.3.0/src/pypecdp/cdp/preload.py +569 -0
- pypecdp-0.3.0/src/pypecdp/cdp/profiler.py +420 -0
- pypecdp-0.3.0/src/pypecdp/cdp/pwa.py +278 -0
- pypecdp-0.3.0/src/pypecdp/cdp/py.typed +0 -0
- pypecdp-0.3.0/src/pypecdp/cdp/runtime.py +1589 -0
- pypecdp-0.3.0/src/pypecdp/cdp/schema.py +50 -0
- pypecdp-0.3.0/src/pypecdp/cdp/security.py +518 -0
- pypecdp-0.3.0/src/pypecdp/cdp/service_worker.py +401 -0
- pypecdp-0.3.0/src/pypecdp/cdp/storage.py +2438 -0
- pypecdp-0.3.0/src/pypecdp/cdp/system_info.py +327 -0
- pypecdp-0.3.0/src/pypecdp/cdp/target.py +818 -0
- pypecdp-0.3.0/src/pypecdp/cdp/tethering.py +65 -0
- pypecdp-0.3.0/src/pypecdp/cdp/tracing.py +377 -0
- pypecdp-0.3.0/src/pypecdp/cdp/util.py +19 -0
- pypecdp-0.3.0/src/pypecdp/cdp/web_audio.py +606 -0
- pypecdp-0.3.0/src/pypecdp/cdp/web_authn.py +581 -0
- pypecdp-0.3.0/src/pypecdp/cdp_pipe.py +122 -0
- pypecdp-0.3.0/src/pypecdp/config.py +106 -0
- pypecdp-0.3.0/src/pypecdp/elem.py +235 -0
- pypecdp-0.3.0/src/pypecdp/logger.py +48 -0
- pypecdp-0.3.0/src/pypecdp/tab.py +358 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Publish distribution 📦 to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build distribution 📦
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v6
|
|
13
|
+
with:
|
|
14
|
+
persist-credentials: false
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v6
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
- name: Install pypa/build
|
|
20
|
+
run: >-
|
|
21
|
+
python3 -m
|
|
22
|
+
pip install
|
|
23
|
+
build
|
|
24
|
+
--user
|
|
25
|
+
- name: Build a binary wheel and a source tarball
|
|
26
|
+
run: python3 -m build
|
|
27
|
+
- name: Verify package can be imported
|
|
28
|
+
run: |
|
|
29
|
+
python3 -m pip install dist/*.whl
|
|
30
|
+
python3 -c "import pypecdp; print(f'✓ Package version: {pypecdp.__version__}')"
|
|
31
|
+
- name: Store the distribution packages
|
|
32
|
+
uses: actions/upload-artifact@v5
|
|
33
|
+
with:
|
|
34
|
+
name: python-package-distributions
|
|
35
|
+
path: dist/
|
|
36
|
+
publish-to-pypi:
|
|
37
|
+
name: >-
|
|
38
|
+
Publish distribution 📦 to PyPI
|
|
39
|
+
if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
|
|
40
|
+
needs:
|
|
41
|
+
- build
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
environment:
|
|
44
|
+
name: pypi
|
|
45
|
+
url: https://pypi.org/p/pypecdp
|
|
46
|
+
permissions:
|
|
47
|
+
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
48
|
+
steps:
|
|
49
|
+
- name: Download all the dists
|
|
50
|
+
uses: actions/download-artifact@v6
|
|
51
|
+
with:
|
|
52
|
+
name: python-package-distributions
|
|
53
|
+
path: dist/
|
|
54
|
+
- name: Validate version matches tag
|
|
55
|
+
run: |
|
|
56
|
+
# Extract version from package filename
|
|
57
|
+
WHEEL_FILE=$(ls dist/*.whl)
|
|
58
|
+
PKG_VERSION=$(echo $WHEEL_FILE | sed -n 's/.*pypecdp-\([0-9.]*\)-.*/\1/p')
|
|
59
|
+
# Extract version from tag (remove 'v' prefix if present)
|
|
60
|
+
TAG_VERSION=${GITHUB_REF#refs/tags/}
|
|
61
|
+
TAG_VERSION=${TAG_VERSION#v}
|
|
62
|
+
echo "Package version: $PKG_VERSION"
|
|
63
|
+
echo "Tag version: $TAG_VERSION"
|
|
64
|
+
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
|
|
65
|
+
echo "❌ Error: Package version ($PKG_VERSION) does not match tag ($TAG_VERSION)"
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
echo "✓ Version validation passed"
|
|
69
|
+
- name: Publish distribution 📦 to PyPI
|
|
70
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
pypecdp-0.3.0/.gitignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Temporary and binary files
|
|
2
|
+
*~
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.so
|
|
5
|
+
*.cfg
|
|
6
|
+
!.isort.cfg
|
|
7
|
+
!setup.cfg
|
|
8
|
+
*.orig
|
|
9
|
+
*.log
|
|
10
|
+
*.pot
|
|
11
|
+
__pycache__/*
|
|
12
|
+
.cache/*
|
|
13
|
+
.*.swp
|
|
14
|
+
*/.ipynb_checkpoints/*
|
|
15
|
+
.DS_Store
|
|
16
|
+
src/*.py
|
|
17
|
+
|
|
18
|
+
# Project files
|
|
19
|
+
.ropeproject
|
|
20
|
+
.project
|
|
21
|
+
.pydevproject
|
|
22
|
+
.settings
|
|
23
|
+
.idea
|
|
24
|
+
.vscode
|
|
25
|
+
tags
|
|
26
|
+
|
|
27
|
+
# Package files
|
|
28
|
+
*.egg
|
|
29
|
+
*.eggs/
|
|
30
|
+
.installed.cfg
|
|
31
|
+
*.egg-info
|
|
32
|
+
|
|
33
|
+
# Unittest and coverage
|
|
34
|
+
htmlcov/*
|
|
35
|
+
.coverage
|
|
36
|
+
.coverage.*
|
|
37
|
+
.tox
|
|
38
|
+
junit*.xml
|
|
39
|
+
coverage.xml
|
|
40
|
+
.pytest_cache/
|
|
41
|
+
|
|
42
|
+
# Build and docs folder/files
|
|
43
|
+
build/*
|
|
44
|
+
dist/*
|
|
45
|
+
sdist/*
|
|
46
|
+
docs/api/*
|
|
47
|
+
docs/_rst/*
|
|
48
|
+
docs/_build/*
|
|
49
|
+
cover/*
|
|
50
|
+
MANIFEST
|
|
51
|
+
|
|
52
|
+
# Per-project virtualenvs
|
|
53
|
+
.venv*/
|
|
54
|
+
.conda*/
|
|
55
|
+
.python-version
|
|
56
|
+
/.mypy_cache/
|
pypecdp-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 sohaib17
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pypecdp-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pypecdp
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Async Chrome DevTools Protocol over POSIX pipes.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sohaib17/pypecdp
|
|
6
|
+
Project-URL: Source, https://github.com/sohaib17/pypecdp
|
|
7
|
+
Project-URL: Issues, https://github.com/sohaib17/pypecdp/issues
|
|
8
|
+
Author: sohaib17
|
|
9
|
+
License: MIT License
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: asyncio,automation,cdp,chrome,chromium,devtools,headless
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: deprecated>=1.2.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# pypecdp
|
|
24
|
+
|
|
25
|
+
Fully async Chrome DevTools Protocol over POSIX pipes with a high-level Browser/Tab/Elem API for Python 3.12+ on Linux.
|
|
26
|
+
|
|
27
|
+
Chrome automation using `--remote-debugging-pipe` (no websockets, no ports, just pipes) with bundled CDP protocol classes.
|
|
28
|
+
|
|
29
|
+
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).
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **Fully Async**: Built from ground up with asyncio for concurrent operations
|
|
34
|
+
- **Fast**: Direct pipe communication via file descriptors - no websockets, no network overhead
|
|
35
|
+
- **Zero dependencies**: No external dependencies required - built-in Python libraries only
|
|
36
|
+
- **Secure**: Browser only communicates over local pipes, no open ports accessible to other processes
|
|
37
|
+
- **No zombies**: No risk of orphaned Chrome processes - automatic lifecycle management
|
|
38
|
+
- **Linux focused**: Leverages POSIX pipes and process management
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install pypecdp
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Install Chromium if needed:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Ubuntu/Debian
|
|
50
|
+
sudo apt-get install chromium-browser
|
|
51
|
+
|
|
52
|
+
# Fedora
|
|
53
|
+
sudo dnf install chromium
|
|
54
|
+
|
|
55
|
+
# Arch
|
|
56
|
+
sudo pacman -S chromium
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import asyncio
|
|
63
|
+
from pypecdp import Browser
|
|
64
|
+
|
|
65
|
+
async def main():
|
|
66
|
+
# Launch browser
|
|
67
|
+
browser = await Browser.start(
|
|
68
|
+
chrome_path="chromium",
|
|
69
|
+
headless=True
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Open a tab
|
|
73
|
+
tab = await browser.navigate("https://example.com")
|
|
74
|
+
|
|
75
|
+
# Select and interact with elements
|
|
76
|
+
h1 = await tab.select("h1")
|
|
77
|
+
if h1:
|
|
78
|
+
text = await h1.text()
|
|
79
|
+
print(f"Page heading: {text}")
|
|
80
|
+
|
|
81
|
+
# Evaluate JavaScript
|
|
82
|
+
result = await tab.eval("document.title")
|
|
83
|
+
print(f"Title: {result.value}")
|
|
84
|
+
|
|
85
|
+
# Close browser
|
|
86
|
+
await browser.close()
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Usage Guide
|
|
92
|
+
|
|
93
|
+
### Browser Management
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from pypecdp import Browser, Config
|
|
97
|
+
|
|
98
|
+
# Simple start
|
|
99
|
+
browser = await Browser.start(chrome_path="chromium", headless=True)
|
|
100
|
+
|
|
101
|
+
# Advanced configuration
|
|
102
|
+
config = Config(
|
|
103
|
+
chrome_path="/usr/bin/google-chrome",
|
|
104
|
+
user_data_dir="/tmp/chrome-profile",
|
|
105
|
+
headless=True,
|
|
106
|
+
extra_args=["--no-sandbox", "--disable-gpu"],
|
|
107
|
+
switches={"disable-blink-features": "AutomationControlled"},
|
|
108
|
+
env={"LANG": "en_US.UTF-8"}
|
|
109
|
+
)
|
|
110
|
+
browser = await Browser.start(config=config)
|
|
111
|
+
|
|
112
|
+
# Close browser
|
|
113
|
+
await browser.close()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Event Handlers
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from pypecdp import cdp
|
|
120
|
+
|
|
121
|
+
# Tab-level events (requires domain enable!)
|
|
122
|
+
await tab.send(cdp.runtime.enable()) # Required for runtime events!
|
|
123
|
+
|
|
124
|
+
async def on_console(event):
|
|
125
|
+
print(f"Console {event.type_}: {event.args}")
|
|
126
|
+
|
|
127
|
+
tab.on(cdp.runtime.ConsoleAPICalled, on_console)
|
|
128
|
+
|
|
129
|
+
# Browser-level events
|
|
130
|
+
async def on_target_created(event):
|
|
131
|
+
info = event.target_info
|
|
132
|
+
print(f"Target created: {info.type_} - {info.url}")
|
|
133
|
+
|
|
134
|
+
browser.on(cdp.target.TargetCreated, on_target_created)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Logging
|
|
138
|
+
|
|
139
|
+
pypecdp uses Python's standard logging module. Configure via environment variables:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
143
|
+
export PYPECDP_LOG_LEVEL=DEBUG
|
|
144
|
+
|
|
145
|
+
# Set custom logger name
|
|
146
|
+
export PYPECDP_LOGGER=myapp.browser
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Or configure the logger directly in Python:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from pypecdp import logger
|
|
153
|
+
import logging
|
|
154
|
+
|
|
155
|
+
# Set log level
|
|
156
|
+
logger.setLevel(logging.DEBUG)
|
|
157
|
+
|
|
158
|
+
# Add custom handler
|
|
159
|
+
handler = logging.FileHandler("pypecdp.log")
|
|
160
|
+
logger.addHandler(handler)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Error Handling
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
try:
|
|
167
|
+
browser = await Browser.start()
|
|
168
|
+
tab = await browser.navigate("https://example.com")
|
|
169
|
+
|
|
170
|
+
# Your automation code
|
|
171
|
+
result = await tab.eval("document.title")
|
|
172
|
+
|
|
173
|
+
except RuntimeError as e:
|
|
174
|
+
# CDP protocol errors
|
|
175
|
+
print(f"CDP Error: {e}")
|
|
176
|
+
except ConnectionError as e:
|
|
177
|
+
# Connection lost
|
|
178
|
+
print(f"Connection Error: {e}")
|
|
179
|
+
except Exception as e:
|
|
180
|
+
# Other errors
|
|
181
|
+
print(f"Error: {e}")
|
|
182
|
+
finally:
|
|
183
|
+
# Always cleanup
|
|
184
|
+
await browser.close()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Requirements
|
|
188
|
+
|
|
189
|
+
- Python 3.12+
|
|
190
|
+
- Linux (uses POSIX pipes and `preexec_fn`)
|
|
191
|
+
- Chromium or Google Chrome
|
|
192
|
+
|
|
193
|
+
## Links
|
|
194
|
+
|
|
195
|
+
- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT License - See LICENSE file for details.
|
|
200
|
+
|
|
201
|
+
## Contributing
|
|
202
|
+
|
|
203
|
+
Contributions welcome! This project aims to provide a clean, type-safe interface to Chrome automation on Linux.
|
pypecdp-0.3.0/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# pypecdp
|
|
2
|
+
|
|
3
|
+
Fully async Chrome DevTools Protocol over POSIX pipes with a high-level Browser/Tab/Elem API for Python 3.12+ on Linux.
|
|
4
|
+
|
|
5
|
+
Chrome automation using `--remote-debugging-pipe` (no websockets, no ports, just pipes) with bundled CDP protocol classes.
|
|
6
|
+
|
|
7
|
+
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).
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Fully Async**: Built from ground up with asyncio for concurrent operations
|
|
12
|
+
- **Fast**: Direct pipe communication via file descriptors - no websockets, no network overhead
|
|
13
|
+
- **Zero dependencies**: No external dependencies required - built-in Python libraries only
|
|
14
|
+
- **Secure**: Browser only communicates over local pipes, no open ports accessible to other processes
|
|
15
|
+
- **No zombies**: No risk of orphaned Chrome processes - automatic lifecycle management
|
|
16
|
+
- **Linux focused**: Leverages POSIX pipes and process management
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install pypecdp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Install Chromium if needed:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Ubuntu/Debian
|
|
28
|
+
sudo apt-get install chromium-browser
|
|
29
|
+
|
|
30
|
+
# Fedora
|
|
31
|
+
sudo dnf install chromium
|
|
32
|
+
|
|
33
|
+
# Arch
|
|
34
|
+
sudo pacman -S chromium
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import asyncio
|
|
41
|
+
from pypecdp import Browser
|
|
42
|
+
|
|
43
|
+
async def main():
|
|
44
|
+
# Launch browser
|
|
45
|
+
browser = await Browser.start(
|
|
46
|
+
chrome_path="chromium",
|
|
47
|
+
headless=True
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Open a tab
|
|
51
|
+
tab = await browser.navigate("https://example.com")
|
|
52
|
+
|
|
53
|
+
# Select and interact with elements
|
|
54
|
+
h1 = await tab.select("h1")
|
|
55
|
+
if h1:
|
|
56
|
+
text = await h1.text()
|
|
57
|
+
print(f"Page heading: {text}")
|
|
58
|
+
|
|
59
|
+
# Evaluate JavaScript
|
|
60
|
+
result = await tab.eval("document.title")
|
|
61
|
+
print(f"Title: {result.value}")
|
|
62
|
+
|
|
63
|
+
# Close browser
|
|
64
|
+
await browser.close()
|
|
65
|
+
|
|
66
|
+
asyncio.run(main())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage Guide
|
|
70
|
+
|
|
71
|
+
### Browser Management
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pypecdp import Browser, Config
|
|
75
|
+
|
|
76
|
+
# Simple start
|
|
77
|
+
browser = await Browser.start(chrome_path="chromium", headless=True)
|
|
78
|
+
|
|
79
|
+
# Advanced configuration
|
|
80
|
+
config = Config(
|
|
81
|
+
chrome_path="/usr/bin/google-chrome",
|
|
82
|
+
user_data_dir="/tmp/chrome-profile",
|
|
83
|
+
headless=True,
|
|
84
|
+
extra_args=["--no-sandbox", "--disable-gpu"],
|
|
85
|
+
switches={"disable-blink-features": "AutomationControlled"},
|
|
86
|
+
env={"LANG": "en_US.UTF-8"}
|
|
87
|
+
)
|
|
88
|
+
browser = await Browser.start(config=config)
|
|
89
|
+
|
|
90
|
+
# Close browser
|
|
91
|
+
await browser.close()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Event Handlers
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from pypecdp import cdp
|
|
98
|
+
|
|
99
|
+
# Tab-level events (requires domain enable!)
|
|
100
|
+
await tab.send(cdp.runtime.enable()) # Required for runtime events!
|
|
101
|
+
|
|
102
|
+
async def on_console(event):
|
|
103
|
+
print(f"Console {event.type_}: {event.args}")
|
|
104
|
+
|
|
105
|
+
tab.on(cdp.runtime.ConsoleAPICalled, on_console)
|
|
106
|
+
|
|
107
|
+
# Browser-level events
|
|
108
|
+
async def on_target_created(event):
|
|
109
|
+
info = event.target_info
|
|
110
|
+
print(f"Target created: {info.type_} - {info.url}")
|
|
111
|
+
|
|
112
|
+
browser.on(cdp.target.TargetCreated, on_target_created)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Logging
|
|
116
|
+
|
|
117
|
+
pypecdp uses Python's standard logging module. Configure via environment variables:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
121
|
+
export PYPECDP_LOG_LEVEL=DEBUG
|
|
122
|
+
|
|
123
|
+
# Set custom logger name
|
|
124
|
+
export PYPECDP_LOGGER=myapp.browser
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Or configure the logger directly in Python:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from pypecdp import logger
|
|
131
|
+
import logging
|
|
132
|
+
|
|
133
|
+
# Set log level
|
|
134
|
+
logger.setLevel(logging.DEBUG)
|
|
135
|
+
|
|
136
|
+
# Add custom handler
|
|
137
|
+
handler = logging.FileHandler("pypecdp.log")
|
|
138
|
+
logger.addHandler(handler)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Error Handling
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
try:
|
|
145
|
+
browser = await Browser.start()
|
|
146
|
+
tab = await browser.navigate("https://example.com")
|
|
147
|
+
|
|
148
|
+
# Your automation code
|
|
149
|
+
result = await tab.eval("document.title")
|
|
150
|
+
|
|
151
|
+
except RuntimeError as e:
|
|
152
|
+
# CDP protocol errors
|
|
153
|
+
print(f"CDP Error: {e}")
|
|
154
|
+
except ConnectionError as e:
|
|
155
|
+
# Connection lost
|
|
156
|
+
print(f"Connection Error: {e}")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
# Other errors
|
|
159
|
+
print(f"Error: {e}")
|
|
160
|
+
finally:
|
|
161
|
+
# Always cleanup
|
|
162
|
+
await browser.close()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Requirements
|
|
166
|
+
|
|
167
|
+
- Python 3.12+
|
|
168
|
+
- Linux (uses POSIX pipes and `preexec_fn`)
|
|
169
|
+
- Chromium or Google Chrome
|
|
170
|
+
|
|
171
|
+
## Links
|
|
172
|
+
|
|
173
|
+
- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/)
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT License - See LICENSE file for details.
|
|
178
|
+
|
|
179
|
+
## Contributing
|
|
180
|
+
|
|
181
|
+
Contributions welcome! This project aims to provide a clean, type-safe interface to Chrome automation on Linux.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Example: Console logging using Runtime.consoleAPICalled events.
|
|
2
|
+
|
|
3
|
+
Demonstrates how to capture and monitor console messages from the browser:
|
|
4
|
+
- Subscribing to Runtime.consoleAPICalled events
|
|
5
|
+
- Capturing console.log, console.warn, console.error messages
|
|
6
|
+
- Extracting and displaying console message arguments
|
|
7
|
+
- Real-time console monitoring during page execution
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import base64
|
|
12
|
+
import os
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pypecdp import Browser, cdp
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def main() -> None:
|
|
19
|
+
"""Main."""
|
|
20
|
+
# Launch browser
|
|
21
|
+
browser = await Browser.start(
|
|
22
|
+
chrome_path=os.environ.get("PYPECDP_CHROME_PATH", "chromium"),
|
|
23
|
+
headless=True,
|
|
24
|
+
extra_args=["--no-sandbox"],
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
tab = await browser.navigate("about:blank")
|
|
28
|
+
await tab.send(cdp.runtime.enable())
|
|
29
|
+
print("Browser launched\n")
|
|
30
|
+
|
|
31
|
+
# Storage for console messages
|
|
32
|
+
console_messages: list[tuple[str, str]] = []
|
|
33
|
+
|
|
34
|
+
# Handler for console API calls
|
|
35
|
+
async def handle_console(event: Any) -> None:
|
|
36
|
+
"""Handle Runtime.consoleAPICalled events."""
|
|
37
|
+
# event is a ConsoleAPICalled object
|
|
38
|
+
msg_type = event.type_
|
|
39
|
+
args = event.args
|
|
40
|
+
|
|
41
|
+
# Extract values from arguments
|
|
42
|
+
values = []
|
|
43
|
+
for arg in args:
|
|
44
|
+
arg_type = arg.type_
|
|
45
|
+
if arg_type == "string":
|
|
46
|
+
values.append(arg.value if hasattr(arg, "value") else "")
|
|
47
|
+
elif arg_type == "number":
|
|
48
|
+
values.append(str(arg.value if hasattr(arg, "value") else ""))
|
|
49
|
+
elif arg_type == "boolean":
|
|
50
|
+
values.append(str(arg.value if hasattr(arg, "value") else ""))
|
|
51
|
+
elif arg_type == "object":
|
|
52
|
+
desc = (
|
|
53
|
+
arg.description
|
|
54
|
+
if hasattr(arg, "description")
|
|
55
|
+
else "Object"
|
|
56
|
+
)
|
|
57
|
+
values.append(desc)
|
|
58
|
+
else:
|
|
59
|
+
values.append(f"[{arg_type}]")
|
|
60
|
+
|
|
61
|
+
message = " ".join(values)
|
|
62
|
+
|
|
63
|
+
# Format with prefix markers
|
|
64
|
+
prefix = {
|
|
65
|
+
"log": "[i]",
|
|
66
|
+
"info": "[i]",
|
|
67
|
+
"warn": "[!]",
|
|
68
|
+
"error": "[x]",
|
|
69
|
+
"debug": "[d]",
|
|
70
|
+
}.get(msg_type, "[*]")
|
|
71
|
+
|
|
72
|
+
formatted = f"{prefix} [{msg_type.upper()}] {message}"
|
|
73
|
+
console_messages.append((msg_type, message))
|
|
74
|
+
print(formatted)
|
|
75
|
+
|
|
76
|
+
# Register the console handler
|
|
77
|
+
tab.on(cdp.runtime.ConsoleAPICalled, handle_console)
|
|
78
|
+
|
|
79
|
+
print("=" * 60)
|
|
80
|
+
print("Capturing console messages...")
|
|
81
|
+
print("=" * 60 + "\n")
|
|
82
|
+
|
|
83
|
+
# Execute various console commands
|
|
84
|
+
await tab.eval("console.log('Hello from pypecdp!')")
|
|
85
|
+
await asyncio.sleep(0.1)
|
|
86
|
+
|
|
87
|
+
await tab.eval("console.log('Multiple', 'arguments', 123)")
|
|
88
|
+
await asyncio.sleep(0.1)
|
|
89
|
+
|
|
90
|
+
await tab.eval("console.warn('This is a warning')")
|
|
91
|
+
await asyncio.sleep(0.1)
|
|
92
|
+
|
|
93
|
+
await tab.eval("console.error('This is an error')")
|
|
94
|
+
await asyncio.sleep(0.1)
|
|
95
|
+
|
|
96
|
+
await tab.eval("console.info('Info message with number:', 42)")
|
|
97
|
+
await asyncio.sleep(0.1)
|
|
98
|
+
|
|
99
|
+
await tab.eval("console.log('Boolean:', true, false)")
|
|
100
|
+
await asyncio.sleep(0.1)
|
|
101
|
+
|
|
102
|
+
await tab.eval("console.log('Object:', {foo: 'bar', num: 123})")
|
|
103
|
+
await asyncio.sleep(0.1)
|
|
104
|
+
|
|
105
|
+
await tab.eval("console.log('Array:', [1, 2, 3])")
|
|
106
|
+
await asyncio.sleep(0.1)
|
|
107
|
+
|
|
108
|
+
# Navigate to a page and capture its console output
|
|
109
|
+
print("\n" + "=" * 60)
|
|
110
|
+
print("Navigating to page with console output...")
|
|
111
|
+
print("=" * 60 + "\n")
|
|
112
|
+
|
|
113
|
+
# Create a page with console output
|
|
114
|
+
html_content = """
|
|
115
|
+
<!DOCTYPE html>
|
|
116
|
+
<html>
|
|
117
|
+
<head><title>Console Test</title></head>
|
|
118
|
+
<body>
|
|
119
|
+
<h1>Console Test Page</h1>
|
|
120
|
+
<script>
|
|
121
|
+
console.log('Page loaded!');
|
|
122
|
+
console.info('Current URL:', window.location.href);
|
|
123
|
+
console.warn('This is a warning from the page');
|
|
124
|
+
|
|
125
|
+
setTimeout(() => {
|
|
126
|
+
console.log('Delayed message after 500ms');
|
|
127
|
+
}, 500);
|
|
128
|
+
|
|
129
|
+
setTimeout(() => {
|
|
130
|
+
console.error('Simulated error after 1000ms');
|
|
131
|
+
}, 1000);
|
|
132
|
+
</script>
|
|
133
|
+
</body>
|
|
134
|
+
</html>
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
# Navigate to data URL with the HTML
|
|
138
|
+
encoded = base64.b64encode(html_content.encode()).decode()
|
|
139
|
+
await tab.navigate(f"data:text/html;base64,{encoded}")
|
|
140
|
+
|
|
141
|
+
# Wait for delayed messages
|
|
142
|
+
await asyncio.sleep(1.5)
|
|
143
|
+
|
|
144
|
+
# Summary
|
|
145
|
+
print("\n" + "=" * 60)
|
|
146
|
+
print("Summary")
|
|
147
|
+
print("=" * 60)
|
|
148
|
+
print(f"Total console messages captured: {len(console_messages)}")
|
|
149
|
+
|
|
150
|
+
# Count by type
|
|
151
|
+
type_counts: dict[str, int] = {}
|
|
152
|
+
for msg_type, _ in console_messages:
|
|
153
|
+
type_counts[msg_type] = type_counts.get(msg_type, 0) + 1
|
|
154
|
+
|
|
155
|
+
print("\nBreakdown by type:")
|
|
156
|
+
for msg_type, count in sorted(type_counts.items()):
|
|
157
|
+
print(f" {msg_type}: {count}")
|
|
158
|
+
|
|
159
|
+
# Clean up
|
|
160
|
+
await browser.close()
|
|
161
|
+
print("\nBrowser closed")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
asyncio.run(main())
|