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.
@@ -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,6 @@
1
+ """Minimal Playwright runner with persistent Chromium."""
2
+
3
+ from .runner import run_snippet, ensure_chromium_running
4
+
5
+ __all__ = ["run_snippet", "ensure_chromium_running"]
6
+ __version__ = "0.4.0"
@@ -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,2 @@
1
+ [console_scripts]
2
+ ezpw = easy_playwright.cli:main
@@ -0,0 +1,4 @@
1
+ playwright>=1.53.0
2
+
3
+ [dev]
4
+ pytest
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )