Files
ersatztv/scripts/generate-endpoint-index.py
T
timothyandClaude Opus 4.8 0290594f0b
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m45s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
docs: testing map + generated endpoint index (#185)
Adds docs/testing.md as the authoritative testing map (consolidated
from docs/contributing.md §8, now shrunk to a pointer), and a
generated docs/endpoint-index.md via scripts/generate-endpoint-index.py
(hooked into scripts/update-openapi.sh). Updates docs/README.md's
reading order and removes the "still to come" placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 07:38:47 +02:00

96 lines
3.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate docs/endpoint-index.md from the OpenAPI spec.
Do not edit docs/endpoint-index.md by hand -- it is generated by this script
from ErsatzTV/wwwroot/openapi/v1.json. Regenerated automatically as part of
scripts/update-openapi.sh.
"""
import json
from pathlib import Path
HTTP_METHODS = ("get", "put", "post", "delete", "options", "head", "patch", "trace")
REPO_ROOT = Path(__file__).resolve().parent.parent
SPEC_PATH = REPO_ROOT / "ErsatzTV" / "wwwroot" / "openapi" / "v1.json"
OUTPUT_PATH = REPO_ROOT / "docs" / "endpoint-index.md"
UNTAGGED = "(untagged)"
def load_operations(spec: dict) -> list[dict]:
"""Flatten the OpenAPI paths object into a list of per-operation records."""
operations = []
for path, path_item in spec.get("paths", {}).items():
for method, operation in path_item.items():
if method.lower() not in HTTP_METHODS:
continue
operations.append(
{
"path": path,
"method": method.upper(),
"operationId": operation.get("operationId", ""),
"summary": operation.get("summary", "") or "",
"tags": operation.get("tags") or [UNTAGGED],
}
)
return operations
def group_by_tag(operations: list[dict]) -> dict[str, list[dict]]:
grouped: dict[str, list[dict]] = {}
for op in operations:
for tag in op["tags"]:
grouped.setdefault(tag, []).append(op)
return grouped
def render(spec: dict) -> str:
operations = load_operations(spec)
grouped = group_by_tag(operations)
tags = sorted(t for t in grouped if t != UNTAGGED)
if UNTAGGED in grouped:
tags.append(UNTAGGED)
lines = []
lines.append("# API endpoint index")
lines.append("")
lines.append(
"*Generated by `scripts/generate-endpoint-index.py` from "
"`ErsatzTV/wwwroot/openapi/v1.json`. Do not edit by hand -- regenerated by "
"`scripts/update-openapi.sh`.*"
)
lines.append("")
lines.append(
f"{len(spec.get('paths', {}))} endpoints, {len(operations)} operations."
)
lines.append("")
for tag in tags:
lines.append(f"## {tag}")
lines.append("")
lines.append("| Method | Path | Operation | Summary |")
lines.append("|---|---|---|---|")
for op in sorted(grouped[tag], key=lambda o: (o["path"], o["method"])):
lines.append(
f"| {op['method']} | `{op['path']}` | {op['operationId']} | {op['summary']} |"
)
lines.append("")
return "\n".join(lines).rstrip("\n") + "\n"
def main() -> None:
spec = json.loads(SPEC_PATH.read_text())
OUTPUT_PATH.write_text(render(spec))
operations = load_operations(spec)
print(
f"Wrote {OUTPUT_PATH.relative_to(REPO_ROOT)}: "
f"{len(spec.get('paths', {}))} endpoints, {len(operations)} operations."
)
if __name__ == "__main__":
main()