"""Audit the payload of every endpoint that could load a ton of data (phase 167, task 05, D5, LOCKED A5). TODO L5 is one sentence: "Fix that **any any other endpoints** that could load a ton of data." ``GET /api/docs/tree`` was the reported offender; this script is the measured answer for the rest of the API, against the app the operator actually runs: uv run python -m scripts.api_payload_audit --base-url http://localhost:8000 # (or against the deployed app: --base-url https://brain.reeseapps.com) It signs in through the real ``POST /api/login`` (the session cookie is the only credential — nothing is hardcoded: ``--password`` or the ``BOR_ADMIN_PASSWORD`` the app itself reads from ``.env``), walks the candidate read routes, and prints one row per endpoint with the IDENTITY and the GZIP transfer size, the ratio, the wall time and a verdict against the budget (default 256 KB — the phase's "look at it" bar; LOCKED A5: this is a report, never a gate, so the exit code is 0 whenever the measurement succeeded). **Credentials are password-only on purpose.** The single-admin session is ``POST /api/login {"password": …}`` (PLAN A10) — there is no username field to send, so there is no ``--username``; ``BOR_ADMIN_PASSWORD`` (or ``--password``) is the whole credential. Both variants are measured on every endpoint because that is the pair the byte-budget log line reports (``bytes=`` / ``enc=``): ``Accept-Encoding: identity`` gives the payload the route produced, ``Accept-Encoding: gzip`` gives what the client puts on the wire (``response.num_bytes_downloaded``, the raw bytes — ``response.content`` is already decompressed by httpx). The routes that publish a validator (the three catalogue reads, phase 167 task 03) get one extra conditional request each: the expected answer is ``304`` with an empty body, which is the row's verdict. Run it against the 1,000-document live-replica snapshot (the ``restore-test-db`` skill) so the numbers are the production order of magnitude and not the fixture KB's — the corpus the run measured is printed with the table for exactly that reason. Exit codes: **0** the table printed (an over-budget row is a FINDING, shown as ``LOOK``, not a failure), **2** a precondition failed — the app unreachable, the login rejected, or no credential available. """ from __future__ import annotations import argparse import os import sys import time from dataclasses import dataclass from typing import Any import httpx from dotenv import load_dotenv load_dotenv() #: The phase's "look at it" bar (256 KB uncompressed — D5/A5). Rows over it #: are verdict ``LOOK`` and must be fixed in the phase or carry a named #: follow-up in ``AUDIT.md``; nothing here rejects a request. DEFAULT_BUDGET = 262_144 #: Per-request wall-clock ceiling. A large catalogue read on the restored #: 1,000-doc replica is fast, but a cold first query can take seconds. DEFAULT_TIMEOUT = 60.0 @dataclass class Row: """One measured endpoint.""" label: str path: str status: int = 0 identity_bytes: int | None = None identity_ms: int | None = None gzip_bytes: int | None = None gzip_ms: int | None = None revalidated: bool | None = None note: str = "" @property def ratio(self) -> str: """identity / gzip — how much the compression layer bought (< 1 is a win; ``—`` when the route did not compress).""" if not self.gzip_bytes or not self.identity_bytes: return "—" return f"{self.gzip_bytes / self.identity_bytes:.3f}" def verdict(self, budget: int) -> str: if self.status == 0: # Nothing was measured — the corpus has no such row (an empty KB, # no saved chats). Honest gap, not a pass and not a finding. return "n/a" if self.status == 304: return "304" if self.status != 200: return f"HTTP {self.status}" if self.revalidated: return "304" if (self.identity_bytes or 0) > budget: return f"LOOK (> {budget})" return "ok" def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="python -m scripts.api_payload_audit", description=( "Measure the response size (identity AND gzip) of every " "candidate large-payload endpoint, against a byte budget." ), ) p.add_argument( "--base-url", default="http://localhost:8000", metavar="URL", help="app to measure (default: http://localhost:8000)", ) p.add_argument( "--password", default=None, metavar="PASSWORD", help=( "admin password; falls back to $BOR_ADMIN_PASSWORD (the app's " "own variable). The single-admin login has no username (A10)." ), ) p.add_argument( "--budget", type=int, default=DEFAULT_BUDGET, metavar="BYTES", help=f"the LOOK threshold in bytes (default: {DEFAULT_BUDGET})", ) p.add_argument( "--timeout", type=float, default=DEFAULT_TIMEOUT, metavar="SECONDS", help=f"per-request timeout (default: {DEFAULT_TIMEOUT:g})", ) return p def _fail(message: str) -> int: print(f"api_payload_audit: {message}", file=sys.stderr) return 2 def _get(client: httpx.Client, path: str, accept: str) -> tuple[int, int, int, int]: """GET ``path`` with an explicit ``Accept-Encoding`` → ``(status, body bytes, wire bytes, ms)``. ``body`` is the decoded payload (what the route produced); ``wire`` is the raw transfer, which is what the browser pays. They differ exactly when the response came back gzipped. """ started = time.perf_counter() response = client.get(path, headers={"Accept-Encoding": accept}) ms = int(round((time.perf_counter() - started) * 1000)) return response.status_code, len(response.content), response.num_bytes_downloaded, ms def measure( client: httpx.Client, label: str, path: str, *, conditional: bool = False ) -> Row: """One endpoint, both encodings, plus the conditional re-request on the routes that publish a validator (``conditional=True``).""" row = Row(label=label, path=path) row.status, row.identity_bytes, _, row.identity_ms = _get(client, path, "identity") gzip_status, _, wire, ms = _get(client, path, "gzip") row.gzip_bytes, row.gzip_ms = wire, ms if gzip_status != row.status: # pragma: no cover - defensive row.status = gzip_status if conditional: # The ETag from a fresh unconditional GET, replayed (fetch does the # same thing on the second page view). etag = client.get(path, headers={"Accept-Encoding": "identity"}).headers.get("etag") if etag is None: row.revalidated = False else: again = client.get( path, headers={"Accept-Encoding": "identity", "If-None-Match": etag} ) row.revalidated = again.status_code == 304 and len(again.content) == 0 return row def _first_folder(tree: dict[str, Any]) -> tuple[str, str] | None: """``(source, folder path)`` of the first folder node the depth-1 tree reports — a representative drill target for the node route.""" for source in tree.get("sources", []): for child in source.get("children", []): if child.get("kind") == "folder": return str(source.get("name", "")), str(child.get("path", "")) return None def run(args: argparse.Namespace) -> tuple[int, list[Row], list[str]]: """Do the whole audit; returns ``(exit code, rows, corpus lines)``.""" password = args.password or os.environ.get("BOR_ADMIN_PASSWORD", "") if not password: return ( _fail( "no admin password — pass --password or set BOR_ADMIN_PASSWORD " "(the app's own variable; it is never hardcoded here)" ), [], [], ) client = httpx.Client( base_url=args.base_url, timeout=args.timeout, follow_redirects=False ) try: login = client.post("/api/login", json={"password": password}) except httpx.HTTPError as exc: return ( _fail( f"cannot reach the app at {args.base_url} ({exc.__class__.__name__}: {exc}) " "— start it with `uv run uvicorn app.main:app`" ), [], [], ) if login.status_code != 204: detail = "" try: detail = str(login.json().get("detail", "")) except Exception: # noqa: BLE001 - a non-JSON error body is still an error detail = login.text[:120] hint = { 401: "wrong password (BOR_ADMIN_PASSWORD of the app under test)", 429: "rate limited (10 failed attempts / 15 min) — wait for the window", }.get(login.status_code, "unexpected answer from the real login route") return ( _fail( f"login to {args.base_url} failed: HTTP {login.status_code} — " f"{hint} {detail}" ), [], [], ) rows: list[Row] = [] corpus: list[str] = [] # --- the corpus the numbers describe (printed with the table) --------- docs_response = client.get("/api/docs", headers={"Accept-Encoding": "identity"}) documents: list[dict[str, Any]] = list(docs_response.json().get("documents", [])) by_chunks = sorted(documents, key=lambda d: int(d.get("chunks", 0)), reverse=True) biggest_doc = by_chunks[0] if by_chunks else None chunks_total = sum(int(d.get("chunks", 0)) for d in documents) corpus.append(f"documents={len(documents)} chunks_total={chunks_total}") chats: list[dict[str, Any]] = list( client.get("/api/chats", headers={"Accept-Encoding": "identity"}).json().get("chats", []) ) biggest_chat = ( sorted(chats, key=lambda c: int(c.get("message_count", 0)), reverse=True)[0] if chats else None ) # --- the candidates --------------------------------------------------- rows.append( measure(client, "tree depth=1 (default)", "/api/docs/tree?depth=1", conditional=True) ) rows.append( measure(client, "tree depth=8 (full)", "/api/docs/tree?depth=8", conditional=True) ) depth1 = client.get( "/api/docs/tree?depth=1", headers={"Accept-Encoding": "identity"} ).json() folder = _first_folder(depth1) if folder is not None and folder[0]: node_path = f"/api/docs/tree/node?source={folder[0]}&path={folder[1]}&depth=1" rows.append( measure(client, f"tree node {folder[0]}/{folder[1]}", node_path, conditional=True) ) else: # the honest gap: an empty KB has no drill to measure rows.append( Row(label="tree node (drill)", path="/api/docs/tree/node", note="no folder in corpus") ) rows.append(measure(client, "docs catalogue", "/api/docs", conditional=True)) if biggest_doc is not None: content_path = ( f"/api/documents/content?source={biggest_doc['source']}&path={biggest_doc['path']}" ) row = measure(client, f"document content (biggest: {biggest_doc['path']})", content_path) body = client.get(content_path, headers={"Accept-Encoding": "identity"}).json() row.note = f"content field {len(str(body.get('content', '')))} chars" rows.append(row) else: rows.append(Row(label="document content", path="/api/documents/content", note="empty KB")) rows.append(measure(client, "saved chats list", "/api/chats")) if biggest_chat is not None: rows.append( measure( client, f"saved chat (biggest: {int(biggest_chat.get('message_count', 0))} msgs)", f"/api/chats/{biggest_chat['id']}", ) ) else: rows.append(Row(label="saved chat (one)", path="/api/chats/{id}", note="no saved chats")) rows.append(measure(client, "git sources", "/api/git-sources")) rows.append(measure(client, "suggestions", "/api/suggestions")) rows.append(measure(client, "sync status (polled)", "/api/sync/status")) rows.append(measure(client, "tokens", "/api/tokens")) rows.append(measure(client, "config", "/api/config")) rows.append(measure(client, "whoami", "/api/whoami")) rows.append(measure(client, "admin settings (catalogue)", "/api/admin/settings")) corpus.append(f"sources={len(depth1.get('sources', []))}") totals = depth1.get("totals") or {} if totals: corpus.append( f"tree totals documents={totals.get('documents')} chunks={totals.get('chunks')}" ) corpus.append(f"saved_chats={len(chats)}") corpus.append(f"biggest_doc={biggest_doc['path'] if biggest_doc else '—'}") return 0, rows, corpus def render(rows: list[Row], *, base_url: str, budget: int, corpus: list[str]) -> None: """The table: endpoint, identity bytes, gzip bytes, ratio, ms, verdict.""" header: tuple[str, ...] = ("endpoint", "identity", "gzip", "ratio", "ms", "verdict") table: list[tuple[str, ...]] = [header] for row in rows: table.append( ( row.label, f"{row.identity_bytes:,}" if row.identity_bytes is not None else "—", f"{row.gzip_bytes:,}" if row.gzip_bytes is not None else "—", row.ratio, f"{row.gzip_ms}" if row.gzip_ms is not None else "—", row.verdict(budget), ) ) widths = [max(len(str(r[i])) for r in table) for i in range(len(header))] def _line(values: tuple[Any, ...]) -> str: return ( f" {str(values[0]):<{widths[0]}} {str(values[1]):>10} " f"{str(values[2]):>10} {str(values[3]):>{widths[3]}} " f"{str(values[4]):>6} {str(values[5])}" ) print(f"api_payload_audit: base={base_url} budget={budget:,} bytes (LOOK threshold)") print(f"api_payload_audit: corpus {' '.join(corpus)}") print(_line(header)) print(f" {'-' * (widths[0] + 44)}") for row, source in zip(rows, table[1:], strict=True): print(_line(source)) if row.note: print(f" note: {row.note}") print( "api_payload_audit: ms is the gzip (real-client) variant; identity is the " "route's own bytes — the pair the ' :: api ' log line reports." ) def main(argv: list[str] | None = None) -> int: args = build_parser().parse_args(argv) code, rows, corpus = run(args) if code != 0 or not rows: return code render(rows, base_url=args.base_url, budget=args.budget, corpus=corpus) return 0 if __name__ == "__main__": sys.exit(main())