style: apply black and isort formatting across entire codebase
- Standardize quote style (single to double quotes) - Reorder and group imports alphabetically - Fix line breaks and indentation for consistency - Apply PEP 8 formatting standards Also updated Makefile to exclude both venv and .venv from code quality checks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,22 +3,20 @@ Code Quality Service
|
||||
Business logic for managing architecture scans and violations
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from pathlib import Path
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, desc
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from app.models.architecture_scan import (
|
||||
ArchitectureScan,
|
||||
ArchitectureViolation,
|
||||
ArchitectureRule,
|
||||
ViolationAssignment,
|
||||
ViolationComment
|
||||
)
|
||||
from sqlalchemy import desc, func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.architecture_scan import (ArchitectureRule, ArchitectureScan,
|
||||
ArchitectureViolation,
|
||||
ViolationAssignment,
|
||||
ViolationComment)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,7 +24,7 @@ logger = logging.getLogger(__name__)
|
||||
class CodeQualityService:
|
||||
"""Service for managing code quality scans and violations"""
|
||||
|
||||
def run_scan(self, db: Session, triggered_by: str = 'manual') -> ArchitectureScan:
|
||||
def run_scan(self, db: Session, triggered_by: str = "manual") -> ArchitectureScan:
|
||||
"""
|
||||
Run architecture validator and store results in database
|
||||
|
||||
@@ -49,10 +47,10 @@ class CodeQualityService:
|
||||
start_time = datetime.now()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['python', 'scripts/validate_architecture.py', '--json'],
|
||||
["python", "scripts/validate_architecture.py", "--json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300 # 5 minute timeout
|
||||
timeout=300, # 5 minute timeout
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Architecture scan timed out after 5 minutes")
|
||||
@@ -63,17 +61,17 @@ class CodeQualityService:
|
||||
# Parse JSON output (get only the JSON part, skip progress messages)
|
||||
try:
|
||||
# Find the JSON part in stdout
|
||||
lines = result.stdout.strip().split('\n')
|
||||
lines = result.stdout.strip().split("\n")
|
||||
json_start = -1
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith('{'):
|
||||
if line.strip().startswith("{"):
|
||||
json_start = i
|
||||
break
|
||||
|
||||
if json_start == -1:
|
||||
raise ValueError("No JSON output found")
|
||||
|
||||
json_output = '\n'.join(lines[json_start:])
|
||||
json_output = "\n".join(lines[json_start:])
|
||||
data = json.loads(json_output)
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
logger.error(f"Failed to parse validator output: {e}")
|
||||
@@ -84,33 +82,33 @@ class CodeQualityService:
|
||||
# Create scan record
|
||||
scan = ArchitectureScan(
|
||||
timestamp=datetime.now(),
|
||||
total_files=data.get('files_checked', 0),
|
||||
total_violations=data.get('total_violations', 0),
|
||||
errors=data.get('errors', 0),
|
||||
warnings=data.get('warnings', 0),
|
||||
total_files=data.get("files_checked", 0),
|
||||
total_violations=data.get("total_violations", 0),
|
||||
errors=data.get("errors", 0),
|
||||
warnings=data.get("warnings", 0),
|
||||
duration_seconds=duration,
|
||||
triggered_by=triggered_by,
|
||||
git_commit_hash=git_commit
|
||||
git_commit_hash=git_commit,
|
||||
)
|
||||
db.add(scan)
|
||||
db.flush() # Get scan.id
|
||||
|
||||
# Create violation records
|
||||
violations_data = data.get('violations', [])
|
||||
violations_data = data.get("violations", [])
|
||||
logger.info(f"Creating {len(violations_data)} violation records")
|
||||
|
||||
for v in violations_data:
|
||||
violation = ArchitectureViolation(
|
||||
scan_id=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.get('context', ''),
|
||||
suggestion=v.get('suggestion', ''),
|
||||
status='open'
|
||||
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.get("context", ""),
|
||||
suggestion=v.get("suggestion", ""),
|
||||
status="open",
|
||||
)
|
||||
db.add(violation)
|
||||
|
||||
@@ -122,7 +120,11 @@ class CodeQualityService:
|
||||
|
||||
def get_latest_scan(self, db: Session) -> Optional[ArchitectureScan]:
|
||||
"""Get the most recent scan"""
|
||||
return db.query(ArchitectureScan).order_by(desc(ArchitectureScan.timestamp)).first()
|
||||
return (
|
||||
db.query(ArchitectureScan)
|
||||
.order_by(desc(ArchitectureScan.timestamp))
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_scan_by_id(self, db: Session, scan_id: int) -> Optional[ArchitectureScan]:
|
||||
"""Get scan by ID"""
|
||||
@@ -139,10 +141,12 @@ class CodeQualityService:
|
||||
Returns:
|
||||
List of ArchitectureScan objects, newest first
|
||||
"""
|
||||
return db.query(ArchitectureScan)\
|
||||
.order_by(desc(ArchitectureScan.timestamp))\
|
||||
.limit(limit)\
|
||||
return (
|
||||
db.query(ArchitectureScan)
|
||||
.order_by(desc(ArchitectureScan.timestamp))
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_violations(
|
||||
self,
|
||||
@@ -153,7 +157,7 @@ class CodeQualityService:
|
||||
rule_id: str = None,
|
||||
file_path: str = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0
|
||||
offset: int = 0,
|
||||
) -> Tuple[List[ArchitectureViolation], int]:
|
||||
"""
|
||||
Get violations with filtering and pagination
|
||||
@@ -194,24 +198,32 @@ class CodeQualityService:
|
||||
query = query.filter(ArchitectureViolation.rule_id == rule_id)
|
||||
|
||||
if file_path:
|
||||
query = query.filter(ArchitectureViolation.file_path.like(f'%{file_path}%'))
|
||||
query = query.filter(ArchitectureViolation.file_path.like(f"%{file_path}%"))
|
||||
|
||||
# Get total count
|
||||
total = query.count()
|
||||
|
||||
# Get page of results
|
||||
violations = query.order_by(
|
||||
ArchitectureViolation.severity.desc(),
|
||||
ArchitectureViolation.file_path
|
||||
).limit(limit).offset(offset).all()
|
||||
violations = (
|
||||
query.order_by(
|
||||
ArchitectureViolation.severity.desc(), ArchitectureViolation.file_path
|
||||
)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.all()
|
||||
)
|
||||
|
||||
return violations, total
|
||||
|
||||
def get_violation_by_id(self, db: Session, violation_id: int) -> Optional[ArchitectureViolation]:
|
||||
def get_violation_by_id(
|
||||
self, db: Session, violation_id: int
|
||||
) -> Optional[ArchitectureViolation]:
|
||||
"""Get single violation with details"""
|
||||
return db.query(ArchitectureViolation).filter(
|
||||
ArchitectureViolation.id == violation_id
|
||||
).first()
|
||||
return (
|
||||
db.query(ArchitectureViolation)
|
||||
.filter(ArchitectureViolation.id == violation_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
def assign_violation(
|
||||
self,
|
||||
@@ -220,7 +232,7 @@ class CodeQualityService:
|
||||
user_id: int,
|
||||
assigned_by: int,
|
||||
due_date: datetime = None,
|
||||
priority: str = 'medium'
|
||||
priority: str = "medium",
|
||||
) -> ViolationAssignment:
|
||||
"""
|
||||
Assign violation to a developer
|
||||
@@ -239,7 +251,7 @@ class CodeQualityService:
|
||||
# Update violation status
|
||||
violation = self.get_violation_by_id(db, violation_id)
|
||||
if violation:
|
||||
violation.status = 'assigned'
|
||||
violation.status = "assigned"
|
||||
violation.assigned_to = user_id
|
||||
|
||||
# Create assignment record
|
||||
@@ -248,7 +260,7 @@ class CodeQualityService:
|
||||
user_id=user_id,
|
||||
assigned_by=assigned_by,
|
||||
due_date=due_date,
|
||||
priority=priority
|
||||
priority=priority,
|
||||
)
|
||||
db.add(assignment)
|
||||
db.commit()
|
||||
@@ -257,11 +269,7 @@ class CodeQualityService:
|
||||
return assignment
|
||||
|
||||
def resolve_violation(
|
||||
self,
|
||||
db: Session,
|
||||
violation_id: int,
|
||||
resolved_by: int,
|
||||
resolution_note: str
|
||||
self, db: Session, violation_id: int, resolved_by: int, resolution_note: str
|
||||
) -> ArchitectureViolation:
|
||||
"""
|
||||
Mark violation as resolved
|
||||
@@ -279,7 +287,7 @@ class CodeQualityService:
|
||||
if not violation:
|
||||
raise ValueError(f"Violation {violation_id} not found")
|
||||
|
||||
violation.status = 'resolved'
|
||||
violation.status = "resolved"
|
||||
violation.resolved_at = datetime.now()
|
||||
violation.resolved_by = resolved_by
|
||||
violation.resolution_note = resolution_note
|
||||
@@ -289,11 +297,7 @@ class CodeQualityService:
|
||||
return violation
|
||||
|
||||
def ignore_violation(
|
||||
self,
|
||||
db: Session,
|
||||
violation_id: int,
|
||||
ignored_by: int,
|
||||
reason: str
|
||||
self, db: Session, violation_id: int, ignored_by: int, reason: str
|
||||
) -> ArchitectureViolation:
|
||||
"""
|
||||
Mark violation as ignored/won't fix
|
||||
@@ -311,7 +315,7 @@ class CodeQualityService:
|
||||
if not violation:
|
||||
raise ValueError(f"Violation {violation_id} not found")
|
||||
|
||||
violation.status = 'ignored'
|
||||
violation.status = "ignored"
|
||||
violation.resolved_at = datetime.now()
|
||||
violation.resolved_by = ignored_by
|
||||
violation.resolution_note = f"Ignored: {reason}"
|
||||
@@ -321,11 +325,7 @@ class CodeQualityService:
|
||||
return violation
|
||||
|
||||
def add_comment(
|
||||
self,
|
||||
db: Session,
|
||||
violation_id: int,
|
||||
user_id: int,
|
||||
comment: str
|
||||
self, db: Session, violation_id: int, user_id: int, comment: str
|
||||
) -> ViolationComment:
|
||||
"""
|
||||
Add comment to violation
|
||||
@@ -340,9 +340,7 @@ class CodeQualityService:
|
||||
ViolationComment object
|
||||
"""
|
||||
comment_obj = ViolationComment(
|
||||
violation_id=violation_id,
|
||||
user_id=user_id,
|
||||
comment=comment
|
||||
violation_id=violation_id, user_id=user_id, comment=comment
|
||||
)
|
||||
db.add(comment_obj)
|
||||
db.commit()
|
||||
@@ -360,79 +358,95 @@ class CodeQualityService:
|
||||
latest_scan = self.get_latest_scan(db)
|
||||
if not latest_scan:
|
||||
return {
|
||||
'total_violations': 0,
|
||||
'errors': 0,
|
||||
'warnings': 0,
|
||||
'open': 0,
|
||||
'assigned': 0,
|
||||
'resolved': 0,
|
||||
'ignored': 0,
|
||||
'technical_debt_score': 100,
|
||||
'trend': [],
|
||||
'by_severity': {},
|
||||
'by_rule': {},
|
||||
'by_module': {},
|
||||
'top_files': []
|
||||
"total_violations": 0,
|
||||
"errors": 0,
|
||||
"warnings": 0,
|
||||
"open": 0,
|
||||
"assigned": 0,
|
||||
"resolved": 0,
|
||||
"ignored": 0,
|
||||
"technical_debt_score": 100,
|
||||
"trend": [],
|
||||
"by_severity": {},
|
||||
"by_rule": {},
|
||||
"by_module": {},
|
||||
"top_files": [],
|
||||
}
|
||||
|
||||
# Get violation counts by status
|
||||
status_counts = db.query(
|
||||
ArchitectureViolation.status,
|
||||
func.count(ArchitectureViolation.id)
|
||||
).filter(
|
||||
ArchitectureViolation.scan_id == latest_scan.id
|
||||
).group_by(ArchitectureViolation.status).all()
|
||||
status_counts = (
|
||||
db.query(ArchitectureViolation.status, func.count(ArchitectureViolation.id))
|
||||
.filter(ArchitectureViolation.scan_id == latest_scan.id)
|
||||
.group_by(ArchitectureViolation.status)
|
||||
.all()
|
||||
)
|
||||
|
||||
status_dict = {status: count for status, count in status_counts}
|
||||
|
||||
# Get violations by severity
|
||||
severity_counts = db.query(
|
||||
ArchitectureViolation.severity,
|
||||
func.count(ArchitectureViolation.id)
|
||||
).filter(
|
||||
ArchitectureViolation.scan_id == latest_scan.id
|
||||
).group_by(ArchitectureViolation.severity).all()
|
||||
severity_counts = (
|
||||
db.query(
|
||||
ArchitectureViolation.severity, func.count(ArchitectureViolation.id)
|
||||
)
|
||||
.filter(ArchitectureViolation.scan_id == latest_scan.id)
|
||||
.group_by(ArchitectureViolation.severity)
|
||||
.all()
|
||||
)
|
||||
|
||||
by_severity = {sev: count for sev, count in severity_counts}
|
||||
|
||||
# Get violations by rule
|
||||
rule_counts = db.query(
|
||||
ArchitectureViolation.rule_id,
|
||||
func.count(ArchitectureViolation.id)
|
||||
).filter(
|
||||
ArchitectureViolation.scan_id == latest_scan.id
|
||||
).group_by(ArchitectureViolation.rule_id).all()
|
||||
rule_counts = (
|
||||
db.query(
|
||||
ArchitectureViolation.rule_id, func.count(ArchitectureViolation.id)
|
||||
)
|
||||
.filter(ArchitectureViolation.scan_id == latest_scan.id)
|
||||
.group_by(ArchitectureViolation.rule_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
by_rule = {rule: count for rule, count in sorted(rule_counts, key=lambda x: x[1], reverse=True)[:10]}
|
||||
by_rule = {
|
||||
rule: count
|
||||
for rule, count in sorted(rule_counts, key=lambda x: x[1], reverse=True)[
|
||||
:10
|
||||
]
|
||||
}
|
||||
|
||||
# Get top violating files
|
||||
file_counts = db.query(
|
||||
ArchitectureViolation.file_path,
|
||||
func.count(ArchitectureViolation.id).label('count')
|
||||
).filter(
|
||||
ArchitectureViolation.scan_id == latest_scan.id
|
||||
).group_by(ArchitectureViolation.file_path)\
|
||||
.order_by(desc('count'))\
|
||||
.limit(10).all()
|
||||
file_counts = (
|
||||
db.query(
|
||||
ArchitectureViolation.file_path,
|
||||
func.count(ArchitectureViolation.id).label("count"),
|
||||
)
|
||||
.filter(ArchitectureViolation.scan_id == latest_scan.id)
|
||||
.group_by(ArchitectureViolation.file_path)
|
||||
.order_by(desc("count"))
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
|
||||
top_files = [{'file': file, 'count': count} for file, count in file_counts]
|
||||
top_files = [{"file": file, "count": count} for file, count in file_counts]
|
||||
|
||||
# Get violations by module (extract module from file path)
|
||||
by_module = {}
|
||||
violations = db.query(ArchitectureViolation.file_path).filter(
|
||||
ArchitectureViolation.scan_id == latest_scan.id
|
||||
).all()
|
||||
violations = (
|
||||
db.query(ArchitectureViolation.file_path)
|
||||
.filter(ArchitectureViolation.scan_id == latest_scan.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
for v in violations:
|
||||
path_parts = v.file_path.split('/')
|
||||
path_parts = v.file_path.split("/")
|
||||
if len(path_parts) >= 2:
|
||||
module = '/'.join(path_parts[:2]) # e.g., 'app/api'
|
||||
module = "/".join(path_parts[:2]) # e.g., 'app/api'
|
||||
else:
|
||||
module = path_parts[0]
|
||||
by_module[module] = by_module.get(module, 0) + 1
|
||||
|
||||
# Sort by count and take top 10
|
||||
by_module = dict(sorted(by_module.items(), key=lambda x: x[1], reverse=True)[:10])
|
||||
by_module = dict(
|
||||
sorted(by_module.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||
)
|
||||
|
||||
# Calculate technical debt score
|
||||
tech_debt_score = self.calculate_technical_debt_score(db, latest_scan.id)
|
||||
@@ -441,29 +455,29 @@ class CodeQualityService:
|
||||
trend_scans = self.get_scan_history(db, limit=7)
|
||||
trend = [
|
||||
{
|
||||
'timestamp': scan.timestamp.isoformat(),
|
||||
'violations': scan.total_violations,
|
||||
'errors': scan.errors,
|
||||
'warnings': scan.warnings
|
||||
"timestamp": scan.timestamp.isoformat(),
|
||||
"violations": scan.total_violations,
|
||||
"errors": scan.errors,
|
||||
"warnings": scan.warnings,
|
||||
}
|
||||
for scan in reversed(trend_scans) # Oldest first for chart
|
||||
]
|
||||
|
||||
return {
|
||||
'total_violations': latest_scan.total_violations,
|
||||
'errors': latest_scan.errors,
|
||||
'warnings': latest_scan.warnings,
|
||||
'open': status_dict.get('open', 0),
|
||||
'assigned': status_dict.get('assigned', 0),
|
||||
'resolved': status_dict.get('resolved', 0),
|
||||
'ignored': status_dict.get('ignored', 0),
|
||||
'technical_debt_score': tech_debt_score,
|
||||
'trend': trend,
|
||||
'by_severity': by_severity,
|
||||
'by_rule': by_rule,
|
||||
'by_module': by_module,
|
||||
'top_files': top_files,
|
||||
'last_scan': latest_scan.timestamp.isoformat() if latest_scan else None
|
||||
"total_violations": latest_scan.total_violations,
|
||||
"errors": latest_scan.errors,
|
||||
"warnings": latest_scan.warnings,
|
||||
"open": status_dict.get("open", 0),
|
||||
"assigned": status_dict.get("assigned", 0),
|
||||
"resolved": status_dict.get("resolved", 0),
|
||||
"ignored": status_dict.get("ignored", 0),
|
||||
"technical_debt_score": tech_debt_score,
|
||||
"trend": trend,
|
||||
"by_severity": by_severity,
|
||||
"by_rule": by_rule,
|
||||
"by_module": by_module,
|
||||
"top_files": top_files,
|
||||
"last_scan": latest_scan.timestamp.isoformat() if latest_scan else None,
|
||||
}
|
||||
|
||||
def calculate_technical_debt_score(self, db: Session, scan_id: int = None) -> int:
|
||||
@@ -497,10 +511,7 @@ class CodeQualityService:
|
||||
"""Get current git commit hash"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'rev-parse', 'HEAD'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
["git", "rev-parse", "HEAD"], capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()[:40]
|
||||
|
||||
Reference in New Issue
Block a user