711 lines
24 KiB
Python
711 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""Audit public HTTP requests exposed by the HLL Vietnam application."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
import time
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.parse import quote
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
DEFAULT_BASE_URL = "http://127.0.0.1:8000"
|
|
DEFAULT_TIMEOUT_SECONDS = 25.0
|
|
KNOWN_MATCH_ID = "comunidad-hispana-01:1781023156:1781028555:purpleheartlanewarfare"
|
|
KNOWN_PLAYER_SEARCH = "Medu"
|
|
|
|
RANKING_METRICS = (
|
|
"kills",
|
|
"deaths",
|
|
"teamkills",
|
|
"matches_considered",
|
|
"kd_ratio",
|
|
"kills_per_match",
|
|
)
|
|
RANKING_SCOPES = ("all", "comunidad-hispana-01", "comunidad-hispana-02")
|
|
HISTORICAL_SCOPES = ("all-servers", "comunidad-hispana-01", "comunidad-hispana-02")
|
|
HISTORICAL_LEGACY_METRICS = ("kills", "deaths", "support", "matches_over_100_kills")
|
|
PLAYER_EVENT_VIEWS = ("most-killed", "death-by", "duels", "weapon-kills", "teamkills")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProbeSpec:
|
|
id: str
|
|
kind: str
|
|
context: str
|
|
method: str
|
|
path: str
|
|
parameters: str
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Measure public HLL Vietnam frontend/backend request surfaces."
|
|
)
|
|
parser.add_argument(
|
|
"--base-url",
|
|
default=DEFAULT_BASE_URL,
|
|
help=f"Base URL to audit. Default: {DEFAULT_BASE_URL}",
|
|
)
|
|
parser.add_argument(
|
|
"--timeout",
|
|
type=float,
|
|
default=DEFAULT_TIMEOUT_SECONDS,
|
|
help=f"Per-request timeout in seconds. Default: {DEFAULT_TIMEOUT_SECONDS}",
|
|
)
|
|
parser.add_argument(
|
|
"--output",
|
|
default=None,
|
|
help="JSON output path. Default: tmp/public_request_audit.json.",
|
|
)
|
|
parser.add_argument(
|
|
"--repo-root",
|
|
default=None,
|
|
help="Repository root for static discovery metadata. Default: auto/cwd.",
|
|
)
|
|
parser.add_argument(
|
|
"--max-probes",
|
|
type=int,
|
|
default=0,
|
|
help="Optional cap for smoke runs. 0 means all probes.",
|
|
)
|
|
parser.add_argument(
|
|
"--filter",
|
|
default=None,
|
|
help="Run only probes whose id, kind, context, path or parameters contain this text.",
|
|
)
|
|
parser.add_argument(
|
|
"--player-id",
|
|
default=None,
|
|
help="Add player-dependent probes for this player id before applying --filter.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
base_url = normalize_base_url(args.base_url)
|
|
repo_root = resolve_repo_root(args.repo_root)
|
|
output_path = resolve_output_path(args.output)
|
|
|
|
specs = build_probe_specs()
|
|
if args.player_id:
|
|
specs.extend(build_player_dependent_specs(args.player_id))
|
|
if args.filter:
|
|
specs = filter_probe_specs(specs, args.filter)
|
|
if args.max_probes and args.max_probes > 0:
|
|
specs = specs[: args.max_probes]
|
|
|
|
results: list[dict[str, Any]] = []
|
|
dynamic_player_id: str | None = None
|
|
index = 0
|
|
while index < len(specs):
|
|
spec = specs[index]
|
|
result = run_probe(base_url=base_url, spec=spec, timeout_seconds=args.timeout)
|
|
results.append(result)
|
|
print_result_row(result)
|
|
|
|
if spec.id == "stats-player-search-medu" and dynamic_player_id is None:
|
|
dynamic_player_id = extract_first_player_id(result.get("json"))
|
|
if dynamic_player_id:
|
|
specs.extend(build_player_dependent_specs(dynamic_player_id))
|
|
|
|
index += 1
|
|
|
|
payload = {
|
|
"audit": {
|
|
"base_url": base_url,
|
|
"timeout_seconds": args.timeout,
|
|
"generated_at_epoch": time.time(),
|
|
"probe_count": len(results),
|
|
},
|
|
"summary": summarize_results(results),
|
|
"discovery": discover_static_requests(repo_root),
|
|
"results": strip_json_payloads(results),
|
|
}
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path.write_text(json.dumps(payload, ensure_ascii=True, indent=2) + "\n", encoding="utf-8")
|
|
|
|
print()
|
|
print_summary(payload["summary"])
|
|
print(f"Saved JSON results to {output_path}")
|
|
return 0
|
|
|
|
|
|
def build_probe_specs() -> list[ProbeSpec]:
|
|
specs: list[ProbeSpec] = []
|
|
seen: set[str] = set()
|
|
|
|
def add(
|
|
id_: str,
|
|
path: str,
|
|
*,
|
|
kind: str = "backend-api",
|
|
context: str = "backend",
|
|
parameters: str = "",
|
|
) -> None:
|
|
if id_ in seen:
|
|
return
|
|
seen.add(id_)
|
|
specs.append(
|
|
ProbeSpec(
|
|
id=id_,
|
|
kind=kind,
|
|
context=context,
|
|
method="GET",
|
|
path=path,
|
|
parameters=parameters,
|
|
)
|
|
)
|
|
|
|
add("health", "/health", context="health", parameters="none")
|
|
add("api-community", "/api/community", context="home", parameters="none")
|
|
add("api-trailer", "/api/trailer", context="home", parameters="none")
|
|
add("api-discord", "/api/discord", context="home", parameters="none")
|
|
add("api-servers", "/api/servers", context="home", parameters="none")
|
|
add("api-servers-latest", "/api/servers/latest", context="home", parameters="none")
|
|
add("api-servers-history", "/api/servers/history?limit=20", context="server-history", parameters="limit=20")
|
|
for server in ("comunidad-hispana-01", "comunidad-hispana-02"):
|
|
add(
|
|
f"api-servers-{server}-history",
|
|
f"/api/servers/{server}/history?limit=20",
|
|
context="server-history",
|
|
parameters="server path, limit=20",
|
|
)
|
|
|
|
for timeframe in ("weekly", "monthly", "annual"):
|
|
for server in RANKING_SCOPES:
|
|
for metric in RANKING_METRICS:
|
|
params = {
|
|
"timeframe": timeframe,
|
|
"server_id": server,
|
|
"metric": metric,
|
|
"limit": "20",
|
|
}
|
|
if timeframe == "annual":
|
|
params["year"] = "2026"
|
|
query = "&".join(f"{key}={quote(value)}" for key, value in params.items())
|
|
add(
|
|
f"ranking-{timeframe}-{server}-{metric}",
|
|
f"/api/ranking?{query}",
|
|
context="ranking.html",
|
|
parameters="timeframe, server_id, metric, limit, year for annual",
|
|
)
|
|
|
|
add(
|
|
"ranking-weekly-all-servers-alias",
|
|
"/api/ranking?timeframe=weekly&server_id=all-servers&metric=kills&limit=20",
|
|
context="ranking.html",
|
|
parameters="server_id=all-servers alias",
|
|
)
|
|
add(
|
|
"stats-player-search-medu",
|
|
f"/api/stats/players/search?q={quote(KNOWN_PLAYER_SEARCH)}&server_id=all&limit=10",
|
|
context="stats.html",
|
|
parameters="q=Medu, server_id=all, limit=10",
|
|
)
|
|
add(
|
|
"stats-annual-ranking",
|
|
"/api/stats/rankings/annual?year=2026&server_id=all&metric=kills&limit=20",
|
|
context="stats.html",
|
|
parameters="year=2026, server_id=all, metric=kills, limit=20",
|
|
)
|
|
|
|
for server in ("comunidad-hispana-01", "comunidad-hispana-02"):
|
|
add(
|
|
f"current-match-{server}",
|
|
f"/api/current-match?server={server}",
|
|
context="partida-actual.html",
|
|
parameters="server",
|
|
)
|
|
add(
|
|
f"current-match-kills-{server}",
|
|
f"/api/current-match/kills?server={server}&limit=30",
|
|
context="partida-actual.html",
|
|
parameters="server, limit=30",
|
|
)
|
|
add(
|
|
f"current-match-players-{server}",
|
|
f"/api/current-match/players?server={server}",
|
|
context="partida-actual.html",
|
|
parameters="server",
|
|
)
|
|
|
|
for server in HISTORICAL_SCOPES:
|
|
add(
|
|
f"historical-server-summary-{server}",
|
|
f"/api/historical/server-summary?server={server}",
|
|
context="historico.html legacy",
|
|
parameters="server",
|
|
)
|
|
add(
|
|
f"historical-recent-matches-{server}",
|
|
f"/api/historical/recent-matches?server={server}&limit=20",
|
|
context="historico.html legacy",
|
|
parameters="server, limit=20",
|
|
)
|
|
add(
|
|
f"snapshot-server-summary-{server}",
|
|
f"/api/historical/snapshots/server-summary?server={server}",
|
|
context="historico.html",
|
|
parameters="server",
|
|
)
|
|
add(
|
|
f"snapshot-recent-matches-{server}",
|
|
f"/api/historical/snapshots/recent-matches?server={server}&limit=100",
|
|
context="historico.html",
|
|
parameters="server, limit=100",
|
|
)
|
|
add(
|
|
f"weekly-top-kills-{server}",
|
|
f"/api/historical/weekly-top-kills?server={server}&limit=10",
|
|
context="historical legacy API",
|
|
parameters="server, limit=10",
|
|
)
|
|
|
|
for timeframe in ("weekly", "monthly"):
|
|
for metric in HISTORICAL_LEGACY_METRICS:
|
|
add(
|
|
f"historical-leaderboard-{timeframe}-{server}-{metric}",
|
|
(
|
|
"/api/historical/leaderboard?"
|
|
f"server={server}&timeframe={timeframe}&metric={metric}&limit=10"
|
|
),
|
|
context="historical legacy API",
|
|
parameters="server, timeframe, metric, limit=10",
|
|
)
|
|
add(
|
|
f"snapshot-leaderboard-{timeframe}-{server}-{metric}",
|
|
(
|
|
"/api/historical/snapshots/leaderboard?"
|
|
f"server={server}&timeframe={timeframe}&metric={metric}&limit=10"
|
|
),
|
|
context="historico.html",
|
|
parameters="server, timeframe, metric, limit=10",
|
|
)
|
|
|
|
add(
|
|
f"historical-weekly-leaderboard-{server}",
|
|
f"/api/historical/weekly-leaderboard?server={server}&metric=kills&limit=10",
|
|
context="historical legacy API",
|
|
parameters="server, metric=kills, limit=10",
|
|
)
|
|
add(
|
|
f"historical-monthly-leaderboard-{server}",
|
|
f"/api/historical/monthly-leaderboard?server={server}&metric=kills&limit=10",
|
|
context="historical legacy API",
|
|
parameters="server, metric=kills, limit=10",
|
|
)
|
|
add(
|
|
f"snapshot-weekly-leaderboard-{server}",
|
|
f"/api/historical/snapshots/weekly-leaderboard?server={server}&metric=kills&limit=10",
|
|
context="historico.html",
|
|
parameters="server, metric=kills, limit=10",
|
|
)
|
|
add(
|
|
f"snapshot-monthly-leaderboard-{server}",
|
|
f"/api/historical/snapshots/monthly-leaderboard?server={server}&metric=kills&limit=10",
|
|
context="historico.html",
|
|
parameters="server, metric=kills, limit=10",
|
|
)
|
|
add(
|
|
f"monthly-mvp-{server}",
|
|
f"/api/historical/monthly-mvp?server={server}&limit=10",
|
|
context="historical public API",
|
|
parameters="server, limit=10",
|
|
)
|
|
add(
|
|
f"monthly-mvp-v2-{server}",
|
|
f"/api/historical/monthly-mvp-v2?server={server}&limit=10",
|
|
context="historical public API",
|
|
parameters="server, limit=10",
|
|
)
|
|
add(
|
|
f"snapshot-monthly-mvp-{server}",
|
|
f"/api/historical/snapshots/monthly-mvp?server={server}&limit=10",
|
|
context="historical snapshots",
|
|
parameters="server, limit=10",
|
|
)
|
|
add(
|
|
f"snapshot-monthly-mvp-v2-{server}",
|
|
f"/api/historical/snapshots/monthly-mvp-v2?server={server}&limit=10",
|
|
context="historical snapshots",
|
|
parameters="server, limit=10",
|
|
)
|
|
for view in PLAYER_EVENT_VIEWS:
|
|
add(
|
|
f"player-events-{server}-{view}",
|
|
f"/api/historical/player-events?server={server}&view={view}&limit=10",
|
|
context="historical public API",
|
|
parameters="server, view, limit=10",
|
|
)
|
|
add(
|
|
f"snapshot-player-events-{server}-{view}",
|
|
f"/api/historical/snapshots/player-events?server={server}&view={view}&limit=10",
|
|
context="historical snapshots",
|
|
parameters="server, view, limit=10",
|
|
)
|
|
|
|
add(
|
|
"historical-match-detail-known",
|
|
(
|
|
"/api/historical/matches/detail?"
|
|
"server=comunidad-hispana-01&match="
|
|
f"{quote(KNOWN_MATCH_ID, safe='')}"
|
|
),
|
|
context="historico-partida.html",
|
|
parameters="server, match",
|
|
)
|
|
|
|
add(
|
|
"elo-mmr-leaderboard-paused",
|
|
"/api/historical/elo-mmr/leaderboard?server=all-servers&limit=10",
|
|
context="paused public API",
|
|
parameters="server, limit=10",
|
|
)
|
|
return specs
|
|
|
|
|
|
def build_player_dependent_specs(player_id: str) -> list[ProbeSpec]:
|
|
quoted_player = quote(player_id, safe="")
|
|
return [
|
|
ProbeSpec(
|
|
id="stats-player-profile-weekly",
|
|
kind="backend-api",
|
|
context="stats.html",
|
|
method="GET",
|
|
path=f"/api/stats/players/{quoted_player}?timeframe=weekly&server_id=all",
|
|
parameters="player_id from search, timeframe=weekly, server_id=all",
|
|
),
|
|
ProbeSpec(
|
|
id="stats-player-profile-monthly",
|
|
kind="backend-api",
|
|
context="stats.html",
|
|
method="GET",
|
|
path=f"/api/stats/players/{quoted_player}?timeframe=monthly&server_id=all",
|
|
parameters="player_id from search, timeframe=monthly, server_id=all",
|
|
),
|
|
ProbeSpec(
|
|
id="historical-player-profile",
|
|
kind="backend-api",
|
|
context="historical public API",
|
|
method="GET",
|
|
path=f"/api/historical/player-profile?player={quoted_player}",
|
|
parameters="player from search",
|
|
),
|
|
ProbeSpec(
|
|
id="elo-mmr-player-paused",
|
|
kind="backend-api",
|
|
context="paused public API",
|
|
method="GET",
|
|
path=f"/api/historical/elo-mmr/player?server=all-servers&player={quoted_player}",
|
|
parameters="player from search, server=all-servers",
|
|
),
|
|
]
|
|
|
|
|
|
def filter_probe_specs(specs: list[ProbeSpec], raw_filter: str) -> list[ProbeSpec]:
|
|
needle = raw_filter.strip().casefold()
|
|
if not needle:
|
|
return specs
|
|
return [
|
|
spec
|
|
for spec in specs
|
|
if needle
|
|
in " ".join(
|
|
(
|
|
spec.id,
|
|
spec.kind,
|
|
spec.context,
|
|
spec.path,
|
|
spec.parameters,
|
|
)
|
|
).casefold()
|
|
]
|
|
|
|
|
|
def run_probe(*, base_url: str, spec: ProbeSpec, timeout_seconds: float) -> dict[str, Any]:
|
|
url = f"{base_url}{spec.path}"
|
|
started = time.perf_counter()
|
|
status_code: int | None = None
|
|
body = b""
|
|
error: str | None = None
|
|
headers: dict[str, str] = {}
|
|
|
|
try:
|
|
request = Request(
|
|
url,
|
|
headers={
|
|
"Accept": "application/json",
|
|
"User-Agent": "HLL-Vietnam-Public-Request-Audit/1.0",
|
|
},
|
|
method=spec.method,
|
|
)
|
|
with urlopen(request, timeout=timeout_seconds) as response:
|
|
status_code = int(response.status)
|
|
headers = {key.lower(): value for key, value in response.headers.items()}
|
|
body = response.read()
|
|
except HTTPError as exc:
|
|
status_code = int(exc.code)
|
|
headers = {key.lower(): value for key, value in exc.headers.items()}
|
|
body = exc.read()
|
|
error = f"HTTPError: {exc.code}"
|
|
except URLError as exc:
|
|
error = f"URLError: {exc.reason}"
|
|
except TimeoutError:
|
|
error = "TimeoutError"
|
|
except Exception as exc: # noqa: BLE001 - audit must continue
|
|
error = f"{type(exc).__name__}: {exc}"
|
|
|
|
elapsed_ms = round((time.perf_counter() - started) * 1000, 2)
|
|
parsed_json = parse_json_body(body)
|
|
summary = summarize_json(parsed_json)
|
|
severity = classify_result(
|
|
status_code=status_code,
|
|
elapsed_ms=elapsed_ms,
|
|
error=error,
|
|
fallback_used=summary.get("fallback_used"),
|
|
snapshot_status=summary.get("snapshot_status"),
|
|
)
|
|
return {
|
|
**asdict(spec),
|
|
"url": url,
|
|
"status_code": status_code,
|
|
"elapsed_ms": elapsed_ms,
|
|
"response_size_bytes": len(body),
|
|
"content_type": headers.get("content-type"),
|
|
"json_status": summary.get("json_status"),
|
|
"data_source": summary.get("data_source"),
|
|
"fallback_used": summary.get("fallback_used"),
|
|
"fallback_reason": summary.get("fallback_reason"),
|
|
"snapshot_status": summary.get("snapshot_status"),
|
|
"item_count": summary.get("item_count"),
|
|
"found": summary.get("found"),
|
|
"severity": severity,
|
|
"error": error,
|
|
"json": parsed_json,
|
|
}
|
|
|
|
|
|
def parse_json_body(body: bytes) -> Any:
|
|
if not body:
|
|
return None
|
|
try:
|
|
return json.loads(body.decode("utf-8"))
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def summarize_json(payload: Any) -> dict[str, Any]:
|
|
if not isinstance(payload, dict):
|
|
return {}
|
|
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
|
|
source_payload = data.get("source") if isinstance(data.get("source"), dict) else {}
|
|
items = data.get("items")
|
|
if isinstance(items, list):
|
|
item_count = len(items)
|
|
elif isinstance(data.get("item"), dict):
|
|
item_count = 1
|
|
elif isinstance(data.get("profile"), dict):
|
|
item_count = 1
|
|
else:
|
|
item_count = None
|
|
return {
|
|
"json_status": payload.get("status"),
|
|
"data_source": data.get("source")
|
|
if isinstance(data.get("source"), str)
|
|
else source_payload.get("read_model") or source_payload.get("primary_source"),
|
|
"fallback_used": data.get("fallback_used")
|
|
if "fallback_used" in data
|
|
else source_payload.get("fallback_used"),
|
|
"fallback_reason": data.get("fallback_reason")
|
|
if "fallback_reason" in data
|
|
else source_payload.get("fallback_reason"),
|
|
"snapshot_status": data.get("snapshot_status"),
|
|
"item_count": item_count,
|
|
"found": data.get("found"),
|
|
}
|
|
|
|
|
|
def classify_result(
|
|
*,
|
|
status_code: int | None,
|
|
elapsed_ms: float,
|
|
error: str | None,
|
|
fallback_used: Any,
|
|
snapshot_status: Any,
|
|
) -> str:
|
|
if error or status_code is None or status_code >= 500 or elapsed_ms >= 10000:
|
|
return "CRITICAL"
|
|
if status_code >= 400 or elapsed_ms >= 2500 or fallback_used is True:
|
|
return "WARNING"
|
|
if str(snapshot_status or "").lower() == "missing":
|
|
return "WARNING"
|
|
return "OK"
|
|
|
|
|
|
def extract_first_player_id(payload: Any) -> str | None:
|
|
if not isinstance(payload, dict):
|
|
return None
|
|
data = payload.get("data")
|
|
if not isinstance(data, dict):
|
|
return None
|
|
items = data.get("items")
|
|
if not isinstance(items, list):
|
|
return None
|
|
for item in items:
|
|
if isinstance(item, dict) and str(item.get("player_id") or "").strip():
|
|
return str(item["player_id"]).strip()
|
|
return None
|
|
|
|
|
|
def discover_static_requests(repo_root: Path) -> dict[str, Any]:
|
|
backend_paths: set[str] = set()
|
|
frontend_literals: set[str] = set()
|
|
fetch_occurrences: list[dict[str, Any]] = []
|
|
localhost_refs: list[dict[str, Any]] = []
|
|
|
|
routes_path = repo_root / "backend" / "app" / "routes.py"
|
|
if routes_path.exists():
|
|
text = routes_path.read_text(encoding="utf-8")
|
|
for value in re.findall(r"['\"](/(?:api/[^'\"]+|health))['\"]", text):
|
|
backend_paths.add(value)
|
|
|
|
frontend_root = repo_root / "frontend"
|
|
if frontend_root.exists():
|
|
for path in sorted(frontend_root.rglob("*")):
|
|
if path.suffix.lower() not in {".html", ".js"}:
|
|
continue
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
for value in re.findall(r"/api/[A-Za-z0-9_./?=&%${}()+:,\\-]+|/health", text):
|
|
frontend_literals.add(value)
|
|
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
if "fetch(" in line:
|
|
fetch_occurrences.append(
|
|
{
|
|
"file": str(path.relative_to(repo_root)),
|
|
"line": line_number,
|
|
"text": line.strip(),
|
|
}
|
|
)
|
|
if "127.0.0.1" in line or "localhost" in line:
|
|
localhost_refs.append(
|
|
{
|
|
"file": str(path.relative_to(repo_root)),
|
|
"line": line_number,
|
|
"text": line.strip(),
|
|
}
|
|
)
|
|
|
|
return {
|
|
"repo_root": str(repo_root),
|
|
"backend_route_paths": sorted(backend_paths),
|
|
"frontend_api_literals": sorted(frontend_literals),
|
|
"frontend_fetch_occurrences": fetch_occurrences,
|
|
"frontend_localhost_references": localhost_refs,
|
|
}
|
|
|
|
|
|
def summarize_results(results: list[dict[str, Any]]) -> dict[str, Any]:
|
|
severity_counts = {"OK": 0, "WARNING": 0, "CRITICAL": 0}
|
|
status_counts: dict[str, int] = {}
|
|
launched = 0
|
|
for result in results:
|
|
launched += 1
|
|
severity = str(result.get("severity") or "WARNING")
|
|
severity_counts[severity] = severity_counts.get(severity, 0) + 1
|
|
status_key = str(result.get("status_code"))
|
|
status_counts[status_key] = status_counts.get(status_key, 0) + 1
|
|
slowest = sorted(
|
|
(
|
|
{
|
|
"id": result.get("id"),
|
|
"path": result.get("path"),
|
|
"elapsed_ms": result.get("elapsed_ms"),
|
|
"status_code": result.get("status_code"),
|
|
"severity": result.get("severity"),
|
|
}
|
|
for result in results
|
|
),
|
|
key=lambda item: float(item.get("elapsed_ms") or 0),
|
|
reverse=True,
|
|
)[:10]
|
|
return {
|
|
"launched": launched,
|
|
"severity_counts": severity_counts,
|
|
"status_counts": status_counts,
|
|
"slowest": slowest,
|
|
}
|
|
|
|
|
|
def strip_json_payloads(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
stripped = []
|
|
for result in results:
|
|
item = dict(result)
|
|
item.pop("json", None)
|
|
stripped.append(item)
|
|
return stripped
|
|
|
|
|
|
def print_result_row(result: dict[str, Any]) -> None:
|
|
status = result.get("status_code")
|
|
elapsed = result.get("elapsed_ms")
|
|
severity = result.get("severity")
|
|
size = result.get("response_size_bytes")
|
|
fallback = result.get("fallback_used")
|
|
print(
|
|
f"{severity:8} {str(status):>4} {elapsed:>9.2f} ms {size:>9} B "
|
|
f"fallback={str(fallback):<5} {result['id']}"
|
|
)
|
|
|
|
|
|
def print_summary(summary: dict[str, Any]) -> None:
|
|
print("Summary")
|
|
print(f" launched: {summary['launched']}")
|
|
print(f" OK: {summary['severity_counts'].get('OK', 0)}")
|
|
print(f" WARNING: {summary['severity_counts'].get('WARNING', 0)}")
|
|
print(f" CRITICAL: {summary['severity_counts'].get('CRITICAL', 0)}")
|
|
print(" slowest:")
|
|
for item in summary.get("slowest", []):
|
|
print(
|
|
" "
|
|
f"{item.get('elapsed_ms')} ms {item.get('status_code')} "
|
|
f"{item.get('severity')} {item.get('id')}"
|
|
)
|
|
|
|
|
|
def normalize_base_url(value: str) -> str:
|
|
return str(value or DEFAULT_BASE_URL).strip().rstrip("/")
|
|
|
|
|
|
def resolve_repo_root(raw_value: str | None) -> Path:
|
|
if raw_value:
|
|
return Path(raw_value).resolve()
|
|
cwd = Path.cwd().resolve()
|
|
if (cwd / "backend" / "app" / "routes.py").exists():
|
|
return cwd
|
|
script_path = Path(__file__).resolve()
|
|
for candidate in (script_path.parent, *script_path.parents):
|
|
if (candidate / "backend" / "app" / "routes.py").exists():
|
|
return candidate
|
|
return cwd
|
|
|
|
|
|
def resolve_output_path(raw_value: str | None) -> Path:
|
|
if raw_value:
|
|
return Path(raw_value)
|
|
tmp_path = Path("tmp") / "public_request_audit.json"
|
|
try:
|
|
tmp_path.parent.mkdir(parents=True, exist_ok=True)
|
|
return tmp_path
|
|
except OSError:
|
|
return Path("docs") / "public_request_audit_results.example.json"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|