scripts/gen_api_reference.py
Rama main · réplica de lectura del repositorio original.
"""Regenerate the developer API reference from the running app's own routes.
python scripts/gen_api_reference.py # rewrite docs/developers/api-reference.md
python scripts/gen_api_reference.py --check # fail if it is out of date
python scripts/gen_api_reference.py --update-contract # re-pin the stable routes' shapes
The reference lists every route the engine serves with its stability label
(server/api_stability.py). It is built from the app itself, so a route that
exists is never missing from it and a route that was removed never lingers.
--update-contract rewrites tests/fixtures/api/stable_contract.json, the shapes
tests/test_api_contract.py holds the stable routes to. Only run it when a
change to a stable route is meant: adding an optional field is fine; removing
or reshaping one is an incompatible change and also bumps API_VERSION.
Needs the engine's own dependencies (FastAPI). Nothing is started: the app is
built in a temporary data folder and only its route table is read.
"""
import argparse
import json
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
REFERENCE = ROOT / "docs" / "developers" / "api-reference.md"
CONTRACT = ROOT / "tests" / "fixtures" / "api" / "stable_contract.json"
def build_app(data_dir: Path):
from main import BUNDLED_CONFIG, load_config
from server.api import create_app
config = load_config(BUNDLED_CONFIG)
config["paths"]["data_dir"] = str(data_dir / "data")
return create_app(config, data_dir / "settings.yaml")
def _area(path: str) -> str:
"""The first path segment, for grouping internal routes."""
return "/" + path.strip("/").split("/", 1)[0]
def render(app) -> str:
from server import api_stability as st
from server.api import API_VERSION
routes = st.app_routes(app)
counts = dict.fromkeys((st.STABLE, st.EXPERIMENTAL, st.INTERNAL), 0)
for method, path, _module in routes:
counts[st.label(method, path)] += 1
out = [
"# Local API reference",
"",
"<!-- Generated by scripts/gen_api_reference.py from the app's own routes. Do not edit by hand. -->",
"",
f"API version **{API_VERSION}** (`GET /health` → `api_version`). The engine listens on "
"`http://127.0.0.1:8765`. How to call it, with examples and the gotchas, is in "
"[`docs/API.md`](../API.md); what the labels promise is in [API](api.md).",
"",
f"{len(routes)} routes: **{counts[st.STABLE]} stable**, **{counts[st.EXPERIMENTAL]} experimental**, "
f"{counts[st.INTERNAL]} internal.",
"",
"| Label | Promise |",
"|---|---|",
"| stable | Documented as supported in `docs/API.md`. Changes only by adding; removing or reshaping it bumps the API version. Its request shape is pinned by `tests/test_api_contract.py`. |",
"| experimental | Meant for outside use, but its shape may still change in a release; every change is noted in `CHANGELOG.md`. |",
"| internal | Serves one screen of the desktop app and changes with it. Works, but depending on it is at your own risk. |",
"",
]
for key, (title, anchor) in st.SECTIONS.items():
rows = [(m, p, st.ROUTES[(m, p)]) for m, p, _ in routes
if (m, p) in st.ROUTES and st.ROUTES[(m, p)][1] == key]
if not rows:
continue
out += [f"## {title}", "", f"Details: [`docs/API.md` › {title}](../API.md#{anchor}).", "",
"| Route | Label | What it is for |", "|---|---|---|"]
for method, path, (lab, _section, purpose) in rows:
out.append(f"| `{method} {path}` | {lab} | {purpose} |")
out.append("")
internal = [(m, p, mod) for m, p, mod in routes if st.label(m, p) == st.INTERNAL]
out += ["## Internal", "",
"Everything else. Grouped by the first part of the path; the module is where it is defined.", ""]
areas: dict[str, list] = {}
for method, path, module in internal:
areas.setdefault(_area(path), []).append((method, path, module))
out += ["| Area | Routes | Module |", "|---|---|---|"]
for area in sorted(areas):
items = areas[area]
listed = "<br>".join(f"`{m} {p}`" for m, p, _ in items)
modules = ", ".join(sorted({f"`{mod}`" for _, _, mod in items}))
out.append(f"| `{area}` | {listed} | {modules} |")
out.append("")
return "\n".join(out)
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--check", action="store_true", help="exit 1 if the reference is out of date")
ap.add_argument("--update-contract", action="store_true", help="re-pin the stable routes' request shapes")
args = ap.parse_args()
with tempfile.TemporaryDirectory() as tmp:
app = build_app(Path(tmp))
text = render(app)
if args.update_contract:
from server import api_stability as st
CONTRACT.parent.mkdir(parents=True, exist_ok=True)
CONTRACT.write_text(json.dumps(st.contract(app), indent=1, sort_keys=True) + "\n", encoding="utf-8")
print(f"pinned {CONTRACT.relative_to(ROOT)}")
if args.check:
current = REFERENCE.read_text(encoding="utf-8") if REFERENCE.exists() else ""
if current != text:
print(f"{REFERENCE.relative_to(ROOT)} is out of date: run python scripts/gen_api_reference.py")
return 1
print("API reference is up to date")
return 0
REFERENCE.parent.mkdir(parents=True, exist_ok=True)
REFERENCE.write_text(text, encoding="utf-8")
print(f"wrote {REFERENCE.relative_to(ROOT)}")
return 0
if __name__ == "__main__":
sys.exit(main())