invisible-playwright 0.3.5__py3-none-any.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.
@@ -0,0 +1,68 @@
1
+ """invisible_playwright - Playwright wrapper for a patched Firefox with stealth profile.
2
+
3
+ Quickstart:
4
+
5
+ from invisible_playwright import InvisiblePlaywright
6
+
7
+ with InvisiblePlaywright() as browser: # random seed
8
+ page = browser.new_page()
9
+ page.goto("https://example.com")
10
+
11
+ with InvisiblePlaywright(seed=42) as browser: # deterministic
12
+ ...
13
+
14
+ with InvisiblePlaywright(humanize=True) as browser: # human-like cursor motion
15
+ page = browser.new_page()
16
+ page.click("#submit") # expanded into a Bezier trajectory
17
+ """
18
+ # ── Import-time core assertion, and repair ───────────────────────────────────
19
+ # Runs BEFORE every other import. Two questions, one check: is the installed
20
+ # invisible-core new enough to carry a release seal at all, and is it the exact
21
+ # version this distribution declares in pyproject? The expected version is read
22
+ # back out of our own metadata, so it is written in exactly one place.
23
+ #
24
+ # A mismatch is REPAIRED, not just reported: the declared core is installed and
25
+ # picked up in this process, so the script that just imported us keeps going.
26
+ # That is only sound here, on the first line, while invisible_core has not been
27
+ # imported by anyone yet; _pin.py captures exactly that fact before it imports
28
+ # the core, and the repair refuses itself if the snapshot says otherwise. Every
29
+ # failure of the repair falls back to the message it replaced.
30
+ #
31
+ # The comparison and the repair both live in invisible_core._pin, shared with the
32
+ # profile manager so both products diagnose and fix an environment the same way.
33
+ # Our _pin.py is the floor: it owns the three states a module inside the core
34
+ # cannot report on (core absent, core present but unimportable, core present but
35
+ # too damaged to derive its own version) and delegates everything else.
36
+ from ._pin import enforce_core_pin as _enforce_core_pin
37
+ _enforce_core_pin()
38
+
39
+ from ._engine import assert_playwright_range as _assert_pw_range
40
+ _assert_pw_range()
41
+
42
+ from .config import get_default_args, get_default_stealth_prefs
43
+ from .constants import BINARY_VERSION, FIREFOX_UPSTREAM_VERSION
44
+ from ._geo import GeoTimezoneError, resolve_session_timezone
45
+ from .download import ensure_binary, ensure_geoip_mmdb
46
+ from .launcher import InvisiblePlaywright
47
+
48
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
49
+
50
+ try:
51
+ __version__ = _pkg_version("invisible-playwright")
52
+ except PackageNotFoundError:
53
+ # Editable / source checkout without an install record: fall back to a
54
+ # marker rather than risk shipping a stale hardcoded string.
55
+ __version__ = "0.0.0+unknown"
56
+
57
+ __all__ = [
58
+ "InvisiblePlaywright",
59
+ "ensure_binary",
60
+ "ensure_geoip_mmdb",
61
+ "get_default_stealth_prefs",
62
+ "get_default_args",
63
+ "resolve_session_timezone",
64
+ "GeoTimezoneError",
65
+ "BINARY_VERSION",
66
+ "FIREFOX_UPSTREAM_VERSION",
67
+ "__version__",
68
+ ]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+ import sys
3
+
4
+ sys.exit(main())
@@ -0,0 +1,90 @@
1
+ """Every route from a chosen path to a spawned Firefox goes through here.
2
+
3
+ ``binary_path=`` (and ``INVPW_BINARY_PATH``, which the test suite and the e2e
4
+ scripts turn into ``binary_path=``) never reaches ``ensure_binary()``, so none
5
+ of the download-side checks run on that route. The guard therefore sits on the
6
+ resolved executable, not inside the fetcher.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import warnings
12
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
13
+ from pathlib import Path
14
+ from typing import Optional, Union
15
+
16
+ from invisible_core.download import ensure_binary
17
+ from invisible_core.seal import EngineMismatch, active_seal, verify_engine
18
+
19
+
20
+ def resolve_executable(binary_path: Optional[Union[str, Path]]) -> Path:
21
+ """binary_path= and INVPW_BINARY_PATH skip the download path entirely, so
22
+ the guard sits on the resolved executable, not inside the fetcher."""
23
+ seal = active_seal()
24
+ if binary_path:
25
+ return verify_engine(Path(binary_path), seal, source=f"binary_path={binary_path}")
26
+ return ensure_binary(seal=seal)
27
+
28
+
29
+ def assert_wire_version(browser) -> None:
30
+ """Compare what the engine reports over the protocol with the seal.
31
+
32
+ browser.version is a cached property from the connection initializer
33
+ (Juggler Browser.getInfo -> MOZ_APP_VERSION_DISPLAY), so this costs zero
34
+ round trips and no pref can spoof it. It is the only check that also
35
+ catches a hand-edited application.ini. Not available on the persistent
36
+ context path, where Playwright exposes no Browser.
37
+ """
38
+ if browser is None:
39
+ return
40
+ seal = active_seal()
41
+ raw = getattr(browser, "version", "")
42
+ # Playwright types Browser.version as str. Anything else (a stub, a mock,
43
+ # a driver that did not populate the initializer) carries no evidence
44
+ # either way, and inventing a mismatch out of it would be a false alarm.
45
+ if not isinstance(raw, str):
46
+ return
47
+ reported = raw.split("/")[-1].strip()
48
+ if reported and reported != seal.upstream_version:
49
+ raise EngineMismatch(
50
+ "engine/seal mismatch after launch - the running browser is not the sealed build\n"
51
+ f" protocol says: Firefox {reported}\n"
52
+ f" seal says : Firefox {seal.upstream_version} (tag {seal.tag}, "
53
+ f"build {seal.build_id})\n"
54
+ " why : application.ini can be edited; this value comes from the "
55
+ "running engine itself.\n"
56
+ " fix : python -m invisible_playwright fetch --force")
57
+
58
+
59
+ def assert_playwright_range() -> None:
60
+ """The Juggler protocol schema is closed-world: a client outside the range
61
+ the binary was tested with dies at context creation, or silently loses a
62
+ feature. The range travels in the seal, so widening it does not need a
63
+ wrapper release."""
64
+ seal = active_seal()
65
+ if not seal.playwright_min or not seal.playwright_max:
66
+ return
67
+ try:
68
+ have = _pkg_version("playwright")
69
+ except PackageNotFoundError:
70
+ return
71
+
72
+ def t(v: str):
73
+ out = []
74
+ for part in v.split(".")[:3]:
75
+ digits = "".join(c for c in part if c.isdigit())
76
+ out.append(int(digits or 0))
77
+ return tuple(out + [0] * (3 - len(out)))
78
+
79
+ if t(seal.playwright_min) <= t(have) <= t(seal.playwright_max):
80
+ return
81
+ msg = (f"playwright {have} is outside the range this engine was tested with "
82
+ f"({seal.playwright_min} .. {seal.playwright_max}, from the seal for {seal.tag}).\n"
83
+ f"The Juggler protocol schema is closed-world: an untested client can kill every "
84
+ f"session at context creation.\n"
85
+ f'Run: pip install "playwright=={seal.playwright_max}" '
86
+ f"(or set INVISIBLE_PLAYWRIGHT_SKEW=allow to proceed anyway)")
87
+ if os.environ.get("INVISIBLE_PLAYWRIGHT_SKEW") == "allow":
88
+ warnings.warn(msg, RuntimeWarning, stacklevel=2)
89
+ else:
90
+ raise RuntimeError(msg)
@@ -0,0 +1,17 @@
1
+ """Backward-compat shim - spostato in invisible_core._fpforge (alias completo).
2
+
3
+ Aliasa il package E i suoi submodule agli stessi oggetti del core, cosi'
4
+ ``from invisible_playwright._fpforge.profile import Profile`` e
5
+ ``from invisible_core._fpforge.profile import Profile`` sono la STESSA classe
6
+ (isinstance funziona) e i nomi privati restano accessibili.
7
+ """
8
+ import sys as _sys
9
+ import invisible_core._fpforge as _pkg
10
+ from invisible_core._fpforge import profile as _profile
11
+ from invisible_core._fpforge import _sampler as _sampler_mod
12
+ from invisible_core._fpforge import _network as _network_mod
13
+
14
+ _sys.modules[__name__] = _pkg
15
+ _sys.modules[__name__ + ".profile"] = _profile
16
+ _sys.modules[__name__ + "._sampler"] = _sampler_mod
17
+ _sys.modules[__name__ + "._network"] = _network_mod
@@ -0,0 +1,4 @@
1
+ """Backward-compat shim - spostato in invisible_core._geo (alias completo)."""
2
+ import sys as _sys
3
+ from invisible_core import _geo as _mod
4
+ _sys.modules[__name__] = _mod
@@ -0,0 +1,4 @@
1
+ """Backward-compat shim - spostato in invisible_core._headless (alias completo)."""
2
+ import sys as _sys
3
+ from invisible_core import _headless as _mod
4
+ _sys.modules[__name__] = _mod
@@ -0,0 +1,143 @@
1
+ """Prelude for the core-pin assertion, then delegation to the core.
2
+
3
+ THE COMPARISON IS NOT HERE. It lives in invisible_core._pin, because the other
4
+ consumer of the core (the profile manager) asks the same question and two copies
5
+ of the answer drift: the day the two products disagree about why an environment
6
+ is broken, the check has stopped being a diagnosis. This module is the small
7
+ part that genuinely cannot be delegated, plus name-bound wrappers so the rest of
8
+ this package can keep calling the functions without repeating our own
9
+ distribution name.
10
+
11
+ WHAT CANNOT BE DELEGATED. A module inside invisible_core cannot report that
12
+ invisible_core is absent, unimportable, or so damaged that it cannot derive its
13
+ own version: by the time any line of it runs, it imported. So the import below
14
+ is the whole floor, and its except branches classify the failure:
15
+
16
+ * ImportError naming a module under invisible_core - either the core is not
17
+ installed at all, or it is older than this check and ships no
18
+ invisible_core._pin (or no invisible_core._pin.enforce_core_pin). Those
19
+ cannot be told apart from here and do not need to be: one remedy fixes all
20
+ of them, `pip install --force-reinstall invisible-playwright`, which makes
21
+ pip resolve the core version from our own Requires-Dist. No version literal
22
+ in the message, so nothing here can drift out of step with pyproject.
23
+ * ImportError naming any other module - the core is installed and one of ITS
24
+ dependencies is missing. That is what `pip install --no-deps` leaves behind,
25
+ and reporting it as "invisible-core is not installed" sends the user to the
26
+ wrong package.
27
+ * anything else - the core is on disk and its import raised something that is
28
+ not an ImportError at all. Measured: delete invisible_core/seal.json and the
29
+ import dies with a raw FileNotFoundError naming an absolute path, because
30
+ invisible_core.__init__ reaches _version.py, which reads the seal. That is a
31
+ partial install, and without this branch it is the one broken state the user
32
+ sees as a traceback instead of a remedy.
33
+
34
+ Both message sets are deliberately short and carry no version number: everything
35
+ that needs one is on the other side of this import.
36
+
37
+ WHY THE REPAIR IS NOT ATTEMPTED IN THIS FILE. enforce_core_pin() repairs a
38
+ version mismatch by installing the declared version and picking it up in
39
+ process. It can only do that because it knows WHICH version is declared, and
40
+ that comes from the one requirement parser, which lives in the core. When the
41
+ core cannot be imported at all there is no parser to ask, and the only remedy
42
+ that needs no version literal is a force-reinstall of THIS distribution, i.e. of
43
+ the package that is halfway through importing right now. Replacing your own
44
+ files mid-import is not a repair, so this floor reports and stops.
45
+ """
46
+ from __future__ import annotations
47
+
48
+ import sys as _sys
49
+
50
+ DIST_NAME = "invisible-playwright"
51
+
52
+ # Captured BEFORE the core is imported, and it is the whole soundness argument
53
+ # for the in-process repair: if nothing has imported invisible_core by the time
54
+ # this package's __init__ starts, then nobody is holding an object from the
55
+ # version the repair is about to replace. Read here, at the top of the first
56
+ # module the package imports, because one line later it is always True.
57
+ _CORE_PREIMPORTED = any(
58
+ name == "invisible_core" or name.startswith("invisible_core.")
59
+ for name in list(_sys.modules)
60
+ )
61
+
62
+ try: # the floor - see the module docstring for why this is the one local part
63
+ from invisible_core._pin import ( # noqa: F401
64
+ CORE_NAME,
65
+ AUTOFIX_ENV,
66
+ SKIP_ENV,
67
+ PinDeclaration,
68
+ Requirement,
69
+ assert_core_pin as _assert_core_pin,
70
+ canonical_requirement,
71
+ declared_core_pin as _declared_core_pin,
72
+ editable_core_path,
73
+ enforce_core_pin as _enforce_core_pin,
74
+ installed_core_version,
75
+ normalise_name,
76
+ parse_requirement,
77
+ pin_declaration as _pin_declaration,
78
+ pin_from_requirements,
79
+ pin_problem,
80
+ pin_report as _pin_report,
81
+ recorded_core_version,
82
+ repair_core,
83
+ )
84
+ except ImportError as _e:
85
+ _missing = getattr(_e, "name", "") or ""
86
+ if _missing and not _missing.startswith("invisible_core"):
87
+ raise ImportError(
88
+ f"invisible-core is installed but cannot be imported: no module named "
89
+ f"{_missing!r}.\n"
90
+ f"That is one of its own dependencies, so it was installed without "
91
+ f"dependency resolution (pip install --no-deps, or a lockfile that "
92
+ f"lists the packages separately).\n"
93
+ f"Run: pip install --force-reinstall {DIST_NAME}"
94
+ ) from _e
95
+ raise ImportError(
96
+ "invisible-core is missing, or too old for this invisible-playwright: it "
97
+ "does not carry the release-pin check (invisible_core._pin), so the engine "
98
+ "it downloads cannot be matched against the configuration this package "
99
+ "ships.\n"
100
+ f"Run: pip install --force-reinstall {DIST_NAME}"
101
+ ) from _e
102
+ except Exception as _e:
103
+ raise ImportError(
104
+ f"invisible-core is installed but it cannot even determine its own version: "
105
+ f"importing it raised {type(_e).__name__}: {_e}\n"
106
+ f"That is a partial or damaged installation, not an old one. A missing or "
107
+ f"unreadable invisible_core/seal.json is the usual cause, and it is read "
108
+ f"while invisible_core is still importing, so nothing inside the core can "
109
+ f"report it.\n"
110
+ f"Run: pip install --force-reinstall {DIST_NAME}"
111
+ ) from _e
112
+
113
+
114
+ def declared_core_pin(dist_name: str = DIST_NAME) -> "str | None":
115
+ """The `invisible-core==X` version THIS distribution declares, or None."""
116
+ return _declared_core_pin(dist_name)
117
+
118
+
119
+ def pin_declaration(dist_name: str = DIST_NAME) -> PinDeclaration:
120
+ """The declaration and why it is or is not checkable."""
121
+ return _pin_declaration(dist_name)
122
+
123
+
124
+ def pin_report(dist_name: str = DIST_NAME) -> dict:
125
+ """Non-raising form. Read ["verdict"], not ["ok"], to report health."""
126
+ return _pin_report(dist_name)
127
+
128
+
129
+ def assert_core_pin(dist_name: str = DIST_NAME) -> None:
130
+ """Report-only form: raise ImportError when the core that will run is not the
131
+ one declared. Installs nothing. Used by the CLI and the tests."""
132
+ _assert_core_pin(dist_name)
133
+
134
+
135
+ def enforce_core_pin(dist_name: str = DIST_NAME, *, stream=None) -> None:
136
+ """Import-time form: check, and repair the environment instead of only
137
+ reporting it. Raises ImportError only when it is still wrong afterwards.
138
+
139
+ The sys.modules snapshot taken at the top of this module is passed through:
140
+ it is the only evidence that no caller is already holding a core object, and
141
+ it stops being available the moment anything imports the core.
142
+ """
143
+ _enforce_core_pin(dist_name, core_preimported=_CORE_PREIMPORTED, stream=stream)
@@ -0,0 +1,4 @@
1
+ """Backward-compat shim - spostato in invisible_core._proxy (alias completo)."""
2
+ import sys as _sys
3
+ from invisible_core import _proxy as _mod
4
+ _sys.modules[__name__] = _mod