wpr-http 0.2.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.
- wpr_http-0.2.0/PKG-INFO +52 -0
- wpr_http-0.2.0/README.md +41 -0
- wpr_http-0.2.0/pyproject.toml +22 -0
- wpr_http-0.2.0/setup.cfg +4 -0
- wpr_http-0.2.0/tests/test_contracts.py +187 -0
- wpr_http-0.2.0/tests/test_native_http.py +124 -0
- wpr_http-0.2.0/wpr_http/__init__.py +60 -0
- wpr_http-0.2.0/wpr_http/__main__.py +28 -0
- wpr_http-0.2.0/wpr_http/_urls.py +31 -0
- wpr_http-0.2.0/wpr_http/client.py +588 -0
- wpr_http-0.2.0/wpr_http/exceptions.py +207 -0
- wpr_http-0.2.0/wpr_http/models.py +331 -0
- wpr_http-0.2.0/wpr_http/transport.py +121 -0
- wpr_http-0.2.0/wpr_http.egg-info/PKG-INFO +52 -0
- wpr_http-0.2.0/wpr_http.egg-info/SOURCES.txt +17 -0
- wpr_http-0.2.0/wpr_http.egg-info/dependency_links.txt +1 -0
- wpr_http-0.2.0/wpr_http.egg-info/entry_points.txt +2 -0
- wpr_http-0.2.0/wpr_http.egg-info/requires.txt +1 -0
- wpr_http-0.2.0/wpr_http.egg-info/top_level.txt +1 -0
wpr_http-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wpr-http
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Standalone HTTP client using version-selected Firefox Necko/NSS Workers
|
|
5
|
+
License: MPL-2.0-compatible client; see runtime bundle notices
|
|
6
|
+
Project-URL: Repository, https://github.com/childclm/wpr-http
|
|
7
|
+
Project-URL: Documentation, https://github.com/childclm/wpr-http/blob/main/README.md
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: wpr-runtime-core<0.3,>=0.2.0
|
|
11
|
+
|
|
12
|
+
# WPR HTTP
|
|
13
|
+
|
|
14
|
+
Standalone HTTP requests using real, version-selected Firefox Necko/NSS network
|
|
15
|
+
Workers. Installing this package does not install the WPR browser client.
|
|
16
|
+
The native runtime is downloaded separately and remains centrally built.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import asyncio
|
|
20
|
+
from wpr_http import AsyncClient
|
|
21
|
+
|
|
22
|
+
async def main():
|
|
23
|
+
async with AsyncClient(firefox_version="155") as client:
|
|
24
|
+
response = await client.get("https://www.baidu.com/")
|
|
25
|
+
async with response:
|
|
26
|
+
response.raise_for_status()
|
|
27
|
+
print(response.status_code, await response.atext())
|
|
28
|
+
|
|
29
|
+
asyncio.run(main())
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`firefox_version` selects a genuine installed Gecko network engine, not a
|
|
33
|
+
User-Agent string or a simulated browser. Only Firefox 155 is currently built
|
|
34
|
+
and supported. Selecting another version raises an explicit compatibility error;
|
|
35
|
+
it never silently uses 155. Different installed engine versions use separate
|
|
36
|
+
runtime/profile locations and separate Worker processes.
|
|
37
|
+
|
|
38
|
+
Runtime installation and supported versions:
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
python -m wpr_http runtimes
|
|
42
|
+
python -m wpr_http install-runtime --version 155
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
An explicit `runtime_home` can select another verified runtime installation.
|
|
46
|
+
Existing `AsyncWprHttpClient`, Request/Response/Timeout and exception names remain
|
|
47
|
+
available. `WprHttpClient` remains the historical asynchronous alias.
|
|
48
|
+
|
|
49
|
+
The native network runtime is a separate download from the private
|
|
50
|
+
`childclm/gecko-web-runtime` Release. Installation needs access to that repository. Set
|
|
51
|
+
`WPR_GITHUB_TOKEN` or `GH_TOKEN`, or use Git Credential Manager, then run
|
|
52
|
+
`python -m wpr_http install-runtime --version 155`.
|
wpr_http-0.2.0/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# WPR HTTP
|
|
2
|
+
|
|
3
|
+
Standalone HTTP requests using real, version-selected Firefox Necko/NSS network
|
|
4
|
+
Workers. Installing this package does not install the WPR browser client.
|
|
5
|
+
The native runtime is downloaded separately and remains centrally built.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import asyncio
|
|
9
|
+
from wpr_http import AsyncClient
|
|
10
|
+
|
|
11
|
+
async def main():
|
|
12
|
+
async with AsyncClient(firefox_version="155") as client:
|
|
13
|
+
response = await client.get("https://www.baidu.com/")
|
|
14
|
+
async with response:
|
|
15
|
+
response.raise_for_status()
|
|
16
|
+
print(response.status_code, await response.atext())
|
|
17
|
+
|
|
18
|
+
asyncio.run(main())
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`firefox_version` selects a genuine installed Gecko network engine, not a
|
|
22
|
+
User-Agent string or a simulated browser. Only Firefox 155 is currently built
|
|
23
|
+
and supported. Selecting another version raises an explicit compatibility error;
|
|
24
|
+
it never silently uses 155. Different installed engine versions use separate
|
|
25
|
+
runtime/profile locations and separate Worker processes.
|
|
26
|
+
|
|
27
|
+
Runtime installation and supported versions:
|
|
28
|
+
|
|
29
|
+
```powershell
|
|
30
|
+
python -m wpr_http runtimes
|
|
31
|
+
python -m wpr_http install-runtime --version 155
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
An explicit `runtime_home` can select another verified runtime installation.
|
|
35
|
+
Existing `AsyncWprHttpClient`, Request/Response/Timeout and exception names remain
|
|
36
|
+
available. `WprHttpClient` remains the historical asynchronous alias.
|
|
37
|
+
|
|
38
|
+
The native network runtime is a separate download from the private
|
|
39
|
+
`childclm/gecko-web-runtime` Release. Installation needs access to that repository. Set
|
|
40
|
+
`WPR_GITHUB_TOKEN` or `GH_TOKEN`, or use Git Credential Manager, then run
|
|
41
|
+
`python -m wpr_http install-runtime --version 155`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wpr-http"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Standalone HTTP client using version-selected Firefox Necko/NSS Workers"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MPL-2.0-compatible client; see runtime bundle notices"}
|
|
12
|
+
dependencies = ["wpr-runtime-core>=0.2.0,<0.3"]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Repository = "https://github.com/childclm/wpr-http"
|
|
16
|
+
Documentation = "https://github.com/childclm/wpr-http/blob/main/README.md"
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
wpr-http = "wpr_http.__main__:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
include = ["wpr_http*"]
|
wpr_http-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from wpr_http import AsyncClient, Request, RuntimeCompatibilityError, Timeout
|
|
8
|
+
from wpr_runtime_core.registry import RuntimeManifest, resolve_runtime
|
|
9
|
+
from wpr_runtime_core.session import AsyncWorkerSession
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def make_runtime(tmp_path, major="155", protocol="wpr-http-v1", abi=None):
|
|
13
|
+
home = tmp_path / major
|
|
14
|
+
worker = home / "runtime" / f"net-v{major}" / "gecko-net-worker.exe"
|
|
15
|
+
worker.parent.mkdir(parents=True)
|
|
16
|
+
worker.write_bytes(b"not-an-executable-fixture")
|
|
17
|
+
relative = worker.relative_to(home).as_posix()
|
|
18
|
+
manifest = {
|
|
19
|
+
"schema": "wpr.runtime-manifest.v1",
|
|
20
|
+
"version": major + ".0.6",
|
|
21
|
+
"platform": "windows-x86_64",
|
|
22
|
+
"protocols": [protocol],
|
|
23
|
+
"network_worker": relative,
|
|
24
|
+
"components": {
|
|
25
|
+
"network": {
|
|
26
|
+
"firefox_major": int(major),
|
|
27
|
+
"protocol": protocol,
|
|
28
|
+
"client_api": abi or {"min": 1, "max": 1},
|
|
29
|
+
"worker": relative,
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": {
|
|
33
|
+
relative: {"sha256": hashlib.sha256(worker.read_bytes()).hexdigest()}
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
(home / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
37
|
+
return home, manifest
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_import_does_not_load_browser():
|
|
41
|
+
subprocess.run(
|
|
42
|
+
[
|
|
43
|
+
sys.executable,
|
|
44
|
+
"-c",
|
|
45
|
+
"import sys,wpr_http; assert not any(n.startswith('gecko_web_runtime') for n in sys.modules)",
|
|
46
|
+
],
|
|
47
|
+
check=True,
|
|
48
|
+
)
|
|
49
|
+
assert not hasattr(AsyncWorkerSession, "page_create")
|
|
50
|
+
assert not hasattr(AsyncWorkerSession, "page_eval")
|
|
51
|
+
assert isinstance(AsyncWorkerSession.is_open, property)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_requests_and_deadlines_are_standalone():
|
|
55
|
+
request = Request(
|
|
56
|
+
"get", "https://example.com", headers={"X-Test": "yes"}, timeout=0.2
|
|
57
|
+
)
|
|
58
|
+
assert request.method == "GET"
|
|
59
|
+
assert request.headers["x-test"] == "yes"
|
|
60
|
+
assert request.timeout == Timeout(total=0.2)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.asyncio
|
|
64
|
+
async def test_response_accepts_a_body_lease_without_a_worker_session():
|
|
65
|
+
from types import SimpleNamespace
|
|
66
|
+
|
|
67
|
+
from wpr_http import Response
|
|
68
|
+
|
|
69
|
+
class MemoryLease:
|
|
70
|
+
descriptor = SimpleNamespace(encoding="utf-8")
|
|
71
|
+
mapping_open_ms = copy_ms = 0.0
|
|
72
|
+
released = False
|
|
73
|
+
|
|
74
|
+
def __init__(self):
|
|
75
|
+
self.release_timings = {}
|
|
76
|
+
|
|
77
|
+
def open(self):
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def read_bytes(self):
|
|
81
|
+
return b"standalone"
|
|
82
|
+
|
|
83
|
+
async def release(self):
|
|
84
|
+
self.released = True
|
|
85
|
+
return {"released": True}
|
|
86
|
+
|
|
87
|
+
lease = MemoryLease()
|
|
88
|
+
response = Response(
|
|
89
|
+
None, {"status": 200, "final_url": "http://fixture/"}, body=lease
|
|
90
|
+
)
|
|
91
|
+
assert await response.atext() == "standalone"
|
|
92
|
+
assert lease.released and response.is_closed
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_selected_runtime_is_actual_manifest_worker(tmp_path):
|
|
96
|
+
home, _ = make_runtime(tmp_path)
|
|
97
|
+
client = AsyncClient(firefox_version=155, runtime_home=home)
|
|
98
|
+
assert client.runtime_manifest.firefox_major == "155"
|
|
99
|
+
assert client.config.runtime_dir == home / "runtime"
|
|
100
|
+
assert client.max_channels_per_worker == 20
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_explicit_future_runtime_is_separate_not_a_published_claim(tmp_path):
|
|
104
|
+
# Fixture only: proves independent selection without claiming a 156 build.
|
|
105
|
+
a, _ = make_runtime(tmp_path, "155")
|
|
106
|
+
b, _ = make_runtime(tmp_path, "156")
|
|
107
|
+
assert (
|
|
108
|
+
resolve_runtime("155", runtime_home=a).network_worker
|
|
109
|
+
!= resolve_runtime("156", runtime_home=b).network_worker
|
|
110
|
+
)
|
|
111
|
+
with pytest.raises(RuntimeCompatibilityError, match="requested Firefox"):
|
|
112
|
+
resolve_runtime("156", runtime_home=a)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_unpublished_version_never_silently_falls_back(monkeypatch):
|
|
116
|
+
monkeypatch.delenv("WPR_RUNTIME_HOME", raising=False)
|
|
117
|
+
with pytest.raises(RuntimeCompatibilityError, match="no published runtime"):
|
|
118
|
+
AsyncClient(firefox_version=999)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_registration_preserves_other_engines_and_exact_builds(tmp_path, monkeypatch):
|
|
122
|
+
from wpr_runtime_core.installer import _write_config, config_path
|
|
123
|
+
|
|
124
|
+
monkeypatch.setenv("LOCALAPPDATA", str(tmp_path / "appdata"))
|
|
125
|
+
monkeypatch.delenv("WPR_RUNTIME_HOME", raising=False)
|
|
126
|
+
a, _ = make_runtime(tmp_path, "155")
|
|
127
|
+
b, _ = make_runtime(tmp_path, "156")
|
|
128
|
+
_write_config(a, product="network")
|
|
129
|
+
_write_config(b, product="network")
|
|
130
|
+
settings = json.loads(config_path().read_text(encoding="utf-8"))
|
|
131
|
+
assert settings["network_runtimes"]["155"] == str(a)
|
|
132
|
+
assert settings["network_runtimes"]["156.0.6"] == str(b)
|
|
133
|
+
assert resolve_runtime("155").home == a
|
|
134
|
+
assert resolve_runtime("156").home == b
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@pytest.mark.parametrize("value", [True, "firefox155", "../155", "", 0])
|
|
138
|
+
def test_invalid_engine_version(value):
|
|
139
|
+
with pytest.raises(RuntimeCompatibilityError):
|
|
140
|
+
resolve_runtime(value)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_exact_build_pin(tmp_path):
|
|
144
|
+
home, _ = make_runtime(tmp_path)
|
|
145
|
+
with pytest.raises(RuntimeCompatibilityError, match="installed runtime"):
|
|
146
|
+
resolve_runtime("155.0.5", runtime_home=home)
|
|
147
|
+
assert resolve_runtime("155.0.6", runtime_home=home).runtime_version == "155.0.6"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_catalog_selects_default_and_exact_build_without_fallback(monkeypatch):
|
|
151
|
+
from copy import deepcopy
|
|
152
|
+
|
|
153
|
+
from wpr_runtime_core import installer
|
|
154
|
+
|
|
155
|
+
catalog = deepcopy(installer.RUNTIME_CATALOG)
|
|
156
|
+
releases = catalog["engines"]["firefox155"]
|
|
157
|
+
older = deepcopy(releases["builds"]["155.0.6"])
|
|
158
|
+
older["version"] = "155.0.5"
|
|
159
|
+
releases["builds"]["155.0.5"] = older
|
|
160
|
+
monkeypatch.setattr(installer, "RUNTIME_CATALOG", catalog)
|
|
161
|
+
assert installer.select_release("155")["version"] == "155.0.6"
|
|
162
|
+
assert installer.select_release("155.0.5")["version"] == "155.0.5"
|
|
163
|
+
with pytest.raises(RuntimeCompatibilityError, match="no published runtime"):
|
|
164
|
+
installer.select_release("155.0.4")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.mark.parametrize("kind", ["protocol", "abi", "hash", "escape"])
|
|
168
|
+
def test_incompatible_runtime_is_rejected_before_launch(tmp_path, kind):
|
|
169
|
+
home, manifest = make_runtime(tmp_path)
|
|
170
|
+
if kind == "protocol":
|
|
171
|
+
manifest["components"]["network"]["protocol"] = "wpr-http-v2"
|
|
172
|
+
elif kind == "abi":
|
|
173
|
+
manifest["components"]["network"]["client_api"] = {"min": 2, "max": 2}
|
|
174
|
+
elif kind == "hash":
|
|
175
|
+
manifest["files"][manifest["network_worker"]]["sha256"] = "0" * 64
|
|
176
|
+
else:
|
|
177
|
+
manifest["components"]["network"]["worker"] = "../outside.exe"
|
|
178
|
+
(home / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
179
|
+
with pytest.raises(RuntimeCompatibilityError):
|
|
180
|
+
RuntimeManifest.load(home, "155")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_legacy_manifest_v1_is_supported(tmp_path):
|
|
184
|
+
home, manifest = make_runtime(tmp_path)
|
|
185
|
+
manifest.pop("components")
|
|
186
|
+
(home / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
187
|
+
assert resolve_runtime("155", runtime_home=home).protocol == "wpr-http-v1"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Optional real-engine tests, independent of the browser client and public sites."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
from wpr_http import AsyncClient, WprHTTPStatusError, WprTimeoutError
|
|
12
|
+
|
|
13
|
+
pytestmark = pytest.mark.skipif(
|
|
14
|
+
os.name != "nt" or not os.environ.get("WPR_TEST_RUNTIME_HOME"),
|
|
15
|
+
reason="requires an explicitly selected installed Windows network runtime",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Api(ThreadingHTTPServer):
|
|
20
|
+
request_queue_size = 128
|
|
21
|
+
active = 0
|
|
22
|
+
peak = 0
|
|
23
|
+
lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
def handle_error(self, *_):
|
|
26
|
+
pass # cancelled/timed-out requests intentionally close their sockets
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Handler(BaseHTTPRequestHandler):
|
|
30
|
+
def log_message(self, *_):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def do_GET(self):
|
|
34
|
+
with self.server.lock:
|
|
35
|
+
self.server.active += 1
|
|
36
|
+
self.server.peak = max(self.server.peak, self.server.active)
|
|
37
|
+
try:
|
|
38
|
+
if self.path.startswith("/slow"):
|
|
39
|
+
time.sleep(0.3)
|
|
40
|
+
if self.path.startswith("/parallel"):
|
|
41
|
+
time.sleep(0.1)
|
|
42
|
+
body = json.dumps(
|
|
43
|
+
{"path": self.path, "cookie": self.headers.get("Cookie")}
|
|
44
|
+
).encode()
|
|
45
|
+
if self.path == "/large":
|
|
46
|
+
body = b"x" * (1024 * 1024)
|
|
47
|
+
status = (
|
|
48
|
+
418
|
|
49
|
+
if self.path == "/error"
|
|
50
|
+
else 302
|
|
51
|
+
if self.path == "/redirect"
|
|
52
|
+
else 200
|
|
53
|
+
)
|
|
54
|
+
self.send_response(status)
|
|
55
|
+
self.send_header("Content-Type", "application/json")
|
|
56
|
+
self.send_header("Content-Length", str(len(body)))
|
|
57
|
+
if self.path == "/cookie":
|
|
58
|
+
self.send_header("Set-Cookie", "native=yes; Path=/")
|
|
59
|
+
if self.path == "/redirect":
|
|
60
|
+
self.send_header("Location", "/final")
|
|
61
|
+
self.end_headers()
|
|
62
|
+
self.wfile.write(body)
|
|
63
|
+
finally:
|
|
64
|
+
with self.server.lock:
|
|
65
|
+
self.server.active -= 1
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_native_sequential_parallel_cookie_redirect_timeout_body():
|
|
69
|
+
api = Api(("127.0.0.1", 0), Handler)
|
|
70
|
+
thread = threading.Thread(target=api.serve_forever, daemon=True)
|
|
71
|
+
thread.start()
|
|
72
|
+
|
|
73
|
+
async def run():
|
|
74
|
+
base = f"http://127.0.0.1:{api.server_port}"
|
|
75
|
+
async with AsyncClient(
|
|
76
|
+
firefox_version="155",
|
|
77
|
+
runtime_home=os.environ["WPR_TEST_RUNTIME_HOME"],
|
|
78
|
+
proxy="",
|
|
79
|
+
base_url=base,
|
|
80
|
+
) as client:
|
|
81
|
+
for i in range(3):
|
|
82
|
+
response = await client.get(f"/serial/{i}")
|
|
83
|
+
assert (await response.ajson())["path"] == f"/serial/{i}"
|
|
84
|
+
|
|
85
|
+
async def one(i):
|
|
86
|
+
response = await client.get(f"/parallel/{i}")
|
|
87
|
+
return await response.ajson()
|
|
88
|
+
|
|
89
|
+
results = await asyncio.gather(*(one(i) for i in range(8)))
|
|
90
|
+
assert len(results) == 8 and api.peak >= 3
|
|
91
|
+
response = await client.get("/cookie")
|
|
92
|
+
await response.aread()
|
|
93
|
+
response = await client.get("/check")
|
|
94
|
+
assert "native=yes" in (await response.ajson())["cookie"]
|
|
95
|
+
response = await client.get("/redirect")
|
|
96
|
+
assert response.url.endswith("/final")
|
|
97
|
+
await response.aread()
|
|
98
|
+
response = await client.get("/error")
|
|
99
|
+
with pytest.raises(WprHTTPStatusError):
|
|
100
|
+
response.raise_for_status()
|
|
101
|
+
await response.aclose()
|
|
102
|
+
response = await client.get("/large")
|
|
103
|
+
assert len(await response.aread()) == 1024 * 1024
|
|
104
|
+
with pytest.raises(WprTimeoutError):
|
|
105
|
+
await client.get("/slow", timeout=0.04)
|
|
106
|
+
response = await client.get("/after-timeout")
|
|
107
|
+
assert response.status_code == 200
|
|
108
|
+
await response.aread()
|
|
109
|
+
task = asyncio.create_task(client.get("/slow-cancel"))
|
|
110
|
+
await asyncio.sleep(0.05)
|
|
111
|
+
task.cancel()
|
|
112
|
+
with pytest.raises(asyncio.CancelledError):
|
|
113
|
+
await task
|
|
114
|
+
response = await client.get("/after-cancel")
|
|
115
|
+
await response.aread()
|
|
116
|
+
await client.flush_releases()
|
|
117
|
+
assert client.transport_stats["pending_blob_count"] == 0
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
asyncio.run(run())
|
|
121
|
+
finally:
|
|
122
|
+
api.shutdown()
|
|
123
|
+
api.server_close()
|
|
124
|
+
thread.join()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Public network client API backed by Firefox Necko."""
|
|
2
|
+
|
|
3
|
+
from wpr_runtime_core.registry import RuntimeCompatibilityError, RuntimeManifest
|
|
4
|
+
|
|
5
|
+
from .client import AsyncWprHttpClient, WprHttpClient
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
WprConnectError,
|
|
8
|
+
WprHttpError,
|
|
9
|
+
WprHTTPStatusError,
|
|
10
|
+
WprPoolTimeoutError,
|
|
11
|
+
WprProxyError,
|
|
12
|
+
WprTimeoutError,
|
|
13
|
+
WprTransportError,
|
|
14
|
+
)
|
|
15
|
+
from .models import Headers, HttpRequest, HttpResponse, NetworkTiming, Request, Timeout
|
|
16
|
+
|
|
17
|
+
__version__ = "0.2.0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncClient(AsyncWprHttpClient):
|
|
21
|
+
"""Standalone client: select and validate an actual Firefox network runtime."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self, *, firefox_version=None, runtime_home=None, config=None, **kwargs
|
|
25
|
+
):
|
|
26
|
+
# Explicit config supports local development builds. Installed engines
|
|
27
|
+
# always go through manifest/version/hash validation.
|
|
28
|
+
if config is None and firefox_version is None:
|
|
29
|
+
firefox_version = "155"
|
|
30
|
+
super().__init__(
|
|
31
|
+
config=config,
|
|
32
|
+
firefox_version=firefox_version,
|
|
33
|
+
runtime_home=runtime_home,
|
|
34
|
+
**kwargs,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Response = HttpResponse
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"AsyncClient",
|
|
42
|
+
"AsyncWprHttpClient",
|
|
43
|
+
"Headers",
|
|
44
|
+
"HttpRequest",
|
|
45
|
+
"HttpResponse",
|
|
46
|
+
"NetworkTiming",
|
|
47
|
+
"Request",
|
|
48
|
+
"Response",
|
|
49
|
+
"RuntimeCompatibilityError",
|
|
50
|
+
"RuntimeManifest",
|
|
51
|
+
"Timeout",
|
|
52
|
+
"WprConnectError",
|
|
53
|
+
"WprHTTPStatusError",
|
|
54
|
+
"WprHttpClient",
|
|
55
|
+
"WprHttpError",
|
|
56
|
+
"WprPoolTimeoutError",
|
|
57
|
+
"WprProxyError",
|
|
58
|
+
"WprTimeoutError",
|
|
59
|
+
"WprTransportError",
|
|
60
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from wpr_runtime_core.installer import RUNTIME_CATALOG, install_runtime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> int:
|
|
10
|
+
parser = argparse.ArgumentParser(prog="python -m wpr_http")
|
|
11
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
12
|
+
commands.add_parser(
|
|
13
|
+
"runtimes", help="list published engine versions in this client's catalog"
|
|
14
|
+
)
|
|
15
|
+
install = commands.add_parser("install-runtime")
|
|
16
|
+
install.add_argument("--version", default="155")
|
|
17
|
+
install.add_argument("--runtime-home")
|
|
18
|
+
install.add_argument("--force", action="store_true")
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
if args.command == "runtimes":
|
|
21
|
+
print(json.dumps(RUNTIME_CATALOG, indent=2))
|
|
22
|
+
else:
|
|
23
|
+
print(install_runtime(args.version, args.runtime_home, force=args.force, product="network"))
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""URL helpers used in diagnostics; credential and query values stay private."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def safe_url(url: str) -> str:
|
|
7
|
+
try:
|
|
8
|
+
parsed = urlsplit(url)
|
|
9
|
+
host = parsed.hostname or ""
|
|
10
|
+
if ":" in host and not host.startswith("["):
|
|
11
|
+
host = f"[{host}]"
|
|
12
|
+
if parsed.port:
|
|
13
|
+
host = f"{host}:{parsed.port}"
|
|
14
|
+
return urlunsplit(
|
|
15
|
+
(parsed.scheme, host, parsed.path, "..." if parsed.query else "", "")
|
|
16
|
+
)
|
|
17
|
+
except ValueError:
|
|
18
|
+
return "<invalid URL>"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def safe_proxy(proxy: str) -> str:
|
|
22
|
+
try:
|
|
23
|
+
parsed = urlsplit(proxy)
|
|
24
|
+
host = parsed.hostname or "<unknown>"
|
|
25
|
+
if ":" in host and not host.startswith("["):
|
|
26
|
+
host = f"[{host}]"
|
|
27
|
+
if parsed.port:
|
|
28
|
+
host = f"{host}:{parsed.port}"
|
|
29
|
+
return f"{parsed.scheme}://{host}"
|
|
30
|
+
except ValueError:
|
|
31
|
+
return "<configured proxy>"
|