- Tier mapping on projects: configurable label→complexity mapping
- Presets: A/B/C, 1/2/3, Gold/Silver/Bronze
- Stored as JSON on project.tier_mapping
- ClientAsset.client_tier field for tracking which tier an asset belongs to
- GMAL family endpoint: GET /gmal/assets/{id}/family returns all complexity variants
- Looks up by asset_name (NOT by GMAL number increment)
- Verified: families share asset_name across non-sequential GMAL IDs
- Expand to Tiers: POST /projects/{id}/expand-tiers
- Splits each matched asset into N tier variants (one per tier)
- Finds correct GMAL variant by asset_name + complexity_level query
- Creates new ClientAsset + Match per tier with correct GMAL
- Removes original un-tiered asset after expansion
- Frontend: tier preset buttons + expand button on Match Review tab
- Tier tags shown with label → complexity mapping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
3.8 KiB
Python
110 lines
3.8 KiB
Python
"""Project CRUD endpoints."""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy import select, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.models.project import Project, ClientAsset, ProjectStatus
|
|
from app.models.gmal import ModelType
|
|
from app.schemas.project import ProjectCreate, ProjectUpdate, ProjectOut
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("", response_model=ProjectOut)
|
|
async def create_project(data: ProjectCreate, db: AsyncSession = Depends(get_db)):
|
|
project = Project(
|
|
name=data.name,
|
|
client_name=data.client_name,
|
|
description=data.description,
|
|
model_type=ModelType(data.model_type),
|
|
)
|
|
db.add(project)
|
|
await db.commit()
|
|
await db.refresh(project)
|
|
return _project_out(project, 0)
|
|
|
|
|
|
@router.get("", response_model=list[ProjectOut])
|
|
async def list_projects(db: AsyncSession = Depends(get_db)):
|
|
result = await db.execute(select(Project).order_by(Project.created_at.desc()))
|
|
projects = result.scalars().all()
|
|
|
|
out = []
|
|
for p in projects:
|
|
count_result = await db.execute(
|
|
select(func.count(ClientAsset.id)).where(ClientAsset.project_id == p.id)
|
|
)
|
|
count = count_result.scalar() or 0
|
|
out.append(_project_out(p, count))
|
|
return out
|
|
|
|
|
|
@router.get("/{project_id}", response_model=ProjectOut)
|
|
async def get_project(project_id: int, db: AsyncSession = Depends(get_db)):
|
|
project = await _get_project(project_id, db)
|
|
count_result = await db.execute(
|
|
select(func.count(ClientAsset.id)).where(ClientAsset.project_id == project.id)
|
|
)
|
|
return _project_out(project, count_result.scalar() or 0)
|
|
|
|
|
|
@router.put("/{project_id}", response_model=ProjectOut)
|
|
async def update_project(project_id: int, data: ProjectUpdate, db: AsyncSession = Depends(get_db)):
|
|
project = await _get_project(project_id, db)
|
|
|
|
if data.name is not None:
|
|
project.name = data.name
|
|
if data.client_name is not None:
|
|
project.client_name = data.client_name
|
|
if data.description is not None:
|
|
project.description = data.description
|
|
if data.model_type is not None:
|
|
project.model_type = ModelType(data.model_type)
|
|
|
|
await db.commit()
|
|
await db.refresh(project)
|
|
|
|
count_result = await db.execute(
|
|
select(func.count(ClientAsset.id)).where(ClientAsset.project_id == project.id)
|
|
)
|
|
return _project_out(project, count_result.scalar() or 0)
|
|
|
|
|
|
@router.delete("/{project_id}")
|
|
async def delete_project(project_id: int, db: AsyncSession = Depends(get_db)):
|
|
project = await _get_project(project_id, db)
|
|
await db.delete(project)
|
|
await db.commit()
|
|
return {"detail": "Project deleted"}
|
|
|
|
|
|
async def _get_project(project_id: int, db: AsyncSession) -> Project:
|
|
result = await db.execute(select(Project).where(Project.id == project_id))
|
|
project = result.scalar_one_or_none()
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
return project
|
|
|
|
|
|
def _project_out(project: Project, asset_count: int) -> ProjectOut:
|
|
return ProjectOut(
|
|
id=project.id,
|
|
name=project.name,
|
|
client_name=project.client_name,
|
|
description=project.description,
|
|
model_type=project.model_type.value,
|
|
status=project.status.value,
|
|
source_filename=project.source_filename,
|
|
parse_stage=project.parse_stage,
|
|
has_brief_analysis=bool(project.brief_analysis),
|
|
has_tier_mapping=bool(project.tier_mapping),
|
|
ai_input_tokens=project.ai_input_tokens or 0,
|
|
ai_output_tokens=project.ai_output_tokens or 0,
|
|
ai_cost_usd=float(project.ai_cost_usd or 0),
|
|
ai_call_count=project.ai_call_count or 0,
|
|
created_at=project.created_at,
|
|
updated_at=project.updated_at,
|
|
asset_count=asset_count,
|
|
)
|