Dockerized web app (FastAPI + React + PostgreSQL) for scoping client ratecards against the GMAL master asset database. Features: - GMAL data ingestion from Excel (390 assets, 120 roles, 5 model types) - AI-powered document parsing and asset extraction (Claude Opus 4.6) - AI matching engine with parallel batching, confidence scoring, caveats - Ratecard builder with hours x volume calculation - Excel and PDF export - GMAL browser and inline editor - AI cost tracking per project (persisted to DB) - Debug panel for AI call inspection - Dark theme UI with gold (#FFC407) accent Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
50 lines
1.7 KiB
Python
50 lines
1.7 KiB
Python
"""GMAL data ingestion endpoint."""
|
|
|
|
import logging
|
|
import os
|
|
|
|
from fastapi import APIRouter, HTTPException, UploadFile, File
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.config import settings
|
|
from app.schemas.gmal import IngestResult
|
|
from app.services.excel_parser import parse_gmal_workbook
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@router.post("/ingest", response_model=IngestResult)
|
|
async def ingest_gmal_data(file: UploadFile | None = File(None)):
|
|
"""Ingest GMAL data from Excel file.
|
|
|
|
If no file is uploaded, uses the default file from the data directory.
|
|
Uses a synchronous DB session since openpyxl is synchronous.
|
|
"""
|
|
if file:
|
|
# Save uploaded file temporarily
|
|
filepath = f"/tmp/{file.filename}"
|
|
content = await file.read()
|
|
with open(filepath, "wb") as f:
|
|
f.write(content)
|
|
else:
|
|
# Use default data file
|
|
filepath = os.path.join(settings.data_dir, "U-Studio GMAL Asset Job Routes Apr25 ForFranky.xlsx")
|
|
if not os.path.exists(filepath):
|
|
raise HTTPException(status_code=404, detail="Default GMAL file not found in data directory")
|
|
|
|
# Use sync engine for openpyxl parsing
|
|
sync_engine = create_engine(settings.database_url_sync)
|
|
|
|
try:
|
|
with Session(sync_engine) as db:
|
|
result = parse_gmal_workbook(filepath, db)
|
|
return IngestResult(**result)
|
|
except Exception as e:
|
|
logger.error(f"Ingestion failed: {e}", exc_info=True)
|
|
raise HTTPException(status_code=500, detail=f"Ingestion failed: {str(e)}")
|
|
finally:
|
|
sync_engine.dispose()
|
|
if file and os.path.exists(filepath):
|
|
os.remove(filepath)
|