litellm/scripts/check_openapi_docs.py

73 lines
2.5 KiB
Python
Executable file

#!/usr/bin/env python3
"""List the POST operations in the proxy's OpenAPI document that Swagger UI cannot explain.
Every finding is one line, ``<METHOD> <path> <code>``, so scripts/openapi_docs_gate.py can
count them per rule the same delta-vs-base way as the other lint gates. The rules:
OA001 no request body schema (the handler reads ``request.body()`` and never told FastAPI
what is in it, see ``inline_request_body`` in custom_openapi_spec.py for the fix)
OA002 no description (the handler has no docstring and the route no ``description=``)
OA003 the 200 response has no schema (no ``response_model`` and no ``responses={200: ...}``)
"""
from __future__ import annotations
import json
import sys
from collections.abc import Iterator, Mapping
from typing import Final, NamedTuple
GATED_METHODS: Final = frozenset({"post"})
class Violation(NamedTuple):
method: str
path: str
code: str
def _as_mapping(node: object) -> Mapping[str, object]:
return node if isinstance(node, Mapping) else {}
def _content_schema(container: object) -> object:
content: Final = _as_mapping(_as_mapping(container).get("content"))
return next((schema for media in content.values() if (schema := _as_mapping(media).get("schema"))), None)
def _operation_codes(operation: Mapping[str, object]) -> Iterator[str]:
if not _content_schema(operation.get("requestBody")):
yield "OA001"
description: Final = operation.get("description")
if not isinstance(description, str) or not description.strip():
yield "OA002"
if not _content_schema(_as_mapping(operation.get("responses")).get("200")):
yield "OA003"
def violations(spec: Mapping[str, object]) -> tuple[Violation, ...]:
paths: Final = _as_mapping(spec.get("paths"))
return tuple(
Violation(method.upper(), path, code)
for path, operations in sorted(paths.items())
for method, operation in sorted(_as_mapping(operations).items())
if method in GATED_METHODS
for code in _operation_codes(_as_mapping(operation))
) # comprehension-ok: one pass over path, method and code is the whole scan
def proxy_spec() -> Mapping[str, object]:
from litellm.proxy.proxy_server import app
return _as_mapping(app.openapi())
def main(argv: list[str] = sys.argv[1:]) -> int:
spec: Final = json.loads(open(argv[0]).read()) if argv else proxy_spec()
for violation in violations(spec):
print(f"{violation.method} {violation.path} {violation.code}")
return 0
if __name__ == "__main__":
raise SystemExit(main())