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

@@ -22,15 +22,17 @@ import argparse
import ast
import re
import sys
from pathlib import Path
from typing import List, Dict, Any, Tuple
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Tuple
import yaml
class Severity(Enum):
"""Validation severity levels"""
ERROR = "error"
WARNING = "warning"
INFO = "info"
@@ -39,6 +41,7 @@ class Severity(Enum):
@dataclass
class Violation:
"""Represents an architectural rule violation"""
rule_id: str
rule_name: str
severity: Severity
@@ -52,6 +55,7 @@ class Violation:
@dataclass
class ValidationResult:
"""Results of architecture validation"""
violations: List[Violation] = field(default_factory=list)
files_checked: int = 0
rules_applied: int = 0
@@ -82,7 +86,7 @@ class ArchitectureValidator:
print(f"❌ Configuration file not found: {self.config_path}")
sys.exit(1)
with open(self.config_path, 'r') as f:
with open(self.config_path, "r") as f:
config = yaml.safe_load(f)
print(f"📋 Loaded architecture rules: {config.get('project', 'unknown')}")
@@ -126,7 +130,7 @@ class ArchitectureValidator:
continue
content = file_path.read_text()
lines = content.split('\n')
lines = content.split("\n")
# API-001: Check for Pydantic model usage
self._check_pydantic_usage(file_path, content, lines)
@@ -147,8 +151,8 @@ class ArchitectureValidator:
return
# Check for response_model in route decorators
route_pattern = r'@router\.(get|post|put|delete|patch)'
dict_return_pattern = r'return\s+\{.*\}'
route_pattern = r"@router\.(get|post|put|delete|patch)"
dict_return_pattern = r"return\s+\{.*\}"
for i, line in enumerate(lines, 1):
# Check for dict returns in endpoints
@@ -166,47 +170,51 @@ class ArchitectureValidator:
if re.search(dict_return_pattern, func_line):
self._add_violation(
rule_id="API-001",
rule_name=rule['name'],
rule_name=rule["name"],
severity=Severity.ERROR,
file_path=file_path,
line_number=j + 1,
message="Endpoint returns raw dict instead of Pydantic model",
context=func_line.strip(),
suggestion="Define a Pydantic response model and use response_model parameter"
suggestion="Define a Pydantic response model and use response_model parameter",
)
def _check_no_business_logic_in_endpoints(self, file_path: Path, content: str, lines: List[str]):
def _check_no_business_logic_in_endpoints(
self, file_path: Path, content: str, lines: List[str]
):
"""API-002: Ensure no business logic in endpoints"""
rule = self._get_rule("API-002")
if not rule:
return
anti_patterns = [
(r'db\.add\(', "Database operations should be in service layer"),
(r'db\.commit\(\)', "Database commits should be in service layer"),
(r'db\.query\(', "Database queries should be in service layer"),
(r'db\.execute\(', "Database operations should be in service layer"),
(r"db\.add\(", "Database operations should be in service layer"),
(r"db\.commit\(\)", "Database commits should be in service layer"),
(r"db\.query\(", "Database queries should be in service layer"),
(r"db\.execute\(", "Database operations should be in service layer"),
]
for i, line in enumerate(lines, 1):
# Skip service method calls (allowed)
if '_service.' in line or 'service.' in line:
if "_service." in line or "service." in line:
continue
for pattern, message in anti_patterns:
if re.search(pattern, line):
self._add_violation(
rule_id="API-002",
rule_name=rule['name'],
rule_name=rule["name"],
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message=message,
context=line.strip(),
suggestion="Move database operations to service layer"
suggestion="Move database operations to service layer",
)
def _check_endpoint_exception_handling(self, file_path: Path, content: str, lines: List[str]):
def _check_endpoint_exception_handling(
self, file_path: Path, content: str, lines: List[str]
):
"""API-003: Check proper exception handling in endpoints"""
rule = self._get_rule("API-003")
if not rule:
@@ -222,40 +230,41 @@ class ArchitectureValidator:
if isinstance(node, ast.FunctionDef):
# Check if it's a route handler
has_router_decorator = any(
isinstance(d, ast.Call) and
isinstance(d.func, ast.Attribute) and
getattr(d.func.value, 'id', None) == 'router'
isinstance(d, ast.Call)
and isinstance(d.func, ast.Attribute)
and getattr(d.func.value, "id", None) == "router"
for d in node.decorator_list
)
if has_router_decorator:
# Check if function body has try/except
has_try_except = any(
isinstance(child, ast.Try)
for child in ast.walk(node)
isinstance(child, ast.Try) for child in ast.walk(node)
)
# Check if function calls service methods
has_service_call = any(
isinstance(child, ast.Call) and
isinstance(child.func, ast.Attribute) and
'service' in getattr(child.func.value, 'id', '').lower()
isinstance(child, ast.Call)
and isinstance(child.func, ast.Attribute)
and "service" in getattr(child.func.value, "id", "").lower()
for child in ast.walk(node)
)
if has_service_call and not has_try_except:
self._add_violation(
rule_id="API-003",
rule_name=rule['name'],
rule_name=rule["name"],
severity=Severity.WARNING,
file_path=file_path,
line_number=node.lineno,
message=f"Endpoint '{node.name}' calls service but lacks exception handling",
context=f"def {node.name}(...)",
suggestion="Wrap service calls in try/except and convert to HTTPException"
suggestion="Wrap service calls in try/except and convert to HTTPException",
)
def _check_endpoint_authentication(self, file_path: Path, content: str, lines: List[str]):
def _check_endpoint_authentication(
self, file_path: Path, content: str, lines: List[str]
):
"""API-004: Check authentication on endpoints"""
rule = self._get_rule("API-004")
if not rule:
@@ -264,24 +273,28 @@ class ArchitectureValidator:
# This is a warning-level check
# Look for endpoints without Depends(get_current_*)
for i, line in enumerate(lines, 1):
if '@router.' in line and ('post' in line or 'put' in line or 'delete' in line):
if "@router." in line and (
"post" in line or "put" in line or "delete" in line
):
# Check next 5 lines for auth
has_auth = False
for j in range(i, min(i + 5, len(lines))):
if 'Depends(get_current_' in lines[j]:
if "Depends(get_current_" in lines[j]:
has_auth = True
break
if not has_auth and 'include_in_schema=False' not in ' '.join(lines[i:i+5]):
if not has_auth and "include_in_schema=False" not in " ".join(
lines[i : i + 5]
):
self._add_violation(
rule_id="API-004",
rule_name=rule['name'],
rule_name=rule["name"],
severity=Severity.WARNING,
file_path=file_path,
line_number=i,
message="Endpoint may be missing authentication",
context=line.strip(),
suggestion="Add Depends(get_current_user) or similar if endpoint should be protected"
suggestion="Add Depends(get_current_user) or similar if endpoint should be protected",
)
def _validate_service_layer(self, target_path: Path):
@@ -296,7 +309,7 @@ class ArchitectureValidator:
continue
content = file_path.read_text()
lines = content.split('\n')
lines = content.split("\n")
# SVC-001: No HTTPException in services
self._check_no_http_exception_in_services(file_path, content, lines)
@@ -307,38 +320,45 @@ class ArchitectureValidator:
# SVC-003: DB session as parameter
self._check_db_session_parameter(file_path, content, lines)
def _check_no_http_exception_in_services(self, file_path: Path, content: str, lines: List[str]):
def _check_no_http_exception_in_services(
self, file_path: Path, content: str, lines: List[str]
):
"""SVC-001: Services must not raise HTTPException"""
rule = self._get_rule("SVC-001")
if not rule:
return
for i, line in enumerate(lines, 1):
if 'raise HTTPException' in line:
if "raise HTTPException" in line:
self._add_violation(
rule_id="SVC-001",
rule_name=rule['name'],
rule_name=rule["name"],
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message="Service raises HTTPException - use domain exceptions instead",
context=line.strip(),
suggestion="Create custom exception class (e.g., VendorNotFoundError) and raise that"
suggestion="Create custom exception class (e.g., VendorNotFoundError) and raise that",
)
if 'from fastapi import HTTPException' in line or 'from fastapi.exceptions import HTTPException' in line:
if (
"from fastapi import HTTPException" in line
or "from fastapi.exceptions import HTTPException" in line
):
self._add_violation(
rule_id="SVC-001",
rule_name=rule['name'],
rule_name=rule["name"],
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message="Service imports HTTPException - services should not know about HTTP",
context=line.strip(),
suggestion="Remove HTTPException import and use domain exceptions"
suggestion="Remove HTTPException import and use domain exceptions",
)
def _check_service_exceptions(self, file_path: Path, content: str, lines: List[str]):
def _check_service_exceptions(
self, file_path: Path, content: str, lines: List[str]
):
"""SVC-002: Check for proper exception handling"""
rule = self._get_rule("SVC-002")
if not rule:
@@ -346,19 +366,21 @@ class ArchitectureValidator:
for i, line in enumerate(lines, 1):
# Check for generic Exception raises
if re.match(r'\s*raise Exception\(', line):
if re.match(r"\s*raise Exception\(", line):
self._add_violation(
rule_id="SVC-002",
rule_name=rule['name'],
rule_name=rule["name"],
severity=Severity.WARNING,
file_path=file_path,
line_number=i,
message="Service raises generic Exception - use specific domain exception",
context=line.strip(),
suggestion="Create custom exception class for this error case"
suggestion="Create custom exception class for this error case",
)
def _check_db_session_parameter(self, file_path: Path, content: str, lines: List[str]):
def _check_db_session_parameter(
self, file_path: Path, content: str, lines: List[str]
):
"""SVC-003: Service methods should accept db session as parameter"""
rule = self._get_rule("SVC-003")
if not rule:
@@ -366,16 +388,16 @@ class ArchitectureValidator:
# Check for SessionLocal() creation in service files
for i, line in enumerate(lines, 1):
if 'SessionLocal()' in line and 'class' not in line:
if "SessionLocal()" in line and "class" not in line:
self._add_violation(
rule_id="SVC-003",
rule_name=rule['name'],
rule_name=rule["name"],
severity=Severity.ERROR,
file_path=file_path,
line_number=i,
message="Service creates database session internally",
context=line.strip(),
suggestion="Accept db: Session as method parameter instead"
suggestion="Accept db: Session as method parameter instead",
)
def _validate_models(self, target_path: Path):
@@ -391,11 +413,11 @@ class ArchitectureValidator:
continue
content = file_path.read_text()
lines = content.split('\n')
lines = content.split("\n")
# Check for mixing SQLAlchemy and Pydantic
for i, line in enumerate(lines, 1):
if re.search(r'class.*\(Base.*,.*BaseModel.*\)', line):
if re.search(r"class.*\(Base.*,.*BaseModel.*\)", line):
self._add_violation(
rule_id="MDL-002",
rule_name="Separate SQLAlchemy and Pydantic models",
@@ -404,7 +426,7 @@ class ArchitectureValidator:
line_number=i,
message="Model mixes SQLAlchemy Base and Pydantic BaseModel",
context=line.strip(),
suggestion="Keep SQLAlchemy models and Pydantic models separate"
suggestion="Keep SQLAlchemy models and Pydantic models separate",
)
def _validate_exceptions(self, target_path: Path):
@@ -418,11 +440,11 @@ class ArchitectureValidator:
continue
content = file_path.read_text()
lines = content.split('\n')
lines = content.split("\n")
# EXC-002: Check for bare except
for i, line in enumerate(lines, 1):
if re.match(r'\s*except\s*:', line):
if re.match(r"\s*except\s*:", line):
self._add_violation(
rule_id="EXC-002",
rule_name="No bare except clauses",
@@ -431,7 +453,7 @@ class ArchitectureValidator:
line_number=i,
message="Bare except clause catches all exceptions including system exits",
context=line.strip(),
suggestion="Specify exception type: except ValueError: or except Exception:"
suggestion="Specify exception type: except ValueError: or except Exception:",
)
def _validate_javascript(self, target_path: Path):
@@ -443,11 +465,16 @@ class ArchitectureValidator:
for file_path in js_files:
content = file_path.read_text()
lines = content.split('\n')
lines = content.split("\n")
# JS-001: Check for window.apiClient
for i, line in enumerate(lines, 1):
if 'window.apiClient' in line and '//' not in line[:line.find('window.apiClient')] if 'window.apiClient' in line else True:
if (
"window.apiClient" in line
and "//" not in line[: line.find("window.apiClient")]
if "window.apiClient" in line
else True
):
self._add_violation(
rule_id="JS-001",
rule_name="Use apiClient directly",
@@ -456,14 +483,14 @@ class ArchitectureValidator:
line_number=i,
message="Use apiClient directly instead of window.apiClient",
context=line.strip(),
suggestion="Replace window.apiClient with apiClient"
suggestion="Replace window.apiClient with apiClient",
)
# JS-002: Check for console usage
for i, line in enumerate(lines, 1):
if re.search(r'console\.(log|warn|error)', line):
if re.search(r"console\.(log|warn|error)", line):
# Skip if it's a comment or bootstrap message
if '//' in line or '' in line or 'eslint-disable' in line:
if "//" in line or "" in line or "eslint-disable" in line:
continue
self._add_violation(
@@ -474,7 +501,7 @@ class ArchitectureValidator:
line_number=i,
message="Use centralized logger instead of console",
context=line.strip()[:80],
suggestion="Use window.LogConfig.createLogger('moduleName')"
suggestion="Use window.LogConfig.createLogger('moduleName')",
)
def _validate_templates(self, target_path: Path):
@@ -486,14 +513,16 @@ class ArchitectureValidator:
for file_path in template_files:
# Skip base template and partials
if 'base.html' in file_path.name or 'partials' in str(file_path):
if "base.html" in file_path.name or "partials" in str(file_path):
continue
content = file_path.read_text()
lines = content.split('\n')
lines = content.split("\n")
# TPL-001: Check for extends
has_extends = any('{% extends' in line and 'admin/base.html' in line for line in lines)
has_extends = any(
"{% extends" in line and "admin/base.html" in line for line in lines
)
if not has_extends:
self._add_violation(
@@ -504,23 +533,29 @@ class ArchitectureValidator:
line_number=1,
message="Admin template does not extend admin/base.html",
context=file_path.name,
suggestion="Add {% extends 'admin/base.html' %} at the top"
suggestion="Add {% extends 'admin/base.html' %} at the top",
)
def _get_rule(self, rule_id: str) -> Dict[str, Any]:
"""Get rule configuration by ID"""
# Look in different rule categories
for category in ['api_endpoint_rules', 'service_layer_rules', 'model_rules',
'exception_rules', 'javascript_rules', 'template_rules']:
for category in [
"api_endpoint_rules",
"service_layer_rules",
"model_rules",
"exception_rules",
"javascript_rules",
"template_rules",
]:
rules = self.config.get(category, [])
for rule in rules:
if rule.get('id') == rule_id:
if rule.get("id") == rule_id:
return rule
return None
def _should_ignore_file(self, file_path: Path) -> bool:
"""Check if file should be ignored"""
ignore_patterns = self.config.get('ignore', {}).get('files', [])
ignore_patterns = self.config.get("ignore", {}).get("files", [])
for pattern in ignore_patterns:
if file_path.match(pattern):
@@ -528,9 +563,17 @@ class ArchitectureValidator:
return False
def _add_violation(self, rule_id: str, rule_name: str, severity: Severity,
file_path: Path, line_number: int, message: str,
context: str = "", suggestion: str = ""):
def _add_violation(
self,
rule_id: str,
rule_name: str,
severity: Severity,
file_path: Path,
line_number: int,
message: str,
context: str = "",
suggestion: str = "",
):
"""Add a violation to results"""
violation = Violation(
rule_id=rule_id,
@@ -540,7 +583,7 @@ class ArchitectureValidator:
line_number=line_number,
message=message,
context=context,
suggestion=suggestion
suggestion=suggestion,
)
self.result.violations.append(violation)
@@ -590,24 +633,34 @@ class ArchitectureValidator:
violations_json = []
for v in self.result.violations:
rel_path = str(v.file_path.relative_to(self.project_root)) if self.project_root in v.file_path.parents else str(v.file_path)
violations_json.append({
'rule_id': v.rule_id,
'rule_name': v.rule_name,
'severity': v.severity.value,
'file_path': rel_path,
'line_number': v.line_number,
'message': v.message,
'context': v.context or '',
'suggestion': v.suggestion or ''
})
rel_path = (
str(v.file_path.relative_to(self.project_root))
if self.project_root in v.file_path.parents
else str(v.file_path)
)
violations_json.append(
{
"rule_id": v.rule_id,
"rule_name": v.rule_name,
"severity": v.severity.value,
"file_path": rel_path,
"line_number": v.line_number,
"message": v.message,
"context": v.context or "",
"suggestion": v.suggestion or "",
}
)
output = {
'files_checked': self.result.files_checked,
'total_violations': len(self.result.violations),
'errors': len([v for v in self.result.violations if v.severity == Severity.ERROR]),
'warnings': len([v for v in self.result.violations if v.severity == Severity.WARNING]),
'violations': violations_json
"files_checked": self.result.files_checked,
"total_violations": len(self.result.violations),
"errors": len(
[v for v in self.result.violations if v.severity == Severity.ERROR]
),
"warnings": len(
[v for v in self.result.violations if v.severity == Severity.WARNING]
),
"violations": violations_json,
}
print(json.dumps(output, indent=2))
@@ -616,7 +669,11 @@ class ArchitectureValidator:
def _print_violation(self, v: Violation):
"""Print a single violation"""
rel_path = v.file_path.relative_to(self.project_root) if self.project_root in v.file_path.parents else v.file_path
rel_path = (
v.file_path.relative_to(self.project_root)
if self.project_root in v.file_path.parents
else v.file_path
)
print(f"\n [{v.rule_id}] {v.rule_name}")
print(f" File: {rel_path}:{v.line_number}")
@@ -634,40 +691,40 @@ def main():
parser = argparse.ArgumentParser(
description="Validate architecture patterns in codebase",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
epilog=__doc__,
)
parser.add_argument(
'path',
nargs='?',
"path",
nargs="?",
type=Path,
default=Path.cwd(),
help="Path to validate (default: current directory)"
help="Path to validate (default: current directory)",
)
parser.add_argument(
'-c', '--config',
"-c",
"--config",
type=Path,
default=Path.cwd() / '.architecture-rules.yaml',
help="Path to architecture rules config (default: .architecture-rules.yaml)"
default=Path.cwd() / ".architecture-rules.yaml",
help="Path to architecture rules config (default: .architecture-rules.yaml)",
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help="Show detailed output including context"
"-v",
"--verbose",
action="store_true",
help="Show detailed output including context",
)
parser.add_argument(
'--errors-only',
action='store_true',
help="Only show errors, suppress warnings"
"--errors-only", action="store_true", help="Only show errors, suppress warnings"
)
parser.add_argument(
'--json',
action='store_true',
help="Output results as JSON (for programmatic use)"
"--json",
action="store_true",
help="Output results as JSON (for programmatic use)",
)
args = parser.parse_args()
@@ -687,5 +744,5 @@ def main():
sys.exit(exit_code)
if __name__ == '__main__':
if __name__ == "__main__":
main()