phasma 0.4.0__py3-none-macosx_10_9_universal2.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.
Files changed (61) hide show
  1. phasma/__init__.py +3 -0
  2. phasma/__main__.py +137 -0
  3. phasma/driver/__init__.py +4 -0
  4. phasma/driver/download.py +222 -0
  5. phasma/driver/driver.py +129 -0
  6. phasma/driver/phantomjs/ChangeLog +401 -0
  7. phasma/driver/phantomjs/LICENSE.BSD +22 -0
  8. phasma/driver/phantomjs/README.md +29 -0
  9. phasma/driver/phantomjs/bin/phantomjs +0 -0
  10. phasma/driver/phantomjs/examples/arguments.js +10 -0
  11. phasma/driver/phantomjs/examples/child_process-examples.js +28 -0
  12. phasma/driver/phantomjs/examples/colorwheel.js +52 -0
  13. phasma/driver/phantomjs/examples/countdown.js +10 -0
  14. phasma/driver/phantomjs/examples/detectsniff.js +60 -0
  15. phasma/driver/phantomjs/examples/echoToFile.js +24 -0
  16. phasma/driver/phantomjs/examples/features.js +30 -0
  17. phasma/driver/phantomjs/examples/fibo.js +10 -0
  18. phasma/driver/phantomjs/examples/hello.js +3 -0
  19. phasma/driver/phantomjs/examples/injectme.js +26 -0
  20. phasma/driver/phantomjs/examples/loadspeed.js +24 -0
  21. phasma/driver/phantomjs/examples/loadurlwithoutcss.js +26 -0
  22. phasma/driver/phantomjs/examples/modernizr.js +1406 -0
  23. phasma/driver/phantomjs/examples/module.js +5 -0
  24. phasma/driver/phantomjs/examples/netlog.js +26 -0
  25. phasma/driver/phantomjs/examples/netsniff.js +144 -0
  26. phasma/driver/phantomjs/examples/openurlwithproxy.js +25 -0
  27. phasma/driver/phantomjs/examples/outputEncoding.js +17 -0
  28. phasma/driver/phantomjs/examples/page_events.js +147 -0
  29. phasma/driver/phantomjs/examples/pagecallback.js +18 -0
  30. phasma/driver/phantomjs/examples/phantomwebintro.js +21 -0
  31. phasma/driver/phantomjs/examples/post.js +15 -0
  32. phasma/driver/phantomjs/examples/postjson.js +19 -0
  33. phasma/driver/phantomjs/examples/postserver.js +35 -0
  34. phasma/driver/phantomjs/examples/printenv.js +10 -0
  35. phasma/driver/phantomjs/examples/printheaderfooter.js +90 -0
  36. phasma/driver/phantomjs/examples/printmargins.js +36 -0
  37. phasma/driver/phantomjs/examples/rasterize.js +49 -0
  38. phasma/driver/phantomjs/examples/render_multi_url.js +74 -0
  39. phasma/driver/phantomjs/examples/responsive-screenshot.js +181 -0
  40. phasma/driver/phantomjs/examples/run-jasmine.js +92 -0
  41. phasma/driver/phantomjs/examples/run-jasmine2.js +94 -0
  42. phasma/driver/phantomjs/examples/run-qunit.js +77 -0
  43. phasma/driver/phantomjs/examples/scandir.js +24 -0
  44. phasma/driver/phantomjs/examples/server.js +44 -0
  45. phasma/driver/phantomjs/examples/serverkeepalive.js +35 -0
  46. phasma/driver/phantomjs/examples/simpleserver.js +43 -0
  47. phasma/driver/phantomjs/examples/sleepsort.js +27 -0
  48. phasma/driver/phantomjs/examples/stdin-stdout-stderr.js +19 -0
  49. phasma/driver/phantomjs/examples/universe.js +10 -0
  50. phasma/driver/phantomjs/examples/unrandomize.js +25 -0
  51. phasma/driver/phantomjs/examples/useragent.js +15 -0
  52. phasma/driver/phantomjs/examples/version.js +6 -0
  53. phasma/driver/phantomjs/examples/waitfor.js +58 -0
  54. phasma/driver/phantomjs/examples/walk_through_frames.js +73 -0
  55. phasma/driver/phantomjs/third-party.txt +36 -0
  56. phasma/phasma.py +165 -0
  57. phasma-0.4.0.dist-info/METADATA +213 -0
  58. phasma-0.4.0.dist-info/RECORD +61 -0
  59. phasma-0.4.0.dist-info/WHEEL +5 -0
  60. phasma-0.4.0.dist-info/licenses/LICENSE +9 -0
  61. phasma-0.4.0.dist-info/top_level.txt +1 -0
