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
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>
92 lines
2.9 KiB
Python
Executable File
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()
|