Adding fits
This commit is contained in:
1 parent
8185f1f8b3
commit
ab39191af5
19 files changed
+1253
-1
No files matched your search
+171
@@ -0,0 +1,171 @@
|
||||
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.crud import fit as fit_crud
|
||||
from app.crud import fit_note as fit_note_crud
|
||||
from app.deps import require_permission
|
||||
from app.models.fit import FitLabel
|
||||
from app.models.user import User
|
||||
from app.schemas.fit import (
|
||||
FitCreate,
|
||||
FitNoteCreate,
|
||||
FitNoteRead,
|
||||
FitRead,
|
||||
FitRecomputeResult,
|
||||
FitUpdate,
|
||||
)
|
||||
from app.services import fit_matching
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=list[FitRead])
|
||||
async def list_fits(
|
||||
augmented_part_id: int | None = None,
|
||||
label: FitLabel | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_permission("fits:read")),
|
||||
):
|
||||
if augmented_part_id is not None:
|
||||
fits = await fit_crud.list_for_part(db, augmented_part_id)
|
||||
if label is not None:
|
||||
fits = [f for f in fits if f.label == label]
|
||||
return fits[skip : skip + limit]
|
||||
return await fit_crud.list_all(db, skip=skip, limit=limit, label=label)
|
||||
|
||||
|
||||
@router.get("/{fit_id}", response_model=FitRead)
|
||||
async def get_fit(
|
||||
fit_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_permission("fits:read")),
|
||||
):
|
||||
fit = await fit_crud.get(db, fit_id)
|
||||
if fit is None:
|
||||
raise HTTPException(status_code=404, detail="Fit not found")
|
||||
return fit
|
||||
|
||||
|
||||
@router.post("", response_model=FitRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_fit(
|
||||
payload: FitCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(require_permission("fits:write")),
|
||||
):
|
||||
"""Manually declare a fit between two attachment points; recorded as Exact."""
|
||||
if payload.attachment_point_a_id == payload.attachment_point_b_id:
|
||||
raise HTTPException(
|
||||
status_code=422, detail="attachment points must be different"
|
||||
)
|
||||
ap_a = await attachment_point_crud.get(db, payload.attachment_point_a_id)
|
||||
ap_b = await attachment_point_crud.get(db, payload.attachment_point_b_id)
|
||||
if ap_a is None or ap_b is None:
|
||||
raise HTTPException(status_code=404, detail="Attachment point not found")
|
||||
if ap_a.augmented_part_id == ap_b.augmented_part_id:
|
||||
raise HTTPException(
|
||||
status_code=422, detail="a part cannot fit itself"
|
||||
)
|
||||
existing = await fit_crud.get_by_attachment_points(db, ap_a, ap_b)
|
||||
if existing is not None:
|
||||
raise HTTPException(status_code=409, detail="Fit already exists")
|
||||
|
||||
fit = await fit_crud.create(db, ap_a=ap_a, ap_b=ap_b, label=FitLabel.EXACT)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="create",
|
||||
resource_type="Fit",
|
||||
resource_id=fit.id,
|
||||
description=audit_crud.describe_create(
|
||||
"Fit", fit.id, f"attachment points {ap_a.id}↔{ap_b.id}"
|
||||
),
|
||||
)
|
||||
return fit
|
||||
|
||||
|
||||
@router.patch("/{fit_id}", response_model=FitRead)
|
||||
async def update_fit_label(
|
||||
fit_id: int,
|
||||
payload: FitUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(require_permission("fits:write")),
|
||||
):
|
||||
"""Promote a Possible fit to Exact, or mark a pairing as None (impossible)."""
|
||||
fit = await fit_crud.get(db, fit_id)
|
||||
if fit is None:
|
||||
raise HTTPException(status_code=404, detail="Fit not found")
|
||||
fit = await fit_crud.set_label(db, fit, payload.label)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="update",
|
||||
resource_type="Fit",
|
||||
resource_id=fit.id,
|
||||
description=audit_crud.describe_update(
|
||||
"Fit", fit.id, {"label": payload.label.value}
|
||||
),
|
||||
)
|
||||
return fit
|
||||
|
||||
|
||||
@router.delete("/{fit_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_fit(
|
||||
fit_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(require_permission("fits:delete")),
|
||||
):
|
||||
fit = await fit_crud.get(db, fit_id)
|
||||
if fit is None:
|
||||
raise HTTPException(status_code=404, detail="Fit not found")
|
||||
label = f"attachment points {fit.attachment_point_a_id}↔{fit.attachment_point_b_id}"
|
||||
await fit_crud.delete(db, fit)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="delete",
|
||||
resource_type="Fit",
|
||||
resource_id=fit_id,
|
||||
description=audit_crud.describe_delete("Fit", fit_id, label),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/recompute", response_model=FitRecomputeResult)
|
||||
async def recompute_fits(
|
||||
augmented_part_id: int | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(require_permission("fits:write")),
|
||||
):
|
||||
"""Re-run the matching algorithm, scoped to one part or across all parts."""
|
||||
if augmented_part_id is not None:
|
||||
result = await fit_matching.recompute_for_part(db, augmented_part_id)
|
||||
else:
|
||||
result = await fit_matching.recompute_all(db)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="update",
|
||||
resource_type="Fit",
|
||||
resource_id=augmented_part_id,
|
||||
description=f"Recomputed fits: {result.created} created, {result.removed} removed",
|
||||
)
|
||||
return FitRecomputeResult(created=result.created, removed=result.removed)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{fit_id}/notes", response_model=FitNoteRead, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def create_fit_note(
|
||||
fit_id: int,
|
||||
payload: FitNoteCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(require_permission("fits:write")),
|
||||
):
|
||||
fit = await fit_crud.get(db, fit_id)
|
||||
if fit is None:
|
||||
raise HTTPException(status_code=404, detail="Fit not found")
|
||||
return await fit_note_crud.create(db, fit=fit, author=actor, body=payload.body)
|
||||
@@ -7,6 +7,7 @@ from app.api import (
|
||||
augmented_parts,
|
||||
auth,
|
||||
clamp_connections,
|
||||
fits,
|
||||
imported_parts,
|
||||
iso_threads,
|
||||
manufacturers,
|
||||
@@ -64,3 +65,4 @@ api_router.include_router(
|
||||
augmented_parts.router, prefix="/augmented-parts", tags=["augmented-parts"]
|
||||
)
|
||||
api_router.include_router(audit_log.router, prefix="/audit-log", tags=["audit-log"])
|
||||
api_router.include_router(fits.router, prefix="/fits", tags=["fits"])
|
||||
Reference in new issue
Block a user