phasma/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .phasma import download_driver, render_page, render_url, execjs
2
+
3
+ __all__ = ["download_driver", "render_page", "render_url", "execjs"]
phasma/__main__.py ADDED
@@ -0,0 +1,137 @@
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
+ dl_parser.add_argument("--force", action="store_true", help="Force download even if already exists")
28
+
29
+ # driver exec
30
+ exec_parser = driver_subparsers.add_parser("exec", help="Execute PhantomJS with arguments")
31
+ exec_parser.add_argument("args", nargs="*", help="Arguments to pass to PhantomJS (e.g., '--version', 'script.js')")
32
+ exec_parser.add_argument("--capture-output", action="store_true", help="Capture stdout and stderr")
33
+ exec_parser.add_argument("--timeout", type=float, help="Timeout in seconds")
34
+ exec_parser.add_argument("--cwd", help="Working directory for PhantomJS process")
35
+ exec_parser.add_argument("--ssl", action="store_true", default=False, help="Enable SSL (default: False)")
36
+ exec_parser.add_argument("--no-ssl", dest="ssl", action="store_false", help="Disable SSL (set OPENSSL_CONF='')")
37
+
38
+ # driver --version and --path as optional arguments of driver command itself
39
+ driver_parser.add_argument("--version", action="store_true", help="Show driver version")
40
+ driver_parser.add_argument("--path", action="store_true", help="Show driver executable path")
41
+
42
+ # render-page
43
+ rp_parser = subparsers.add_parser("render-page", help="Render an HTML page")
44
+ rp_parser.add_argument("input", help="HTML file path or HTML string")
45
+ rp_parser.add_argument("--output", "-o", help="Output file path")
46
+ rp_parser.add_argument("--viewport", default="1024x768", help="Viewport size (widthxheight)")
47
+ rp_parser.add_argument("--wait", type=int, default=100, help="Wait time in milliseconds")
48
+
49
+ # render-url
50
+ ru_parser = subparsers.add_parser("render-url", help="Render a URL")
51
+ ru_parser.add_argument("url", help="URL to render")
52
+ ru_parser.add_argument("--output", "-o", help="Output file path")
53
+ ru_parser.add_argument("--viewport", default="1024x768", help="Viewport size (widthxheight)")
54
+ ru_parser.add_argument("--wait", type=int, default=0, help="Wait time in milliseconds")
55
+
56
+ # execjs
57
+ js_parser = subparsers.add_parser("execjs", help="Execute JavaScript code")
58
+ js_parser.add_argument("script", help="JavaScript code (use '-' to read from stdin)")
59
+ js_parser.add_argument("--arg", action="append", help="Additional arguments to pass")
60
+
61
+ args = parser.parse_args()
62
+
63
+ if args.command == "driver":
64
+ if args.driver_action == "download":
65
+ success = Driver.download(os_name=args.os, arch=args.arch, force=args.force)
66
+ if success:
67
+ print("Driver downloaded successfully.")
68
+ sys.exit(0)
69
+ else:
70
+ print("Driver download failed.")
71
+ sys.exit(1)
72
+ elif args.driver_action == "exec":
73
+ driver = Driver()
74
+ try:
75
+ result = driver.exec(
76
+ args.args,
77
+ capture_output=args.capture_output,
78
+ timeout=args.timeout,
79
+ ssl=args.ssl,
80
+ cwd=args.cwd,
81
+ )
82
+ except Exception as e:
83
+ print(f"Error: {e}", file=sys.stderr)
84
+ sys.exit(1)
85
+
86
+ if args.capture_output:
87
+ if result.stdout:
88
+ sys.stdout.buffer.write(result.stdout)
89
+ if result.stderr:
90
+ sys.stderr.buffer.write(result.stderr)
91
+ sys.exit(result.returncode)
92
+ elif args.version:
93
+ driver = Driver()
94
+ version = driver.version
95
+ print(f"PhantomJS driver version: {version}")
96
+ elif args.path:
97
+ driver = Driver()
98
+ path = driver.bin_path
99
+ print(path)
100
+ else:
101
+ driver_parser.print_help()
102
+ sys.exit(1)
103
+
104
+ elif args.command == "render-page":
105
+ # Determine if input is a file
106
+ input_path = Path(args.input)
107
+ if input_path.is_file():
108
+ page = input_path
109
+ else:
110
+ page = args.input
111
+ rendered = render_page(page, output=args.output, viewport_size=args.viewport, wait_time=args.wait)
112
+ if not args.output:
113
+ print(rendered)
114
+ else:
115
+ print(f"Rendered content saved to {args.output}")
116
+
117
+ elif args.command == "render-url":
118
+ rendered = render_url(args.url, output=args.output, viewport_size=args.viewport, wait_time=args.wait)
119
+ if not args.output:
120
+ print(rendered)
121
+ else:
122
+ print(f"Rendered content saved to {args.output}")
123
+
124
+ elif args.command == "execjs":
125
+ if args.script == "-":
126
+ script = sys.stdin.read()
127
+ else:
128
+ script = args.script
129
+ output = execjs(script, args=args.arg)
130
+ print(output)
131
+
132
+ else:
133
+ parser.print_help()
134
+ sys.exit(1)
135
+
136
+ if __name__ == "__main__":
137
+ main()
@@ -0,0 +1,4 @@
1
+ from .driver import Driver
2
+
3
+ __all__ = ["Driver"]
4
+
@@ -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
+ shutil.rmtree(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()
@@ -0,0 +1,129 @@
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
+
10
+ from .download import DRIVER_PATH, DRIVER_VERSION, download_driver
11
+
12
+ PHASMA_PATH = DRIVER_PATH / "phantomjs"
13
+
14
+
15
+ class Driver:
16
+ """
17
+ Manages the PhantomJS executable path in an OS-aware manner.
18
+ - Always expects the binary inside a `bin/` subdirectory.
19
+ - Uses `phantomjs.exe` on Windows and `phantomjs` on Unix-like systems.
20
+ - Ensures the binary is executable (applies chmod +x on non-Windows).
21
+ - Downloads the driver automatically if not present.
22
+ """
23
+
24
+ @staticmethod
25
+ def download(os_name: str | None = None, arch: str | None = None, force: bool = False):
26
+ return download_driver(dest=DRIVER_PATH, os_name=os_name, arch=arch, force=force)
27
+
28
+ def __init__(self):
29
+ # Determine the correct executable name based on the OS
30
+ self.system = platform.system()
31
+ self.exe_name = "phantomjs.exe" if self.system == "Windows" else "phantomjs"
32
+
33
+ # Final expected path: <DRIVER_PATH>/bin/<phantomjs or phantomjs.exe>
34
+ self._bin_path = PHASMA_PATH / "bin" / self.exe_name
35
+
36
+ # If the binary doesn't exist, download and set it up
37
+ if not self._bin_path.is_file():
38
+ # Download the driver to the root directory first
39
+ self.download()
40
+
41
+ self.get_exe_access()
42
+
43
+ def get_exe_access(self):
44
+ # On non-Windows systems, ensure the file is executable
45
+ if self.system != "Windows":
46
+ if not os.access(self._bin_path, os.X_OK):
47
+ try:
48
+ current_mode = self._bin_path.stat().st_mode
49
+ self._bin_path.chmod(current_mode | stat.S_IEXEC)
50
+ except OSError:
51
+ # Ignore if permission cannot be changed (e.g., read-only FS)
52
+ pass
53
+
54
+ @property
55
+ def bin_path(self) -> Path:
56
+ """Returns the absolute path to the PhantomJS executable."""
57
+ return self._bin_path
58
+
59
+ @property
60
+ def examples_path(self) -> Path:
61
+ return PHASMA_PATH / "examples"
62
+
63
+ @property
64
+ def examples_list(self) -> List:
65
+ return list(self.examples_path.iterdir())
66
+
67
+ @property
68
+ def version(self) -> str:
69
+ return DRIVER_VERSION
70
+
71
+ def exec(
72
+ self,
73
+ args: Union[str, Sequence[str]],
74
+ *,
75
+ capture_output: bool = False,
76
+ timeout: Optional[float] = 30,
77
+ check: bool = False,
78
+ ssl: bool = False,
79
+ env: Optional[dict] = None,
80
+ cwd: Optional[Union[str, Path]] = None,
81
+ **kwargs,
82
+ ) -> subprocess.CompletedProcess:
83
+ """
84
+ Execute PhantomJS with the given arguments.
85
+
86
+ Args:
87
+ args: Command line arguments as a string or sequence of strings.
88
+ capture_output: If True, capture stdout and stderr.
89
+ timeout: Timeout in seconds.
90
+ check: If True, raise CalledProcessError on non-zero exit code.
91
+ ssl: If False, set OPENSSL_CONF environment variable to empty string.
92
+ env: Optional environment variables dictionary for subprocess.
93
+ cwd: Optional working directory for subprocess.
94
+ **kwargs: Additional arguments passed to subprocess.run.
95
+
96
+ Returns:
97
+ subprocess.CompletedProcess instance.
98
+
99
+ Example:
100
+ >>> driver = Driver()
101
+ >>> result = driver.exec(["--version"])
102
+ >>> print(result.stdout)
103
+ """
104
+ if isinstance(args, str):
105
+ # Split by spaces (simple split, no quoted string handling)
106
+ args = args.split()
107
+
108
+ cmd = [str(self.bin_path), *list(args)]
109
+
110
+ # Handle SSL environment
111
+ if not ssl:
112
+ if env is None:
113
+ env = os.environ.copy()
114
+ env['OPENSSL_CONF'] = ''
115
+
116
+ return subprocess.run(
117
+ cmd,
118
+ capture_output=capture_output,
119
+ timeout=timeout,
120
+ check=check,
121
+ env=env,
122
+ cwd=cwd,
123
+ **kwargs,
124
+ )
125
+
126
+ def run(self, *args, **kwargs) -> subprocess.CompletedProcess:
127
+ """Alias for exec."""
128
+ return self.exec(*args, **kwargs)
129
+