Add queued bank statement analysis with global concurrency limits
This commit is contained in:
@@ -3,8 +3,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, File, Form, Request, UploadFile
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from starlette.background import BackgroundTask
|
||||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
||||
|
||||
from app.core.db.common import CommonSessionLocal
|
||||
from app.core.http_responses import ui_access_denied, not_found_response
|
||||
@@ -14,7 +13,7 @@ from app.core.templating import templates
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
|
||||
from .parsers.registry import BANK_OPTIONS
|
||||
from .service import can_use, create_job, save_uploads, analyze_job, resolve_owned_job, delete_job
|
||||
from .service import can_use, create_job_folder, delete_owned_job, enqueue_job, ensure_worker_started, get_owned_job, job_view, list_user_jobs, save_uploads
|
||||
|
||||
router = APIRouter(prefix="/tools/bank-statement-analyzer", tags=["bank-statement-analyzer-ui"])
|
||||
|
||||
@@ -47,105 +46,99 @@ def _auth(request, db):
|
||||
|
||||
|
||||
@router.get("")
|
||||
def index(request: Request):
|
||||
def index(request: Request, job: str | None = None):
|
||||
ensure_worker_started()
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, roles, denied = _auth(request, db)
|
||||
if denied:
|
||||
return denied
|
||||
return templates.TemplateResponse(
|
||||
"modules/bank_statement_analyzer/templates/bank_statement_analyzer/index.html",
|
||||
_ctx(request, db, user, error=""),
|
||||
)
|
||||
selected_job = get_owned_job(user.id, job) if job else None
|
||||
recent = [job_view(item) for item in list_user_jobs(user.id, limit=8)]
|
||||
return templates.TemplateResponse("modules/bank_statement_analyzer/templates/bank_statement_analyzer/index.html", _ctx(request, db, user, error="", active_job=job_view(selected_job) if selected_job else None, recent_jobs=recent))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/analyze")
|
||||
async def analyze(
|
||||
request: Request,
|
||||
csrf_token: str = Form(...),
|
||||
bank_selection: str = Form("auto"),
|
||||
financial_year: str = Form(""),
|
||||
customer_name: str = Form(""),
|
||||
account_number: str = Form(""),
|
||||
enable_classification: str | None = Form(None),
|
||||
statements: list[UploadFile] = File(...),
|
||||
):
|
||||
async def analyze(request: Request, csrf_token: str = Form(...), bank_selection: str = Form("auto"), financial_year: str = Form(""), customer_name: str = Form(""), account_number: str = Form(""), enable_classification: str | None = Form(None), statements: list[UploadFile] = File(...)):
|
||||
db = CommonSessionLocal()
|
||||
job_dir: Path | None = None
|
||||
selected_bank = bank_selection if bank_selection in dict(BANK_OPTIONS) else "auto"
|
||||
classification_enabled = enable_classification == "1"
|
||||
job_dir: Path | None = None
|
||||
try:
|
||||
user, roles, denied = _auth(request, db)
|
||||
if denied:
|
||||
return denied
|
||||
validate_csrf(request, csrf_token)
|
||||
job_id, input_dir, output_dir = create_job(user, roles)
|
||||
job_id, input_dir, _output_dir = create_job_folder(user, roles)
|
||||
job_dir = input_dir.parent
|
||||
paths = await save_uploads(statements, input_dir)
|
||||
summary = analyze_job(
|
||||
user=user,
|
||||
roles=roles,
|
||||
job_id=job_id,
|
||||
paths=paths,
|
||||
output_dir=output_dir,
|
||||
customer_override=customer_name,
|
||||
account_override=account_number,
|
||||
bank_selection=selected_bank,
|
||||
financial_year=financial_year,
|
||||
classification_enabled=classification_enabled,
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
"modules/bank_statement_analyzer/templates/bank_statement_analyzer/result.html",
|
||||
_ctx(request, db, user, summary=summary),
|
||||
)
|
||||
enqueue_job(user=user, roles=roles, job_id=job_id, paths=paths, job_dir=job_dir, bank_selection=selected_bank, financial_year=financial_year, customer_override=customer_name, account_override=account_number, classification_enabled=classification_enabled)
|
||||
return RedirectResponse(f"/tools/bank-statement-analyzer?job={job_id}#analysis-status", status_code=303)
|
||||
except Exception as exc:
|
||||
if job_dir:
|
||||
import shutil
|
||||
shutil.rmtree(job_dir, ignore_errors=True)
|
||||
user = get_current_user(request, db=db)
|
||||
if not user:
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
return templates.TemplateResponse(
|
||||
"modules/bank_statement_analyzer/templates/bank_statement_analyzer/index.html",
|
||||
_ctx(
|
||||
request,
|
||||
db,
|
||||
user,
|
||||
error=str(exc),
|
||||
selected_bank=selected_bank,
|
||||
financial_year=financial_year,
|
||||
classification_enabled=classification_enabled,
|
||||
),
|
||||
status_code=400,
|
||||
)
|
||||
recent = [job_view(item) for item in list_user_jobs(user.id, limit=8)]
|
||||
return templates.TemplateResponse("modules/bank_statement_analyzer/templates/bank_statement_analyzer/index.html", _ctx(request, db, user, error=str(exc), active_job=None, recent_jobs=recent, selected_bank=selected_bank, financial_year=financial_year, classification_enabled=classification_enabled), status_code=400)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/{job_id}/download")
|
||||
@router.get("/jobs")
|
||||
def jobs(request: Request):
|
||||
ensure_worker_started()
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, roles, denied = _auth(request, db)
|
||||
if denied:
|
||||
return denied
|
||||
items = [job_view(item) for item in list_user_jobs(user.id, limit=100)]
|
||||
return templates.TemplateResponse("modules/bank_statement_analyzer/templates/bank_statement_analyzer/jobs.html", _ctx(request, db, user, jobs=items))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/jobs/{job_id}/status")
|
||||
def status(job_id: str, request: Request):
|
||||
ensure_worker_started()
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, roles, denied = _auth(request, db)
|
||||
if denied:
|
||||
return JSONResponse({"detail": "Access denied"}, status_code=403)
|
||||
job = get_owned_job(user.id, job_id)
|
||||
if not job:
|
||||
return JSONResponse({"detail": "Analysis job not found"}, status_code=404)
|
||||
view = job_view(job)
|
||||
return JSONResponse({"id": view["id"], "status": view["status"], "progress_percent": view["progress_percent"], "queue_position": view["queue_position"], "estimated_wait": view["estimated_wait"], "download_ready": view["download_ready"], "error_message": view["error_message"]})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/jobs/{job_id}/download")
|
||||
def download(job_id: str, request: Request):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, roles, denied = _auth(request, db)
|
||||
if denied:
|
||||
return denied
|
||||
try:
|
||||
job, meta = resolve_owned_job(user, roles, job_id)
|
||||
except FileNotFoundError:
|
||||
return not_found_response(request, "Analysis job not found or already cleaned up.")
|
||||
output = job / "Output" / meta["output_file"]
|
||||
if not output.is_file():
|
||||
return not_found_response(request, "Analysis workbook not found.")
|
||||
return FileResponse(
|
||||
path=output,
|
||||
filename="Bank_Statement_Analysis.xlsx",
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
background=BackgroundTask(delete_job, job),
|
||||
)
|
||||
job = get_owned_job(user.id, job_id)
|
||||
if not job:
|
||||
return not_found_response(request, "Analysis job not found or expired.")
|
||||
view = job_view(job)
|
||||
if not view["download_ready"]:
|
||||
return not_found_response(request, "Analysis workbook is not ready or has expired.")
|
||||
return FileResponse(path=job.output_file, filename="Bank_Statement_Analysis.xlsx", media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/{job_id}/delete")
|
||||
@router.post("/jobs/{job_id}/delete")
|
||||
def delete(job_id: str, request: Request, csrf_token: str = Form(...)):
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
@@ -153,11 +146,7 @@ def delete(job_id: str, request: Request, csrf_token: str = Form(...)):
|
||||
if denied:
|
||||
return denied
|
||||
validate_csrf(request, csrf_token)
|
||||
try:
|
||||
job, _ = resolve_owned_job(user, roles, job_id)
|
||||
delete_job(job)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return RedirectResponse("/tools/bank-statement-analyzer", status_code=303)
|
||||
delete_owned_job(user.id, job_id)
|
||||
return RedirectResponse("/tools/bank-statement-analyzer/jobs", status_code=303)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
Reference in New Issue
Block a user