Metadata-Version: 2.4
Name: gecko-web-runtime-client
Version: 1.1.0
Summary: Thin Python client for the Firefox-derived WPR Gecko Worker protocol
Author: WPR project
License: MPL-2.0-compatible client; see the runtime bundle notices
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Provides-Extra: diagnostics
Requires-Dist: loguru>=0.7.2; extra == "diagnostics"

# Python client

The client provides a standard-library core plus an optional Loguru-backed
diagnostics layer. From this directory:

```powershell
$env:PYTHONPATH = "$PWD"
python ..\examples\normal_navigation.py
```

The package resolves `runtime/gecko-web-worker.exe` relative to its own
bundle. Override paths with `WPR_RUNTIME_HOME`, `WPR_RUNTIME_DIR`,
`WPR_WORKER_EXE`, `WPR_PROFILE_ROOT`, or `WPR_LOG_ROOT` when embedding it in a
larger application.

## Firefox Necko network client

The version-neutral `wpr-http-v1` API is exposed by
`gecko_web_runtime.AsyncWprHttpClient`:

```python
from gecko_web_runtime import AsyncWprHttpClient

async with AsyncWprHttpClient() as client:
    response = await client.get("https://example.com/")
    html = await response.atext()
```

The selected Firefox bundle launches `runtime/net-v155/gecko-net-worker.exe`
(the versioned adapter may be replaced by a later bundle). Its response body
uses shared memory and is released when `atext()`, `aread()`, or `aclose()`
completes. Set `WPR_RUNTIME_HOME`, `WPR_RUNTIME_DIR`, and
`WPR_NETWORK_WORKER_EXE` to select a different Firefox-version adapter.

To install the pinned runtime automatically from the GitHub Release:

```powershell
python -m gecko_web_runtime install-runtime --version 155
```

The command verifies the pinned SHA256 and stores the selected runtime under
`%LOCALAPPDATA%\WPR\runtime\155`.

For development, use `uv` from this directory so the Python environment and
lock file are reproducible:

```powershell
cd G:\wpr-runtime-155-win64\python
uv sync --extra diagnostics
uv run python -m unittest discover -s tests\unit -v
```

The Gecko binary is intentionally not a Python dependency.  Install or select
the matching runtime separately after the environment is ready:

```powershell
uv run python -m gecko_web_runtime install-runtime --version 155
```

Live network workflow tests are opt-in:

```powershell
$env:WPR_RUN_ONLINE_TESTS = "1"
uv run python -m unittest discover -s tests\integration -v
```

To measure cold/warm navigation and the current isolation boundary:

```powershell
uv run python tests\integration\context_reuse_benchmark.py `
  --url https://example.com/ `
  --out context-reuse.json
```

The benchmark records first/second navigation timing, frame remote types,
WindowGlobal replacement, Worker PIDs and whether an isolated context sees a
Cookie from the persistent context.

An optional per-Worker `fpfile` path can be passed to the session. The Worker
reads the file before creating the remote page; its Firefox profile remains a
separate Cookie/Storage directory:

```python
async with AsyncGeckoWorkerSession(
    fpfile=r"G:\browser-runtime\fpfile.txt"
) as session:
    await session.page_create()
```

For application code, prefer the high-level runtime facade and workflow
adapter.  Frame handles, pointer coordinates, WindowGlobal rebinding and
challenge waits remain internal:

```python
from gecko_web_runtime import AsyncGeckoRuntime

async with AsyncGeckoRuntime(fpfile=r"G:\browser-runtime\fpfile.txt") as runtime:
    page = await runtime.new_page()
    result = await page.navigate(
        "https://nopecha.com/demo/cloudflare",
        workflow="turnstile",
    )
    print(result.ok, result.url, len(result.html))
```

For WeChat public-account articles, use the site adapter instead of the
Cloudflare-specific Turnstile workflow:

```python
async with AsyncGeckoRuntime() as runtime:
    page = await runtime.new_page()
    result = await page.navigate(article_url, workflow="wechat_article")
    if result.ok:
        article_html = result.html
```

Use `WorkflowMetrics` when an application needs a stable result/metrics
boundary. The default output contains the product chain stages (`T0`...`T9`)
plus a compact report with success, final URL, HTML size,
total/navigation/final-ready time, and parent/whole-process-tree memory peaks.
Detailed attempts, phase timings, and per-stage resource snapshots are
emitted only with `debug=True` or `emit="debug"`:

```python
from gecko_web_runtime import WorkflowMetrics

metrics = WorkflowMetrics(name="wechat", emit="summary", print_summary=True)
summary = metrics.emit(result)
```

Library code can set `emit="none"` and consume the returned dictionary without
printing. `WPR_METRICS_EMIT`, `WPR_METRICS_LOG_LEVEL`, `WPR_METRICS_DEBUG`,
`WPR_METRICS_PRINT`, and `WPR_METRICS_MEMORY` provide process-level defaults.

The adapter recognizes the WeChat/Tencent gate, rebinds the current top
WindowGlobal, and dispatches a real pointer sequence to the visible
verification target.  It reports slider/image challenges as a bounded result
for a future dedicated solver rather than treating the challenge page as an
article.  It is one-shot by default and closes the Page after the workflow
boundary; pass `WechatArticleWorkflow(close_after=False)` when intentionally
reusing the Page.

For multiple logical sessions, keep the Worker runtime alive but make the
storage boundary explicit:

```python
async with AsyncGeckoRuntime(fpfile="fpfile-a.txt") as runtime:
    async with runtime.new_context(isolation="persistent") as context_a:
        page_a = await context_a.new_page()
        result_a = await page_a.navigate(url_a)

    async with runtime.new_context(isolation="isolated") as context_b:
        page_b = await context_b.new_page()
        result_b = await page_b.navigate(url_b)
