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>
47 lines
1.6 KiB
Python
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)
|