Files
content-factory/apps/backend/src/application/workflow_templates.py
T

373 lines
11 KiB
Python

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)