teljes backend_mentés
This commit is contained in:
236
backend/app/scripts/audit_scanner.py
Normal file
236
backend/app/scripts/audit_scanner.py
Normal file
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Audit Scanner for Codebase Analysis (#42)
|
||||
|
||||
This script performs a comprehensive audit of the Python codebase:
|
||||
1. Recursively scans the backend/app directory for .py files
|
||||
2. Excludes __init__.py files and alembic/versions directory
|
||||
3. Groups files by directory structure (api, services, models, etc.)
|
||||
4. Extracts docstrings and class/function names from each file
|
||||
5. Generates a Markdown audit ledger with checkboxes for tracking
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import ast
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Set
|
||||
import datetime
|
||||
|
||||
# Project root (relative to script location in container)
|
||||
PROJECT_ROOT = Path("/app")
|
||||
BACKEND_DIR = PROJECT_ROOT / "app" # /app/app is the backend root in container
|
||||
OUTPUT_FILE = Path("/app/.roo/audit_ledger_94.md")
|
||||
|
||||
# Directories to exclude
|
||||
EXCLUDE_DIRS = {"__pycache__", ".git", "alembic/versions", "migrations"}
|
||||
EXCLUDE_FILES = {"__init__.py"}
|
||||
|
||||
def extract_python_info(file_path: Path) -> Tuple[str, List[str], List[str]]:
|
||||
"""
|
||||
Extract docstring and class/function names from a Python file.
|
||||
Returns: (docstring, class_names, function_names)
|
||||
"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Try to parse with AST
|
||||
try:
|
||||
tree = ast.parse(content)
|
||||
|
||||
# Extract module docstring
|
||||
docstring = ast.get_docstring(tree) or ""
|
||||
|
||||
# Extract class and function names
|
||||
class_names = []
|
||||
function_names = []
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ClassDef):
|
||||
class_names.append(node.name)
|
||||
elif isinstance(node, ast.FunctionDef):
|
||||
# Only top-level functions (not methods)
|
||||
if not isinstance(node.parent, ast.ClassDef):
|
||||
function_names.append(node.name)
|
||||
|
||||
return docstring, class_names, function_names
|
||||
|
||||
except (SyntaxError, ValueError):
|
||||
# If AST parsing fails, use simple regex extraction
|
||||
docstring_match = re.search(r'"""(.*?)"""', content, re.DOTALL)
|
||||
docstring = docstring_match.group(1).strip() if docstring_match else ""
|
||||
|
||||
# Simple regex for class and function definitions
|
||||
class_matches = re.findall(r'^class\s+(\w+)', content, re.MULTILINE)
|
||||
func_matches = re.findall(r'^def\s+(\w+)', content, re.MULTILINE)
|
||||
|
||||
return docstring, class_matches, func_matches
|
||||
|
||||
except Exception as e:
|
||||
return f"Error reading file: {e}", [], []
|
||||
|
||||
def get_file_summary(docstring: str, class_names: List[str], function_names: List[str]) -> str:
|
||||
"""Create a summary string from extracted information."""
|
||||
parts = []
|
||||
|
||||
if docstring:
|
||||
# Take first line of docstring, max 100 chars
|
||||
first_line = docstring.split('\n')[0].strip()
|
||||
if len(first_line) > 100:
|
||||
first_line = first_line[:97] + "..."
|
||||
parts.append(f'"{first_line}"')
|
||||
|
||||
if class_names:
|
||||
parts.append(f"Classes: {', '.join(class_names[:5])}")
|
||||
if len(class_names) > 5:
|
||||
parts[-1] += f" (+{len(class_names)-5} more)"
|
||||
|
||||
if function_names:
|
||||
parts.append(f"Functions: {', '.join(function_names[:5])}")
|
||||
if len(function_names) > 5:
|
||||
parts[-1] += f" (+{len(function_names)-5} more)"
|
||||
|
||||
return " - ".join(parts) if parts else "No docstring or definitions found"
|
||||
|
||||
def scan_python_files(root_dir: Path) -> Dict[str, List[Tuple[Path, str]]]:
|
||||
"""
|
||||
Scan for Python files and group them by directory category.
|
||||
Returns: {category: [(file_path, summary), ...]}
|
||||
"""
|
||||
categories = {}
|
||||
|
||||
for py_file in root_dir.rglob("*.py"):
|
||||
# Skip excluded directories
|
||||
if any(excluded in str(py_file) for excluded in EXCLUDE_DIRS):
|
||||
continue
|
||||
|
||||
# Skip excluded files
|
||||
if py_file.name in EXCLUDE_FILES:
|
||||
continue
|
||||
|
||||
# Determine category based on directory structure
|
||||
rel_path = py_file.relative_to(root_dir)
|
||||
path_parts = list(rel_path.parts)
|
||||
|
||||
# Categorize based on first few directory levels
|
||||
category = "Other"
|
||||
if len(path_parts) >= 2:
|
||||
if path_parts[0] == "api":
|
||||
category = "API Endpoints"
|
||||
elif path_parts[0] == "services":
|
||||
category = "Services"
|
||||
elif path_parts[0] == "models":
|
||||
category = "Models"
|
||||
elif path_parts[0] == "core":
|
||||
category = "Core"
|
||||
elif path_parts[0] == "workers":
|
||||
category = "Workers"
|
||||
elif path_parts[0] == "scripts":
|
||||
category = "Scripts"
|
||||
elif path_parts[0] == "tests" or path_parts[0] == "tests_internal" or path_parts[0] == "test_outside":
|
||||
category = "Tests"
|
||||
elif path_parts[0] == "crud":
|
||||
category = "CRUD"
|
||||
elif path_parts[0] == "schemas":
|
||||
category = "Schemas"
|
||||
elif path_parts[0] == "templates":
|
||||
category = "Templates"
|
||||
elif path_parts[0] == "static":
|
||||
category = "Static"
|
||||
|
||||
# Extract file info
|
||||
docstring, class_names, function_names = extract_python_info(py_file)
|
||||
summary = get_file_summary(docstring, class_names, function_names)
|
||||
|
||||
# Add to category
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
|
||||
categories[category].append((rel_path, summary))
|
||||
|
||||
return categories
|
||||
|
||||
def generate_markdown(categories: Dict[str, List[Tuple[Path, str]]]) -> str:
|
||||
"""Generate Markdown content from categorized files."""
|
||||
lines = []
|
||||
|
||||
# Header
|
||||
lines.append("# Codebase Audit Ledger (#42)")
|
||||
lines.append("")
|
||||
lines.append(f"*Generated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
|
||||
lines.append(f"*Total files scanned: {sum(len(files) for files in categories.values())}*")
|
||||
lines.append("")
|
||||
lines.append("## 📋 Audit Checklist")
|
||||
lines.append("")
|
||||
lines.append("Check each file after audit completion. Use this ledger to track progress.")
|
||||
lines.append("")
|
||||
|
||||
# Sort categories for consistent output
|
||||
sorted_categories = sorted(categories.items(), key=lambda x: x[0])
|
||||
|
||||
for category, files in sorted_categories:
|
||||
lines.append(f"## {category} (`backend/app/{category.lower().replace(' ', '_')}/...`)")
|
||||
lines.append("")
|
||||
|
||||
# Sort files alphabetically
|
||||
files.sort(key=lambda x: str(x[0]))
|
||||
|
||||
for file_path, summary in files:
|
||||
# Create checkbox and file entry
|
||||
lines.append(f"- [ ] `{file_path}` - {summary}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
# Add statistics
|
||||
lines.append("## 📊 Statistics")
|
||||
lines.append("")
|
||||
lines.append("| Category | File Count |")
|
||||
lines.append("|----------|------------|")
|
||||
for category, files in sorted_categories:
|
||||
lines.append(f"| {category} | {len(files)} |")
|
||||
|
||||
lines.append("")
|
||||
lines.append("## 🎯 Next Steps")
|
||||
lines.append("")
|
||||
lines.append("1. **Review each file** for functionality and dependencies")
|
||||
lines.append("2. **Document findings** in individual audit reports")
|
||||
lines.append("3. **Identify gaps** in test coverage and documentation")
|
||||
lines.append("4. **Prioritize refactoring** based on complexity and criticality")
|
||||
lines.append("")
|
||||
lines.append("*This ledger is automatically generated by `audit_scanner.py`*")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def main():
|
||||
print("🔍 Starting codebase audit scan...")
|
||||
print(f"Scanning directory: {BACKEND_DIR}")
|
||||
|
||||
if not BACKEND_DIR.exists():
|
||||
print(f"Error: Directory {BACKEND_DIR} does not exist!")
|
||||
return 1
|
||||
|
||||
# Scan files
|
||||
categories = scan_python_files(BACKEND_DIR)
|
||||
|
||||
# Generate markdown
|
||||
markdown_content = generate_markdown(categories)
|
||||
|
||||
# Write output
|
||||
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
total_files = sum(len(files) for files in categories.values())
|
||||
print(f"✅ Scan complete! Found {total_files} Python files.")
|
||||
print(f"📄 Report generated: {OUTPUT_FILE}")
|
||||
|
||||
# Print summary
|
||||
print("\n📊 Category breakdown:")
|
||||
for category, files in sorted(categories.items(), key=lambda x: x[0]):
|
||||
print(f" {category}: {len(files)} files")
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
263
backend/app/scripts/seed_v2_0.py
Normal file
263
backend/app/scripts/seed_v2_0.py
Normal file
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Service Finder v2.0 Seed Script (Gamification 2.0 + Mock Service Profiles)
|
||||
Modern, asynchronous SQLAlchemy 2.0 seed script for development and testing.
|
||||
Includes: Superadmin user, Gamification levels (-3 to +10), 15 mock service profiles.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import List, Tuple
|
||||
|
||||
from sqlalchemy import select, delete, text
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
from geoalchemy2 import WKTElement
|
||||
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.identity.identity import User
|
||||
from app.models.gamification.gamification import LevelConfig
|
||||
from app.models.marketplace.service import ServiceProfile, ServiceStatus
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
# Environment safety check
|
||||
ENVIRONMENT = "development" # Change to 'production' in production deployments
|
||||
|
||||
|
||||
async def cleanup_existing_seeds(db):
|
||||
"""Clean up previously seeded data (only in non-production environments)."""
|
||||
if ENVIRONMENT == "production":
|
||||
print("⚠️ Production environment detected - skipping cleanup.")
|
||||
return
|
||||
|
||||
print("🧹 Cleaning up previously seeded data...")
|
||||
|
||||
# Delete mock service profiles (fingerprint starts with 'MOCK-')
|
||||
result = await db.execute(
|
||||
delete(ServiceProfile).where(ServiceProfile.fingerprint.like("MOCK-%"))
|
||||
)
|
||||
print(f" Deleted {result.rowcount} mock service profiles")
|
||||
|
||||
# Delete gamification levels we're about to insert (levels -3 to +10)
|
||||
result = await db.execute(
|
||||
delete(LevelConfig).where(LevelConfig.level_number.between(-3, 10))
|
||||
)
|
||||
print(f" Deleted {result.rowcount} gamification level configs")
|
||||
|
||||
# Delete superadmin user if exists (by email)
|
||||
result = await db.execute(
|
||||
delete(User).where(User.email == "admin@servicefinder.hu")
|
||||
)
|
||||
print(f" Deleted {result.rowcount} superadmin users")
|
||||
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def create_superadmin(db):
|
||||
"""Create superadmin user with admin@servicefinder.hu / admin123 credentials."""
|
||||
stmt = select(User).where(User.email == "admin@servicefinder.hu")
|
||||
existing = (await db.execute(stmt)).scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
print("✅ Superadmin user already exists")
|
||||
return existing
|
||||
|
||||
hashed_password = get_password_hash("admin123")
|
||||
admin = User(
|
||||
email="admin@servicefinder.hu",
|
||||
hashed_password=hashed_password,
|
||||
full_name="System Administrator",
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
is_verified=True,
|
||||
email_verified_at=datetime.utcnow(),
|
||||
)
|
||||
db.add(admin)
|
||||
await db.commit()
|
||||
await db.refresh(admin)
|
||||
print("✅ Superadmin user created: admin@servicefinder.hu / admin123")
|
||||
return admin
|
||||
|
||||
|
||||
async def seed_gamification_levels(db):
|
||||
"""Create Gamification 2.0 levels from -3 (penalty) to +10 (prestige)."""
|
||||
levels = [
|
||||
# Penalty levels (is_penalty = True)
|
||||
(-3, 0, "Börtönviselt", True),
|
||||
(-2, 10, "Büntetőszint 2", True),
|
||||
(-1, 25, "Büntetőszint 1", True),
|
||||
|
||||
# Regular levels (is_penalty = False)
|
||||
(0, 0, "Újonc", False),
|
||||
(1, 50, "Felfedező", False),
|
||||
(2, 150, "Gyakornok", False),
|
||||
(3, 300, "Szakképzett", False),
|
||||
(4, 500, "Szakértő", False),
|
||||
(5, 750, "Mester", False),
|
||||
(6, 1050, "Legenda", False),
|
||||
(7, 1400, "Hős", False),
|
||||
(8, 1800, "Elit", False),
|
||||
(9, 2250, "Zsoldos", False),
|
||||
(10, 2750, "Kalandor", False),
|
||||
]
|
||||
|
||||
inserted = 0
|
||||
for level_num, min_points, rank_name, is_penalty in levels:
|
||||
# Use PostgreSQL upsert to avoid duplicates
|
||||
insert_stmt = insert(LevelConfig).values(
|
||||
level_number=level_num,
|
||||
min_points=min_points,
|
||||
rank_name=rank_name,
|
||||
is_penalty=is_penalty
|
||||
)
|
||||
upsert_stmt = insert_stmt.on_conflict_do_update(
|
||||
index_elements=['level_number'],
|
||||
set_=dict(
|
||||
min_points=min_points,
|
||||
rank_name=rank_name,
|
||||
is_penalty=is_penalty
|
||||
)
|
||||
)
|
||||
await db.execute(upsert_stmt)
|
||||
inserted += 1
|
||||
|
||||
await db.commit()
|
||||
print(f"✅ {inserted} gamification levels seeded (-3 to +10)")
|
||||
return inserted
|
||||
|
||||
|
||||
def generate_hungarian_coordinates(index: int) -> Tuple[float, float]:
|
||||
"""Generate realistic Hungarian coordinates for mock service profiles."""
|
||||
# Major Hungarian cities with their coordinates
|
||||
cities = [
|
||||
(47.4979, 19.0402), # Budapest
|
||||
(46.2530, 20.1482), # Szeged
|
||||
(47.5316, 21.6273), # Debrecen
|
||||
(46.0759, 18.2280), # Pécs
|
||||
(47.2300, 16.6216), # Szombathely
|
||||
(47.9025, 20.3772), # Eger
|
||||
(47.1890, 18.4103), # Székesfehérvár
|
||||
(46.8412, 16.8416), # Zalaegerszeg
|
||||
(48.1033, 20.7786), # Miskolc
|
||||
(46.3833, 18.1333), # Kaposvár
|
||||
(47.4980, 19.0399), # Budapest (different district)
|
||||
(47.5300, 21.6200), # Debrecen (slightly offset)
|
||||
(46.2600, 20.1500), # Szeged (slightly offset)
|
||||
(47.1900, 18.4200), # Székesfehérvár (slightly offset)
|
||||
(46.8400, 16.8500), # Zalaegerszeg (slightly offset)
|
||||
]
|
||||
|
||||
# Add small random offset to make each location unique
|
||||
import random
|
||||
base_lat, base_lon = cities[index % len(cities)]
|
||||
offset_lat = random.uniform(-0.01, 0.01)
|
||||
offset_lon = random.uniform(-0.01, 0.01)
|
||||
|
||||
return (base_lat + offset_lat, base_lon + offset_lon)
|
||||
|
||||
|
||||
async def seed_service_profiles(db, admin_user):
|
||||
"""Create 15 mock service profiles with different statuses and Hungarian coordinates."""
|
||||
statuses = [ServiceStatus.ghost, ServiceStatus.active, ServiceStatus.flagged]
|
||||
status_distribution = [5, 7, 3] # 5 ghost, 7 active, 3 flagged
|
||||
|
||||
service_names = [
|
||||
"AutoCenter Budapest",
|
||||
"Speedy Garage Szeged",
|
||||
"MesterMűhely Debrecen",
|
||||
"First Class Autószerviz Pécs",
|
||||
"Profik Szerviz Szombathely",
|
||||
"TopGear Eger",
|
||||
"Gold Service Székesfehérvár",
|
||||
"Zala Autó Zalaegerszeg",
|
||||
"Borsodi Műhely Miskolc",
|
||||
"Kaposvári Autó Centrum",
|
||||
"Budapest East Garage",
|
||||
"Debrecen North Workshop",
|
||||
"Szeged South Auto",
|
||||
"Fehérvári Speedy",
|
||||
"Zala Pro Motors"
|
||||
]
|
||||
|
||||
inserted = 0
|
||||
status_idx = 0
|
||||
|
||||
for i in range(15):
|
||||
# Determine status based on distribution
|
||||
if i < status_distribution[0]:
|
||||
status = ServiceStatus.ghost
|
||||
elif i < status_distribution[0] + status_distribution[1]:
|
||||
status = ServiceStatus.active
|
||||
else:
|
||||
status = ServiceStatus.flagged
|
||||
|
||||
# Generate coordinates
|
||||
lat, lon = generate_hungarian_coordinates(i)
|
||||
|
||||
# Create WKT element for PostGIS
|
||||
location = WKTElement(f'POINT({lon} {lat})', srid=4326)
|
||||
|
||||
# Create service profile
|
||||
service = ServiceProfile(
|
||||
fingerprint=f"MOCK-{i:03d}-{datetime.utcnow().timestamp():.0f}",
|
||||
location=location,
|
||||
status=status,
|
||||
trust_score=30 if status == ServiceStatus.ghost else 75,
|
||||
is_verified=(status == ServiceStatus.active),
|
||||
contact_phone=f"+36 30 {1000 + i} {2000 + i}",
|
||||
contact_email=f"info@{service_names[i].replace(' ', '').lower()}.hu",
|
||||
website=f"https://{service_names[i].replace(' ', '').lower()}.hu",
|
||||
bio=f"{service_names[i]} - Profi autószerviz Magyarországon.",
|
||||
rating=4.0 + (i % 5) * 0.2,
|
||||
user_ratings_total=10 + i * 5,
|
||||
last_audit_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(service)
|
||||
inserted += 1
|
||||
|
||||
# Commit in batches
|
||||
if inserted % 5 == 0:
|
||||
await db.commit()
|
||||
|
||||
await db.commit()
|
||||
print(f"✅ {inserted} mock service profiles created (ghost:5, active:7, flagged:3)")
|
||||
return inserted
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main seed function."""
|
||||
print("🚀 Service Finder v2.0 Seed Script")
|
||||
print("=" * 50)
|
||||
|
||||
async with SessionLocal() as db:
|
||||
try:
|
||||
# 1. Cleanup (only in non-production)
|
||||
await cleanup_existing_seeds(db)
|
||||
|
||||
# 2. Create superadmin user
|
||||
admin = await create_superadmin(db)
|
||||
|
||||
# 3. Seed gamification levels
|
||||
await seed_gamification_levels(db)
|
||||
|
||||
# 4. Seed service profiles
|
||||
await seed_service_profiles(db, admin)
|
||||
|
||||
print("=" * 50)
|
||||
print("🎉 Seed completed successfully!")
|
||||
print(" - Superadmin: admin@servicefinder.hu / admin123")
|
||||
print(" - Gamification: Levels -3 to +10 configured")
|
||||
print(" - Service Profiles: 15 mock profiles with Hungarian coordinates")
|
||||
print(" - Status distribution: 5 ghost, 7 active, 3 flagged")
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
print(f"❌ Seed failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user