import uuid from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from api.database import get_session from api.models.task import Task from api.models.workplan import Workplan from api.models.workplan_dependency import WorkplanDependency from api.schemas.workplan_dependency import WorkplanDependencyCreate, WorkplanDependencyRead from api.routers.workstreams import _legacy_key, _meter_legacy_route router = APIRouter(prefix="/workstreams", tags=["dependencies"]) workplan_router = APIRouter(prefix="/workplans", tags=["dependencies"]) async def _create_dependency( *, workplan_id: uuid.UUID, body: WorkplanDependencyCreate, session: AsyncSession, ) -> WorkplanDependency: if await session.get(Workplan, workplan_id) is None: raise HTTPException(status_code=404, detail="from workplan not found") has_workplan_target = body.to_workplan_id is not None has_task_target = body.to_task_id is not None if has_workplan_target == has_task_target: raise HTTPException(status_code=422, detail="provide exactly one dependency target") if body.to_workplan_id and await session.get(Workplan, body.to_workplan_id) is None: raise HTTPException(status_code=404, detail="target workplan not found") if body.to_task_id and await session.get(Task, body.to_task_id) is None: raise HTTPException(status_code=404, detail="target task not found") if workplan_id == body.to_workplan_id: raise HTTPException(status_code=422, detail="a workplan cannot depend on itself") dep = WorkplanDependency( from_workplan_id=workplan_id, to_workplan_id=body.to_workplan_id, to_task_id=body.to_task_id, relationship_type=body.relationship_type, description=body.description, ) session.add(dep) await session.commit() await session.refresh(dep) return dep async def _list_dependencies( *, workplan_id: uuid.UUID, session: AsyncSession, ) -> list[WorkplanDependency]: if await session.get(Workplan, workplan_id) is None: raise HTTPException(status_code=404, detail="workplan not found") rows = await session.execute( select(WorkplanDependency).where( (WorkplanDependency.from_workplan_id == workplan_id) | (WorkplanDependency.to_workplan_id == workplan_id) ) ) return list(rows.scalars().all()) async def _delete_dependency( *, workplan_id: uuid.UUID, dep_id: uuid.UUID, session: AsyncSession, ) -> None: dep = await session.get(WorkplanDependency, dep_id) if dep is None: raise HTTPException(status_code=404, detail="dependency not found") if dep.from_workplan_id != workplan_id: raise HTTPException(status_code=403, detail="dependency does not belong to this workplan") await session.delete(dep) await session.commit() @router.post( "/{workstream_id}/dependencies/", response_model=WorkplanDependencyRead, status_code=status.HTTP_201_CREATED, ) async def create_dependency( request: Request, response: Response, workstream_id: uuid.UUID, body: WorkplanDependencyCreate, session: AsyncSession = Depends(get_session), ) -> WorkplanDependency: """Record that workstream_id depends on another workplan or a task.""" await _meter_legacy_route( session=session, request=request, response=response, interface_key=_legacy_key("POST", "/workstreams/{workstream_id}/dependencies/"), replacement_ref="/workplans/{workplan_id}/dependencies/", ) return await _create_dependency(workplan_id=workstream_id, body=body, session=session) @workplan_router.post( "/{workplan_id}/dependencies/", response_model=WorkplanDependencyRead, status_code=status.HTTP_201_CREATED, ) async def create_workplan_dependency( workplan_id: uuid.UUID, body: WorkplanDependencyCreate, session: AsyncSession = Depends(get_session), ) -> WorkplanDependency: return await _create_dependency(workplan_id=workplan_id, body=body, session=session) @router.get( "/{workstream_id}/dependencies/", response_model=list[WorkplanDependencyRead], ) async def list_dependencies( request: Request, response: Response, workstream_id: uuid.UUID, session: AsyncSession = Depends(get_session), ) -> list[WorkplanDependency]: """Return all dependency edges touching this workplan (both directions).""" await _meter_legacy_route( session=session, request=request, response=response, interface_key=_legacy_key("GET", "/workstreams/{workstream_id}/dependencies/"), replacement_ref="/workplans/{workplan_id}/dependencies/", ) return await _list_dependencies(workplan_id=workstream_id, session=session) @workplan_router.get( "/{workplan_id}/dependencies/", response_model=list[WorkplanDependencyRead], ) async def list_workplan_dependencies( workplan_id: uuid.UUID, session: AsyncSession = Depends(get_session), ) -> list[WorkplanDependency]: return await _list_dependencies(workplan_id=workplan_id, session=session) @router.delete( "/{workstream_id}/dependencies/{dep_id}", status_code=status.HTTP_204_NO_CONTENT, ) async def delete_dependency( request: Request, response: Response, workstream_id: uuid.UUID, dep_id: uuid.UUID, session: AsyncSession = Depends(get_session), ) -> None: """Hard-delete a dependency edge. Removing a constraint is safe — no information is lost.""" await _meter_legacy_route( session=session, request=request, response=response, interface_key=_legacy_key("DELETE", "/workstreams/{workstream_id}/dependencies/{dep_id}"), replacement_ref="/workplans/{workplan_id}/dependencies/{dep_id}", ) await _delete_dependency(workplan_id=workstream_id, dep_id=dep_id, session=session) @workplan_router.delete( "/{workplan_id}/dependencies/{dep_id}", status_code=status.HTTP_204_NO_CONTENT, ) async def delete_workplan_dependency( workplan_id: uuid.UUID, dep_id: uuid.UUID, session: AsyncSession = Depends(get_session), ) -> None: await _delete_dependency(workplan_id=workplan_id, dep_id=dep_id, session=session)