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:
2025-11-28 19:30:17 +01:00
parent 13f0094743
commit 21c13ca39b
236 changed files with 8450 additions and 6545 deletions

View File

@@ -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]