oliver-sales-ops-platform/backend/app/api/ingest.py
DJP 5259032b22 Port GMAL ingestion + browse endpoints from V1
The full GMAL catalog now ingests cleanly into V2 — verified counts match
V1: 390 assets / 243 with hour routes / 120 roles / 8,660 hour records /
695 service lines / 165 role-level mappings.

The parser is the V1 implementation, narrowed to clear only GMAL-owned
tables (V1 also blew away projects/matches/ratecard_lines, which V2
doesn't own — opportunities live in their own state-machine tables).

Browse endpoints (/api/gmal/{assets,assets/{id},assets/{id}/family,roles,
stats}) are ported from V1 unchanged. Editor write endpoints and AI
description regeneration are deferred until the GMAL Editor UI lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:38:00 -04:00

47 lines
1.6 KiB
Python

"""GMAL data ingestion endpoint — admin-only in Phase 2 once role gating lands."""
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 because openpyxl is synchronous.
"""
if file:
filepath = f"/tmp/{file.filename}"
content = await file.read()
with open(filepath, "wb") as f:
f.write(content)
else:
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")
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)