feat(task-021): add admin workflow template api
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID
|
||||
|
||||
from src.domain.contracts import (
|
||||
CurrentUser,
|
||||
Role,
|
||||
WorkflowStageCreateRequest,
|
||||
WorkflowStageResponse,
|
||||
WorkflowStageUpdateRequest,
|
||||
WorkflowTemplateAuditEventListResponse,
|
||||
WorkflowTemplateCreateRequest,
|
||||
WorkflowTemplateListResponse,
|
||||
WorkflowTemplateResponse,
|
||||
WorkflowTemplateStatus,
|
||||
WorkflowTemplateSummary,
|
||||
WorkflowTemplateUpdateRequest,
|
||||
)
|
||||
|
||||
|
||||
_TEMPLATE_CREATED_EVENT_TYPE = "WORKFLOW_TEMPLATE_CREATED"
|
||||
_TEMPLATE_UPDATED_EVENT_TYPE = "WORKFLOW_TEMPLATE_UPDATED"
|
||||
_STAGE_CREATED_EVENT_TYPE = "WORKFLOW_STAGE_CREATED"
|
||||
_STAGE_UPDATED_EVENT_TYPE = "WORKFLOW_STAGE_UPDATED"
|
||||
_STAGE_DELETED_EVENT_TYPE = "WORKFLOW_STAGE_DELETED"
|
||||
_STAGES_REORDERED_EVENT_TYPE = "WORKFLOW_STAGES_REORDERED"
|
||||
_TEMPLATE_ACTIVATED_EVENT_TYPE = "WORKFLOW_TEMPLATE_ACTIVATED"
|
||||
_TEMPLATE_ARCHIVED_EVENT_TYPE = "WORKFLOW_TEMPLATE_ARCHIVED"
|
||||
|
||||
|
||||
def create_workflow_template(
|
||||
repository: object,
|
||||
*,
|
||||
request: WorkflowTemplateCreateRequest,
|
||||
current_user: CurrentUser,
|
||||
) -> WorkflowTemplateResponse:
|
||||
now = _now()
|
||||
workflow = repository.workflow_templates.create(
|
||||
name=request.name,
|
||||
slug=request.slug,
|
||||
description=request.description,
|
||||
status=WorkflowTemplateStatus.DRAFT,
|
||||
version=1,
|
||||
created_by=current_user.id,
|
||||
updated_by=current_user.id,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
_create_event(
|
||||
repository,
|
||||
workflow_id=workflow.id,
|
||||
event_type=_TEMPLATE_CREATED_EVENT_TYPE,
|
||||
current_user=current_user,
|
||||
payload={"slug": workflow.slug, "version": workflow.version},
|
||||
created_at=now,
|
||||
)
|
||||
return WorkflowTemplateResponse(workflow=workflow)
|
||||
|
||||
|
||||
def list_workflow_templates(
|
||||
repository: object,
|
||||
*,
|
||||
current_user: CurrentUser,
|
||||
) -> WorkflowTemplateListResponse:
|
||||
if current_user.role == Role.ADMIN:
|
||||
workflows = repository.workflow_templates.list()
|
||||
else:
|
||||
workflows = repository.workflow_templates.list_active()
|
||||
return WorkflowTemplateListResponse(workflows=workflows)
|
||||
|
||||
|
||||
def get_workflow_template(
|
||||
repository: object,
|
||||
*,
|
||||
workflow_id: UUID,
|
||||
current_user: CurrentUser,
|
||||
) -> WorkflowTemplateResponse:
|
||||
workflow = repository.workflow_templates.get(workflow_id)
|
||||
if current_user.role != Role.ADMIN and workflow.status != WorkflowTemplateStatus.ACTIVE:
|
||||
raise LookupError(f"Workflow template not found: {workflow_id}")
|
||||
return WorkflowTemplateResponse(workflow=workflow)
|
||||
|
||||
|
||||
def update_workflow_template(
|
||||
repository: object,
|
||||
*,
|
||||
workflow_id: UUID,
|
||||
request: WorkflowTemplateUpdateRequest,
|
||||
current_user: CurrentUser,
|
||||
) -> WorkflowTemplateResponse:
|
||||
existing = repository.workflow_templates.get(workflow_id)
|
||||
_require_draft(existing)
|
||||
|
||||
changes = request.model_dump(exclude_unset=True)
|
||||
now = _now()
|
||||
workflow = repository.workflow_templates.update(
|
||||
workflow_id=workflow_id,
|
||||
name=changes.get("name", existing.name),
|
||||
slug=changes.get("slug", existing.slug),
|
||||
description=changes.get("description", existing.description),
|
||||
updated_by=current_user.id,
|
||||
updated_at=now,
|
||||
)
|
||||
_create_event(
|
||||
repository,
|
||||
workflow_id=workflow.id,
|
||||
event_type=_TEMPLATE_UPDATED_EVENT_TYPE,
|
||||
current_user=current_user,
|
||||
payload={"changed_fields": sorted(changes.keys())},
|
||||
created_at=now,
|
||||
)
|
||||
return WorkflowTemplateResponse(workflow=workflow)
|
||||
|
||||
|
||||
def add_workflow_stage(
|
||||
repository: object,
|
||||
*,
|
||||
workflow_id: UUID,
|
||||
request: WorkflowStageCreateRequest,
|
||||
current_user: CurrentUser,
|
||||
) -> WorkflowStageResponse:
|
||||
workflow = repository.workflow_templates.get(workflow_id)
|
||||
_require_draft(workflow)
|
||||
|
||||
now = _now()
|
||||
stage = repository.workflow_template_stages.create(
|
||||
workflow_id=workflow_id,
|
||||
stable_key=request.stable_key,
|
||||
display_name=request.display_name,
|
||||
description=request.description,
|
||||
position=request.position,
|
||||
owner_role=request.owner_role,
|
||||
runner_profile_key=request.runner_profile_key,
|
||||
required_inputs=request.required_inputs,
|
||||
expected_outputs=request.expected_outputs,
|
||||
acceptance_criteria=request.acceptance_criteria,
|
||||
requires_human_approval=request.requires_human_approval,
|
||||
retry_policy=request.retry_policy,
|
||||
parts=_parts_json(request.parts),
|
||||
updated_by=current_user.id,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
_create_event(
|
||||
repository,
|
||||
workflow_id=workflow_id,
|
||||
event_type=_STAGE_CREATED_EVENT_TYPE,
|
||||
current_user=current_user,
|
||||
payload={
|
||||
"stage_id": str(stage.id),
|
||||
"stable_key": stage.stable_key,
|
||||
"position": stage.position,
|
||||
},
|
||||
created_at=now,
|
||||
)
|
||||
return WorkflowStageResponse(stage=stage)
|
||||
|
||||
|
||||
def update_workflow_stage(
|
||||
repository: object,
|
||||
*,
|
||||
workflow_id: UUID,
|
||||
stage_id: UUID,
|
||||
request: WorkflowStageUpdateRequest,
|
||||
current_user: CurrentUser,
|
||||
) -> WorkflowStageResponse:
|
||||
workflow = repository.workflow_templates.get(workflow_id)
|
||||
_require_draft(workflow)
|
||||
existing = repository.workflow_template_stages.get(
|
||||
workflow_id=workflow_id,
|
||||
stage_id=stage_id,
|
||||
)
|
||||
|
||||
changes = request.model_dump(exclude_unset=True)
|
||||
now = _now()
|
||||
stage = repository.workflow_template_stages.update(
|
||||
workflow_id=workflow_id,
|
||||
stage_id=stage_id,
|
||||
stable_key=changes.get("stable_key", existing.stable_key),
|
||||
display_name=changes.get("display_name", existing.display_name),
|
||||
description=changes.get("description", existing.description),
|
||||
position=changes.get("position", existing.position),
|
||||
owner_role=request.owner_role or existing.owner_role,
|
||||
runner_profile_key=changes.get(
|
||||
"runner_profile_key",
|
||||
existing.runner_profile_key,
|
||||
),
|
||||
required_inputs=changes.get("required_inputs", existing.required_inputs),
|
||||
expected_outputs=changes.get("expected_outputs", existing.expected_outputs),
|
||||
acceptance_criteria=changes.get(
|
||||
"acceptance_criteria",
|
||||
existing.acceptance_criteria,
|
||||
),
|
||||
requires_human_approval=changes.get(
|
||||
"requires_human_approval",
|
||||
existing.requires_human_approval,
|
||||
),
|
||||
retry_policy=changes.get("retry_policy", existing.retry_policy),
|
||||
parts=(
|
||||
_parts_json(request.parts)
|
||||
if request.parts is not None
|
||||
else _parts_json(existing.parts)
|
||||
),
|
||||
updated_by=current_user.id,
|
||||
updated_at=now,
|
||||
)
|
||||
_create_event(
|
||||
repository,
|
||||
workflow_id=workflow_id,
|
||||
event_type=_STAGE_UPDATED_EVENT_TYPE,
|
||||
current_user=current_user,
|
||||
payload={"stage_id": str(stage_id), "changed_fields": sorted(changes.keys())},
|
||||
created_at=now,
|
||||
)
|
||||
return WorkflowStageResponse(stage=stage)
|
||||
|
||||
|
||||
def delete_workflow_stage(
|
||||
repository: object,
|
||||
*,
|
||||
workflow_id: UUID,
|
||||
stage_id: UUID,
|
||||
current_user: CurrentUser,
|
||||
) -> WorkflowTemplateResponse:
|
||||
workflow = repository.workflow_templates.get(workflow_id)
|
||||
_require_draft(workflow)
|
||||
existing = repository.workflow_template_stages.get(
|
||||
workflow_id=workflow_id,
|
||||
stage_id=stage_id,
|
||||
)
|
||||
|
||||
now = _now()
|
||||
repository.workflow_template_stages.delete(
|
||||
workflow_id=workflow_id,
|
||||
stage_id=stage_id,
|
||||
updated_by=current_user.id,
|
||||
updated_at=now,
|
||||
)
|
||||
_create_event(
|
||||
repository,
|
||||
workflow_id=workflow_id,
|
||||
event_type=_STAGE_DELETED_EVENT_TYPE,
|
||||
current_user=current_user,
|
||||
payload={"stage_id": str(stage_id), "stable_key": existing.stable_key},
|
||||
created_at=now,
|
||||
)
|
||||
return WorkflowTemplateResponse(workflow=repository.workflow_templates.get(workflow_id))
|
||||
|
||||
|
||||
def reorder_workflow_stages(
|
||||
repository: object,
|
||||
*,
|
||||
workflow_id: UUID,
|
||||
stage_ids: list[UUID],
|
||||
current_user: CurrentUser,
|
||||
) -> WorkflowTemplateResponse:
|
||||
workflow = repository.workflow_templates.get(workflow_id)
|
||||
_require_draft(workflow)
|
||||
|
||||
now = _now()
|
||||
repository.workflow_template_stages.reorder(
|
||||
workflow_id=workflow_id,
|
||||
stage_ids=stage_ids,
|
||||
updated_by=current_user.id,
|
||||
updated_at=now,
|
||||
)
|
||||
_create_event(
|
||||
repository,
|
||||
workflow_id=workflow_id,
|
||||
event_type=_STAGES_REORDERED_EVENT_TYPE,
|
||||
current_user=current_user,
|
||||
payload={"stage_ids": [str(stage_id) for stage_id in stage_ids]},
|
||||
created_at=now,
|
||||
)
|
||||
return WorkflowTemplateResponse(workflow=repository.workflow_templates.get(workflow_id))
|
||||
|
||||
|
||||
def activate_workflow_template(
|
||||
repository: object,
|
||||
*,
|
||||
workflow_id: UUID,
|
||||
current_user: CurrentUser,
|
||||
) -> WorkflowTemplateResponse:
|
||||
workflow = repository.workflow_templates.get(workflow_id)
|
||||
_require_draft(workflow)
|
||||
|
||||
now = _now()
|
||||
activated = repository.workflow_templates.activate(
|
||||
workflow_id=workflow_id,
|
||||
updated_by=current_user.id,
|
||||
activated_at=now,
|
||||
)
|
||||
_create_event(
|
||||
repository,
|
||||
workflow_id=workflow_id,
|
||||
event_type=_TEMPLATE_ACTIVATED_EVENT_TYPE,
|
||||
current_user=current_user,
|
||||
payload={"version": activated.version},
|
||||
created_at=now,
|
||||
)
|
||||
return WorkflowTemplateResponse(workflow=activated)
|
||||
|
||||
|
||||
def archive_workflow_template(
|
||||
repository: object,
|
||||
*,
|
||||
workflow_id: UUID,
|
||||
current_user: CurrentUser,
|
||||
) -> WorkflowTemplateResponse:
|
||||
repository.workflow_templates.get(workflow_id)
|
||||
|
||||
now = _now()
|
||||
archived = repository.workflow_templates.archive(
|
||||
workflow_id=workflow_id,
|
||||
updated_by=current_user.id,
|
||||
archived_at=now,
|
||||
)
|
||||
_create_event(
|
||||
repository,
|
||||
workflow_id=workflow_id,
|
||||
event_type=_TEMPLATE_ARCHIVED_EVENT_TYPE,
|
||||
current_user=current_user,
|
||||
payload={"version": archived.version},
|
||||
created_at=now,
|
||||
)
|
||||
return WorkflowTemplateResponse(workflow=archived)
|
||||
|
||||
|
||||
def list_workflow_template_audit_events(
|
||||
repository: object,
|
||||
*,
|
||||
workflow_id: UUID,
|
||||
) -> WorkflowTemplateAuditEventListResponse:
|
||||
repository.workflow_templates.get(workflow_id)
|
||||
return WorkflowTemplateAuditEventListResponse(
|
||||
events=repository.workflow_template_events.list_for_workflow(workflow_id)
|
||||
)
|
||||
|
||||
|
||||
def _create_event(
|
||||
repository: object,
|
||||
*,
|
||||
workflow_id: UUID,
|
||||
event_type: str,
|
||||
current_user: CurrentUser,
|
||||
payload: dict[str, object],
|
||||
created_at: datetime,
|
||||
) -> None:
|
||||
repository.workflow_template_events.create(
|
||||
workflow_id=workflow_id,
|
||||
event_type=event_type,
|
||||
actor_user_id=current_user.id,
|
||||
payload=payload,
|
||||
created_at=created_at,
|
||||
)
|
||||
|
||||
|
||||
def _require_draft(workflow: WorkflowTemplateSummary) -> None:
|
||||
if workflow.status != WorkflowTemplateStatus.DRAFT:
|
||||
raise ValueError("Only draft workflow templates can be mutated.")
|
||||
|
||||
|
||||
def _parts_json(parts: list[object]) -> list[dict[str, object]]:
|
||||
return [
|
||||
part.model_dump(mode="json") if hasattr(part, "model_dump") else dict(part)
|
||||
for part in parts
|
||||
]
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(UTC)
|
||||
Reference in New Issue
Block a user