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.
Files changed (61) hide show
  1. phasma/__init__.py +3 -0
  2. phasma/__main__.py +107 -0
  3. phasma/driver/__init__.py +4 -0
  4. phasma/driver/download.py +222 -0
  5. phasma/driver/driver.py +157 -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.exe +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.1.0.dist-info/METADATA +193 -0
  58. phasma-0.1.0.dist-info/RECORD +61 -0
  59. phasma-0.1.0.dist-info/WHEEL +5 -0
  60. phasma-0.1.0.dist-info/licenses/LICENSE +9 -0
  61. phasma-0.1.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,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,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
+ 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()
@@ -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)