179 lines
5.1 KiB
Python
179 lines
5.1 KiB
Python
|
|
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("/<int:analysis_id>")
|
||
|
|
@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("/<int:analysis_id>")
|
||
|
|
@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("/<int:analysis_id>/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))
|