Refactor config loading and add status code docs
- Move config loading to dedicated ConfigLoader class with validation - Add DATABASE_MIGRATIONS.md content to TROUBLESHOOTING.md - Add API_STATUS_CODES.md documenting all API response codes - Update runner.csx to use new config structure - Add check_responses.py validation script - Update config tests for new structure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
90
backend/check_responses.py
Normal file
90
backend/check_responses.py
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate that every API router endpoint has an explicit `responses={}` dict.
|
||||
|
||||
This script runs in CI to ensure no endpoint is merged without OpenAPI
|
||||
response documentation. An endpoint without `responses={}` makes status-code
|
||||
branching impossible for frontend clients.
|
||||
|
||||
Exit codes:
|
||||
0 — all endpoints documented
|
||||
1 — one or more endpoints missing responses={}
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROUTES = {"get", "post", "put", "delete", "patch", "options", "head"}
|
||||
ROUTER_DIR = Path(__file__).parent / "app" / "routers"
|
||||
|
||||
|
||||
class EndpointVisitor(ast.NodeVisitor):
|
||||
"""Walk router files and collect endpoints lacking `responses={}`."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.errors: list[str] = []
|
||||
self._current_path = ""
|
||||
|
||||
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
||||
for decorator in node.decorator_list:
|
||||
if self._is_router_decorator(decorator):
|
||||
self._check_decorator(decorator, node)
|
||||
self.generic_visit(node)
|
||||
|
||||
def _is_router_decorator(self, node: ast.AST) -> bool:
|
||||
match node:
|
||||
case ast.Name():
|
||||
return node.id in ROUTES
|
||||
case ast.Attribute():
|
||||
return node.attr in ROUTES
|
||||
return False
|
||||
|
||||
def _check_decorator(self, decorator: ast.AST, node: ast.FunctionDef) -> None:
|
||||
found_responses = False
|
||||
for child in ast.walk(decorator):
|
||||
if isinstance(child, ast.keyword) and child.arg == "responses":
|
||||
found_responses = True
|
||||
break
|
||||
|
||||
if not found_responses:
|
||||
lineno = node.lineno
|
||||
self.errors.append(
|
||||
f"{self._current_path}:{lineno} — "
|
||||
f"endpoint in {node.name}() lacks `responses={{}}`"
|
||||
)
|
||||
|
||||
|
||||
def check_file(path: Path) -> list[str]:
|
||||
"""Return list of errors for one router file."""
|
||||
source = path.read_text()
|
||||
tree = ast.parse(source, filename=str(path))
|
||||
|
||||
visitor = EndpointVisitor()
|
||||
visitor._current_path = str(path)
|
||||
visitor.visit(tree)
|
||||
return visitor.errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors: list[str] = []
|
||||
|
||||
for py_file in sorted(ROUTER_DIR.glob("*.py")):
|
||||
if py_file.name.startswith("_"):
|
||||
continue
|
||||
errors.extend(check_file(py_file))
|
||||
|
||||
if errors:
|
||||
print("ERRORS: Endpoints missing `responses={}`:")
|
||||
for e in errors:
|
||||
print(f" {e}")
|
||||
print(f"\n{len(errors)} endpoint(s) lack response documentation.")
|
||||
return 1
|
||||
|
||||
print("OK: all router endpoints have `responses={}`")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user