373 lines
11 KiB
Python
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)
|