easy-playwright 0.0.4__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.
- easy_playwright-0.0.4/LICENSE +24 -0
- easy_playwright-0.0.4/PKG-INFO +70 -0
- easy_playwright-0.0.4/README.md +48 -0
- easy_playwright-0.0.4/easy_playwright/__init__.py +6 -0
- easy_playwright-0.0.4/easy_playwright/cli.py +115 -0
- easy_playwright-0.0.4/easy_playwright/config.py +51 -0
- easy_playwright-0.0.4/easy_playwright/runner.py +257 -0
- easy_playwright-0.0.4/easy_playwright.egg-info/PKG-INFO +70 -0
- easy_playwright-0.0.4/easy_playwright.egg-info/SOURCES.txt +14 -0
- easy_playwright-0.0.4/easy_playwright.egg-info/dependency_links.txt +1 -0
- easy_playwright-0.0.4/easy_playwright.egg-info/entry_points.txt +2 -0
- easy_playwright-0.0.4/easy_playwright.egg-info/requires.txt +4 -0
- easy_playwright-0.0.4/easy_playwright.egg-info/top_level.txt +1 -0
- easy_playwright-0.0.4/pyproject.toml +44 -0
- easy_playwright-0.0.4/setup.cfg +4 -0
- easy_playwright-0.0.4/setup.py +64 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Quentin REMPLAKOWSKI
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easy_playwright
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Playwright runner with persistent Chromium and one-line CLI.
|
|
5
|
+
Author-email: Quentin REMPLAKOWSKI <q.rempla@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/qremplak/easy_playwright
|
|
7
|
+
Project-URL: Issues, https://github.com/qremplak/easy_playwright/issues
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: playwright>=1.53.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# easy_playwright
|
|
24
|
+
|
|
25
|
+
An ultra-simple Playwright runner that executes Python snippets while reusing a persistent Chromium session across calls.
|
|
26
|
+
|
|
27
|
+
# Why ?
|
|
28
|
+
|
|
29
|
+
Traditionnal playwright MCP allow agent to call a subset of playwright functions one by one.
|
|
30
|
+
An alternative is to allow agent to execute arbitrary python code using playwright package to freely access full API, but it general the browser is closed after each code execution.
|
|
31
|
+
|
|
32
|
+
easy_playwright allows :
|
|
33
|
+
- Mandatory Chromium/deps installation during pip build/install (fails fast on error)
|
|
34
|
+
- Auto chromium startup and persistence
|
|
35
|
+
- Single tool call for agent to execute any python playwright routine
|
|
36
|
+
- Reusing previous session accross executions
|
|
37
|
+
- Easy traces and screenshots captures and storage in local folder to allow human and agent monitoring
|
|
38
|
+
- Auto-embedding of development workflow instructions to improve agent coding autonomy
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
easy_playwright is designed to allow easy and light setup of playwright and chromium in same host (local dev machine or dev container)
|
|
43
|
+
|
|
44
|
+
> pip install -v --no-binary easy-playwright easy-playwright
|
|
45
|
+
(--no-binary is needed to force auto chromium installation in expected folder)
|
|
46
|
+
|
|
47
|
+
Then, add following instructions in your agent context :
|
|
48
|
+
```
|
|
49
|
+
Always use the ezpw tool to check your work. Run the following command, read the output carefully, and strictly follow the instructions provided.
|
|
50
|
+
> ezpw -h
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Persistent Chromium
|
|
54
|
+
|
|
55
|
+
The browser is started automatically if not already running. The persistent profile is stored in:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
.playwright/runtime/chromium-profile/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This keeps state between calls (cookies, login session, etc.).
|
|
62
|
+
|
|
63
|
+
Tip: `playwright` reuses the first open tab if possible, otherwise it creates a new one.
|
|
64
|
+
|
|
65
|
+
## Environment variables
|
|
66
|
+
|
|
67
|
+
- `EASY_PLAYWRIGHT_DIR`: base folder (default `.playwright`).
|
|
68
|
+
- `EASY_PLAYWRIGHT_CDP_PORT`: CDP port (default `9222`).
|
|
69
|
+
- `EASY_PLAYWRIGHT_START_URL`: URL opened on startup (default `about:blank`).
|
|
70
|
+
- `EASY_PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD`: if `1`, do not download Chromium during `pip install`.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# easy_playwright
|
|
2
|
+
|
|
3
|
+
An ultra-simple Playwright runner that executes Python snippets while reusing a persistent Chromium session across calls.
|
|
4
|
+
|
|
5
|
+
# Why ?
|
|
6
|
+
|
|
7
|
+
Traditionnal playwright MCP allow agent to call a subset of playwright functions one by one.
|
|
8
|
+
An alternative is to allow agent to execute arbitrary python code using playwright package to freely access full API, but it general the browser is closed after each code execution.
|
|
9
|
+
|
|
10
|
+
easy_playwright allows :
|
|
11
|
+
- Mandatory Chromium/deps installation during pip build/install (fails fast on error)
|
|
12
|
+
- Auto chromium startup and persistence
|
|
13
|
+
- Single tool call for agent to execute any python playwright routine
|
|
14
|
+
- Reusing previous session accross executions
|
|
15
|
+
- Easy traces and screenshots captures and storage in local folder to allow human and agent monitoring
|
|
16
|
+
- Auto-embedding of development workflow instructions to improve agent coding autonomy
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
easy_playwright is designed to allow easy and light setup of playwright and chromium in same host (local dev machine or dev container)
|
|
21
|
+
|
|
22
|
+
> pip install -v --no-binary easy-playwright easy-playwright
|
|
23
|
+
(--no-binary is needed to force auto chromium installation in expected folder)
|
|
24
|
+
|
|
25
|
+
Then, add following instructions in your agent context :
|
|
26
|
+
```
|
|
27
|
+
Always use the ezpw tool to check your work. Run the following command, read the output carefully, and strictly follow the instructions provided.
|
|
28
|
+
> ezpw -h
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Persistent Chromium
|
|
32
|
+
|
|
33
|
+
The browser is started automatically if not already running. The persistent profile is stored in:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
.playwright/runtime/chromium-profile/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This keeps state between calls (cookies, login session, etc.).
|
|
40
|
+
|
|
41
|
+
Tip: `playwright` reuses the first open tab if possible, otherwise it creates a new one.
|
|
42
|
+
|
|
43
|
+
## Environment variables
|
|
44
|
+
|
|
45
|
+
- `EASY_PLAYWRIGHT_DIR`: base folder (default `.playwright`).
|
|
46
|
+
- `EASY_PLAYWRIGHT_CDP_PORT`: CDP port (default `9222`).
|
|
47
|
+
- `EASY_PLAYWRIGHT_START_URL`: URL opened on startup (default `about:blank`).
|
|
48
|
+
- `EASY_PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD`: if `1`, do not download Chromium during `pip install`.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .config import DEFAULT_HEADLESS, DEFAULT_START_URL, DEFAULT_WINDOW_SIZE
|
|
7
|
+
from .runner import RunOptions, run_snippet
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _read_code_from_stdin() -> str:
|
|
11
|
+
if sys.stdin is None or sys.stdin.isatty():
|
|
12
|
+
return ""
|
|
13
|
+
return sys.stdin.read()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parse_window_size(value: str) -> tuple[int, int]:
|
|
17
|
+
try:
|
|
18
|
+
width_str, height_str = value.lower().split("x", 1)
|
|
19
|
+
width = int(width_str)
|
|
20
|
+
height = int(height_str)
|
|
21
|
+
except ValueError as exc:
|
|
22
|
+
raise argparse.ArgumentTypeError(
|
|
23
|
+
"Invalid format for --window-size. Use WIDTHxHEIGHT, for example 1920x1080."
|
|
24
|
+
) from exc
|
|
25
|
+
|
|
26
|
+
if width <= 0 or height <= 0:
|
|
27
|
+
raise argparse.ArgumentTypeError("Window size must be positive integers.")
|
|
28
|
+
return (width, height)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main() -> None:
|
|
32
|
+
parser = argparse.ArgumentParser(
|
|
33
|
+
prog="ezpw",
|
|
34
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
35
|
+
description="""All-in-one tool to easily check web app behavior using playwright.
|
|
36
|
+
|
|
37
|
+
ezpw allows executing a playwright Python snippet within a persistent Chromium session.
|
|
38
|
+
|
|
39
|
+
Example 1 : Navigate + Screenshot
|
|
40
|
+
ezpw "
|
|
41
|
+
# NOW, EXPORT_FOLDER, playwright, page, context and browser are directly available in ezpw scope
|
|
42
|
+
#print(page)
|
|
43
|
+
#print(context)
|
|
44
|
+
|
|
45
|
+
# By default, you are on the page that was left open during the previous call, but you can change it.
|
|
46
|
+
# page.goto('http://127.0.0.1:8080', wait_until='networkidle')
|
|
47
|
+
|
|
48
|
+
page.fill('#name', 'Agent_IA_001')
|
|
49
|
+
page.click('.start-btn')
|
|
50
|
+
page.screenshot(path=EXPORT_FOLDER/f'screenshots/{NOW}_after_click.png', full_page=True)
|
|
51
|
+
"
|
|
52
|
+
|
|
53
|
+
# Example 2 : Traces capture
|
|
54
|
+
ezpw "
|
|
55
|
+
context.tracing.start(screenshots=True, snapshots=True, sources=True)
|
|
56
|
+
page.goto('http://127.0.0.1:8080/page_to_test', wait_until='networkidle')
|
|
57
|
+
context.tracing.stop(path=EXPORT_FOLDER/f'traces/{NOW}_page_to_test.zip')
|
|
58
|
+
"
|
|
59
|
+
|
|
60
|
+
You have access to entire playwright API.
|
|
61
|
+
|
|
62
|
+
**Iterative Test & Adjust Development Workflow**
|
|
63
|
+
|
|
64
|
+
Systematically:
|
|
65
|
+
|
|
66
|
+
- Produce the code to meet the requested requirement.
|
|
67
|
+
- Use Playwright to test the application:
|
|
68
|
+
- Navigate and interact within the application to ensure everything works as expected.
|
|
69
|
+
- Capture traces and take screenshots to visually verify that the application's appearance matches the expectations.
|
|
70
|
+
- Review the traces and screenshots you have generated to analyze them. Examples:
|
|
71
|
+
- Reading an image via `functions.view_image` ([https://developers.openai.com/cookbook/examples/gpt-5/codex\_prompting\_guide/](https://developers.openai.com/cookbook/examples/gpt-5/codex_prompting_guide/))
|
|
72
|
+
- Reading logs via `cat` / `sed`.
|
|
73
|
+
- Adjust the code until everything functions as expected and the UI is visually clean and compliant with the requirements.
|
|
74
|
+
|
|
75
|
+
Any capture or trace must be saved and organized in the dedicated folder, as showed in examples below.
|
|
76
|
+
""",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument("code", nargs="?", help="Python code to execute.")
|
|
79
|
+
parser.add_argument("--port", type=int, default=None, help="CDP port (default: 9222).")
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--headless",
|
|
82
|
+
action=argparse.BooleanOptionalAction,
|
|
83
|
+
default=DEFAULT_HEADLESS,
|
|
84
|
+
help="Run Chromium in headless mode (default: true).",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--start-url",
|
|
88
|
+
default=None,
|
|
89
|
+
help="URL to open when Chromium starts (default: about:blank).",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--window-size",
|
|
93
|
+
type=_parse_window_size,
|
|
94
|
+
default=DEFAULT_WINDOW_SIZE,
|
|
95
|
+
help="Chromium window size in pixels as WIDTHxHEIGHT (default: 1920x1080).",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
args = parser.parse_args()
|
|
99
|
+
code = args.code or _read_code_from_stdin()
|
|
100
|
+
if not code or not code.strip():
|
|
101
|
+
parser.error("Provide code as an argument or via stdin.")
|
|
102
|
+
|
|
103
|
+
options = RunOptions(
|
|
104
|
+
code=code,
|
|
105
|
+
port=args.port,
|
|
106
|
+
headless=args.headless,
|
|
107
|
+
start_url=args.start_url or DEFAULT_START_URL,
|
|
108
|
+
window_size=args.window_size,
|
|
109
|
+
)
|
|
110
|
+
result = run_snippet(options)
|
|
111
|
+
print(f"OK | port={result['port']} | export_folder={result['export_folder']}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
main()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
DEFAULT_BASE_DIRNAME = ".playwright"
|
|
7
|
+
DEFAULT_CDP_PORT = 9222
|
|
8
|
+
DEFAULT_START_URL = "about:blank"
|
|
9
|
+
DEFAULT_HEADLESS = True
|
|
10
|
+
DEFAULT_WINDOW_SIZE = (1920, 1080)
|
|
11
|
+
DEFAULT_PLAYWRIGHT_BROWSERS_PATH = "/ms-playwright"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_project_dir() -> Path:
|
|
15
|
+
env = os.environ.get("EASY_PLAYWRIGHT_PROJECT_DIR")
|
|
16
|
+
if env:
|
|
17
|
+
return Path(env).expanduser().resolve()
|
|
18
|
+
return Path.cwd().resolve()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_base_dir(project_dir: Path) -> Path:
|
|
22
|
+
env = os.environ.get("EASY_PLAYWRIGHT_DIR")
|
|
23
|
+
base = Path(env).expanduser().resolve() if env else (project_dir / DEFAULT_BASE_DIRNAME)
|
|
24
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
return base
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_playwright_browsers_path() -> str:
|
|
29
|
+
return DEFAULT_PLAYWRIGHT_BROWSERS_PATH
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_runtime_dir(base_dir: Path) -> Path:
|
|
33
|
+
runtime = base_dir / "runtime"
|
|
34
|
+
runtime.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
return runtime
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_profile_dir(runtime_dir: Path) -> Path:
|
|
39
|
+
profile = runtime_dir / "chromium-profile"
|
|
40
|
+
profile.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
return profile
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_export_dir(base_dir: Path) -> Path:
|
|
45
|
+
exports = base_dir / "exports"
|
|
46
|
+
exports.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
return exports
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_log_path(runtime_dir: Path) -> Path:
|
|
51
|
+
return runtime_dir / "chromium.log"
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
from collections import deque
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
from urllib.error import URLError
|
|
15
|
+
from urllib.request import urlopen
|
|
16
|
+
|
|
17
|
+
from playwright.sync_api import sync_playwright
|
|
18
|
+
|
|
19
|
+
from .config import (
|
|
20
|
+
DEFAULT_CDP_PORT,
|
|
21
|
+
DEFAULT_HEADLESS,
|
|
22
|
+
DEFAULT_START_URL,
|
|
23
|
+
DEFAULT_WINDOW_SIZE,
|
|
24
|
+
get_base_dir,
|
|
25
|
+
get_export_dir,
|
|
26
|
+
get_log_path,
|
|
27
|
+
get_profile_dir,
|
|
28
|
+
get_project_dir,
|
|
29
|
+
get_playwright_browsers_path,
|
|
30
|
+
get_runtime_dir,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class RunOptions:
|
|
36
|
+
code: str
|
|
37
|
+
port: Optional[int] = None
|
|
38
|
+
headless: bool = DEFAULT_HEADLESS
|
|
39
|
+
start_url: str = DEFAULT_START_URL
|
|
40
|
+
window_size: Optional[tuple[int, int]] = DEFAULT_WINDOW_SIZE
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _fetch_cdp_version(port: int, timeout: float = 0.5) -> Optional[dict]:
|
|
44
|
+
url = f"http://127.0.0.1:{port}/json/version"
|
|
45
|
+
try:
|
|
46
|
+
with urlopen(url, timeout=timeout) as resp:
|
|
47
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
48
|
+
if "webSocketDebuggerUrl" in data:
|
|
49
|
+
return data
|
|
50
|
+
return None
|
|
51
|
+
except URLError:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_cdp_ready(port: int) -> bool:
|
|
56
|
+
return _fetch_cdp_version(port) is not None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_port_in_use(port: int) -> bool:
|
|
60
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
61
|
+
sock.settimeout(0.2)
|
|
62
|
+
return sock.connect_ex(("127.0.0.1", port)) == 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _tail_log(path: Path, max_lines: int = 50) -> str:
|
|
66
|
+
if not path.exists():
|
|
67
|
+
return ""
|
|
68
|
+
try:
|
|
69
|
+
with path.open("r", encoding="utf-8", errors="replace") as handle:
|
|
70
|
+
lines = deque(handle, maxlen=max_lines)
|
|
71
|
+
return "".join(lines)
|
|
72
|
+
except Exception:
|
|
73
|
+
return ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _wait_for_cdp(port: int, timeout: float, proc: Optional[subprocess.Popen] = None) -> None:
|
|
77
|
+
start = time.time()
|
|
78
|
+
while time.time() - start < timeout:
|
|
79
|
+
if _is_cdp_ready(port):
|
|
80
|
+
return
|
|
81
|
+
if proc is not None and proc.poll() is not None:
|
|
82
|
+
raise RuntimeError(f"Chromium process exited early (code {proc.returncode}).")
|
|
83
|
+
time.sleep(0.25)
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
f"Chromium ne repond pas sur le port {port}. "
|
|
86
|
+
"Verifie que le port est libre ou change-le via --port."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _apply_window_size(page, window_size: tuple[int, int]) -> None:
|
|
91
|
+
width, height = window_size
|
|
92
|
+
# Try to resize the real browser window (headful mode), then fall back to viewport.
|
|
93
|
+
try:
|
|
94
|
+
session = page.context.new_cdp_session(page)
|
|
95
|
+
window_info = session.send("Browser.getWindowForTarget")
|
|
96
|
+
session.send(
|
|
97
|
+
"Browser.setWindowBounds",
|
|
98
|
+
{
|
|
99
|
+
"windowId": window_info["windowId"],
|
|
100
|
+
"bounds": {"width": width, "height": height},
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
return
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
page.set_viewport_size({"width": width, "height": height})
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _configure_playwright_browsers_path() -> None:
|
|
114
|
+
path = get_playwright_browsers_path()
|
|
115
|
+
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = path
|
|
116
|
+
try:
|
|
117
|
+
Path(path).mkdir(parents=True, exist_ok=True)
|
|
118
|
+
except Exception:
|
|
119
|
+
# If path creation fails, Playwright will raise a clearer install/runtime error.
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _ensure_chromium_installed() -> Path:
|
|
124
|
+
_configure_playwright_browsers_path()
|
|
125
|
+
browsers_path = os.environ.get("PLAYWRIGHT_BROWSERS_PATH", "")
|
|
126
|
+
with sync_playwright() as p:
|
|
127
|
+
exe = Path(p.chromium.executable_path)
|
|
128
|
+
if exe.exists():
|
|
129
|
+
return exe
|
|
130
|
+
|
|
131
|
+
raise RuntimeError(
|
|
132
|
+
"Chromium is not installed, you have 2 choices :\n"
|
|
133
|
+
"- Force chromium installation during pip install :\n" \
|
|
134
|
+
"> pip install --no-binary easy-playwright easy-playwright\n"
|
|
135
|
+
"- Install chromium manually :\n"
|
|
136
|
+
f"> PLAYWRIGHT_BROWSERS_PATH={browsers_path} {sys.executable} -m playwright install chromium "
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def ensure_chromium_running(
|
|
141
|
+
port: Optional[int] = None,
|
|
142
|
+
headless: Optional[bool] = None,
|
|
143
|
+
start_url: Optional[str] = None,
|
|
144
|
+
window_size: Optional[tuple[int, int]] = None,
|
|
145
|
+
base_dir: Optional[Path] = None,
|
|
146
|
+
) -> None:
|
|
147
|
+
_configure_playwright_browsers_path()
|
|
148
|
+
project_dir = get_project_dir()
|
|
149
|
+
base = base_dir or get_base_dir(project_dir)
|
|
150
|
+
port = port or int(os.environ.get("EASY_PLAYWRIGHT_CDP_PORT", DEFAULT_CDP_PORT))
|
|
151
|
+
headless = DEFAULT_HEADLESS if headless is None else headless
|
|
152
|
+
start_url = start_url or os.environ.get("EASY_PLAYWRIGHT_START_URL", DEFAULT_START_URL)
|
|
153
|
+
|
|
154
|
+
if _is_cdp_ready(port):
|
|
155
|
+
return
|
|
156
|
+
if _is_port_in_use(port):
|
|
157
|
+
raise RuntimeError(
|
|
158
|
+
f"Le port {port} est deja utilise mais ne repond pas en CDP. "
|
|
159
|
+
"Change le port ou arrete le processus qui l'utilise."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
exe = _ensure_chromium_installed()
|
|
163
|
+
runtime_dir = get_runtime_dir(base)
|
|
164
|
+
profile_dir = get_profile_dir(runtime_dir)
|
|
165
|
+
log_path = get_log_path(runtime_dir)
|
|
166
|
+
|
|
167
|
+
args = [
|
|
168
|
+
str(exe),
|
|
169
|
+
f"--remote-debugging-port={port}",
|
|
170
|
+
"--remote-debugging-address=127.0.0.1",
|
|
171
|
+
"--remote-allow-origins=*",
|
|
172
|
+
f"--user-data-dir={profile_dir}",
|
|
173
|
+
"--no-sandbox",
|
|
174
|
+
"--disable-setuid-sandbox",
|
|
175
|
+
"--disable-dev-shm-usage",
|
|
176
|
+
"--no-first-run",
|
|
177
|
+
"--no-default-browser-check",
|
|
178
|
+
"--disable-gpu",
|
|
179
|
+
]
|
|
180
|
+
if headless:
|
|
181
|
+
args.append("--headless")
|
|
182
|
+
width, height = window_size or DEFAULT_WINDOW_SIZE
|
|
183
|
+
args.append(f"--window-size={width},{height}")
|
|
184
|
+
args.append(start_url)
|
|
185
|
+
|
|
186
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
log_file = open(log_path, "a", encoding="utf-8")
|
|
188
|
+
proc = subprocess.Popen(
|
|
189
|
+
args,
|
|
190
|
+
stdout=log_file,
|
|
191
|
+
stderr=log_file,
|
|
192
|
+
start_new_session=True,
|
|
193
|
+
)
|
|
194
|
+
log_file.close()
|
|
195
|
+
|
|
196
|
+
timeout_env = os.environ.get("EASY_PLAYWRIGHT_CDP_TIMEOUT", "15")
|
|
197
|
+
try:
|
|
198
|
+
timeout = float(timeout_env)
|
|
199
|
+
except ValueError:
|
|
200
|
+
timeout = 15.0
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
_wait_for_cdp(port, timeout=timeout, proc=proc)
|
|
204
|
+
except RuntimeError as exc:
|
|
205
|
+
log_tail = _tail_log(log_path)
|
|
206
|
+
details = f"{exc} (see {log_path})"
|
|
207
|
+
if log_tail:
|
|
208
|
+
details = f"{details}\n--- chromium.log (tail) ---\n{log_tail}"
|
|
209
|
+
raise RuntimeError(details) from exc
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def run_snippet(options: RunOptions) -> dict:
|
|
213
|
+
project_dir = get_project_dir()
|
|
214
|
+
base_dir = get_base_dir(project_dir)
|
|
215
|
+
export_dir = get_export_dir(base_dir)
|
|
216
|
+
port = options.port or int(os.environ.get("EASY_PLAYWRIGHT_CDP_PORT", DEFAULT_CDP_PORT))
|
|
217
|
+
|
|
218
|
+
ensure_chromium_running(
|
|
219
|
+
port=port,
|
|
220
|
+
headless=options.headless,
|
|
221
|
+
start_url=options.start_url,
|
|
222
|
+
window_size=options.window_size,
|
|
223
|
+
base_dir=base_dir,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
with sync_playwright() as p:
|
|
227
|
+
browser = p.chromium.connect_over_cdp(f"http://127.0.0.1:{port}")
|
|
228
|
+
context = browser.contexts[0] if browser.contexts else browser.new_context()
|
|
229
|
+
page = context.pages[0] if context.pages else context.new_page()
|
|
230
|
+
_apply_window_size(page, options.window_size or DEFAULT_WINDOW_SIZE)
|
|
231
|
+
|
|
232
|
+
exec_error: Optional[BaseException] = None
|
|
233
|
+
try:
|
|
234
|
+
now_stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
235
|
+
scope = {
|
|
236
|
+
"page": page,
|
|
237
|
+
"context": context,
|
|
238
|
+
"browser": browser,
|
|
239
|
+
"playwright": p,
|
|
240
|
+
"NOW": now_stamp,
|
|
241
|
+
"EXPORT_FOLDER": export_dir,
|
|
242
|
+
}
|
|
243
|
+
compiled = compile(options.code, "<easy_playwright>", "exec")
|
|
244
|
+
exec(compiled, scope, scope)
|
|
245
|
+
except BaseException as exc:
|
|
246
|
+
exec_error = exc
|
|
247
|
+
finally:
|
|
248
|
+
browser.close()
|
|
249
|
+
|
|
250
|
+
if exec_error:
|
|
251
|
+
raise exec_error
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"now": now_stamp,
|
|
255
|
+
"export_folder": str(export_dir),
|
|
256
|
+
"port": port,
|
|
257
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: easy_playwright
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Playwright runner with persistent Chromium and one-line CLI.
|
|
5
|
+
Author-email: Quentin REMPLAKOWSKI <q.rempla@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/qremplak/easy_playwright
|
|
7
|
+
Project-URL: Issues, https://github.com/qremplak/easy_playwright/issues
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: playwright>=1.53.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# easy_playwright
|
|
24
|
+
|
|
25
|
+
An ultra-simple Playwright runner that executes Python snippets while reusing a persistent Chromium session across calls.
|
|
26
|
+
|
|
27
|
+
# Why ?
|
|
28
|
+
|
|
29
|
+
Traditionnal playwright MCP allow agent to call a subset of playwright functions one by one.
|
|
30
|
+
An alternative is to allow agent to execute arbitrary python code using playwright package to freely access full API, but it general the browser is closed after each code execution.
|
|
31
|
+
|
|
32
|
+
easy_playwright allows :
|
|
33
|
+
- Mandatory Chromium/deps installation during pip build/install (fails fast on error)
|
|
34
|
+
- Auto chromium startup and persistence
|
|
35
|
+
- Single tool call for agent to execute any python playwright routine
|
|
36
|
+
- Reusing previous session accross executions
|
|
37
|
+
- Easy traces and screenshots captures and storage in local folder to allow human and agent monitoring
|
|
38
|
+
- Auto-embedding of development workflow instructions to improve agent coding autonomy
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
easy_playwright is designed to allow easy and light setup of playwright and chromium in same host (local dev machine or dev container)
|
|
43
|
+
|
|
44
|
+
> pip install -v --no-binary easy-playwright easy-playwright
|
|
45
|
+
(--no-binary is needed to force auto chromium installation in expected folder)
|
|
46
|
+
|
|
47
|
+
Then, add following instructions in your agent context :
|
|
48
|
+
```
|
|
49
|
+
Always use the ezpw tool to check your work. Run the following command, read the output carefully, and strictly follow the instructions provided.
|
|
50
|
+
> ezpw -h
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Persistent Chromium
|
|
54
|
+
|
|
55
|
+
The browser is started automatically if not already running. The persistent profile is stored in:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
.playwright/runtime/chromium-profile/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This keeps state between calls (cookies, login session, etc.).
|
|
62
|
+
|
|
63
|
+
Tip: `playwright` reuses the first open tab if possible, otherwise it creates a new one.
|
|
64
|
+
|
|
65
|
+
## Environment variables
|
|
66
|
+
|
|
67
|
+
- `EASY_PLAYWRIGHT_DIR`: base folder (default `.playwright`).
|
|
68
|
+
- `EASY_PLAYWRIGHT_CDP_PORT`: CDP port (default `9222`).
|
|
69
|
+
- `EASY_PLAYWRIGHT_START_URL`: URL opened on startup (default `about:blank`).
|
|
70
|
+
- `EASY_PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD`: if `1`, do not download Chromium during `pip install`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
easy_playwright/__init__.py
|
|
6
|
+
easy_playwright/cli.py
|
|
7
|
+
easy_playwright/config.py
|
|
8
|
+
easy_playwright/runner.py
|
|
9
|
+
easy_playwright.egg-info/PKG-INFO
|
|
10
|
+
easy_playwright.egg-info/SOURCES.txt
|
|
11
|
+
easy_playwright.egg-info/dependency_links.txt
|
|
12
|
+
easy_playwright.egg-info/entry_points.txt
|
|
13
|
+
easy_playwright.egg-info/requires.txt
|
|
14
|
+
easy_playwright.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
easy_playwright
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "easy_playwright"
|
|
3
|
+
version = "0.0.4"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="Quentin REMPLAKOWSKI", email="q.rempla@gmail.com" },
|
|
6
|
+
]
|
|
7
|
+
description = "Playwright runner with persistent Chromium and one-line CLI."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
|
|
10
|
+
# playwright requires 3.9
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
classifiers=[
|
|
13
|
+
'Development Status :: 5 - Production/Stable',
|
|
14
|
+
'Intended Audience :: Developers',
|
|
15
|
+
"Topic :: Scientific/Engineering",
|
|
16
|
+
'License :: OSI Approved :: BSD License',
|
|
17
|
+
'Operating System :: OS Independent',
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
'Programming Language :: Python :: 3.10',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
"playwright>=1.53.0"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = [
|
|
28
|
+
"pytest"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
ezpw = "easy_playwright.cli:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
include = ["easy_playwright", "easy_playwright.*"]
|
|
36
|
+
namespaces = false
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/qremplak/easy_playwright"
|
|
40
|
+
Issues = "https://github.com/qremplak/easy_playwright/issues"
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["setuptools>=61.0", "playwright"]
|
|
44
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from setuptools import setup
|
|
8
|
+
from setuptools.command.build_py import build_py
|
|
9
|
+
from setuptools.command.develop import develop
|
|
10
|
+
from setuptools.command.install import install
|
|
11
|
+
|
|
12
|
+
_BROWSER_INSTALL_DONE = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_browsers_path() -> str:
|
|
16
|
+
return "/ms-playwright"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _install_chromium() -> None:
|
|
20
|
+
global _BROWSER_INSTALL_DONE
|
|
21
|
+
if _BROWSER_INSTALL_DONE:
|
|
22
|
+
return
|
|
23
|
+
if os.environ.get("EASY_PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD") == "1":
|
|
24
|
+
print("[easy_playwright] Skipping browser installation (EASY_PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1)")
|
|
25
|
+
_BROWSER_INSTALL_DONE = True
|
|
26
|
+
return
|
|
27
|
+
browsers_path = _get_browsers_path()
|
|
28
|
+
os.makedirs(browsers_path, exist_ok=True)
|
|
29
|
+
env = os.environ.copy()
|
|
30
|
+
env["PLAYWRIGHT_BROWSERS_PATH"] = browsers_path
|
|
31
|
+
print("[easy_playwright] Installing Chromium during build/install...")
|
|
32
|
+
print(f"[easy_playwright] PLAYWRIGHT_BROWSERS_PATH={browsers_path}")
|
|
33
|
+
print(f"[easy_playwright] > {sys.executable} -m playwright install chromium")
|
|
34
|
+
subprocess.check_call([sys.executable, "-m", "playwright", "install", "chromium"], env=env)
|
|
35
|
+
print(f"[easy_playwright] > {sys.executable} -m playwright install-deps chromium")
|
|
36
|
+
subprocess.check_call([sys.executable, "-m", "playwright", "install-deps", "chromium"], env=env)
|
|
37
|
+
_BROWSER_INSTALL_DONE = True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BuildPyWithPlaywright(build_py):
|
|
41
|
+
def run(self) -> None:
|
|
42
|
+
_install_chromium()
|
|
43
|
+
super().run()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InstallWithPlaywright(install):
|
|
47
|
+
def run(self) -> None:
|
|
48
|
+
_install_chromium()
|
|
49
|
+
super().run()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DevelopWithPlaywright(develop):
|
|
53
|
+
def run(self) -> None:
|
|
54
|
+
_install_chromium()
|
|
55
|
+
super().run()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
setup(
|
|
59
|
+
cmdclass={
|
|
60
|
+
"build_py": BuildPyWithPlaywright,
|
|
61
|
+
"install": InstallWithPlaywright,
|
|
62
|
+
"develop": DevelopWithPlaywright,
|
|
63
|
+
}
|
|
64
|
+
)
|