feat(workbench): persist operational workflows and events
This commit is contained in:
@@ -0,0 +1,570 @@
|
||||
"""Project-scoped APIs for operational tasks, evidence and approved actions."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, status
|
||||
from psycopg import AsyncConnection
|
||||
|
||||
from app.auth.permissions import (
|
||||
OPERATIONAL_TASK_EXECUTE,
|
||||
OPERATIONAL_TASK_EXTERNAL_SYNC,
|
||||
OPERATIONAL_TASK_VIEW,
|
||||
OPERATIONAL_TASK_WRITE,
|
||||
permissions_for_context,
|
||||
require_permission,
|
||||
)
|
||||
from app.auth.project_dependencies import (
|
||||
ProjectContext,
|
||||
get_project_context,
|
||||
get_project_pg_connection,
|
||||
)
|
||||
from app.domain.domain_errors import DomainError
|
||||
from app.domain.schemas.operational_data import TaskDataQuery, TaskDataSnapshot
|
||||
from app.domain.schemas.operational_task import (
|
||||
ActionDecisionCommand,
|
||||
ActionExecuteCommand,
|
||||
ActionProposalCommand,
|
||||
ActionReconcileCommand,
|
||||
AgentArtifactsCommand,
|
||||
AgentArtifactsResult,
|
||||
AgentRunCancelCommand,
|
||||
AgentRunCommand,
|
||||
AgentRunDispatchFailureCommand,
|
||||
AgentRunFailureCommand,
|
||||
AgentTaskContext,
|
||||
DispatchDueSchedulesCommand,
|
||||
DispatchDueSchedulesResult,
|
||||
EvidenceRecord,
|
||||
ExternalFeedbackCommand,
|
||||
ExternalFeedbackResult,
|
||||
HumanApprovalAction,
|
||||
HumanEvidenceCommand,
|
||||
NotificationAcknowledgeCommand,
|
||||
OperationalTaskCreate,
|
||||
OperationalTaskDetail,
|
||||
OperationalTaskNotification,
|
||||
OperationalTaskNotificationPage,
|
||||
OperationalTaskPage,
|
||||
OperationalTaskRetryCommand,
|
||||
OperationalTaskRun,
|
||||
OperationalTaskRunPage,
|
||||
OperationalTaskSchedule,
|
||||
OperationalTaskScheduleCreate,
|
||||
OperationalTaskSchedulePage,
|
||||
OperationalTaskScheduleUpdate,
|
||||
RecoverExpiredLeasesCommand,
|
||||
RecoverExpiredLeasesResult,
|
||||
TaskStatus,
|
||||
TaskDataQueryResult,
|
||||
TaskType,
|
||||
)
|
||||
from app.infra.db.postgresql.operational_tasks import OperationalTaskRepository
|
||||
from app.infra.integrations.operational_actions import (
|
||||
OperationalActionDispatcher,
|
||||
get_operational_action_dispatcher,
|
||||
)
|
||||
from app.infra.integrations.operational_data import TaskDataError, read_task_data
|
||||
from app.services.operational_tasks import OperationalTaskService
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _service(ctx: ProjectContext, conn: AsyncConnection) -> OperationalTaskService:
|
||||
return OperationalTaskService(
|
||||
OperationalTaskRepository(conn, ctx.project_id),
|
||||
ctx,
|
||||
permissions_for_context(ctx),
|
||||
)
|
||||
|
||||
|
||||
def _raise_domain(exc: DomainError):
|
||||
raise HTTPException(status_code=exc.status, detail=exc.payload()) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks",
|
||||
response_model=OperationalTaskDetail,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_operational_task(
|
||||
command: OperationalTaskCreate,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).create(command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.get("/operational-tasks", response_model=OperationalTaskPage)
|
||||
async def list_operational_tasks(
|
||||
task_type: TaskType | None = None,
|
||||
task_status: TaskStatus | None = Query(default=None, alias="status"),
|
||||
limit: int = Query(default=50, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
items, next_offset = await _service(ctx, conn).list(
|
||||
task_type=task_type, status=task_status, limit=limit, offset=offset
|
||||
)
|
||||
return OperationalTaskPage(
|
||||
items=items, limit=limit, offset=offset, next_offset=next_offset
|
||||
)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.get("/operational-task-runs", response_model=OperationalTaskRunPage)
|
||||
async def list_operational_task_runs(
|
||||
run_status: Literal[
|
||||
"queued", "claimed", "running", "succeeded", "failed", "timed_out", "cancelled"
|
||||
] | None = Query(default=None, alias="status"),
|
||||
limit: int = Query(default=50, ge=1, le=100),
|
||||
cursor: str | None = None,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
cursor_created_at = None
|
||||
cursor_run_id = None
|
||||
if cursor:
|
||||
try:
|
||||
timestamp, run_id = cursor.rsplit("|", 1)
|
||||
cursor_created_at = datetime.fromisoformat(timestamp)
|
||||
cursor_run_id = UUID(run_id)
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise HTTPException(status_code=400, detail="Invalid run cursor") from exc
|
||||
try:
|
||||
items, next_cursor = await _service(ctx, conn).list_runs(
|
||||
status=run_status,
|
||||
limit=limit,
|
||||
cursor_created_at=cursor_created_at,
|
||||
cursor_run_id=cursor_run_id,
|
||||
)
|
||||
return OperationalTaskRunPage(items=items, next_cursor=next_cursor)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/schedules",
|
||||
response_model=OperationalTaskSchedule,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_operational_schedule(
|
||||
command: OperationalTaskScheduleCreate,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).create_schedule(command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/operational-tasks/schedules", response_model=OperationalTaskSchedulePage
|
||||
)
|
||||
async def list_operational_schedules(
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return OperationalTaskSchedulePage(items=await _service(ctx, conn).list_schedules())
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/operational-tasks/schedules/{schedule_id}",
|
||||
response_model=OperationalTaskSchedule,
|
||||
)
|
||||
async def update_operational_schedule(
|
||||
schedule_id: UUID,
|
||||
command: OperationalTaskScheduleUpdate,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).update_schedule(schedule_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/schedules/dispatch-due",
|
||||
response_model=DispatchDueSchedulesResult,
|
||||
)
|
||||
async def dispatch_due_operational_schedules(
|
||||
command: DispatchDueSchedulesCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).dispatch_due_schedules(command.limit)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/leases/recover",
|
||||
response_model=RecoverExpiredLeasesResult,
|
||||
)
|
||||
async def recover_operational_task_leases(
|
||||
command: RecoverExpiredLeasesCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).recover_expired_leases(command.limit)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/operational-task-notifications",
|
||||
response_model=OperationalTaskNotificationPage,
|
||||
)
|
||||
async def list_operational_task_notifications(
|
||||
unread_only: bool = False,
|
||||
limit: int = Query(default=50, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
items, next_offset = await _service(ctx, conn).list_notifications(
|
||||
unread_only=unread_only, limit=limit, offset=offset
|
||||
)
|
||||
return OperationalTaskNotificationPage(
|
||||
items=items, limit=limit, offset=offset, next_offset=next_offset
|
||||
)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-task-notifications/{notification_id}/acknowledge",
|
||||
response_model=OperationalTaskNotification,
|
||||
)
|
||||
async def acknowledge_operational_task_notification(
|
||||
notification_id: UUID,
|
||||
command: NotificationAcknowledgeCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).acknowledge_notification(notification_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.get("/operational-tasks/{task_id}", response_model=OperationalTaskDetail)
|
||||
async def get_operational_task(
|
||||
task_id: UUID,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).detail(task_id)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post("/operational-tasks/{task_id}/retry", response_model=OperationalTaskDetail)
|
||||
async def retry_operational_task(
|
||||
task_id: UUID,
|
||||
command: OperationalTaskRetryCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).retry_task(task_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/runs/{run_id}/claim",
|
||||
response_model=OperationalTaskRun,
|
||||
)
|
||||
async def claim_operational_task_run(
|
||||
task_id: UUID,
|
||||
run_id: UUID,
|
||||
command: AgentRunCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).claim_run(task_id, run_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/runs/{run_id}/heartbeat",
|
||||
response_model=OperationalTaskRun,
|
||||
)
|
||||
async def heartbeat_operational_task_run(
|
||||
task_id: UUID,
|
||||
run_id: UUID,
|
||||
command: AgentRunCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).heartbeat_run(task_id, run_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/runs/{run_id}/complete",
|
||||
response_model=OperationalTaskDetail,
|
||||
)
|
||||
async def complete_operational_task_run(
|
||||
task_id: UUID,
|
||||
run_id: UUID,
|
||||
command: AgentRunCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).complete_run(task_id, run_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/runs/{run_id}/fail",
|
||||
response_model=OperationalTaskDetail,
|
||||
)
|
||||
async def fail_operational_task_run(
|
||||
task_id: UUID,
|
||||
run_id: UUID,
|
||||
command: AgentRunFailureCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).fail_run(task_id, run_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/runs/{run_id}/cancel",
|
||||
response_model=OperationalTaskDetail,
|
||||
)
|
||||
async def cancel_operational_task_run(
|
||||
task_id: UUID,
|
||||
run_id: UUID,
|
||||
command: AgentRunCancelCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).cancel_run(task_id, run_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/runs/{run_id}/dispatch-failure",
|
||||
response_model=OperationalTaskRun,
|
||||
)
|
||||
async def record_operational_task_run_dispatch_failure(
|
||||
task_id: UUID,
|
||||
run_id: UUID,
|
||||
command: AgentRunDispatchFailureCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).record_dispatch_failure(task_id, run_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/operational-tasks/{task_id}/agent-context", response_model=AgentTaskContext
|
||||
)
|
||||
async def get_operational_task_agent_context(
|
||||
task_id: UUID,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).agent_context(task_id)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/agent-artifacts",
|
||||
response_model=AgentArtifactsResult,
|
||||
)
|
||||
async def publish_operational_task_artifacts(
|
||||
task_id: UUID,
|
||||
command: AgentArtifactsCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).publish_artifacts(task_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/data-query", response_model=TaskDataQueryResult
|
||||
)
|
||||
async def query_operational_task_data(
|
||||
task_id: UUID,
|
||||
query: TaskDataQuery,
|
||||
authorization: str | None = Header(default=None, alias="Authorization"),
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXECUTE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
service = _service(ctx, conn)
|
||||
try:
|
||||
await service.detail(task_id)
|
||||
snapshot = await read_task_data(
|
||||
context=ctx, query=query, credential=authorization
|
||||
)
|
||||
evidence, stored_snapshot = await service.record_data_snapshot(task_id, snapshot)
|
||||
return TaskDataQueryResult(evidence=evidence, snapshot=stored_snapshot)
|
||||
except TaskDataError as exc:
|
||||
raise HTTPException(status_code=exc.status, detail=exc.detail) from exc
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/human-evidence",
|
||||
response_model=OperationalTaskDetail,
|
||||
)
|
||||
async def add_operational_task_human_evidence(
|
||||
task_id: UUID,
|
||||
command: HumanEvidenceCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).add_human_evidence(task_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/operational-tasks/{task_id}/evidence/{evidence_id}/snapshot",
|
||||
response_model=TaskDataSnapshot,
|
||||
)
|
||||
async def get_operational_task_data_snapshot(
|
||||
task_id: UUID,
|
||||
evidence_id: str,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).data_snapshot(task_id, evidence_id)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/actions",
|
||||
response_model=HumanApprovalAction,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def propose_operational_task_action(
|
||||
task_id: UUID,
|
||||
command: ActionProposalCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_WRITE)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).propose_action(task_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/actions/{action_id}/decision",
|
||||
response_model=HumanApprovalAction,
|
||||
)
|
||||
async def decide_operational_task_action(
|
||||
task_id: UUID,
|
||||
action_id: UUID,
|
||||
command: ActionDecisionCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).decide_action(task_id, action_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/actions/{action_id}/execute",
|
||||
response_model=HumanApprovalAction,
|
||||
)
|
||||
async def execute_operational_task_action(
|
||||
task_id: UUID,
|
||||
action_id: UUID,
|
||||
command: ActionExecuteCommand,
|
||||
authorization: str | None = Header(default=None, alias="Authorization"),
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
dispatcher: OperationalActionDispatcher = Depends(get_operational_action_dispatcher),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).submit_action(
|
||||
task_id, action_id, command, dispatcher, authorization
|
||||
)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/actions/{action_id}/reconcile",
|
||||
response_model=HumanApprovalAction,
|
||||
)
|
||||
async def reconcile_operational_task_action(
|
||||
task_id: UUID,
|
||||
action_id: UUID,
|
||||
command: ActionReconcileCommand,
|
||||
authorization: str | None = Header(default=None, alias="Authorization"),
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_VIEW)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
dispatcher: OperationalActionDispatcher = Depends(get_operational_action_dispatcher),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).reconcile_action(
|
||||
task_id, action_id, command, dispatcher, authorization
|
||||
)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/operational-tasks/{task_id}/external-feedback",
|
||||
response_model=ExternalFeedbackResult,
|
||||
)
|
||||
async def synchronize_operational_task_feedback(
|
||||
task_id: UUID,
|
||||
command: ExternalFeedbackCommand,
|
||||
ctx: ProjectContext = Depends(require_permission(OPERATIONAL_TASK_EXTERNAL_SYNC)),
|
||||
conn: AsyncConnection = Depends(get_project_pg_connection),
|
||||
):
|
||||
try:
|
||||
return await _service(ctx, conn).external_feedback(task_id, command)
|
||||
except DomainError as exc:
|
||||
_raise_domain(exc)
|
||||
Reference in New Issue
Block a user