```

`persistent` reuses the runtime's current Worker while each Context receives
its native Firefox OriginAttributes boundary (`user_context_id` and optional
`private_browsing_id`).  `isolated` remains the explicit separate Worker and
profile mode for callers that require process-level isolation.

Turnstile results expose phase timings in `result.timing_ms`, including
per-attempt iframe/action waits, POST, redirect, final Document readiness, and
HTML serialization.  Worker PID, Working Set, Private Bytes, and CPU samples
are available in `result.resource_usage`.  Sampling can be disabled for a
low-overhead benchmark with `TurnstileWorkflow(resource_sampling=False)` or
`WPR_RESOURCE_SAMPLING=0`.  The integration test writes the complete result
to the path in `WPR_RESULT_OUT` when set.

Challenge retries use the `adaptive` policy by default.  After a top-level
state transition it waits for the network settle window and stops immediately
on POST/redirect; it only dispatches another action when no submit arrives.
`retry_policy="legacy"` preserves the previous retry loop for A/B experiments,
while `retry_policy="generation"` is a strict diagnostic policy that requires a
new `BrowsingContext`/`WindowGlobal` before another action.  The third-party
client exposes the same switch as `--retry-policy`.  Debug results include
`state_change_to_submit_ms` when both observations were captured on the same
client monotonic clock.

The final boundary is configurable.  `final_wait_until="complete"` waits for
the full DOM/load boundary and is the compatibility default.  Set
`WPR_TURNSTILE_WAIT_UNTIL=document` (or pass `final_wait_until="document"`)
when the caller only needs the final Document HTML; the result then reports
`final_document`, `final_load`, and the selected `final_ready` boundary
separately.  Document mode does not wait for the page's scripts, stylesheets,
images, or analytics resources to finish.

Large results use a separate data plane.  `page.eval_shared_blob()` uses a
one-shot mapping, while `page.eval_shared_ring()`/`page.html_shared_ring()` use
a Worker-lifetime fixed-slot ring.  Both return a small descriptor over the
control pipe and a read-only Windows named mapping for the bytes.  Read it as
a `memoryview` or decode it with `read_text()`, then release the lease
explicitly:

```python
blob = await page.html_shared_ring()
try:
    html = blob.read_text()
finally:
    await blob.release()
```

This keeps multi-megabyte HTML out of JSON serialization and the named-pipe
response.  The ring mapping is created once per Worker and reuses slots using
generation-bearing leases; `data.release` returns a slot to the ring.  Set
`WPR_HTML_TRANSPORT=json`, `blob`, or `ring` to A/B the adapter boundary.  The
WeChat and Turnstile adapters default to `ring` and fall back to ordinary
`page.eval` when an older Worker has no data-plane method.

The long-lived comparison probe keeps one Worker per site/transport, uses a
fresh OriginAttributes context for every sample, and records success rate,
P95, CPU, Worker/process-tree memory, and Gecko reporter stages:

```powershell
uv run python tests\integration\data_plane_ab_benchmark.py `
  --runs 10 --modes json ring
```

The low-level `AsyncGeckoWorkerSession` API remains available for custom
workflows and backward compatibility.  Turnstile is implemented in
`gecko_web_runtime.workflows.turnstile`; future site adapters belong in the
same package and are not part of the Gecko core.

The client project keeps its tests alongside the package source:

```text
python/tests/unit/          # import/API contract tests
python/tests/integration/   # opt-in live Worker/workflow tests
```

The larger `G:\browser-runtime\tests\probes` tree is a separate black-box
runtime audit harness.  It exercises the complete bundled Gecko runtime and
stores diagnostic artifacts, so it is intentionally not included in the
Python wheel.

To enable structured application diagnostics:

```bash
pip install gecko-web-runtime-client[diagnostics]
```

```python
from gecko_web_runtime.diagnostics import configure_logging

logger = configure_logging(log_file="runtime.log")
logger.info("application.start")
```

For a real page WebRTC permission boundary, use the same session and answer
the native pending request.  The Worker forwards Firefox's
`PeerConnection:response:allow` or `PeerConnection:response:deny` topic; it
does not manufacture SDP, ICE candidates, or media devices:

```python
pending = await session.page_permission_requests()
for request in pending:
    await session.page_permission_respond(request["callID"], "allow")
```

Leaving the list unanswered preserves Firefox's normal prompt-pending
behavior.  A response is scoped to the current WindowGlobal, so a navigation
that replaces it requires polling the new page's request list.

For a private GitHub Release, set `GITHUB_TOKEN`, `GH_TOKEN`, or
`WPR_GITHUB_TOKEN`, or configure Git Credential Manager for the GitHub account.
# Firefox Necko network client

The version-neutral `wpr-http-v1` API is exposed by
`gecko_web_runtime.AsyncWprHttpClient`:

```python
from gecko_web_runtime import AsyncWprHttpClient

async with AsyncWprHttpClient() as client:
    response = await client.get("https://example.com/")
    html = await response.atext()
```

The selected Firefox bundle launches `gecko-net-worker.exe`. Its response body
uses shared memory and is released when `atext()`, `aread()`, or `aclose()`
completes. Set `WPR_RUNTIME_HOME`, `WPR_RUNTIME_DIR`, and
`WPR_NETWORK_WORKER_EXE` to select a different Firefox-version adapter.
