diff --git a/surfsense_backend/app/automations/api/__init__.py b/surfsense_backend/app/automations/api/__init__.py new file mode 100644 index 000000000..459c6c1b4 --- /dev/null +++ b/surfsense_backend/app/automations/api/__init__.py @@ -0,0 +1,12 @@ +"""HTTP layer for the automations feature.""" + +from __future__ import annotations + +from fastapi import APIRouter + +from .automation import router as automation_router + +router = APIRouter() +router.include_router(automation_router) + +__all__ = ["router"] diff --git a/surfsense_backend/app/automations/api/automation.py b/surfsense_backend/app/automations/api/automation.py new file mode 100644 index 000000000..42163f74d --- /dev/null +++ b/surfsense_backend/app/automations/api/automation.py @@ -0,0 +1,22 @@ +"""Routes for the ``Automation`` resource.""" + +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, Body, Depends + +from app.automations.services import AutomationService, get_automation_service + +router = APIRouter() + + +@router.post("/automations/{automation_id}/run") +async def run_automation_now( + automation_id: int, + payload: dict[str, Any] | None = Body(default=None), + service: AutomationService = Depends(get_automation_service), +) -> dict[str, Any]: + """Fire a manual run.""" + run = await service.run_now(automation_id=automation_id, payload=payload) + return {"run_id": run.id, "status": run.status.value} diff --git a/surfsense_backend/app/automations/services/__init__.py b/surfsense_backend/app/automations/services/__init__.py new file mode 100644 index 000000000..f0a97d216 --- /dev/null +++ b/surfsense_backend/app/automations/services/__init__.py @@ -0,0 +1,7 @@ +"""Service layer for the automations feature.""" + +from __future__ import annotations + +from .automation import AutomationService, get_automation_service + +__all__ = ["AutomationService", "get_automation_service"] diff --git a/surfsense_backend/app/automations/services/automation.py b/surfsense_backend/app/automations/services/automation.py new file mode 100644 index 000000000..2a921e331 --- /dev/null +++ b/surfsense_backend/app/automations/services/automation.py @@ -0,0 +1,65 @@ +"""``AutomationService`` — orchestration for the ``Automation`` resource.""" + +from __future__ import annotations + +from typing import Any + +from fastapi import Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app.automations.dispatch import DispatchError +from app.automations.persistence.models.automation import Automation +from app.automations.persistence.models.run import AutomationRun +from app.automations.triggers.manual import dispatch_manual_run +from app.db import Permission, User, get_async_session +from app.users import current_active_user +from app.utils.rbac import check_permission + + +class AutomationService: + """Service for the ``Automation`` resource.""" + + def __init__(self, *, session: AsyncSession, user: User) -> None: + self.session = session + self.user = user + + async def run_now( + self, + *, + automation_id: int, + payload: dict[str, Any] | None, + ) -> AutomationRun: + """Fire a manual run for ``automation_id``.""" + automation = await self._get_automation_or_raise(automation_id) + await check_permission( + self.session, + self.user, + automation.search_space_id, + Permission.AUTOMATIONS_EXECUTE.value, + "You don't have permission to execute automations in this search space", + ) + + try: + return await dispatch_manual_run( + session=self.session, + automation_id=automation_id, + payload=payload, + ) + except DispatchError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + + async def _get_automation_or_raise(self, automation_id: int) -> Automation: + """Get the automation by id; 404 if missing.""" + automation = await self.session.get(Automation, automation_id) + if automation is None: + raise HTTPException( + status_code=404, detail=f"automation {automation_id} not found" + ) + return automation + + +def get_automation_service( + session: AsyncSession = Depends(get_async_session), + user: User = Depends(current_active_user), +) -> AutomationService: + return AutomationService(session=session, user=user) diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index 1d3ca2141..64c8c6585 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -7,7 +7,7 @@ from .agent_revert_route import router as agent_revert_router from .airtable_add_connector_route import ( router as airtable_add_connector_router, ) -from .automations_routes import router as automations_router +from app.automations.api import router as automations_router from .chat_comments_routes import router as chat_comments_router from .circleback_webhook_route import router as circleback_webhook_router from .clickup_add_connector_route import router as clickup_add_connector_router diff --git a/surfsense_backend/app/routes/automations_routes.py b/surfsense_backend/app/routes/automations_routes.py deleted file mode 100644 index 6c169b199..000000000 --- a/surfsense_backend/app/routes/automations_routes.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Routes for automations. v1: manual ``Run now``.""" - -from __future__ import annotations - -from typing import Any - -from fastapi import APIRouter, Body, Depends, HTTPException -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.automations.dispatch import DispatchError -from app.automations.persistence.models.automation import Automation -from app.automations.triggers.manual import dispatch_manual_run -from app.db import Permission, User, get_async_session -from app.users import current_active_user -from app.utils.rbac import check_permission - -router = APIRouter() - - -@router.post("/automations/{automation_id}/run") -async def run_automation_now( - automation_id: int, - payload: dict[str, Any] | None = Body(default=None), - session: AsyncSession = Depends(get_async_session), - user: User = Depends(current_active_user), -) -> dict[str, Any]: - """Fire an automation manually. Returns the new run id and status.""" - search_space_id = ( - await session.execute( - select(Automation.search_space_id).where(Automation.id == automation_id) - ) - ).scalar_one_or_none() - if search_space_id is None: - raise HTTPException( - status_code=404, detail=f"automation {automation_id} not found" - ) - - await check_permission( - session, - user, - search_space_id, - Permission.AUTOMATIONS_EXECUTE.value, - "You don't have permission to execute automations in this search space", - ) - - try: - run = await dispatch_manual_run( - session=session, - automation_id=automation_id, - payload=payload, - ) - except DispatchError as exc: - raise HTTPException(status_code=422, detail=str(exc)) from exc - - return {"run_id": run.id, "status": run.status.value}