build: add deps and test scaffolding for auto-reverse
Adds runtime deps (playwright, mitmproxy, anthropic, genson) and dev deps (pytest, pytest-asyncio); creates the tests/ scaffold with fixture_site.py, conftest.py, and a smoke test. Documents the mitmproxy aioquic/mitmproxy-rs stub workaround needed for free-threaded CPython 3.14. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
# auto-reverse
|
||||||
|
|
||||||
|
Automated API reverse-engineering CLI tool built on Python 3.14 (free-threaded).
|
||||||
|
|
||||||
|
## Dependency Fallback Notes
|
||||||
|
|
||||||
|
### mitmproxy on free-threaded Python 3.14
|
||||||
|
|
||||||
|
`mitmproxy` depends on `aioquic` (for HTTP/3 + QUIC support) and `mitmproxy-rs` (for WireGuard/process-mode features). Both packages ship only as `abi3` wheels that use CPython's Limited API (`Py_LIMITED_API`), which is **explicitly unsupported** on free-threaded Python 3.14 (`cpython-3.14t`). Neither package has source builds compatible with the free-threaded ABI either.
|
||||||
|
|
||||||
|
**Workaround applied:** `[tool.uv.override-dependencies]` in `pyproject.toml` stubs out both packages with minimal pure-Python packages located at `/tmp/aioquic-stub` and `/tmp/mitmproxy-rs-stub`. The proxy will function for HTTP/1.1 and HTTP/2 traffic; HTTP/3 (QUIC) and WireGuard/process-capture modes are unavailable until upstream packages ship free-threaded wheels.
|
||||||
|
|
||||||
|
To regenerate the stubs on a fresh checkout, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/create_stubs.sh # (to be created in a later task)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or create them manually before `uv sync`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# aioquic stub
|
||||||
|
mkdir -p /tmp/aioquic-stub/src/aioquic
|
||||||
|
printf '[project]\nname="aioquic"\nversion="1.2.0"\nrequires-python=">=3.14"\ndependencies=[]\n[build-system]\nrequires=["hatchling"]\nbuild-backend="hatchling.build"\n' > /tmp/aioquic-stub/pyproject.toml
|
||||||
|
echo '"""Stub: HTTP/3 unavailable on free-threaded Python 3.14."""' > /tmp/aioquic-stub/src/aioquic/__init__.py
|
||||||
|
|
||||||
|
# mitmproxy-rs stub
|
||||||
|
mkdir -p /tmp/mitmproxy-rs-stub/src/mitmproxy_rs
|
||||||
|
printf '[project]\nname="mitmproxy-rs"\nversion="0.12.9"\nrequires-python=">=3.14"\ndependencies=[]\n[build-system]\nrequires=["hatchling"]\nbuild-backend="hatchling.build"\n' > /tmp/mitmproxy-rs-stub/pyproject.toml
|
||||||
|
echo '"""Stub: WireGuard/process-mode unavailable on free-threaded Python 3.14."""' > /tmp/mitmproxy-rs-stub/src/mitmproxy_rs/__init__.py
|
||||||
|
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|||||||
+22
-1
@@ -7,7 +7,12 @@ authors = [
|
|||||||
{ name = "Wong Ding Feng", email = "dingfengwong@gmail.com" }
|
{ name = "Wong Ding Feng", email = "dingfengwong@gmail.com" }
|
||||||
]
|
]
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"anthropic>=0.105.2",
|
||||||
|
"genson>=1.3.0",
|
||||||
|
"mitmproxy>=12.2.3",
|
||||||
|
"playwright>=1.60.0",
|
||||||
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
auto-reverse = "auto_reverse:main"
|
auto-reverse = "auto_reverse:main"
|
||||||
@@ -28,3 +33,19 @@ pythonVersion = "3.14"
|
|||||||
typeCheckingMode = "strict"
|
typeCheckingMode = "strict"
|
||||||
venvPath = "."
|
venvPath = "."
|
||||||
venv = ".venv"
|
venv = ".venv"
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
override-dependencies = [
|
||||||
|
"aioquic @ file:///tmp/aioquic-stub",
|
||||||
|
"mitmproxy-rs @ file:///tmp/mitmproxy-rs-stub",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=9.0.3",
|
||||||
|
"pytest-asyncio>=1.4.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
testpaths = ["tests"]
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.fixture_site import start_fixture_site
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fixture_site() -> Iterator[str]:
|
||||||
|
server, base_url = start_fixture_site()
|
||||||
|
try:
|
||||||
|
yield base_url
|
||||||
|
finally:
|
||||||
|
server.shutdown()
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""A tiny dependency-free JSON site for integration tests, served over HTTP."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
|
|
||||||
|
class _Handler(BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, *args: object) -> None: # silence test output
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _send_json(self, status: int, payload: object) -> None:
|
||||||
|
body = json.dumps(payload).encode()
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def do_GET(self) -> None:
|
||||||
|
if self.path == "/":
|
||||||
|
html = b"<html><body><script>fetch('/api/users')</script></body></html>"
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/html")
|
||||||
|
self.send_header("Content-Length", str(len(html)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(html)
|
||||||
|
elif self.path == "/api/users":
|
||||||
|
self._send_json(200, [{"id": 1, "name": "Ada"}])
|
||||||
|
elif self.path.startswith("/api/users/"):
|
||||||
|
self._send_json(200, {"id": int(self.path.rsplit("/", 1)[1]), "name": "Ada"})
|
||||||
|
else:
|
||||||
|
self._send_json(404, {"error": "not found"})
|
||||||
|
|
||||||
|
def do_POST(self) -> None:
|
||||||
|
length = int(self.headers.get("Content-Length", "0"))
|
||||||
|
raw = self.rfile.read(length) if length else b"{}"
|
||||||
|
self._send_json(201, {"received": json.loads(raw or b"{}")})
|
||||||
|
|
||||||
|
|
||||||
|
def start_fixture_site() -> tuple[ThreadingHTTPServer, str]:
|
||||||
|
"""Start the site on an ephemeral port; return (server, base_url)."""
|
||||||
|
server = ThreadingHTTPServer(("127.0.0.1", 0), _Handler)
|
||||||
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
host, port = server.server_address
|
||||||
|
return server, f"http://{host}:{port}"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""Smoke test: verify the test infrastructure itself works."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from tests.fixture_site import start_fixture_site
|
||||||
|
|
||||||
|
|
||||||
|
def test_fixture_site_reachable() -> None:
|
||||||
|
server, base_url = start_fixture_site()
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(f"{base_url}/api/users") as resp:
|
||||||
|
assert resp.status == 200
|
||||||
|
finally:
|
||||||
|
server.shutdown()
|
||||||
Reference in New Issue
Block a user