Files
ersatztv/scripts/generate-endpoint-index.py
timothyandtimothy d4c72697f2
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Skipped
Build ErsatzTV Image / Delimiter ban (release path) (push) Successful in 28s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 8m29s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 5m54s
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (push) Successful in 5m59s
Build ErsatzTV Image / Build & push image (amd64) (push) Failing after 15s
feat(780): commit a ruff config and enforce it in CI (#813)
Python lint here was a property of the operator's laptop: the global instructions
say to run ruff, no workflow ran it, and with no committed config ruff fell back
to whichever ~/.config/ruff/ruff.toml the machine happened to have.

- ruff.toml at the root, pinned ruff==0.12.11 in the script-tests job.
- Both lint steps pass an EXPLICIT population from `git ls-files` with
  `--no-force-exclude`, never `ruff check .` — an `exclude` empties a
  discovery-based run into a GREEN one (top level empties both commands, [lint]
  empties check, [format] empties format --check), and `ruff check .` over zero
  files exits 0 with only a stderr warning. Guarded by an empty-population arm.
- Tree clean: 74 findings at 706674272, 57 fixed in code, 17 per-site noqa with
  reasons inline. S105 deliberately per-site, not a directory blanket. RUF100
  selected so a suppression that suppresses nothing is itself a finding.
- pyright stays ungated; reasoning in the record.

Both steps witnessed red on the runner against the shipped bodies: run 2173 job
9176 (ruff check) and run 2170 job 9163 (ruff format --check).

Docs: new record ci.python-lint-ruff-config-committed, ci.script-tests-job
cross-ref, docs/ci-cd.md (also correcting a stale ~190-tests/~10s figure to the
measured 773 tests / ~4.5 min), docs/defect-shapes-773.md §5.2 resolved.

fixes #780

Co-authored-by: Timothy <timothy@noreply.gitea.tblindustries.be>
2026-08-22 00:33:18 +00:00

92 lines
2.9 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()