Vibe coded a bit more ... now we have session, attached picture and analysis, MOA group computation
This commit is contained in:
178
blueprints/api/analyses.py
Normal file
178
blueprints/api/analyses.py
Normal file
@@ -0,0 +1,178 @@
|
||||
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))
|
||||
Reference in New Issue
Block a user