phasma 0.1.0__py3-none-win_amd64.whl
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.
- phasma/__init__.py +3 -0
- phasma/__main__.py +107 -0
- phasma/driver/__init__.py +4 -0
- phasma/driver/download.py +222 -0
- phasma/driver/driver.py +157 -0
- phasma/driver/phantomjs/ChangeLog +401 -0
- phasma/driver/phantomjs/LICENSE.BSD +22 -0
- phasma/driver/phantomjs/README.md +29 -0
- phasma/driver/phantomjs/bin/phantomjs.exe +0 -0
- phasma/driver/phantomjs/examples/arguments.js +10 -0
- phasma/driver/phantomjs/examples/child_process-examples.js +28 -0
- phasma/driver/phantomjs/examples/colorwheel.js +52 -0
- phasma/driver/phantomjs/examples/countdown.js +10 -0
- phasma/driver/phantomjs/examples/detectsniff.js +60 -0
- phasma/driver/phantomjs/examples/echoToFile.js +24 -0
- phasma/driver/phantomjs/examples/features.js +30 -0
- phasma/driver/phantomjs/examples/fibo.js +10 -0
- phasma/driver/phantomjs/examples/hello.js +3 -0
- phasma/driver/phantomjs/examples/injectme.js +26 -0
- phasma/driver/phantomjs/examples/loadspeed.js +24 -0
- phasma/driver/phantomjs/examples/loadurlwithoutcss.js +26 -0
- phasma/driver/phantomjs/examples/modernizr.js +1406 -0
- phasma/driver/phantomjs/examples/module.js +5 -0
- phasma/driver/phantomjs/examples/netlog.js +26 -0
- phasma/driver/phantomjs/examples/netsniff.js +144 -0
- phasma/driver/phantomjs/examples/openurlwithproxy.js +25 -0
- phasma/driver/phantomjs/examples/outputEncoding.js +17 -0
- phasma/driver/phantomjs/examples/page_events.js +147 -0
- phasma/driver/phantomjs/examples/pagecallback.js +18 -0
- phasma/driver/phantomjs/examples/phantomwebintro.js +21 -0
- phasma/driver/phantomjs/examples/post.js +15 -0
- phasma/driver/phantomjs/examples/postjson.js +19 -0
- phasma/driver/phantomjs/examples/postserver.js +35 -0
- phasma/driver/phantomjs/examples/printenv.js +10 -0
- phasma/driver/phantomjs/examples/printheaderfooter.js +90 -0
- phasma/driver/phantomjs/examples/printmargins.js +36 -0
- phasma/driver/phantomjs/examples/rasterize.js +49 -0
- phasma/driver/phantomjs/examples/render_multi_url.js +74 -0
- phasma/driver/phantomjs/examples/responsive-screenshot.js +181 -0
- phasma/driver/phantomjs/examples/run-jasmine.js +92 -0
- phasma/driver/phantomjs/examples/run-jasmine2.js +94 -0
- phasma/driver/phantomjs/examples/run-qunit.js +77 -0
- phasma/driver/phantomjs/examples/scandir.js +24 -0
- phasma/driver/phantomjs/examples/server.js +44 -0
- phasma/driver/phantomjs/examples/serverkeepalive.js +35 -0
- phasma/driver/phantomjs/examples/simpleserver.js +43 -0
- phasma/driver/phantomjs/examples/sleepsort.js +27 -0
- phasma/driver/phantomjs/examples/stdin-stdout-stderr.js +19 -0
- phasma/driver/phantomjs/examples/universe.js +10 -0
- phasma/driver/phantomjs/examples/unrandomize.js +25 -0
- phasma/driver/phantomjs/examples/useragent.js +15 -0
- phasma/driver/phantomjs/examples/version.js +6 -0
- phasma/driver/phantomjs/examples/waitfor.js +58 -0
- phasma/driver/phantomjs/examples/walk_through_frames.js +73 -0
- phasma/driver/phantomjs/third-party.txt +36 -0
- phasma/phasma.py +165 -0
- phasma-0.1.0.dist-info/METADATA +193 -0
- phasma-0.1.0.dist-info/RECORD +61 -0
- phasma-0.1.0.dist-info/WHEEL +5 -0
- phasma-0.1.0.dist-info/licenses/LICENSE +9 -0
- phasma-0.1.0.dist-info/top_level.txt +1 -0
phasma/__init__.py
ADDED
phasma/__main__.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phasma - PhantomJS driver for Python.
|
|
3
|
+
Command-line interface.
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# Add src directory to sys.path to allow absolute imports
|
|
11
|
+
|
|
12
|
+
from phasma.phasma import download_driver, render_page, render_url, execjs
|
|
13
|
+
from phasma.driver import Driver
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
parser = argparse.ArgumentParser(description="Phasma: PhantomJS driver for Python")
|
|
17
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
|
18
|
+
|
|
19
|
+
# driver command
|
|
20
|
+
driver_parser = subparsers.add_parser("driver", help="Manage PhantomJS driver")
|
|
21
|
+
driver_subparsers = driver_parser.add_subparsers(dest="driver_action", help="Driver action")
|
|
22
|
+
|
|
23
|
+
# driver download
|
|
24
|
+
dl_parser = driver_subparsers.add_parser("download", help="Download PhantomJS driver")
|
|
25
|
+
dl_parser.add_argument("--os", help="Operating system (windows, linux, darwin)")
|
|
26
|
+
dl_parser.add_argument("--arch", help="Architecture (32bit, 64bit)")
|
|
27
|
+
|
|
28
|
+
# driver --version and --path as optional arguments of driver command itself
|
|
29
|
+
driver_parser.add_argument("--version", action="store_true", help="Show driver version")
|
|
30
|
+
driver_parser.add_argument("--path", action="store_true", help="Show driver executable path")
|
|
31
|
+
|
|
32
|
+
# render-page
|
|
33
|
+
rp_parser = subparsers.add_parser("render-page", help="Render an HTML page")
|
|
34
|
+
rp_parser.add_argument("input", help="HTML file path or HTML string")
|
|
35
|
+
rp_parser.add_argument("--output", "-o", help="Output file path")
|
|
36
|
+
rp_parser.add_argument("--viewport", default="1024x768", help="Viewport size (widthxheight)")
|
|
37
|
+
rp_parser.add_argument("--wait", type=int, default=100, help="Wait time in milliseconds")
|
|
38
|
+
|
|
39
|
+
# render-url
|
|
40
|
+
ru_parser = subparsers.add_parser("render-url", help="Render a URL")
|
|
41
|
+
ru_parser.add_argument("url", help="URL to render")
|
|
42
|
+
ru_parser.add_argument("--output", "-o", help="Output file path")
|
|
43
|
+
ru_parser.add_argument("--viewport", default="1024x768", help="Viewport size (widthxheight)")
|
|
44
|
+
ru_parser.add_argument("--wait", type=int, default=0, help="Wait time in milliseconds")
|
|
45
|
+
|
|
46
|
+
# execjs
|
|
47
|
+
js_parser = subparsers.add_parser("execjs", help="Execute JavaScript code")
|
|
48
|
+
js_parser.add_argument("script", help="JavaScript code (use '-' to read from stdin)")
|
|
49
|
+
js_parser.add_argument("--arg", action="append", help="Additional arguments to pass")
|
|
50
|
+
|
|
51
|
+
args = parser.parse_args()
|
|
52
|
+
|
|
53
|
+
if args.command == "driver":
|
|
54
|
+
if args.driver_action == "download":
|
|
55
|
+
success = download_driver(os_name=args.os, arch=args.arch)
|
|
56
|
+
if success:
|
|
57
|
+
print("Driver downloaded successfully.")
|
|
58
|
+
sys.exit(0)
|
|
59
|
+
else:
|
|
60
|
+
print("Driver download failed.")
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
elif args.version:
|
|
63
|
+
driver = Driver()
|
|
64
|
+
version = driver.version
|
|
65
|
+
print(f"PhantomJS driver version: {version}")
|
|
66
|
+
elif args.path:
|
|
67
|
+
driver = Driver()
|
|
68
|
+
path = driver.bin_path
|
|
69
|
+
print(path)
|
|
70
|
+
else:
|
|
71
|
+
driver_parser.print_help()
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
elif args.command == "render-page":
|
|
75
|
+
# Determine if input is a file
|
|
76
|
+
input_path = Path(args.input)
|
|
77
|
+
if input_path.is_file():
|
|
78
|
+
page = input_path
|
|
79
|
+
else:
|
|
80
|
+
page = args.input
|
|
81
|
+
rendered = render_page(page, output=args.output, viewport_size=args.viewport, wait_time=args.wait)
|
|
82
|
+
if not args.output:
|
|
83
|
+
print(rendered)
|
|
84
|
+
else:
|
|
85
|
+
print(f"Rendered content saved to {args.output}")
|
|
86
|
+
|
|
87
|
+
elif args.command == "render-url":
|
|
88
|
+
rendered = render_url(args.url, output=args.output, viewport_size=args.viewport, wait_time=args.wait)
|
|
89
|
+
if not args.output:
|
|
90
|
+
print(rendered)
|
|
91
|
+
else:
|
|
92
|
+
print(f"Rendered content saved to {args.output}")
|
|
93
|
+
|
|
94
|
+
elif args.command == "execjs":
|
|
95
|
+
if args.script == "-":
|
|
96
|
+
script = sys.stdin.read()
|
|
97
|
+
else:
|
|
98
|
+
script = args.script
|
|
99
|
+
output = execjs(script, args=args.arg)
|
|
100
|
+
print(output)
|
|
101
|
+
|
|
102
|
+
else:
|
|
103
|
+
parser.print_help()
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
main()
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import hashlib
|
|
6
|
+
import logging
|
|
7
|
+
import os, platform, sys
|
|
8
|
+
import shutil
|
|
9
|
+
import tarfile, zipfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import urllib.request
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
DRIVER_PATH = Path(__file__).parent
|
|
16
|
+
|
|
17
|
+
BASE_URL = "https://github.com/MohammadRaziei/phasma/releases/download/drivers"
|
|
18
|
+
|
|
19
|
+
DRIVER_VERSION = "2.1.1"
|
|
20
|
+
|
|
21
|
+
FILES = {
|
|
22
|
+
("windows", "64bit", "2.1.1"): (
|
|
23
|
+
"phantomjs-2.1.1-windows.zip",
|
|
24
|
+
"d9fb05623d6b26d3654d008eab3adafd1f6350433dfd16138c46161f42c7dcc8",
|
|
25
|
+
),
|
|
26
|
+
("darwin", "64bit", "2.1.1"): (
|
|
27
|
+
"phantomjs-2.1.1-macosx.zip",
|
|
28
|
+
"538cf488219ab27e309eafc629e2bcee9976990fe90b1ec334f541779150f8c1",
|
|
29
|
+
),
|
|
30
|
+
("linux", "64bit", "2.1.1"): (
|
|
31
|
+
"phantomjs-2.1.1-linux-x86_64.tar.bz2",
|
|
32
|
+
"86dd9a4bf4aee45f1a84c9f61cf1947c1d6dce9b9e8d2a907105da7852460d2f",
|
|
33
|
+
),
|
|
34
|
+
("linux", "32bit", "2.1.1"): (
|
|
35
|
+
"phantomjs-2.1.1-linux-i686.tar.bz2",
|
|
36
|
+
"80e03cfeb22cc4dfe4e73b68ab81c9fdd7c78968cfd5358e6af33960464f15e3",
|
|
37
|
+
),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ARCHIVE_SUFFIXES = (
|
|
41
|
+
".tar.bz2",
|
|
42
|
+
".zip",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def sha256(path: Path) -> str:
|
|
46
|
+
h = hashlib.sha256()
|
|
47
|
+
with path.open("rb") as f:
|
|
48
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
49
|
+
h.update(chunk)
|
|
50
|
+
return h.hexdigest()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def detect_target(os_name: str, arch: str, version: str):
|
|
54
|
+
key = (os_name, arch, version)
|
|
55
|
+
if key not in FILES:
|
|
56
|
+
raise RuntimeError(f"Unsupported platform/version: {key}")
|
|
57
|
+
return FILES[key]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def download(url: str, dst: Path):
|
|
61
|
+
urllib.request.urlretrieve(url, dst)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def extract(archive: Path, dst: Path):
|
|
65
|
+
logger.info(f"Extracting: {archive.name}")
|
|
66
|
+
if archive.suffix == ".zip":
|
|
67
|
+
with zipfile.ZipFile(archive) as z:
|
|
68
|
+
z.extractall(dst)
|
|
69
|
+
elif archive.suffixes[-2:] == [".tar", ".bz2"]:
|
|
70
|
+
with tarfile.open(archive) as t:
|
|
71
|
+
t.extractall(dst)
|
|
72
|
+
else:
|
|
73
|
+
raise RuntimeError("Unknown archive format")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def find_binary(root: Path) -> Path:
|
|
77
|
+
for p in root.rglob("phantomjs*"):
|
|
78
|
+
if p.is_file() and os.access(p, os.X_OK):
|
|
79
|
+
return p
|
|
80
|
+
raise RuntimeError("phantomjs binary not found")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def download_and_extract(
|
|
84
|
+
*,
|
|
85
|
+
url: str,
|
|
86
|
+
archive: Path,
|
|
87
|
+
checksum: str,
|
|
88
|
+
extract_dir: Path,
|
|
89
|
+
force: bool,
|
|
90
|
+
):
|
|
91
|
+
if force and archive.exists():
|
|
92
|
+
archive.unlink()
|
|
93
|
+
|
|
94
|
+
dest = extract_dir / "phantomjs"
|
|
95
|
+
|
|
96
|
+
is_ok = (dest / "bin").exists()
|
|
97
|
+
|
|
98
|
+
if is_ok:
|
|
99
|
+
logger.info("Path exists: %s", dest)
|
|
100
|
+
if force:
|
|
101
|
+
os.remove(dest)
|
|
102
|
+
else:
|
|
103
|
+
return is_ok
|
|
104
|
+
|
|
105
|
+
logger.info("Downloading %s to %s", url, archive)
|
|
106
|
+
download(url, archive)
|
|
107
|
+
|
|
108
|
+
logger.info("Verifying checksum")
|
|
109
|
+
if sha256(archive) != checksum:
|
|
110
|
+
raise RuntimeError("Checksum mismatch")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if dest.exists():
|
|
114
|
+
shutil.rmtree(dest)
|
|
115
|
+
|
|
116
|
+
logger.info("Extracting")
|
|
117
|
+
extract(archive, extract_dir)
|
|
118
|
+
|
|
119
|
+
name = os.path.basename(archive)
|
|
120
|
+
for suf in ARCHIVE_SUFFIXES:
|
|
121
|
+
if name.endswith(suf):
|
|
122
|
+
name = name[:-len(suf)]
|
|
123
|
+
extract_path = extract_dir / name
|
|
124
|
+
|
|
125
|
+
os.rename(extract_path, dest)
|
|
126
|
+
os.remove(archive)
|
|
127
|
+
|
|
128
|
+
return (dest / "bin").exists()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def download_driver(
|
|
132
|
+
dest: Path | None = None,
|
|
133
|
+
version: str = "2.1.1",
|
|
134
|
+
os_name: str | None = None,
|
|
135
|
+
arch: str | None = None,
|
|
136
|
+
force: bool = False,
|
|
137
|
+
) -> bool:
|
|
138
|
+
if dest is None:
|
|
139
|
+
dest = DRIVER_PATH
|
|
140
|
+
if os_name is None:
|
|
141
|
+
os_name = platform.system().lower()
|
|
142
|
+
if arch is None:
|
|
143
|
+
arch = platform.architecture()[0]
|
|
144
|
+
if arch not in ("32bit", "64bit"):
|
|
145
|
+
machine = platform.machine().lower()
|
|
146
|
+
arch = "64bit" if "64" in machine else "32bit"
|
|
147
|
+
|
|
148
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
|
|
150
|
+
filename, checksum = detect_target(os_name, arch, version)
|
|
151
|
+
url = f"{BASE_URL}/{filename}"
|
|
152
|
+
archive = dest / filename
|
|
153
|
+
|
|
154
|
+
binary = download_and_extract(
|
|
155
|
+
url=url,
|
|
156
|
+
archive=archive,
|
|
157
|
+
checksum=checksum,
|
|
158
|
+
extract_dir=dest,
|
|
159
|
+
force=force,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return binary
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def setup_logging(quiet: bool = False):
|
|
166
|
+
"""
|
|
167
|
+
Configure global logging settings.
|
|
168
|
+
|
|
169
|
+
:param quiet: If True, only warnings and errors are shown.
|
|
170
|
+
"""
|
|
171
|
+
level = logging.WARNING if quiet else logging.INFO
|
|
172
|
+
|
|
173
|
+
logging.basicConfig(
|
|
174
|
+
level=level,
|
|
175
|
+
format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
|
|
176
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
177
|
+
handlers=[
|
|
178
|
+
logging.StreamHandler(sys.stdout)
|
|
179
|
+
],
|
|
180
|
+
force=True # overwrite any existing logging config
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def main():
|
|
184
|
+
parser = argparse.ArgumentParser(description="Download PhantomJS driver")
|
|
185
|
+
parser.add_argument("--dest", default=DRIVER_PATH.as_posix())
|
|
186
|
+
parser.add_argument("--version", default=DRIVER_VERSION)
|
|
187
|
+
parser.add_argument("--os", default=None, help="Operating system (e.g., Linux, Windows, Darwin)")
|
|
188
|
+
parser.add_argument("--arch", default=None, help="Architecture (e.g., 32bit, 64bit)")
|
|
189
|
+
parser.add_argument("--force", action="store_true")
|
|
190
|
+
parser.add_argument("--quiet", action="store_true")
|
|
191
|
+
|
|
192
|
+
args = parser.parse_args()
|
|
193
|
+
|
|
194
|
+
setup_logging(quiet=args.quiet)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# Detect OS and arch if not provided
|
|
198
|
+
os_name = args.os or platform.system()
|
|
199
|
+
arch = args.arch or platform.architecture()[0]
|
|
200
|
+
|
|
201
|
+
# Normalize architecture string (some systems return '32bit' or '64bit' already)
|
|
202
|
+
if arch not in ("32bit", "64bit"):
|
|
203
|
+
# Fallback: try to guess from machine
|
|
204
|
+
machine = platform.machine().lower()
|
|
205
|
+
if "64" in machine:
|
|
206
|
+
arch = "64bit"
|
|
207
|
+
else:
|
|
208
|
+
arch = "32bit"
|
|
209
|
+
|
|
210
|
+
logger.info("Target: OS=%s, Arch=%s, Version=%s", os_name, arch, args.version)
|
|
211
|
+
|
|
212
|
+
download_driver(
|
|
213
|
+
dest=Path(args.dest),
|
|
214
|
+
version=args.version,
|
|
215
|
+
os_name=os_name,
|
|
216
|
+
arch=arch,
|
|
217
|
+
force=args.force,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
main()
|
phasma/driver/driver.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import stat
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional, Sequence, Union
|
|
8
|
+
|
|
9
|
+
# Handle relative import when script is run directly
|
|
10
|
+
if __package__ is None:
|
|
11
|
+
# Add src directory to sys.path to allow absolute imports
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
13
|
+
from download import DRIVER_PATH, DRIVER_VERSION, download_driver
|
|
14
|
+
else:
|
|
15
|
+
from .download import DRIVER_PATH, DRIVER_VERSION, download_driver
|
|
16
|
+
|
|
17
|
+
PHASMA_PATH = DRIVER_PATH / "phantomjs"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Driver:
|
|
21
|
+
"""
|
|
22
|
+
Manages the PhantomJS executable path in an OS-aware manner.
|
|
23
|
+
- Always expects the binary inside a `bin/` subdirectory.
|
|
24
|
+
- Uses `phantomjs.exe` on Windows and `phantomjs` on Unix-like systems.
|
|
25
|
+
- Ensures the binary is executable (applies chmod +x on non-Windows).
|
|
26
|
+
- Downloads the driver automatically if not present.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def download(os_name: str | None = None, arch: str | None = None):
|
|
31
|
+
return download_driver(dest=DRIVER_PATH, os_name=os_name, arch=arch)
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
# Determine the correct executable name based on the OS
|
|
35
|
+
self.system = platform.system()
|
|
36
|
+
self.exe_name = "phantomjs.exe" if self.system == "Windows" else "phantomjs"
|
|
37
|
+
|
|
38
|
+
# Final expected path: <DRIVER_PATH>/bin/<phantomjs or phantomjs.exe>
|
|
39
|
+
self._bin_path = PHASMA_PATH / "bin" / self.exe_name
|
|
40
|
+
|
|
41
|
+
# If the binary doesn't exist, download and set it up
|
|
42
|
+
if not self._bin_path.is_file():
|
|
43
|
+
# Download the driver to the root directory first
|
|
44
|
+
self.download()
|
|
45
|
+
|
|
46
|
+
self.get_exe_access()
|
|
47
|
+
|
|
48
|
+
def get_exe_access(self):
|
|
49
|
+
# On non-Windows systems, ensure the file is executable
|
|
50
|
+
if self.system != "Windows":
|
|
51
|
+
if not os.access(self._bin_path, os.X_OK):
|
|
52
|
+
try:
|
|
53
|
+
current_mode = self._bin_path.stat().st_mode
|
|
54
|
+
self._bin_path.chmod(current_mode | stat.S_IEXEC)
|
|
55
|
+
except OSError:
|
|
56
|
+
# Ignore if permission cannot be changed (e.g., read-only FS)
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def bin_path(self) -> Path:
|
|
61
|
+
"""Returns the absolute path to the PhantomJS executable."""
|
|
62
|
+
return self._bin_path
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def examples_path(self) -> Path:
|
|
66
|
+
return PHASMA_PATH / "examples"
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def examples_list(self) -> List:
|
|
70
|
+
return list(self.examples_path.iterdir())
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def version(self) -> str:
|
|
74
|
+
return DRIVER_VERSION
|
|
75
|
+
|
|
76
|
+
def exec(
|
|
77
|
+
self,
|
|
78
|
+
args: Union[str, Sequence[str]],
|
|
79
|
+
*,
|
|
80
|
+
capture_output: bool = False,
|
|
81
|
+
timeout: Optional[float] = None,
|
|
82
|
+
check: bool = False,
|
|
83
|
+
**kwargs,
|
|
84
|
+
) -> subprocess.CompletedProcess:
|
|
85
|
+
"""
|
|
86
|
+
Execute PhantomJS with the given arguments.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
args: Command line arguments as a string or sequence of strings.
|
|
90
|
+
capture_output: If True, capture stdout and stderr.
|
|
91
|
+
timeout: Timeout in seconds.
|
|
92
|
+
check: If True, raise CalledProcessError on non-zero exit code.
|
|
93
|
+
**kwargs: Additional arguments passed to subprocess.run.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
subprocess.CompletedProcess instance.
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
>>> driver = Driver()
|
|
100
|
+
>>> result = driver.exec(["--version"])
|
|
101
|
+
>>> print(result.stdout)
|
|
102
|
+
"""
|
|
103
|
+
if isinstance(args, str):
|
|
104
|
+
# Split by spaces (simple split, no quoted string handling)
|
|
105
|
+
args = args.split()
|
|
106
|
+
|
|
107
|
+
cmd = [str(self.bin_path), *list(args)]
|
|
108
|
+
return subprocess.run(
|
|
109
|
+
cmd,
|
|
110
|
+
capture_output=capture_output,
|
|
111
|
+
timeout=timeout,
|
|
112
|
+
check=check,
|
|
113
|
+
**kwargs,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def run(self, *args, **kwargs) -> subprocess.CompletedProcess:
|
|
117
|
+
"""Alias for exec."""
|
|
118
|
+
return self.exec(*args, **kwargs)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
import argparse
|
|
123
|
+
import os
|
|
124
|
+
import sys
|
|
125
|
+
|
|
126
|
+
# Add src directory to sys.path to allow relative imports
|
|
127
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
128
|
+
|
|
129
|
+
parser = argparse.ArgumentParser(
|
|
130
|
+
description="Run PhantomJS via Driver", epilog="Example: python -m phasma.driver.driver --version"
|
|
131
|
+
)
|
|
132
|
+
parser.add_argument("args", nargs="*", help="Arguments to pass to PhantomJS (e.g., '--version', 'script.js')")
|
|
133
|
+
parser.add_argument("--capture-output", action="store_true", help="Capture stdout and stderr")
|
|
134
|
+
parser.add_argument("--timeout", type=float, help="Timeout in seconds")
|
|
135
|
+
parser.add_argument("--check", action="store_true", help="Raise CalledProcessError on non-zero exit code")
|
|
136
|
+
parsed, unknown = parser.parse_known_args()
|
|
137
|
+
|
|
138
|
+
driver = Driver()
|
|
139
|
+
try:
|
|
140
|
+
# Combine known positional args with unknown args (which are likely PhantomJS options)
|
|
141
|
+
all_args = parsed.args + unknown
|
|
142
|
+
result = driver.exec(
|
|
143
|
+
all_args,
|
|
144
|
+
capture_output=parsed.capture_output,
|
|
145
|
+
timeout=parsed.timeout,
|
|
146
|
+
check=parsed.check,
|
|
147
|
+
)
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
|
|
152
|
+
if parsed.capture_output:
|
|
153
|
+
if result.stdout:
|
|
154
|
+
sys.stdout.buffer.write(result.stdout)
|
|
155
|
+
if result.stderr:
|
|
156
|
+
sys.stderr.buffer.write(result.stderr)
|
|
157
|
+
sys.exit(result.returncode)
|