generated from coulomb/repo-seed
Task flow engine implementation
This commit is contained in:
@@ -6,37 +6,12 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.database import get_session
|
||||
from api.flow_defs import assertion_result_to_dict, evaluate_transition, flow_result_to_dict
|
||||
from api.models.contribution import Contribution, ContributionStatus, ContributionType
|
||||
from api.schemas.contribution import ContributionCreate, ContributionRead, ContributionStatusPatch
|
||||
|
||||
router = APIRouter(prefix="/contributions", tags=["contributions"])
|
||||
|
||||
# Valid forward transitions in the lifecycle
|
||||
_VALID_TRANSITIONS: dict[ContributionStatus, set[ContributionStatus]] = {
|
||||
ContributionStatus.draft: {
|
||||
ContributionStatus.submitted,
|
||||
ContributionStatus.withdrawn,
|
||||
},
|
||||
ContributionStatus.submitted: {
|
||||
ContributionStatus.acknowledged,
|
||||
ContributionStatus.rejected,
|
||||
ContributionStatus.withdrawn,
|
||||
},
|
||||
ContributionStatus.acknowledged: {
|
||||
ContributionStatus.accepted,
|
||||
ContributionStatus.rejected,
|
||||
ContributionStatus.withdrawn,
|
||||
},
|
||||
ContributionStatus.accepted: {
|
||||
ContributionStatus.merged,
|
||||
ContributionStatus.withdrawn,
|
||||
},
|
||||
ContributionStatus.rejected: set(),
|
||||
ContributionStatus.merged: set(),
|
||||
ContributionStatus.withdrawn: set(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/", response_model=list[ContributionRead])
|
||||
async def list_contributions(
|
||||
type: ContributionType | None = Query(None),
|
||||
@@ -93,14 +68,25 @@ async def patch_contribution_status(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> Contribution:
|
||||
contrib = await _get_or_404(contribution_id, session)
|
||||
allowed = _VALID_TRANSITIONS.get(contrib.status, set())
|
||||
if body.status not in allowed:
|
||||
current = _status_value(contrib.status)
|
||||
target = _status_value(body.status)
|
||||
can_reach, failures, flow_result = evaluate_transition(
|
||||
"contribution",
|
||||
current,
|
||||
target,
|
||||
)
|
||||
if not can_reach:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=(
|
||||
f"Cannot transition from '{contrib.status}' to '{body.status}'. "
|
||||
f"Allowed: {[s.value for s in allowed] or 'none (terminal state)'}"
|
||||
),
|
||||
detail={
|
||||
"message": f"Cannot transition from '{current}' to '{target}'.",
|
||||
"current_workstation": current,
|
||||
"target_workstation": target,
|
||||
"blocking_assertions": [
|
||||
assertion_result_to_dict(item) for item in failures
|
||||
],
|
||||
"flow_result": flow_result_to_dict(flow_result),
|
||||
},
|
||||
)
|
||||
contrib.status = body.status
|
||||
if body.notes:
|
||||
@@ -145,3 +131,7 @@ async def _get_or_404(contribution_id: uuid.UUID, session: AsyncSession) -> Cont
|
||||
if contrib is None:
|
||||
raise HTTPException(status_code=404, detail=f"Contribution '{contribution_id}' not found")
|
||||
return contrib
|
||||
|
||||
|
||||
def _status_value(status: ContributionStatus | str) -> str:
|
||||
return status.value if isinstance(status, ContributionStatus) else str(status)
|
||||
|
||||
Reference in New Issue
Block a user