import base64 import io from pathlib import Path from flask import Blueprint, current_app, request from flask_jwt_extended import jwt_required from sqlalchemy import func, select from extensions import db from models import Analysis from .utils import ( created, err, no_content, ok, current_api_user, serialize_analysis, ) analyses_bp = Blueprint("api_analyses", __name__, url_prefix="/analyses") # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _remove_analysis_files(analysis: Analysis, storage_root: str) -> None: root = Path(storage_root) for path_attr in ("csv_path", "pdf_path"): rel = getattr(analysis, path_attr, None) if rel: try: root.joinpath(rel).unlink(missing_ok=True) except Exception: pass # --------------------------------------------------------------------------- # Routes # --------------------------------------------------------------------------- @analyses_bp.post("/upload") @jwt_required(optional=True) def upload(): from analyzer.parser import parse_csv from analyzer.grouper import detect_groups from analyzer.stats import compute_overall_stats, compute_group_stats from analyzer.charts import render_group_charts, render_overview_chart from analyzer.pdf_report import generate_pdf file = request.files.get("csv_file") if not file or not file.filename: return err("No csv_file provided.", 400) try: csv_bytes = file.read() df = parse_csv(io.BytesIO(csv_bytes)) groups = detect_groups(df) overall = compute_overall_stats(df) group_stats = compute_group_stats(groups) charts = render_group_charts(groups, y_min=overall["min_speed"], y_max=overall["max_speed"]) overview_chart = render_overview_chart(group_stats) pdf_bytes = generate_pdf(overall, group_stats, charts, overview_chart) except ValueError as e: return err(str(e), 422) pdf_b64 = base64.b64encode(pdf_bytes).decode() saved_id = None user = current_api_user() if user: from storage import save_analysis saved_id = save_analysis( user=user, csv_bytes=csv_bytes, pdf_bytes=pdf_bytes, overall=overall, group_stats=group_stats, filename=file.filename or "upload.csv", ) return ok({ "overall_stats": overall, "group_stats": group_stats, "charts": charts, "overview_chart": overview_chart, "pdf_b64": pdf_b64, "saved_id": saved_id, }) @analyses_bp.get("/") @jwt_required() def list_analyses(): user = current_api_user() if not user: return err("User not found.", 404) try: page = max(1, int(request.args.get("page", 1))) per_page = min(100, max(1, int(request.args.get("per_page", 20)))) except (TypeError, ValueError): page, per_page = 1, 20 total = db.session.scalar( select(func.count()).select_from(Analysis) .where(Analysis.user_id == user.id) ) or 0 analyses = db.session.scalars( select(Analysis) .where(Analysis.user_id == user.id) .order_by(Analysis.created_at.desc()) .offset((page - 1) * per_page) .limit(per_page) ).all() return ok({ "data": [serialize_analysis(a) for a in analyses], "total": total, "page": page, "per_page": per_page, }) @analyses_bp.get("/") @jwt_required(optional=True) def get_analysis(analysis_id: int): a = db.session.get(Analysis, analysis_id) if not a: return err("Analysis not found.", 404) user = current_api_user() is_owner = user and a.user_id == user.id if not a.is_public and not is_owner: return err("Access denied.", 403) return ok(serialize_analysis(a)) @analyses_bp.delete("/") @jwt_required() def delete_analysis(analysis_id: int): user = current_api_user() if not user: return err("User not found.", 404) a = db.session.get(Analysis, analysis_id) if not a: return err("Analysis not found.", 404) if a.user_id != user.id: return err("Access denied.", 403) storage_root = current_app.config["STORAGE_ROOT"] _remove_analysis_files(a, storage_root) db.session.delete(a) db.session.commit() return no_content() @analyses_bp.patch("//visibility") @jwt_required() def toggle_visibility(analysis_id: int): user = current_api_user() if not user: return err("User not found.", 404) a = db.session.get(Analysis, analysis_id) if not a: return err("Analysis not found.", 404) if a.user_id != user.id: return err("Access denied.", 403) body = request.get_json(silent=True) or {} if "is_public" not in body: return err("is_public field is required.", 400) a.is_public = bool(body["is_public"]) db.session.commit() return ok(serialize_analysis(a))