from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.crud import attachment_point as attachment_point_crud from app.crud import audit as audit_crud from app.deps import require_permission from app.models.user import User from app.schemas.attachment_point import ( AttachmentPointCreate, AttachmentPointRead, AttachmentPointUpdate, ) router = APIRouter() def _connection_label(ap) -> str: if ap.iso_thread: return f"M{ap.iso_thread.diameter_mm}×{ap.iso_thread.pitch_mm}" if ap.unc_thread: return f"{ap.unc_thread.diameter_fraction}-{ap.unc_thread.tpi} {ap.unc_thread.series}" if ap.clamp_connection: cc = ap.clamp_connection if cc.min_mm is not None: return f"{cc.min_mm}–{cc.max_mm} mm" return f"{cc.min_in}–{cc.max_in} in" if ap.static_connection: return f"{ap.static_connection.od_mm} mm OD" return "—" @router.get("", response_model=list[AttachmentPointRead]) async def list_attachment_points( skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_db), _: User = Depends(require_permission("attachment_points:read")), ): return await attachment_point_crud.list_all(db, skip=skip, limit=limit) @router.post("", response_model=AttachmentPointRead, status_code=status.HTTP_201_CREATED) async def create_attachment_point( payload: AttachmentPointCreate, db: AsyncSession = Depends(get_db), actor: User = Depends(require_permission("attachment_points:write")), ): conn_ids = [ payload.iso_thread_id, payload.unc_thread_id, payload.clamp_connection_id, payload.static_connection_id, ] if sum(v is not None for v in conn_ids) != 1: raise HTTPException( status_code=400, detail="Exactly one connection (iso_thread, unc_thread, clamp_connection, or static_connection) must be provided.", ) ap = await attachment_point_crud.create(db, **payload.model_dump()) await audit_crud.log( db, actor=actor, action="create", resource_type="AttachmentPoint", resource_id=ap.id, description=audit_crud.describe_create( "AttachmentPoint", ap.id, _connection_label(ap) ), ) return ap @router.get("/{attachment_point_id}", response_model=AttachmentPointRead) async def get_attachment_point( attachment_point_id: int, db: AsyncSession = Depends(get_db), _: User = Depends(require_permission("attachment_points:read")), ): ap = await attachment_point_crud.get(db, attachment_point_id) if ap is None: raise HTTPException(status_code=404, detail="Attachment point not found") return ap @router.patch("/{attachment_point_id}", response_model=AttachmentPointRead) async def update_attachment_point( attachment_point_id: int, payload: AttachmentPointUpdate, db: AsyncSession = Depends(get_db), actor: User = Depends(require_permission("attachment_points:write")), ): ap = await attachment_point_crud.get(db, attachment_point_id) if ap is None: raise HTTPException(status_code=404, detail="Attachment point not found") changes = payload.model_dump(exclude_unset=True) ap = await attachment_point_crud.update(db, ap, changes) await audit_crud.log( db, actor=actor, action="update", resource_type="AttachmentPoint", resource_id=ap.id, description=audit_crud.describe_update("AttachmentPoint", ap.id, changes), ) return ap @router.delete("/{attachment_point_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_attachment_point( attachment_point_id: int, db: AsyncSession = Depends(get_db), actor: User = Depends(require_permission("attachment_points:delete")), ): ap = await attachment_point_crud.get(db, attachment_point_id) if ap is None: raise HTTPException(status_code=404, detail="Attachment point not found") label = _connection_label(ap) await attachment_point_crud.delete(db, ap) await audit_crud.log( db, actor=actor, action="delete", resource_type="AttachmentPoint", resource_id=attachment_point_id, description=audit_crud.describe_delete("AttachmentPoint", attachment_point_id, label), )