gmal-scope-builder/backend/app/api/ingest.py
DJP e18976fdb2 Initial commit - GMAL Scope Builder
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>
2026-03-27 17:35:14 -04:00

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)