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.
61 lines
2.8 KiB
Python
61 lines
2.8 KiB
Python
"""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"
|