gecko-web-runtime-client 1.0.0__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,17 @@
1
+ """Portable Python client for the Firefox-derived WPR runtime bundle."""
2
+
3
+ from .session import (
4
+ AsyncGeckoWorkerSession,
5
+ GeckoWorkerSession,
6
+ RuntimeConfig,
7
+ WprProtocolError,
8
+ )
9
+ from .installer import install_runtime
10
+
11
+ __all__ = [
12
+ "AsyncGeckoWorkerSession",
13
+ "GeckoWorkerSession",
14
+ "RuntimeConfig",
15
+ "WprProtocolError",
16
+ "install_runtime",
17
+ ]
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from .installer import install_runtime
6
+
7
+
8
+ def main() -> int:
9
+ parser = argparse.ArgumentParser(prog="python -m gecko_web_runtime")
10
+ commands = parser.add_subparsers(dest="command", required=True)
11
+ install = commands.add_parser("install-runtime", help="download a pinned Gecko runtime")
12
+ install.add_argument("--version", required=True, help="runtime major version, e.g. 155")
13
+ install.add_argument("--runtime-home", help="optional installation directory")
14
+ install.add_argument("--force", action="store_true", help="replace an existing installation")
15
+ args = parser.parse_args()
16
+ if args.command == "install-runtime":
17
+ location = install_runtime(args.version, args.runtime_home, force=args.force)
18
+ print(location)
19
+ return 0
20
+
21
+
22
+ if __name__ == "__main__":
23
+ raise SystemExit(main())
@@ -0,0 +1,187 @@
1
+ """Download and install a pinned WPR runtime release."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import tempfile
11
+ import urllib.request
12
+ import uuid
13
+ import zipfile
14
+ from pathlib import Path
15
+
16
+
17
+ RUNTIME_RELEASES = {
18
+ "155": {
19
+ "version": "155.0.3",
20
+ "repository": "childclm/gecko-web-runtime",
21
+ "tag": "v155.0.3",
22
+ "asset": "wpr-runtime-155-win64.zip",
23
+ "url": "https://github.com/childclm/gecko-web-runtime/releases/download/v155.0.3/wpr-runtime-155-win64.zip",
24
+ "sha256": "c8321d3eeed0dfa0978c70c84ad57126c0a131f825aa0a475db5f6ae7c178f7d",
25
+ }
26
+ }
27
+
28
+
29
+ def _github_token() -> str | None:
30
+ for name in ("WPR_GITHUB_TOKEN", "GITHUB_TOKEN", "GH_TOKEN"):
31
+ value = os.environ.get(name)
32
+ if value:
33
+ return value.strip()
34
+ # Git Credential Manager may already hold the user's HTTPS credential.
35
+ try:
36
+ query = "protocol=https\nhost=github.com\n\n"
37
+ result = subprocess.run(
38
+ ["git", "credential", "fill"],
39
+ input=query,
40
+ text=True,
41
+ capture_output=True,
42
+ timeout=10,
43
+ check=True,
44
+ )
45
+ for line in result.stdout.splitlines():
46
+ if line.startswith("password=") and line[9:]:
47
+ return line[9:].strip()
48
+ except (OSError, subprocess.SubprocessError):
49
+ pass
50
+ return None
51
+
52
+
53
+ def _github_asset_request(release: dict[str, str]) -> urllib.request.Request:
54
+ token = _github_token()
55
+ if not token:
56
+ return urllib.request.Request(
57
+ release["url"],
58
+ headers={"User-Agent": "gecko-web-runtime-client/1.0.0"},
59
+ )
60
+
61
+ api = (
62
+ f"https://api.github.com/repos/{release['repository']}"
63
+ f"/releases/tags/{release['tag']}"
64
+ )
65
+ metadata_request = urllib.request.Request(
66
+ api,
67
+ headers={
68
+ "Accept": "application/vnd.github+json",
69
+ "Authorization": f"Bearer {token}",
70
+ "User-Agent": "gecko-web-runtime-client/1.0.0",
71
+ "X-GitHub-Api-Version": "2022-11-28",
72
+ },
73
+ )
74
+ with urllib.request.urlopen(metadata_request, timeout=30) as response:
75
+ metadata = json.load(response)
76
+ asset = next(
77
+ (item for item in metadata.get("assets", []) if item.get("name") == release["asset"]),
78
+ None,
79
+ )
80
+ if not asset:
81
+ raise RuntimeError(f"GitHub release asset not found: {release['asset']}")
82
+ return urllib.request.Request(
83
+ f"https://api.github.com/repos/{release['repository']}"
84
+ f"/releases/assets/{asset['id']}",
85
+ headers={
86
+ "Accept": "application/octet-stream",
87
+ "Authorization": f"Bearer {token}",
88
+ "User-Agent": "gecko-web-runtime-client/1.0.0",
89
+ "X-GitHub-Api-Version": "2022-11-28",
90
+ },
91
+ )
92
+
93
+
94
+ def config_path() -> Path:
95
+ base = os.environ.get("LOCALAPPDATA")
96
+ if not base:
97
+ base = str(Path.home() / "AppData" / "Local")
98
+ return Path(base) / "WPR" / "config.json"
99
+
100
+
101
+ def configured_runtime_home() -> Path | None:
102
+ path = config_path()
103
+ try:
104
+ data = json.loads(path.read_text(encoding="utf-8"))
105
+ value = data.get("runtime_home")
106
+ return Path(value).resolve() if value else None
107
+ except (FileNotFoundError, OSError, json.JSONDecodeError):
108
+ return None
109
+
110
+
111
+ def _sha256(path: Path) -> str:
112
+ digest = hashlib.sha256()
113
+ with path.open("rb") as stream:
114
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
115
+ digest.update(chunk)
116
+ return digest.hexdigest()
117
+
118
+
119
+ def _extract_safely(archive: Path, destination: Path) -> None:
120
+ destination.mkdir(parents=True, exist_ok=True)
121
+ root = destination.resolve()
122
+ with zipfile.ZipFile(archive) as source:
123
+ for member in source.infolist():
124
+ target = (destination / member.filename).resolve()
125
+ if target != root and root not in target.parents:
126
+ raise RuntimeError(f"unsafe path in runtime archive: {member.filename}")
127
+ source.extractall(destination)
128
+
129
+
130
+ def install_runtime(version: str, runtime_home: str | os.PathLike[str] | None = None,
131
+ *, force: bool = False) -> Path:
132
+ """Install a pinned runtime and persist its location for the client."""
133
+ if os.name != "nt":
134
+ raise OSError("wpr-runtime-155 requires Windows")
135
+ key = str(version).split(".", 1)[0]
136
+ release = RUNTIME_RELEASES.get(key)
137
+ if release is None:
138
+ raise ValueError(f"unsupported runtime version: {version}")
139
+
140
+ if runtime_home is None:
141
+ local = os.environ.get("LOCALAPPDATA") or str(Path.home() / "AppData" / "Local")
142
+ destination = Path(local) / "WPR" / "runtime" / key
143
+ else:
144
+ destination = Path(runtime_home)
145
+ destination = destination.expanduser().resolve()
146
+
147
+ if destination.exists() and not force:
148
+ worker = destination / "runtime" / "gecko-web-worker.exe"
149
+ if worker.is_file():
150
+ _write_config(destination)
151
+ return destination
152
+ raise FileExistsError(f"runtime destination already exists: {destination}")
153
+
154
+ destination.parent.mkdir(parents=True, exist_ok=True)
155
+ stage = destination.parent / f".{destination.name}.staging-{uuid.uuid4().hex}"
156
+ try:
157
+ with tempfile.TemporaryDirectory(prefix="wpr-runtime-download-") as temp:
158
+ archive = Path(temp) / "runtime.zip"
159
+ request = _github_asset_request(release)
160
+ with urllib.request.urlopen(request, timeout=120) as response, archive.open("wb") as output:
161
+ shutil.copyfileobj(response, output)
162
+ actual = _sha256(archive)
163
+ if actual != release["sha256"]:
164
+ raise RuntimeError(
165
+ f"runtime SHA256 mismatch: expected {release['sha256']}, got {actual}"
166
+ )
167
+ _extract_safely(archive, stage)
168
+
169
+ if not (stage / "runtime" / "gecko-web-worker.exe").is_file():
170
+ raise RuntimeError("runtime archive does not contain runtime/gecko-web-worker.exe")
171
+ if destination.exists():
172
+ shutil.rmtree(destination)
173
+ stage.replace(destination)
174
+ _write_config(destination)
175
+ return destination
176
+ finally:
177
+ if stage.exists():
178
+ shutil.rmtree(stage, ignore_errors=True)
179
+
180
+
181
+ def _write_config(destination: Path) -> None:
182
+ path = config_path()
183
+ path.parent.mkdir(parents=True, exist_ok=True)
184
+ path.write_text(
185
+ json.dumps({"runtime_home": str(destination)}, indent=2) + "\n",
186
+ encoding="utf-8",
187
+ )
@@ -0,0 +1,52 @@
1
+ """Windows named-pipe transport for the public WPR JSON protocol."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import json
7
+ import msvcrt
8
+ import os
9
+ import time
10
+ from typing import Any
11
+
12
+
13
+ KERNEL32 = ctypes.WinDLL("kernel32", use_last_error=True)
14
+ GENERIC_READ = 0x80000000
15
+ GENERIC_WRITE = 0x40000000
16
+ OPEN_EXISTING = 3
17
+ INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
18
+
19
+
20
+ def connect_pipe(pipe_name: str, timeout_seconds: float = 60.0):
21
+ deadline = time.monotonic() + timeout_seconds
22
+ while time.monotonic() < deadline:
23
+ handle = KERNEL32.CreateFileW(
24
+ pipe_name,
25
+ GENERIC_READ | GENERIC_WRITE,
26
+ 0,
27
+ None,
28
+ OPEN_EXISTING,
29
+ 0,
30
+ None,
31
+ )
32
+ invalid = {-1, 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF, INVALID_HANDLE_VALUE}
33
+ if int(handle) not in invalid and int(handle) != 0:
34
+ return os.fdopen(msvcrt.open_osfhandle(handle, 0), "r+b", buffering=0)
35
+ KERNEL32.WaitNamedPipeW(pipe_name, 1000)
36
+ raise TimeoutError(f"named pipe did not become available: {pipe_name}")
37
+
38
+
39
+ def request(payload: dict[str, Any], pipe_name: str) -> dict[str, Any]:
40
+ """Send one JSON request. The Worker creates one pipe instance per RPC."""
41
+ pipe = connect_pipe(pipe_name)
42
+ try:
43
+ pipe.write((json.dumps(payload, separators=(",", ":")) + "\n").encode())
44
+ line = pipe.readline()
45
+ if not line:
46
+ raise RuntimeError("gecko worker closed the RPC pipe")
47
+ value = json.loads(line)
48
+ if not isinstance(value, dict):
49
+ raise RuntimeError("invalid WPR RPC envelope")
50
+ return value
51
+ finally:
52
+ pipe.close()
@@ -0,0 +1,281 @@
1
+ """Relocatable sync/async client for one real Gecko Worker session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ import uuid
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from .pipe import request
16
+ from .installer import configured_runtime_home
17
+
18
+
19
+ PACKAGE_ROOT = Path(__file__).resolve().parents[2]
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class RuntimeConfig:
24
+ """Runtime paths resolved from the bundle, not from a developer checkout."""
25
+
26
+ package_root: Path
27
+ runtime_dir: Path
28
+ worker_exe: Path
29
+ profile_root: Path | None = None
30
+ log_root: Path | None = None
31
+
32
+ @classmethod
33
+ def bundled(cls) -> "RuntimeConfig":
34
+ configured = configured_runtime_home()
35
+ root = Path(
36
+ os.environ.get("WPR_RUNTIME_HOME")
37
+ or (str(configured) if configured else str(PACKAGE_ROOT))
38
+ ).resolve()
39
+ runtime = Path(os.environ.get("WPR_RUNTIME_DIR", root / "runtime")).resolve()
40
+ worker = Path(
41
+ os.environ.get("WPR_WORKER_EXE", runtime / "gecko-web-worker.exe")
42
+ ).resolve()
43
+ profile = os.environ.get("WPR_PROFILE_ROOT")
44
+ logs = os.environ.get("WPR_LOG_ROOT")
45
+ return cls(
46
+ package_root=root,
47
+ runtime_dir=runtime,
48
+ worker_exe=worker,
49
+ profile_root=Path(profile).resolve() if profile else None,
50
+ log_root=Path(logs).resolve() if logs else None,
51
+ )
52
+
53
+ def validate(self) -> None:
54
+ if os.name != "nt":
55
+ raise OSError("wpr-runtime-155-win64 requires Windows")
56
+ if not self.worker_exe.is_file():
57
+ raise FileNotFoundError(f"Worker executable not found: {self.worker_exe}")
58
+
59
+
60
+ class WprProtocolError(RuntimeError):
61
+ def __init__(self, method: str, response: dict[str, Any]):
62
+ error = response.get("error")
63
+ if isinstance(error, dict):
64
+ self.code = str(error.get("code", "UNKNOWN_ERROR"))
65
+ self.phase = str(error.get("phase", "unknown"))
66
+ message = str(error.get("message", self.code))
67
+ else:
68
+ self.code = "UNKNOWN_ERROR"
69
+ self.phase = "unknown"
70
+ message = str(error or "request failed")
71
+ self.method = method
72
+ self.response = response
73
+ super().__init__(f"{method} [{self.code}] {message}")
74
+
75
+
76
+ def require_ok(method: str, response: dict[str, Any]) -> Any:
77
+ if not response.get("ok"):
78
+ raise WprProtocolError(method, response)
79
+ return response.get("value") or {}
80
+
81
+
82
+ class GeckoWorkerSession:
83
+ """One Worker process and one BrowsingContext/CookieStore session."""
84
+
85
+ def __init__(self, config: RuntimeConfig | None = None):
86
+ self.config = config or RuntimeConfig.bundled()
87
+ self.config.validate()
88
+ self.session_id = uuid.uuid4().hex[:12]
89
+ self.pipe_name = rf"\\.\pipe\wpr-gecko-{self.session_id}"
90
+ profile_parent = self.config.profile_root
91
+ if profile_parent:
92
+ profile_parent.mkdir(parents=True, exist_ok=True)
93
+ else:
94
+ profile_parent = Path(tempfile.gettempdir())
95
+ self.profile_dir = Path(
96
+ tempfile.mkdtemp(prefix=f"wpr-profile-{self.session_id}-", dir=profile_parent)
97
+ )
98
+ (self.profile_dir / "local").mkdir(parents=True, exist_ok=True)
99
+ log_root = self.config.log_root or Path(tempfile.gettempdir()) / "wpr-runtime-logs"
100
+ log_root.mkdir(parents=True, exist_ok=True)
101
+ self.stdout = (log_root / f"wpr-{self.session_id}.stdout.log").open(
102
+ "w", encoding="utf-8", errors="replace"
103
+ )
104
+ self.stderr = (log_root / f"wpr-{self.session_id}.stderr.log").open(
105
+ "w", encoding="utf-8", errors="replace"
106
+ )
107
+ env = os.environ.copy()
108
+ env.update(
109
+ {
110
+ "WPR_RPC": "1",
111
+ "WPR_REMOTE_PAGE": "1",
112
+ "WPR_PIPE_NAME": self.pipe_name,
113
+ "WPR_PROFILE_DIR": str(self.profile_dir),
114
+ }
115
+ )
116
+ env["PATH"] = str(self.config.runtime_dir) + os.pathsep + env.get("PATH", "")
117
+ self.process = subprocess.Popen(
118
+ [str(self.config.worker_exe)],
119
+ cwd=str(self.config.runtime_dir),
120
+ env=env,
121
+ stdout=self.stdout,
122
+ stderr=self.stderr,
123
+ )
124
+ self.closed = False
125
+
126
+ def request(self, payload: dict[str, Any]) -> dict[str, Any]:
127
+ if self.closed:
128
+ raise RuntimeError("GeckoWorkerSession is closed")
129
+ return request(payload, self.pipe_name)
130
+
131
+ def page_create(self) -> dict[str, Any]:
132
+ return self.request({"method": "page.create"})
133
+
134
+ def page_navigate(self, url: str) -> dict[str, Any]:
135
+ return self.request({"method": "page.navigate", "url": url})
136
+
137
+ def page_eval(self, source: str, args: Any = None) -> Any:
138
+ payload: dict[str, Any] = {"method": "page.eval", "source": source}
139
+ if args is not None:
140
+ payload["args"] = args
141
+ return require_ok("page.eval", self.request(payload))
142
+
143
+ def controlled_navigation_begin(self, url: str) -> Any:
144
+ return require_ok(
145
+ "controlled_navigation_begin",
146
+ self.request({"method": "controlled_navigation_begin", "url": url}),
147
+ )
148
+
149
+ def navigation_allow_scripts(self, transaction_id: str, scripts: list[str]) -> Any:
150
+ return require_ok(
151
+ "navigation.allow_scripts",
152
+ self.request(
153
+ {
154
+ "method": "navigation.allow_scripts",
155
+ "transaction_id": transaction_id,
156
+ "scripts": scripts,
157
+ }
158
+ ),
159
+ )
160
+
161
+ def navigation_commit(self, transaction_id: str) -> Any:
162
+ return require_ok(
163
+ "navigation.commit",
164
+ self.request({"method": "navigation.commit", "transaction_id": transaction_id}),
165
+ )
166
+
167
+ def navigation_wait(self, transaction_id: str) -> Any:
168
+ return require_ok(
169
+ "navigation.wait",
170
+ self.request({"method": "navigation.wait", "transaction_id": transaction_id}),
171
+ )
172
+
173
+ def page_cookie_snapshot(self) -> Any:
174
+ return self.page_eval(
175
+ "(() => ({url: location.href, cookie: document.cookie, "
176
+ "readyState: document.readyState}))()"
177
+ )
178
+
179
+ def page_close(self) -> dict[str, Any]:
180
+ return self.request({"method": "page.close"})
181
+
182
+ def close(self) -> None:
183
+ if self.closed:
184
+ return
185
+ try:
186
+ if self.process.poll() is None:
187
+ try:
188
+ self.request({"method": "runtime.shutdown"})
189
+ except Exception:
190
+ pass
191
+ try:
192
+ self.process.wait(timeout=15)
193
+ except subprocess.TimeoutExpired:
194
+ self.process.kill()
195
+ self.process.wait(timeout=5)
196
+ finally:
197
+ self.closed = True
198
+ self.stdout.close()
199
+ self.stderr.close()
200
+ shutil.rmtree(self.profile_dir, ignore_errors=True)
201
+
202
+ def __enter__(self) -> "GeckoWorkerSession":
203
+ return self
204
+
205
+ def __exit__(self, exc_type, exc, tb) -> None:
206
+ self.close()
207
+
208
+
209
+ class AsyncGeckoWorkerSession:
210
+ """Async facade; DOM/JS still run only on Gecko's owner thread."""
211
+
212
+ def __init__(self, config: RuntimeConfig | None = None):
213
+ self.config = config
214
+ self._sync: GeckoWorkerSession | None = None
215
+ self._lock = asyncio.Lock()
216
+
217
+ async def __aenter__(self) -> "AsyncGeckoWorkerSession":
218
+ self._sync = await asyncio.to_thread(GeckoWorkerSession, self.config)
219
+ return self
220
+
221
+ async def request(self, payload: dict[str, Any]) -> dict[str, Any]:
222
+ if self._sync is None:
223
+ raise RuntimeError("AsyncGeckoWorkerSession is not open")
224
+ async with self._lock:
225
+ return await asyncio.to_thread(self._sync.request, payload)
226
+
227
+ async def page_create(self) -> dict[str, Any]:
228
+ return await self.request({"method": "page.create"})
229
+
230
+ async def page_navigate(self, url: str) -> dict[str, Any]:
231
+ return await self.request({"method": "page.navigate", "url": url})
232
+
233
+ async def page_eval(self, source: str, args: Any = None) -> Any:
234
+ payload: dict[str, Any] = {"method": "page.eval", "source": source}
235
+ if args is not None:
236
+ payload["args"] = args
237
+ return require_ok("page.eval", await self.request(payload))
238
+
239
+ async def controlled_navigation_begin(self, url: str) -> Any:
240
+ return require_ok(
241
+ "controlled_navigation_begin",
242
+ await self.request({"method": "controlled_navigation_begin", "url": url}),
243
+ )
244
+
245
+ async def navigation_allow_scripts(self, transaction_id: str, scripts: list[str]) -> Any:
246
+ return require_ok(
247
+ "navigation.allow_scripts",
248
+ await self.request(
249
+ {
250
+ "method": "navigation.allow_scripts",
251
+ "transaction_id": transaction_id,
252
+ "scripts": scripts,
253
+ }
254
+ ),
255
+ )
256
+
257
+ async def navigation_commit(self, transaction_id: str) -> Any:
258
+ return require_ok(
259
+ "navigation.commit",
260
+ await self.request({"method": "navigation.commit", "transaction_id": transaction_id}),
261
+ )
262
+
263
+ async def navigation_wait(self, transaction_id: str) -> Any:
264
+ return require_ok(
265
+ "navigation.wait",
266
+ await self.request({"method": "navigation.wait", "transaction_id": transaction_id}),
267
+ )
268
+
269
+ async def page_cookie_snapshot(self) -> Any:
270
+ return await self.page_eval(
271
+ "(() => ({url: location.href, cookie: document.cookie, "
272
+ "readyState: document.readyState}))()"
273
+ )
274
+
275
+ async def page_close(self) -> dict[str, Any]:
276
+ return await self.request({"method": "page.close"})
277
+
278
+ async def __aexit__(self, exc_type, exc, tb) -> None:
279
+ if self._sync is not None:
280
+ await asyncio.to_thread(self._sync.close)
281
+ self._sync = None
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: gecko-web-runtime-client
3
+ Version: 1.0.0
4
+ Summary: Thin Python client for the Firefox-derived WPR Gecko Worker protocol
5
+ Author: WPR project
6
+ License: MPL-2.0-compatible client; see the runtime bundle notices
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Python client
11
+
12
+ The client uses only Python's standard library. From this directory:
13
+
14
+ ```powershell
15
+ $env:PYTHONPATH = "$PWD"
16
+ python ..\examples\normal_navigation.py
17
+ ```
18
+
19
+ The package resolves `runtime/gecko-web-worker.exe` relative to its own
20
+ bundle. Override paths with `WPR_RUNTIME_HOME`, `WPR_RUNTIME_DIR`,
21
+ `WPR_WORKER_EXE`, `WPR_PROFILE_ROOT`, or `WPR_LOG_ROOT` when embedding it in a
22
+ larger application.
23
+
24
+ To install the pinned runtime automatically from the GitHub Release:
25
+
26
+ ```powershell
27
+ python -m gecko_web_runtime install-runtime --version 155
28
+ ```
29
+
30
+ The command verifies the pinned SHA256 and stores the selected runtime under
31
+ `%LOCALAPPDATA%\WPR\runtime\155`.
32
+
33
+ For a private GitHub Release, set `GITHUB_TOKEN`, `GH_TOKEN`, or
34
+ `WPR_GITHUB_TOKEN`, or configure Git Credential Manager for the GitHub account.
@@ -0,0 +1,9 @@
1
+ gecko_web_runtime/__init__.py,sha256=hkHerbBDOqtedS3xHYPWvgBwOPf5V-aRl-A5q1LojJc,372
2
+ gecko_web_runtime/__main__.py,sha256=1crNdy71fYiQfTqH0FV-5SzlRftCQEO9rgtoUDMkT8g,875
3
+ gecko_web_runtime/installer.py,sha256=htGjrYYzka8CTI51JPAEGNKdMXapsB-0-y0V_K2egbQ,6579
4
+ gecko_web_runtime/pipe.py,sha256=DApeCdA0qa1iEvmPb_v9u5E-lXj4mJnXzIYbF_mWyew,1663
5
+ gecko_web_runtime/session.py,sha256=2kPoqyNzK2ffy3HI8DwSHYY32DY8DIQqab5felnzxe0,10104
6
+ gecko_web_runtime_client-1.0.0.dist-info/METADATA,sha256=DcutF_FYVsJJlyI5bruBAsklymChLAuRdSsrW4tsJhE,1146
7
+ gecko_web_runtime_client-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ gecko_web_runtime_client-1.0.0.dist-info/top_level.txt,sha256=oSRLteQmBqmmSldH5FG_meNE87A7DortZyec99kekIs,18
9
+ gecko_web_runtime_client-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ gecko_web_runtime