vibe-and-thrive 1.6.6 → 1.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/ralph/checks/check-fastapi-responses.py +155 -0
- package/ralph/verify.sh +42 -0
package/package.json
CHANGED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Check that FastAPI endpoints have Pydantic response models defined.
|
|
4
|
+
Ensures Swagger/OpenAPI docs show proper response schemas.
|
|
5
|
+
|
|
6
|
+
Usage: python check-fastapi-responses.py [directory]
|
|
7
|
+
|
|
8
|
+
Exit codes:
|
|
9
|
+
0 - All endpoints have response models
|
|
10
|
+
1 - Some endpoints missing response models
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import NamedTuple
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EndpointIssue(NamedTuple):
|
|
20
|
+
file: str
|
|
21
|
+
line: int
|
|
22
|
+
method: str
|
|
23
|
+
path: str
|
|
24
|
+
issue: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_file(filepath: Path) -> list[EndpointIssue]:
|
|
28
|
+
"""Check a single Python file for FastAPI endpoints without response models."""
|
|
29
|
+
issues = []
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
content = filepath.read_text()
|
|
33
|
+
tree = ast.parse(content)
|
|
34
|
+
except (SyntaxError, UnicodeDecodeError):
|
|
35
|
+
return issues
|
|
36
|
+
|
|
37
|
+
# HTTP methods that should have response models
|
|
38
|
+
http_methods = {'get', 'post', 'put', 'patch', 'delete'}
|
|
39
|
+
|
|
40
|
+
for node in ast.walk(tree):
|
|
41
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
# Check decorators for FastAPI route decorators
|
|
45
|
+
for decorator in node.decorator_list:
|
|
46
|
+
# Handle @router.get("/path") or @app.get("/path")
|
|
47
|
+
if isinstance(decorator, ast.Call):
|
|
48
|
+
if isinstance(decorator.func, ast.Attribute):
|
|
49
|
+
method = decorator.func.attr
|
|
50
|
+
if method not in http_methods:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# Get the path from first argument
|
|
54
|
+
path = "unknown"
|
|
55
|
+
if decorator.args and isinstance(decorator.args[0], ast.Constant):
|
|
56
|
+
path = decorator.args[0].value
|
|
57
|
+
|
|
58
|
+
# Check for response_model in keyword arguments
|
|
59
|
+
has_response_model = any(
|
|
60
|
+
kw.arg == 'response_model'
|
|
61
|
+
for kw in decorator.keywords
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Check for return type annotation
|
|
65
|
+
has_return_annotation = node.returns is not None
|
|
66
|
+
|
|
67
|
+
# Check if return annotation is None or missing
|
|
68
|
+
is_none_return = (
|
|
69
|
+
isinstance(node.returns, ast.Constant) and
|
|
70
|
+
node.returns.value is None
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if not has_response_model and (not has_return_annotation or is_none_return):
|
|
74
|
+
# Skip certain common patterns that don't need response models
|
|
75
|
+
if method == 'delete' and path.endswith('}'):
|
|
76
|
+
continue # DELETE /items/{id} often returns nothing
|
|
77
|
+
|
|
78
|
+
issues.append(EndpointIssue(
|
|
79
|
+
file=str(filepath),
|
|
80
|
+
line=node.lineno,
|
|
81
|
+
method=method.upper(),
|
|
82
|
+
path=path,
|
|
83
|
+
issue="Missing response_model or return type annotation"
|
|
84
|
+
))
|
|
85
|
+
|
|
86
|
+
return issues
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check_directory(directory: Path) -> list[EndpointIssue]:
|
|
90
|
+
"""Check all Python files in directory for FastAPI response model issues."""
|
|
91
|
+
all_issues = []
|
|
92
|
+
|
|
93
|
+
# Common patterns for API files
|
|
94
|
+
patterns = [
|
|
95
|
+
'**/router*.py',
|
|
96
|
+
'**/routes*.py',
|
|
97
|
+
'**/api*.py',
|
|
98
|
+
'**/endpoints*.py',
|
|
99
|
+
'**/views*.py',
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
checked_files = set()
|
|
103
|
+
|
|
104
|
+
for pattern in patterns:
|
|
105
|
+
for filepath in directory.glob(pattern):
|
|
106
|
+
if filepath in checked_files:
|
|
107
|
+
continue
|
|
108
|
+
checked_files.add(filepath)
|
|
109
|
+
all_issues.extend(check_file(filepath))
|
|
110
|
+
|
|
111
|
+
# Also check any file with FastAPI imports
|
|
112
|
+
for filepath in directory.rglob('*.py'):
|
|
113
|
+
if filepath in checked_files:
|
|
114
|
+
continue
|
|
115
|
+
try:
|
|
116
|
+
content = filepath.read_text()
|
|
117
|
+
if 'from fastapi' in content or 'import fastapi' in content:
|
|
118
|
+
if 'APIRouter' in content or '@app.' in content or '@router.' in content:
|
|
119
|
+
all_issues.extend(check_file(filepath))
|
|
120
|
+
checked_files.add(filepath)
|
|
121
|
+
except (UnicodeDecodeError, PermissionError):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
return all_issues
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main():
|
|
128
|
+
directory = Path(sys.argv[1]) if len(sys.argv) > 1 else Path('.')
|
|
129
|
+
|
|
130
|
+
if not directory.exists():
|
|
131
|
+
print(f"Error: Directory not found: {directory}")
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
issues = check_directory(directory)
|
|
135
|
+
|
|
136
|
+
if not issues:
|
|
137
|
+
print("✓ All FastAPI endpoints have response models defined")
|
|
138
|
+
sys.exit(0)
|
|
139
|
+
|
|
140
|
+
print(f"Found {len(issues)} endpoint(s) without response models:\n")
|
|
141
|
+
|
|
142
|
+
for issue in sorted(issues, key=lambda x: (x.file, x.line)):
|
|
143
|
+
print(f" {issue.file}:{issue.line}")
|
|
144
|
+
print(f" {issue.method} {issue.path}")
|
|
145
|
+
print(f" → {issue.issue}\n")
|
|
146
|
+
|
|
147
|
+
print("Fix: Add response_model parameter or return type annotation:")
|
|
148
|
+
print(' @router.get("/items", response_model=list[ItemSchema])')
|
|
149
|
+
print(" async def get_items() -> list[ItemSchema]:")
|
|
150
|
+
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == '__main__':
|
|
155
|
+
main()
|
package/ralph/verify.sh
CHANGED
|
@@ -411,6 +411,45 @@ verify_lint() {
|
|
|
411
411
|
return $failed
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
# Check FastAPI endpoints have Pydantic response models (for Swagger docs)
|
|
415
|
+
run_fastapi_response_check() {
|
|
416
|
+
local check_script="$SCRIPT_DIR/checks/check-fastapi-responses.py"
|
|
417
|
+
|
|
418
|
+
# Skip if check script doesn't exist
|
|
419
|
+
[[ ! -f "$check_script" ]] && return 0
|
|
420
|
+
|
|
421
|
+
# Find FastAPI directories
|
|
422
|
+
local fastapi_dirs=()
|
|
423
|
+
for dir in "." "apps/api" "api" "backend" "server"; do
|
|
424
|
+
if [[ -d "$dir" ]]; then
|
|
425
|
+
# Check for FastAPI imports
|
|
426
|
+
if grep -rq "from fastapi" "$dir"/*.py 2>/dev/null || \
|
|
427
|
+
grep -rq "from fastapi" "$dir"/**/*.py 2>/dev/null; then
|
|
428
|
+
fastapi_dirs+=("$dir")
|
|
429
|
+
fi
|
|
430
|
+
fi
|
|
431
|
+
done
|
|
432
|
+
|
|
433
|
+
[[ ${#fastapi_dirs[@]} -eq 0 ]] && return 0
|
|
434
|
+
|
|
435
|
+
for dir in "${fastapi_dirs[@]}"; do
|
|
436
|
+
echo -n " FastAPI response models ($dir)... "
|
|
437
|
+
|
|
438
|
+
local output
|
|
439
|
+
if output=$(python3 "$check_script" "$dir" 2>&1); then
|
|
440
|
+
print_success "passed"
|
|
441
|
+
else
|
|
442
|
+
print_warning "issues found"
|
|
443
|
+
echo ""
|
|
444
|
+
echo "$output" | head -30 | sed 's/^/ /'
|
|
445
|
+
# Don't fail verification - just warn for now
|
|
446
|
+
# return 1
|
|
447
|
+
fi
|
|
448
|
+
done
|
|
449
|
+
|
|
450
|
+
return 0
|
|
451
|
+
}
|
|
452
|
+
|
|
414
453
|
# Run all checks defined in config.json
|
|
415
454
|
run_configured_checks() {
|
|
416
455
|
local config="$RALPH_DIR/config.json"
|
|
@@ -423,6 +462,9 @@ run_configured_checks() {
|
|
|
423
462
|
return 1
|
|
424
463
|
fi
|
|
425
464
|
|
|
465
|
+
# Auto-detect and run FastAPI response model check
|
|
466
|
+
run_fastapi_response_check
|
|
467
|
+
|
|
426
468
|
# Run pre-commit hooks if available (catches errors before commit attempt)
|
|
427
469
|
if command -v pre-commit &>/dev/null && [[ -f ".pre-commit-config.yaml" ]]; then
|
|
428
470
|
echo -n " pre-commit hooks... "
|