state-hub/api/routers/workstream_dependencies.py
tegwick 7036e2aa6b
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Has been cancelled
Build and Publish Multi-Context Image / build-and-push (push) Successful in 2m18s
feat(api): retire phase-1/2 /workstreams REST aliases with 410 stranglers
Add retire_legacy_route() helper and convert CRUD, dependency, and
execution intent legacy routes to 410 Gone while preserving final-call
legacy-meter attribution. Update tests, retirement backlog, and close
STATE-WP-0075 with handoff to STATE-WP-0070 T02 and STATE-WP-0073 T04.
2026-07-10 13:31:57 +02:00

185 lines
No EOL
6.3 KiB
Python

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
from api.services.legacy_compat import retire_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/",
status_code=status.HTTP_410_GONE,
)
async def create_dependency(
request: Request,
response: Response,
workstream_id: uuid.UUID,
body: WorkplanDependencyCreate,
session: AsyncSession = Depends(get_session),
) -> None:
"""Legacy alias retired — use POST /workplans/{workplan_id}/dependencies/."""
await retire_legacy_route(
session=session,
request=request,
response=response,
interface_key=_legacy_key("POST", "/workstreams/{workstream_id}/dependencies/"),
replacement_ref="/workplans/{workplan_id}/dependencies/",
detail="Legacy POST /workstreams/{workstream_id}/dependencies/ retired; "
"use POST /workplans/{workplan_id}/dependencies/",
)
@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/",
status_code=status.HTTP_410_GONE,
)
async def list_dependencies(
request: Request,
response: Response,
workstream_id: uuid.UUID,
session: AsyncSession = Depends(get_session),
) -> None:
"""Legacy alias retired — use GET /workplans/{workplan_id}/dependencies/."""
await retire_legacy_route(
session=session,
request=request,
response=response,
interface_key=_legacy_key("GET", "/workstreams/{workstream_id}/dependencies/"),
replacement_ref="/workplans/{workplan_id}/dependencies/",
detail="Legacy GET /workstreams/{workstream_id}/dependencies/ retired; "
"use GET /workplans/{workplan_id}/dependencies/",
)
@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_410_GONE,
)
async def delete_dependency(
request: Request,
response: Response,
workstream_id: uuid.UUID,
dep_id: uuid.UUID,
session: AsyncSession = Depends(get_session),
) -> None:
"""Legacy alias retired — use DELETE /workplans/{workplan_id}/dependencies/{dep_id}."""
await retire_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}",
detail="Legacy DELETE /workstreams/{workstream_id}/dependencies/{dep_id} retired; "
"use DELETE /workplans/{workplan_id}/dependencies/{dep_id}",
)
@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)