chromiumfish 0.1.0__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,31 @@
1
+ # Node
2
+ node_modules/
3
+ dist/
4
+ *.tsbuildinfo
5
+ npm-debug.log*
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.py[cod]
10
+ *.egg-info/
11
+ .eggs/
12
+ build/
13
+ .venv/
14
+ venv/
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+
19
+ # Browser cache / downloaded builds
20
+ .cache/
21
+ *.tar.gz
22
+ *.zip
23
+
24
+ # OS / editor
25
+ .DS_Store
26
+ *.swp
27
+ .idea/
28
+
29
+ # Local secrets / env
30
+ .env
31
+ *.local
@@ -0,0 +1,30 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arman Hossain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ This package distributes builds of the Chromium project, which is licensed
26
+ under the BSD 3-Clause License and includes third-party components under their
27
+ respective licenses. The full Chromium license text and credits are bundled
28
+ with each browser release. "Chromium" and "Google Chrome" are trademarks of
29
+ Google LLC. ChromiumFish is an independent fork and is not affiliated with,
30
+ sponsored by, or endorsed by Google LLC.
@@ -0,0 +1,90 @@
1
+ Metadata-Version: 2.4
2
+ Name: chromiumfish
3
+ Version: 0.1.0
4
+ Summary: Stealth Chromium build with a drop-in Playwright harness — fetches and launches the ChromiumFish browser.
5
+ Project-URL: Homepage, https://chromiumfish.com
6
+ Project-URL: Repository, https://github.com/arman-bd/chromiumfish
7
+ Project-URL: Documentation, https://chromiumfish.com
8
+ Project-URL: Releases, https://github.com/arman-bd/chromiumfish/releases
9
+ Author: Arman Hossain
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: anti-detect,automation,chromium,fingerprint,playwright,stealth
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
18
+ Classifier: Topic :: Software Development :: Testing
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: playwright>=1.40
21
+ Description-Content-Type: text/markdown
22
+
23
+ # chromiumfish (Python)
24
+
25
+ Stealth Chromium with a drop-in [Playwright](https://playwright.dev) harness.
26
+
27
+ ```bash
28
+ pip install chromiumfish
29
+ chromiumfish fetch # download + cache the browser build
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ **Sync**
35
+
36
+ ```python
37
+ from chromiumfish.sync_api import Chromiumfish
38
+
39
+ with Chromiumfish(persona_seed=27182, headless=True) as browser:
40
+ page = browser.new_page()
41
+ page.goto("https://abrahamjuliot.github.io/creepjs/")
42
+ page.screenshot(path="fp.png")
43
+ ```
44
+
45
+ **Async**
46
+
47
+ ```python
48
+ import asyncio
49
+ from chromiumfish.async_api import AsyncChromiumfish
50
+
51
+ async def main():
52
+ async with AsyncChromiumfish(persona_seed=27182) as browser:
53
+ page = await browser.new_page()
54
+ await page.goto("https://example.com")
55
+ print(await page.title())
56
+
57
+ asyncio.run(main())
58
+ ```
59
+
60
+ The returned object is a standard Playwright `Browser`, so `new_context`,
61
+ `new_page`, routing, tracing, etc. all work as usual.
62
+
63
+ ## Options
64
+
65
+ | Argument | Default | Description |
66
+ |----------|---------|-------------|
67
+ | `persona_seed` | `None` | Integer seed for a stable, internally-consistent fingerprint persona. |
68
+ | `headless` | `True` | Run headless (SwiftShader). |
69
+ | `proxy` | `None` | Playwright proxy dict, e.g. `{"server": "http://host:port", "username": ..., "password": ...}`. |
70
+ | `window_size` | `(1920, 1080)` | Window dimensions. |
71
+ | `version` | pinned | Override the browser build version. |
72
+ | `download` | `True` | Auto-download the build if missing. |
73
+ | `args` | `None` | Extra Chromium flags. |
74
+ | `**launch_kwargs` | — | Forwarded to `chromium.launch()`. |
75
+
76
+ ## CLI
77
+
78
+ ```bash
79
+ chromiumfish fetch [--browser-version X] [--force] # download + cache
80
+ chromiumfish path # print binary path
81
+ chromiumfish clear # wipe the cache
82
+ chromiumfish --version
83
+ ```
84
+
85
+ Builds are cached under `~/.cache/chromiumfish/<version>/` (override with
86
+ `CHROMIUMFISH_CACHE_DIR`). Pin a build with `CHROMIUMFISH_VERSION`.
87
+
88
+ ## License
89
+
90
+ MIT © Arman Hossain. See the [repository](https://github.com/arman-bd/chromiumfish).
@@ -0,0 +1,68 @@
1
+ # chromiumfish (Python)
2
+
3
+ Stealth Chromium with a drop-in [Playwright](https://playwright.dev) harness.
4
+
5
+ ```bash
6
+ pip install chromiumfish
7
+ chromiumfish fetch # download + cache the browser build
8
+ ```
9
+
10
+ ## Usage
11
+
12
+ **Sync**
13
+
14
+ ```python
15
+ from chromiumfish.sync_api import Chromiumfish
16
+
17
+ with Chromiumfish(persona_seed=27182, headless=True) as browser:
18
+ page = browser.new_page()
19
+ page.goto("https://abrahamjuliot.github.io/creepjs/")
20
+ page.screenshot(path="fp.png")
21
+ ```
22
+
23
+ **Async**
24
+
25
+ ```python
26
+ import asyncio
27
+ from chromiumfish.async_api import AsyncChromiumfish
28
+
29
+ async def main():
30
+ async with AsyncChromiumfish(persona_seed=27182) as browser:
31
+ page = await browser.new_page()
32
+ await page.goto("https://example.com")
33
+ print(await page.title())
34
+
35
+ asyncio.run(main())
36
+ ```
37
+
38
+ The returned object is a standard Playwright `Browser`, so `new_context`,
39
+ `new_page`, routing, tracing, etc. all work as usual.
40
+
41
+ ## Options
42
+
43
+ | Argument | Default | Description |
44
+ |----------|---------|-------------|
45
+ | `persona_seed` | `None` | Integer seed for a stable, internally-consistent fingerprint persona. |
46
+ | `headless` | `True` | Run headless (SwiftShader). |
47
+ | `proxy` | `None` | Playwright proxy dict, e.g. `{"server": "http://host:port", "username": ..., "password": ...}`. |
48
+ | `window_size` | `(1920, 1080)` | Window dimensions. |
49
+ | `version` | pinned | Override the browser build version. |
50
+ | `download` | `True` | Auto-download the build if missing. |
51
+ | `args` | `None` | Extra Chromium flags. |
52
+ | `**launch_kwargs` | — | Forwarded to `chromium.launch()`. |
53
+
54
+ ## CLI
55
+
56
+ ```bash
57
+ chromiumfish fetch [--browser-version X] [--force] # download + cache
58
+ chromiumfish path # print binary path
59
+ chromiumfish clear # wipe the cache
60
+ chromiumfish --version
61
+ ```
62
+
63
+ Builds are cached under `~/.cache/chromiumfish/<version>/` (override with
64
+ `CHROMIUMFISH_CACHE_DIR`). Pin a build with `CHROMIUMFISH_VERSION`.
65
+
66
+ ## License
67
+
68
+ MIT © Arman Hossain. See the [repository](https://github.com/arman-bd/chromiumfish).
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "chromiumfish"
7
+ dynamic = ["version"]
8
+ description = "Stealth Chromium build with a drop-in Playwright harness — fetches and launches the ChromiumFish browser."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Arman Hossain" }]
14
+ keywords = ["playwright", "chromium", "stealth", "fingerprint", "automation", "anti-detect"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
21
+ "Topic :: Software Development :: Testing",
22
+ ]
23
+ dependencies = [
24
+ "playwright>=1.40",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://chromiumfish.com"
29
+ Repository = "https://github.com/arman-bd/chromiumfish"
30
+ Documentation = "https://chromiumfish.com"
31
+ Releases = "https://github.com/arman-bd/chromiumfish/releases"
32
+
33
+ [project.scripts]
34
+ chromiumfish = "chromiumfish.cli:main"
35
+
36
+ [tool.hatch.version]
37
+ path = "src/chromiumfish/version.py"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/chromiumfish"]
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ include = ["src/chromiumfish", "README.md", "LICENSE"]
@@ -0,0 +1,30 @@
1
+ """ChromiumFish — a stealth Chromium build with a Playwright harness.
2
+
3
+ Quick start (sync)::
4
+
5
+ from chromiumfish.sync_api import Chromiumfish
6
+
7
+ with Chromiumfish(persona_seed=27182) as browser:
8
+ page = browser.new_page()
9
+ page.goto("https://example.com")
10
+
11
+ Quick start (async)::
12
+
13
+ from chromiumfish.async_api import AsyncChromiumfish
14
+
15
+ async with AsyncChromiumfish(persona_seed=27182) as browser:
16
+ page = await browser.new_page()
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from .fetch import binary_path, fetch, install_dir
21
+ from .version import DEFAULT_BROWSER_VERSION, __version__, browser_version
22
+
23
+ __all__ = [
24
+ "__version__",
25
+ "DEFAULT_BROWSER_VERSION",
26
+ "browser_version",
27
+ "fetch",
28
+ "binary_path",
29
+ "install_dir",
30
+ ]
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,65 @@
1
+ """Async Playwright wrapper for ChromiumFish.
2
+
3
+ from chromiumfish.async_api import AsyncChromiumfish
4
+
5
+ async with AsyncChromiumfish(persona_seed=27182, headless=True) as browser:
6
+ page = await browser.new_page()
7
+ await page.goto("https://example.com")
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from playwright.async_api import Browser, async_playwright
14
+
15
+ from .fetch import binary_path
16
+ from .launcher import launch_options
17
+
18
+
19
+ class AsyncChromiumfish:
20
+ def __init__(
21
+ self,
22
+ *,
23
+ persona_seed: int | None = None,
24
+ headless: bool = True,
25
+ proxy: dict[str, Any] | None = None,
26
+ window_size: tuple[int, int] | None = (1920, 1080),
27
+ version: str | None = None,
28
+ download: bool = True,
29
+ args: list[str] | None = None,
30
+ **launch_kwargs: Any,
31
+ ) -> None:
32
+ self._opts = dict(
33
+ persona_seed=persona_seed,
34
+ headless=headless,
35
+ proxy=proxy,
36
+ window_size=window_size,
37
+ args=args,
38
+ extra=launch_kwargs,
39
+ )
40
+ self._version = version
41
+ self._download = download
42
+ self._pw = None
43
+ self._browser: Browser | None = None
44
+
45
+ async def start(self) -> Browser:
46
+ exe = binary_path(self._version, download=self._download)
47
+ self._pw = await async_playwright().start()
48
+ self._browser = await self._pw.chromium.launch(
49
+ **launch_options(executable_path=exe, **self._opts)
50
+ )
51
+ return self._browser
52
+
53
+ async def close(self) -> None:
54
+ if self._browser:
55
+ await self._browser.close()
56
+ self._browser = None
57
+ if self._pw:
58
+ await self._pw.stop()
59
+ self._pw = None
60
+
61
+ async def __aenter__(self) -> Browser:
62
+ return await self.start()
63
+
64
+ async def __aexit__(self, *exc: object) -> None:
65
+ await self.close()
@@ -0,0 +1,51 @@
1
+ """`chromiumfish` command-line interface."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import shutil
6
+ import sys
7
+
8
+ from .fetch import binary_path, cache_root, fetch, install_dir
9
+ from .version import __version__, browser_version
10
+
11
+
12
+ def main(argv: list[str] | None = None) -> int:
13
+ parser = argparse.ArgumentParser(
14
+ prog="chromiumfish",
15
+ description="Fetch and manage the ChromiumFish browser build.",
16
+ )
17
+ parser.add_argument("-V", "--version", action="version",
18
+ version=f"chromiumfish {__version__} (browser {browser_version()})")
19
+ sub = parser.add_subparsers(dest="cmd")
20
+
21
+ f = sub.add_parser("fetch", help="download + cache the browser build")
22
+ f.add_argument("--browser-version", default=None, help="override the build version")
23
+ f.add_argument("--force", action="store_true", help="re-download even if cached")
24
+
25
+ sub.add_parser("path", help="print the cached binary path (fetching if missing)")
26
+ sub.add_parser("clear", help="remove all cached browser builds")
27
+
28
+ args = parser.parse_args(argv)
29
+
30
+ if args.cmd == "fetch":
31
+ path = fetch(args.browser_version, force=args.force)
32
+ print(path)
33
+ return 0
34
+ if args.cmd == "path":
35
+ print(binary_path())
36
+ return 0
37
+ if args.cmd == "clear":
38
+ root = cache_root()
39
+ if root.exists():
40
+ shutil.rmtree(root, ignore_errors=True)
41
+ print(f"removed {root}")
42
+ else:
43
+ print("nothing to remove")
44
+ return 0
45
+
46
+ parser.print_help()
47
+ return 1
48
+
49
+
50
+ if __name__ == "__main__":
51
+ sys.exit(main())
@@ -0,0 +1,201 @@
1
+ """Download, verify, and cache the ChromiumFish browser build.
2
+
3
+ Fetch model: resolve ``version × platform`` to a GitHub
4
+ Release asset, verify its SHA-256, extract it to a per-version cache dir, and
5
+ return the path to the launchable binary.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import os
11
+ import platform
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ import tarfile
16
+ import urllib.request
17
+ import zipfile
18
+ from pathlib import Path
19
+
20
+ from .version import browser_version, release_base_url
21
+
22
+
23
+ class UnsupportedPlatformError(RuntimeError):
24
+ pass
25
+
26
+
27
+ def cache_root() -> Path:
28
+ env = os.environ.get("CHROMIUMFISH_CACHE_DIR")
29
+ if env:
30
+ return Path(env).expanduser()
31
+ if sys.platform == "darwin":
32
+ return Path.home() / "Library" / "Caches" / "chromiumfish"
33
+ if os.name == "nt":
34
+ base = os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local"))
35
+ return Path(base) / "chromiumfish"
36
+ return Path(os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache"))) / "chromiumfish"
37
+
38
+
39
+ def platform_slug() -> str:
40
+ """e.g. ``linux-x64``, ``win-x64``, ``mac-arm64``."""
41
+ machine = platform.machine().lower()
42
+ arch = {
43
+ "x86_64": "x64", "amd64": "x64",
44
+ "aarch64": "arm64", "arm64": "arm64",
45
+ }.get(machine)
46
+ if arch is None:
47
+ raise UnsupportedPlatformError(f"unsupported architecture: {machine}")
48
+ if sys.platform.startswith("linux"):
49
+ return f"linux-{arch}"
50
+ if sys.platform == "darwin":
51
+ return f"mac-{arch}"
52
+ if os.name == "nt":
53
+ return f"win-{arch}"
54
+ raise UnsupportedPlatformError(f"unsupported platform: {sys.platform}")
55
+
56
+
57
+ def _asset_name(version: str) -> str:
58
+ slug = platform_slug()
59
+ ext = "zip" if slug.startswith("win") else "tar.gz"
60
+ return f"chromiumfish-{version}-{slug}.{ext}"
61
+
62
+
63
+ def _binary_name() -> str:
64
+ if os.name == "nt":
65
+ return "chromiumfish.exe"
66
+ return "chromiumfish" # falls back to "chrome" during discovery
67
+
68
+
69
+ def install_dir(version: str | None = None) -> Path:
70
+ version = version or browser_version()
71
+ return cache_root() / version / platform_slug()
72
+
73
+
74
+ def find_binary(root: Path) -> Path | None:
75
+ """Locate the launchable binary inside an extracted build."""
76
+ candidates = ["chromiumfish", "chrome", "chromiumfish.exe", "chrome.exe", "ChromiumFish"]
77
+ for name in candidates:
78
+ direct = root / name
79
+ if direct.is_file():
80
+ return direct
81
+ for name in candidates:
82
+ for hit in root.rglob(name):
83
+ if hit.is_file():
84
+ return hit
85
+ return None
86
+
87
+
88
+ def _download(url: str, dest: Path) -> None:
89
+ dest.parent.mkdir(parents=True, exist_ok=True)
90
+ print(f"[chromiumfish] downloading {url}", file=sys.stderr)
91
+ with urllib.request.urlopen(url) as resp, open(dest, "wb") as out: # noqa: S310
92
+ total = int(resp.headers.get("Content-Length", 0))
93
+ read = 0
94
+ while chunk := resp.read(1 << 20):
95
+ out.write(chunk)
96
+ read += len(chunk)
97
+ if total:
98
+ pct = read * 100 // total
99
+ print(f"\r[chromiumfish] {pct:3d}% ({read >> 20} / {total >> 20} MiB)",
100
+ end="", file=sys.stderr)
101
+ print("", file=sys.stderr)
102
+
103
+
104
+ def _sha256(path: Path) -> str:
105
+ h = hashlib.sha256()
106
+ with open(path, "rb") as f:
107
+ while chunk := f.read(1 << 20):
108
+ h.update(chunk)
109
+ return h.hexdigest()
110
+
111
+
112
+ def _verify(archive: Path, base_url: str, asset: str) -> None:
113
+ try:
114
+ with urllib.request.urlopen(f"{base_url}/{asset}.sha256") as r: # noqa: S310
115
+ expected = r.read().decode().split()[0].strip()
116
+ except Exception: # noqa: BLE001
117
+ print("[chromiumfish] warning: no .sha256 published, skipping verification", file=sys.stderr)
118
+ return
119
+ actual = _sha256(archive)
120
+ if actual != expected:
121
+ archive.unlink(missing_ok=True)
122
+ raise RuntimeError(f"checksum mismatch for {asset}: {actual} != {expected}")
123
+
124
+
125
+ def _extract(archive: Path, dest: Path) -> None:
126
+ dest.mkdir(parents=True, exist_ok=True)
127
+ if archive.name.endswith(".zip"):
128
+ with zipfile.ZipFile(archive) as z:
129
+ z.extractall(dest)
130
+ else:
131
+ with tarfile.open(archive) as t:
132
+ t.extractall(dest) # noqa: S202 - trusted first-party asset
133
+
134
+
135
+ def _macos_prepare(target: Path) -> None:
136
+ """Identity-clean macOS prep.
137
+
138
+ 1. Strip the ``com.apple.quarantine`` flag. Programmatic downloads usually
139
+ don't set it, but browsers/tools might — removing it avoids Gatekeeper's
140
+ "unidentified developer" block without any notarization.
141
+ 2. Ensure the bundle is ad-hoc signed (``codesign -s -``). Apple Silicon
142
+ refuses to run an unsigned binary; ad-hoc signing fixes that and embeds
143
+ NO certificate, name, or identity. (Release builds ship ad-hoc signed;
144
+ this is a defensive fallback.)
145
+ """
146
+ if sys.platform != "darwin":
147
+ return
148
+ app = next(target.glob("*.app"), None)
149
+ sign_target = app or find_binary(target)
150
+ subprocess.run(["xattr", "-dr", "com.apple.quarantine", str(target)],
151
+ check=False, capture_output=True)
152
+ if sign_target:
153
+ valid = subprocess.run(["codesign", "--verify", "--quiet", str(sign_target)],
154
+ capture_output=True).returncode == 0
155
+ if not valid:
156
+ subprocess.run(["codesign", "--force", "--deep", "--sign", "-", str(sign_target)],
157
+ check=False, capture_output=True)
158
+
159
+
160
+ def fetch(version: str | None = None, *, force: bool = False) -> Path:
161
+ """Ensure the browser build is present and return the binary path."""
162
+ version = version or browser_version()
163
+ target = install_dir(version)
164
+
165
+ if force and target.exists():
166
+ shutil.rmtree(target, ignore_errors=True)
167
+
168
+ if target.exists():
169
+ binp = find_binary(target)
170
+ if binp:
171
+ return binp
172
+
173
+ base = release_base_url(version)
174
+ asset = _asset_name(version)
175
+ archive = cache_root() / version / asset
176
+ _download(f"{base}/{asset}", archive)
177
+ _verify(archive, base, asset)
178
+ _extract(archive, target)
179
+ archive.unlink(missing_ok=True)
180
+ _macos_prepare(target)
181
+
182
+ binp = find_binary(target)
183
+ if not binp:
184
+ raise RuntimeError(f"no browser binary found in extracted build at {target}")
185
+ if os.name != "nt":
186
+ binp.chmod(0o755)
187
+ print(f"[chromiumfish] ready: {binp}", file=sys.stderr)
188
+ return binp
189
+
190
+
191
+ def binary_path(version: str | None = None, *, download: bool = True) -> Path:
192
+ """Path to the cached binary, fetching it if needed (and allowed)."""
193
+ version = version or browser_version()
194
+ existing = find_binary(install_dir(version))
195
+ if existing:
196
+ return existing
197
+ if not download:
198
+ raise FileNotFoundError(
199
+ f"ChromiumFish {version} not installed. Run `chromiumfish fetch`."
200
+ )
201
+ return fetch(version)
@@ -0,0 +1,56 @@
1
+ """Shared launch-argument construction for the sync/async wrappers."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ # Flags that keep the GPU-less / SwiftShader path working and the persona
8
+ # engine happy. Mirrors the production launch_lean.sh defaults (minus anything
9
+ # that is now baked into the build / bundled addon).
10
+ BASE_ARGS: list[str] = [
11
+ "--no-sandbox",
12
+ "--no-zygote",
13
+ "--disable-dev-shm-usage",
14
+ "--use-gl=angle",
15
+ "--use-angle=swiftshader",
16
+ "--enable-unsafe-swiftshader",
17
+ ]
18
+
19
+
20
+ def build_args(
21
+ *,
22
+ persona_seed: int | None = None,
23
+ window_size: tuple[int, int] | None = (1920, 1080),
24
+ extra_args: list[str] | None = None,
25
+ ) -> list[str]:
26
+ args = list(BASE_ARGS)
27
+ if persona_seed is not None:
28
+ args.append(f"--persona-seed={persona_seed}")
29
+ if window_size is not None:
30
+ args.append(f"--window-size={window_size[0]},{window_size[1]}")
31
+ if extra_args:
32
+ args.extend(extra_args)
33
+ return args
34
+
35
+
36
+ def launch_options(
37
+ *,
38
+ executable_path: Path,
39
+ headless: bool,
40
+ persona_seed: int | None,
41
+ proxy: dict[str, Any] | None,
42
+ window_size: tuple[int, int] | None,
43
+ args: list[str] | None,
44
+ extra: dict[str, Any],
45
+ ) -> dict[str, Any]:
46
+ opts: dict[str, Any] = {
47
+ "executable_path": str(executable_path),
48
+ "headless": headless,
49
+ "args": build_args(
50
+ persona_seed=persona_seed, window_size=window_size, extra_args=args
51
+ ),
52
+ }
53
+ if proxy is not None:
54
+ opts["proxy"] = proxy
55
+ opts.update(extra)
56
+ return opts
@@ -0,0 +1,65 @@
1
+ """Sync Playwright wrapper for ChromiumFish.
2
+
3
+ from chromiumfish.sync_api import Chromiumfish
4
+
5
+ with Chromiumfish(persona_seed=27182, headless=True) as browser:
6
+ page = browser.new_page()
7
+ page.goto("https://example.com")
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from playwright.sync_api import Browser, sync_playwright
14
+
15
+ from .fetch import binary_path
16
+ from .launcher import launch_options
17
+
18
+
19
+ class Chromiumfish:
20
+ def __init__(
21
+ self,
22
+ *,
23
+ persona_seed: int | None = None,
24
+ headless: bool = True,
25
+ proxy: dict[str, Any] | None = None,
26
+ window_size: tuple[int, int] | None = (1920, 1080),
27
+ version: str | None = None,
28
+ download: bool = True,
29
+ args: list[str] | None = None,
30
+ **launch_kwargs: Any,
31
+ ) -> None:
32
+ self._opts = dict(
33
+ persona_seed=persona_seed,
34
+ headless=headless,
35
+ proxy=proxy,
36
+ window_size=window_size,
37
+ args=args,
38
+ extra=launch_kwargs,
39
+ )
40
+ self._version = version
41
+ self._download = download
42
+ self._pw = None
43
+ self._browser: Browser | None = None
44
+
45
+ def start(self) -> Browser:
46
+ exe = binary_path(self._version, download=self._download)
47
+ self._pw = sync_playwright().start()
48
+ self._browser = self._pw.chromium.launch(
49
+ **launch_options(executable_path=exe, **self._opts)
50
+ )
51
+ return self._browser
52
+
53
+ def close(self) -> None:
54
+ if self._browser:
55
+ self._browser.close()
56
+ self._browser = None
57
+ if self._pw:
58
+ self._pw.stop()
59
+ self._pw = None
60
+
61
+ def __enter__(self) -> Browser:
62
+ return self.start()
63
+
64
+ def __exit__(self, *exc: object) -> None:
65
+ self.close()
@@ -0,0 +1,30 @@
1
+ """Pinned browser build + release coordinates.
2
+
3
+ The browser is built privately and published to this repo's GitHub Releases.
4
+ `BROWSER_VERSION` is the release tag (without the leading ``v``) the SDK
5
+ downloads by default; override it at runtime with ``CHROMIUMFISH_VERSION``.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+
11
+ # SDK package version. Single source of truth: pyproject.toml reads this via
12
+ # [tool.hatch.version] (dynamic = ["version"]).
13
+ __version__ = "0.1.0"
14
+
15
+ # Default ChromiumFish browser build to fetch. Matches src/chrome/VERSION.
16
+ DEFAULT_BROWSER_VERSION = "150.0.7844"
17
+
18
+ # GitHub repo that hosts the release assets (public; binary built from the
19
+ # private chromiumfish-browser repo).
20
+ RELEASE_REPO = "arman-bd/chromiumfish"
21
+
22
+
23
+ def browser_version() -> str:
24
+ """Resolved browser version (env override wins)."""
25
+ return os.environ.get("CHROMIUMFISH_VERSION", DEFAULT_BROWSER_VERSION)
26
+
27
+
28
+ def release_base_url(version: str | None = None) -> str:
29
+ version = version or browser_version()
30
+ return f"https://github.com/{RELEASE_REPO}/releases/download/v{version}"