rmi-backend/scripts/export_openapi.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

102 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""Export the auto-generated OpenAPI schema from the FastAPI app.
Per T14 (G09 FIX) - never hand-maintain openapi.yaml/openapi.json. Always
auto-generate from the live FastAPI routes. SDK generation (Python, TS)
and MCP manifests depend on this being a faithful snapshot of the code.
Usage:
python scripts/export_openapi.py # writes openapi.json to repo root
python scripts/export_openapi.py --check # verify path count + exit 1 if < threshold
python scripts/export_openapi.py --stdout # print to stdout (for CI piping)
python scripts/export_openapi.py --out path # write to a custom path
Why this exists:
- SDK generators (sdks/python/, sdks/typescript/) consume openapi.json
- MCP directory manifests need accurate tool paths
- Schemathesis contract tests run against the live schema
- Drift between code and docs = drift between reality and what we promise
The export is committed to git so SDK generation works without running the
server. CI re-validates it on every PR.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
def get_openapi_schema() -> dict:
"""Build the FastAPI app and return its auto-generated OpenAPI schema."""
# Ensure repo root is on sys.path so `app.factory` imports work
repo_root = Path(__file__).resolve().parents[1]
if str(repo_root) not in sys.path:
sys.path.insert(0, str(repo_root))
from app.factory import create_app
app = create_app()
return app.openapi()
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--out",
default=str(Path(__file__).resolve().parents[1] / "openapi.json"),
help="Output path for the schema (default: <repo>/openapi.json)",
)
parser.add_argument(
"--check",
action="store_true",
help="Verify path count meets minimum and exit 1 if not",
)
parser.add_argument(
"--stdout",
action="store_true",
help="Print schema to stdout instead of writing a file",
)
parser.add_argument(
"--min-paths",
type=int,
default=40,
help="Minimum path count for --check (default: 40)",
)
args = parser.parse_args()
schema = get_openapi_schema()
path_count = len(schema.get("paths", {}))
schema_count = len(schema.get("components", {}).get("schemas", {}))
if args.stdout:
print(json.dumps(schema, indent=2))
if args.check:
if path_count < args.min_paths:
print(
f"FAIL: OpenAPI has only {path_count} paths "
f"(minimum {args.min_paths}). "
f"Factory imports are likely broken - check that all "
f"routers in app/api/v1/ and app/domain/* import cleanly.",
file=sys.stderr,
)
return 1
print(f"OK: OpenAPI has {path_count} paths, {schema_count} schemas")
if not args.stdout:
out_path = Path(args.out)
out_path.write_text(json.dumps(schema, indent=2))
print(
f"Wrote {out_path} ({path_count} paths, {schema_count} schemas, "
f"{out_path.stat().st_size // 1024} KB)"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())