Add queued bank statement analysis with global concurrency limits

This commit is contained in:
A R R R Associates
2026-07-13 15:02:58 +05:30
parent afb098aa75
commit 897c7dad38
6 changed files with 517 additions and 220 deletions
+59 -70
View File
@@ -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()