refactor: decompose monolith — app.py 10,600→143 lines, 30+ modules

Fase 0 complete: extract all business logic, auth, database, and 176
endpoints from monolithic app.py into dedicated modules.

Structure:
- app.py: FastAPI hub (CORS, routers, startup/shutdown)
- models.py: 13 Pydantic request models
- utils.py: shared utilities (embed status, upload validation, process registries)
- config.py: constants, env vars, model catalogs
- auth/: crypto, jwt_auth, oidc, rate_limit
- database/: db(), init_db(), schema DDL
- services/: genai, compliance, chat, terraform, embeddings, cis_reports, mcp
- routes/: 13 APIRouter modules (auth, users, oci_config, oci_explorer,
  genai, mcp, adb, embeddings, reports, chat, terraform, settings, cis_engine)

Also: README updated with OCIR auth instructions for manual docker run.

86 tests passing.
This commit is contained in:
nogueiraguh
2026-04-06 15:20:10 -03:00
parent 426bde563e
commit 1135e9d6a9
36 changed files with 11179 additions and 10514 deletions

60
backend/utils.py Normal file
View File

@@ -0,0 +1,60 @@
"""Shared utilities — upload validation, embed status tracking, global process registries."""
import json
import asyncio
from pathlib import Path
from fastapi import HTTPException, UploadFile
from config import _EMBED_STATUS_DIR, _MAX_UPLOAD_BYTES
# ── Global process registries (used by routes and shutdown handler) ───────────
running_reports: dict[str, asyncio.subprocess.Process] = {}
running_terraform: dict[str, asyncio.subprocess.Process] = {}
# ── Embed status (file-based) ────────────────────────────────────────────────
def set_embed_status(task_id: str, data: dict):
(_EMBED_STATUS_DIR / f"{task_id}.json").write_text(json.dumps(data), encoding="utf-8")
def get_embed_status(task_id: str) -> dict | None:
p = _EMBED_STATUS_DIR / f"{task_id}.json"
if p.exists():
try:
return json.loads(p.read_text(encoding="utf-8"))
except Exception:
return None
return None
def update_embed_status(task_id: str, updates: dict):
current = get_embed_status(task_id) or {}
current.update(updates)
set_embed_status(task_id, current)
# ── File upload validation ────────────────────────────────────────────────────
async def validate_upload(file: UploadFile, allowed_exts: set[str] | None = None, max_bytes: int = _MAX_UPLOAD_BYTES):
"""Validate uploaded file size and extension. Returns file data bytes."""
data = await file.read()
if len(data) > max_bytes:
raise HTTPException(400, f"Arquivo excede o limite de {max_bytes // (1024*1024)}MB")
if allowed_exts and file.filename:
ext = Path(file.filename).suffix.lower()
if ext not in allowed_exts:
raise HTTPException(400, f"Extensão '{ext}' não permitida. Aceitas: {', '.join(sorted(allowed_exts))}")
return data
# ── Report file classification ────────────────────────────────────────────────
def classify_report_file(fname: str) -> str:
"""Classify a report file into a category based on its filename."""
fl = fname.lower()
if "summary_report" in fl: return "summary"
if "error_report" in fl or "error" in fl and fl.endswith(".csv"): return "error"
if fl.startswith("obp_") and "findings" in fl: return "obp_finding"
if fl.startswith("obp_") and "best_practices" in fl: return "obp_best_practice"
if fl.startswith("obp_"): return "obp_finding"
if fl.startswith("raw_data_"): return "raw_data"
if fl.startswith("cis_"): return "cis_finding"
if "consolidated_report" in fl: return "consolidated"
if fl.endswith(".png"): return "diagram"
return "other"