From 84d99f19a253ecf34c5e53285ce6cb65cd2d98c7 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 27 May 2026 19:10:20 +0200 Subject: [PATCH] automations(api): API request/response schemas --- .../app/automations/api/automation.py | 7 +- .../app/automations/api/schemas/__init__.py | 28 ++++++++ .../app/automations/api/schemas/automation.py | 64 +++++++++++++++++++ .../app/automations/api/schemas/run.py | 50 +++++++++++++++ .../app/automations/api/schemas/trigger.py | 43 +++++++++++++ 5 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 surfsense_backend/app/automations/api/schemas/__init__.py create mode 100644 surfsense_backend/app/automations/api/schemas/automation.py create mode 100644 surfsense_backend/app/automations/api/schemas/run.py create mode 100644 surfsense_backend/app/automations/api/schemas/trigger.py diff --git a/surfsense_backend/app/automations/api/automation.py b/surfsense_backend/app/automations/api/automation.py index 42163f74d..4d0ce7209 100644 --- a/surfsense_backend/app/automations/api/automation.py +++ b/surfsense_backend/app/automations/api/automation.py @@ -6,17 +6,18 @@ from typing import Any from fastapi import APIRouter, Body, Depends +from app.automations.api.schemas import RunDispatched from app.automations.services import AutomationService, get_automation_service router = APIRouter() -@router.post("/automations/{automation_id}/run") +@router.post("/automations/{automation_id}/run", response_model=RunDispatched) 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]: +) -> RunDispatched: """Fire a manual run.""" run = await service.run_now(automation_id=automation_id, payload=payload) - return {"run_id": run.id, "status": run.status.value} + return RunDispatched(run_id=run.id, status=run.status) diff --git a/surfsense_backend/app/automations/api/schemas/__init__.py b/surfsense_backend/app/automations/api/schemas/__init__.py new file mode 100644 index 000000000..a8a010a2c --- /dev/null +++ b/surfsense_backend/app/automations/api/schemas/__init__.py @@ -0,0 +1,28 @@ +"""Request/response schemas for the automations HTTP layer.""" + +from __future__ import annotations + +from .automation import ( + AutomationCreate, + AutomationDetail, + AutomationList, + AutomationSummary, + AutomationUpdate, +) +from .run import RunDetail, RunDispatched, RunList, RunSummary +from .trigger import TriggerCreate, TriggerDetail, TriggerUpdate + +__all__ = [ + "AutomationCreate", + "AutomationDetail", + "AutomationList", + "AutomationSummary", + "AutomationUpdate", + "RunDetail", + "RunDispatched", + "RunList", + "RunSummary", + "TriggerCreate", + "TriggerDetail", + "TriggerUpdate", +] diff --git a/surfsense_backend/app/automations/api/schemas/automation.py b/surfsense_backend/app/automations/api/schemas/automation.py new file mode 100644 index 000000000..c1defd417 --- /dev/null +++ b/surfsense_backend/app/automations/api/schemas/automation.py @@ -0,0 +1,64 @@ +"""Request/response schemas for the ``Automation`` resource.""" + +from __future__ import annotations + +from datetime import datetime + +from pydantic import BaseModel, ConfigDict, Field + +from app.automations.persistence.enums.automation_status import AutomationStatus +from app.automations.schemas.definition import AutomationDefinition + +from .trigger import TriggerCreate, TriggerDetail + + +class AutomationCreate(BaseModel): + """Create an automation, optionally with initial triggers (atomic).""" + + model_config = ConfigDict(extra="forbid") + + search_space_id: int + name: str = Field(..., min_length=1, max_length=200) + description: str | None = None + definition: AutomationDefinition + triggers: list[TriggerCreate] = Field(default_factory=list) + + +class AutomationUpdate(BaseModel): + """Partial update of an automation. Triggers are managed separately.""" + + model_config = ConfigDict(extra="forbid") + + name: str | None = Field(default=None, min_length=1, max_length=200) + description: str | None = None + status: AutomationStatus | None = None + definition: AutomationDefinition | None = None + + +class AutomationSummary(BaseModel): + """Lightweight automation view for list endpoints.""" + + model_config = ConfigDict(from_attributes=True) + + id: int + search_space_id: int + name: str + description: str | None = None + status: AutomationStatus + version: int + created_at: datetime + updated_at: datetime + + +class AutomationDetail(AutomationSummary): + """Full automation view including definition and attached triggers.""" + + definition: AutomationDefinition + triggers: list[TriggerDetail] = Field(default_factory=list) + + +class AutomationList(BaseModel): + """Paginated list of automations.""" + + items: list[AutomationSummary] + total: int diff --git a/surfsense_backend/app/automations/api/schemas/run.py b/surfsense_backend/app/automations/api/schemas/run.py new file mode 100644 index 000000000..789b6f674 --- /dev/null +++ b/surfsense_backend/app/automations/api/schemas/run.py @@ -0,0 +1,50 @@ +"""Response schemas for run sub-resources and run dispatch.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from pydantic import BaseModel, ConfigDict + +from app.automations.persistence.enums.run_status import RunStatus + + +class RunSummary(BaseModel): + """Lightweight run view for list endpoints.""" + + model_config = ConfigDict(from_attributes=True) + + id: int + automation_id: int + trigger_id: int | None = None + status: RunStatus + started_at: datetime | None = None + finished_at: datetime | None = None + created_at: datetime + + +class RunDetail(RunSummary): + """Full run view including snapshot, results and artifacts.""" + + definition_snapshot: dict[str, Any] + trigger_payload: dict[str, Any] | None = None + resolved_inputs: dict[str, Any] + step_results: list[dict[str, Any]] + output: dict[str, Any] | None = None + artifacts: list[dict[str, Any]] + error: dict[str, Any] | None = None + + +class RunList(BaseModel): + """Paginated list of runs.""" + + items: list[RunSummary] + total: int + + +class RunDispatched(BaseModel): + """Response of a successful run dispatch.""" + + run_id: int + status: RunStatus diff --git a/surfsense_backend/app/automations/api/schemas/trigger.py b/surfsense_backend/app/automations/api/schemas/trigger.py new file mode 100644 index 000000000..32afe7c60 --- /dev/null +++ b/surfsense_backend/app/automations/api/schemas/trigger.py @@ -0,0 +1,43 @@ +"""Request/response schemas for trigger sub-resources.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field + +from app.automations.persistence.enums.trigger_type import TriggerType + + +class TriggerCreate(BaseModel): + """Attach a trigger to an automation.""" + + model_config = ConfigDict(extra="forbid") + + type: TriggerType + params: dict[str, Any] = Field(default_factory=dict) + enabled: bool = True + + +class TriggerUpdate(BaseModel): + """Partial update of an existing trigger.""" + + model_config = ConfigDict(extra="forbid") + + enabled: bool | None = None + params: dict[str, Any] | None = None + + +class TriggerDetail(BaseModel): + """Trigger as returned to clients.""" + + model_config = ConfigDict(from_attributes=True) + + id: int + type: TriggerType + params: dict[str, Any] + enabled: bool + last_fired_at: datetime | None = None + next_fire_at: datetime | None = None + created_at: datetime