""" Code Quality API Endpoints RESTful API for architecture validation and violation management """ from datetime import datetime from fastapi import APIRouter, Depends, Query from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db from app.exceptions import ViolationNotFoundException from app.services.code_quality_service import code_quality_service from models.database.user import User from models.schema.stats import CodeQualityDashboardStatsResponse router = APIRouter() # Pydantic Models for API class ScanResponse(BaseModel): """Response model for a scan""" id: int timestamp: str total_files: int total_violations: int errors: int warnings: int duration_seconds: float triggered_by: str git_commit_hash: str | None class Config: from_attributes = True class ViolationResponse(BaseModel): """Response model for a violation""" id: int scan_id: int rule_id: str rule_name: str severity: str file_path: str line_number: int message: str context: str | None suggestion: str | None status: str assigned_to: int | None resolved_at: str | None resolved_by: int | None resolution_note: str | None created_at: str class Config: from_attributes = True class ViolationListResponse(BaseModel): """Response model for paginated violations list""" violations: list[ViolationResponse] total: int page: int page_size: int total_pages: int class ViolationDetailResponse(ViolationResponse): """Response model for single violation with relationships""" assignments: list = [] comments: list = [] class AssignViolationRequest(BaseModel): """Request model for assigning a violation""" user_id: int = Field(..., description="User ID to assign to") due_date: datetime | None = Field(None, description="Due date for resolution") priority: str = Field( "medium", description="Priority level (low, medium, high, critical)" ) class ResolveViolationRequest(BaseModel): """Request model for resolving a violation""" resolution_note: str = Field(..., description="Note about the resolution") class IgnoreViolationRequest(BaseModel): """Request model for ignoring a violation""" reason: str = Field(..., description="Reason for ignoring") class AddCommentRequest(BaseModel): """Request model for adding a comment""" comment: str = Field(..., min_length=1, description="Comment text") # API Endpoints @router.post("/scan", response_model=ScanResponse) async def trigger_scan( db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api) ): """ Trigger a new architecture scan Requires authentication. Runs the validator script and stores results. Domain exceptions (ScanTimeoutException, ScanParseException) bubble up to global handler. """ scan = code_quality_service.run_scan( db, triggered_by=f"manual:{current_user.username}" ) db.commit() return ScanResponse( id=scan.id, timestamp=scan.timestamp.isoformat(), total_files=scan.total_files, total_violations=scan.total_violations, errors=scan.errors, warnings=scan.warnings, duration_seconds=scan.duration_seconds, triggered_by=scan.triggered_by, git_commit_hash=scan.git_commit_hash, ) @router.get("/scans", response_model=list[ScanResponse]) async def list_scans( limit: int = Query(30, ge=1, le=100, description="Number of scans to return"), db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api), ): """ Get scan history Returns recent scans for trend analysis. """ scans = code_quality_service.get_scan_history(db, limit=limit) return [ ScanResponse( id=scan.id, timestamp=scan.timestamp.isoformat(), total_files=scan.total_files, total_violations=scan.total_violations, errors=scan.errors, warnings=scan.warnings, duration_seconds=scan.duration_seconds, triggered_by=scan.triggered_by, git_commit_hash=scan.git_commit_hash, ) for scan in scans ] @router.get("/violations", response_model=ViolationListResponse) async def list_violations( scan_id: int | None = Query( None, description="Filter by scan ID (defaults to latest)" ), severity: str | None = Query( None, description="Filter by severity (error, warning)" ), status: str | None = Query( None, description="Filter by status (open, assigned, resolved, ignored)" ), rule_id: str | None = Query(None, description="Filter by rule ID"), file_path: str | None = Query( None, description="Filter by file path (partial match)" ), page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(50, ge=1, le=200, description="Items per page"), db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api), ): """ Get violations with filtering and pagination Returns violations from latest scan by default. """ offset = (page - 1) * page_size violations, total = code_quality_service.get_violations( db, scan_id=scan_id, severity=severity, status=status, rule_id=rule_id, file_path=file_path, limit=page_size, offset=offset, ) total_pages = (total + page_size - 1) // page_size return ViolationListResponse( violations=[ ViolationResponse( id=v.id, scan_id=v.scan_id, rule_id=v.rule_id, rule_name=v.rule_name, severity=v.severity, file_path=v.file_path, line_number=v.line_number, message=v.message, context=v.context, suggestion=v.suggestion, status=v.status, assigned_to=v.assigned_to, resolved_at=v.resolved_at.isoformat() if v.resolved_at else None, resolved_by=v.resolved_by, resolution_note=v.resolution_note, created_at=v.created_at.isoformat(), ) for v in violations ], total=total, page=page, page_size=page_size, total_pages=total_pages, ) @router.get("/violations/{violation_id}", response_model=ViolationDetailResponse) async def get_violation( violation_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api), ): """ Get single violation with details Includes assignments and comments. """ violation = code_quality_service.get_violation_by_id(db, violation_id) if not violation: raise ViolationNotFoundException(violation_id) # Format assignments assignments = [ { "id": a.id, "user_id": a.user_id, "assigned_at": a.assigned_at.isoformat(), "assigned_by": a.assigned_by, "due_date": a.due_date.isoformat() if a.due_date else None, "priority": a.priority, } for a in violation.assignments ] # Format comments comments = [ { "id": c.id, "user_id": c.user_id, "comment": c.comment, "created_at": c.created_at.isoformat(), } for c in violation.comments ] return ViolationDetailResponse( id=violation.id, scan_id=violation.scan_id, rule_id=violation.rule_id, rule_name=violation.rule_name, severity=violation.severity, file_path=violation.file_path, line_number=violation.line_number, message=violation.message, context=violation.context, suggestion=violation.suggestion, status=violation.status, assigned_to=violation.assigned_to, resolved_at=( violation.resolved_at.isoformat() if violation.resolved_at else None ), resolved_by=violation.resolved_by, resolution_note=violation.resolution_note, created_at=violation.created_at.isoformat(), assignments=assignments, comments=comments, ) @router.post("/violations/{violation_id}/assign") async def assign_violation( violation_id: int, request: AssignViolationRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api), ): """ Assign violation to a developer Updates violation status to 'assigned'. """ assignment = code_quality_service.assign_violation( db, violation_id=violation_id, user_id=request.user_id, assigned_by=current_user.id, due_date=request.due_date, priority=request.priority, ) db.commit() return { "id": assignment.id, "violation_id": assignment.violation_id, "user_id": assignment.user_id, "assigned_at": assignment.assigned_at.isoformat(), "assigned_by": assignment.assigned_by, "due_date": ( assignment.due_date.isoformat() if assignment.due_date else None ), "priority": assignment.priority, } @router.post("/violations/{violation_id}/resolve") async def resolve_violation( violation_id: int, request: ResolveViolationRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api), ): """ Mark violation as resolved Records resolution timestamp and user. ViolationNotFoundException bubbles up if violation doesn't exist. """ violation = code_quality_service.resolve_violation( db, violation_id=violation_id, resolved_by=current_user.id, resolution_note=request.resolution_note, ) db.commit() return { "id": violation.id, "status": violation.status, "resolved_at": ( violation.resolved_at.isoformat() if violation.resolved_at else None ), "resolved_by": violation.resolved_by, "resolution_note": violation.resolution_note, } @router.post("/violations/{violation_id}/ignore") async def ignore_violation( violation_id: int, request: IgnoreViolationRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api), ): """ Mark violation as ignored (won't fix) Records reason for ignoring. ViolationNotFoundException bubbles up if violation doesn't exist. """ violation = code_quality_service.ignore_violation( db, violation_id=violation_id, ignored_by=current_user.id, reason=request.reason, ) db.commit() return { "id": violation.id, "status": violation.status, "resolved_at": ( violation.resolved_at.isoformat() if violation.resolved_at else None ), "resolved_by": violation.resolved_by, "resolution_note": violation.resolution_note, } @router.post("/violations/{violation_id}/comments") async def add_comment( violation_id: int, request: AddCommentRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api), ): """ Add comment to violation For team collaboration and discussion. """ comment = code_quality_service.add_comment( db, violation_id=violation_id, user_id=current_user.id, comment=request.comment, ) db.commit() return { "id": comment.id, "violation_id": comment.violation_id, "user_id": comment.user_id, "comment": comment.comment, "created_at": comment.created_at.isoformat(), } @router.get("/stats", response_model=CodeQualityDashboardStatsResponse) async def get_dashboard_stats( db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api) ): """ Get dashboard statistics Returns comprehensive stats for the dashboard including: - Total counts by severity and status - Technical debt score - Trend data (last 7 scans) - Top violating files - Violations by rule and module """ stats = code_quality_service.get_dashboard_stats(db) return CodeQualityDashboardStatsResponse(**stats)