mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-31 19:45:15 +02:00
chore: linting
This commit is contained in:
parent
4dda02c06c
commit
94e834134f
80 changed files with 443 additions and 404 deletions
|
|
@ -21,4 +21,4 @@ __all__ = [
|
|||
]
|
||||
|
||||
# Built-in actions self-register at import time.
|
||||
from . import agent_task # noqa: E402, F401
|
||||
from . import agent_task # noqa: F401
|
||||
|
|
|
|||
|
|
@ -12,4 +12,4 @@ from .params import AgentTaskActionParams
|
|||
__all__ = ["AgentTaskActionParams", "build_handler"]
|
||||
|
||||
# Side-effect: register on the actions store.
|
||||
from . import definition # noqa: E402, F401
|
||||
from . import definition # noqa: F401
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from app.agents.multi_agent_chat import create_multi_agent_chat_deep_agent
|
|||
from app.db import ChatVisibility, async_session_maker
|
||||
|
||||
from ..types import ActionContext
|
||||
|
||||
from .auto_decide import build_auto_decisions
|
||||
from .dependencies import build_dependencies
|
||||
from .finalize import extract_final_assistant_message
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class AutomationRun(BaseModel, TimestampMixin):
|
|||
definition_snapshot = Column(JSONB, nullable=False)
|
||||
|
||||
# merged & validated inputs the run was dispatched with
|
||||
# (trigger.static_inputs ∪ producer runtime data, static wins on collision)
|
||||
# (trigger.static_inputs union producer runtime data, static wins on collision)
|
||||
inputs = Column(JSONB, nullable=False, server_default="{}")
|
||||
# one entry per executed step; agent_task entries carry their own
|
||||
# `agent_session_id` inside their entry
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ from typing import Any
|
|||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.automations.actions.types import ActionContext
|
||||
from app.automations.persistence.enums.run_status import RunStatus
|
||||
from app.automations.persistence.models.run import AutomationRun
|
||||
from app.automations.actions.types import ActionContext
|
||||
from app.automations.schemas.definition.envelope import AutomationDefinition
|
||||
from app.automations.schemas.definition.plan_step import PlanStep
|
||||
from app.automations.templating import build_run_context
|
||||
|
|
@ -32,7 +32,10 @@ async def execute_run(session: AsyncSession, run_id: int) -> None:
|
|||
await repository.mark_failed(
|
||||
session,
|
||||
run,
|
||||
{"message": f"definition_snapshot invalid: {exc}", "type": type(exc).__name__},
|
||||
{
|
||||
"message": f"definition_snapshot invalid: {exc}",
|
||||
"type": type(exc).__name__,
|
||||
},
|
||||
)
|
||||
await session.commit()
|
||||
return
|
||||
|
|
@ -92,7 +95,9 @@ async def _run_on_failure(
|
|||
await session.commit()
|
||||
|
||||
|
||||
def _build_template_ctx(run: AutomationRun, step_outputs: dict[str, Any]) -> dict[str, Any]:
|
||||
def _build_template_ctx(
|
||||
run: AutomationRun, step_outputs: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
automation = run.automation
|
||||
trigger = run.trigger
|
||||
return build_run_context(
|
||||
|
|
|
|||
|
|
@ -30,14 +30,18 @@ async def execute_step(
|
|||
try:
|
||||
should_run = evaluate_predicate(step.when, template_context)
|
||||
except Exception as exc:
|
||||
return _result(step, "failed", started_at, attempts=0, error=_error(exc, "when"))
|
||||
return _result(
|
||||
step, "failed", started_at, attempts=0, error=_error(exc, "when")
|
||||
)
|
||||
if not should_run:
|
||||
return _result(step, "skipped", started_at, attempts=0)
|
||||
|
||||
try:
|
||||
resolved_params = render_value(step.params, template_context)
|
||||
except Exception as exc:
|
||||
return _result(step, "failed", started_at, attempts=0, error=_error(exc, "render"))
|
||||
return _result(
|
||||
step, "failed", started_at, attempts=0, error=_error(exc, "render")
|
||||
)
|
||||
|
||||
action = get_action(step.action)
|
||||
if action is None:
|
||||
|
|
@ -46,12 +50,17 @@ async def execute_step(
|
|||
"failed",
|
||||
started_at,
|
||||
attempts=0,
|
||||
error={"message": f"action not registered: {step.action}", "type": "ActionNotFound"},
|
||||
error={
|
||||
"message": f"action not registered: {step.action}",
|
||||
"type": "ActionNotFound",
|
||||
},
|
||||
)
|
||||
|
||||
handler = action.build_handler(action_context)
|
||||
|
||||
max_retries = step.max_retries if step.max_retries is not None else default_max_retries
|
||||
max_retries = (
|
||||
step.max_retries if step.max_retries is not None else default_max_retries
|
||||
)
|
||||
timeout = step.timeout_seconds or default_timeout_seconds
|
||||
|
||||
try:
|
||||
|
|
@ -62,7 +71,9 @@ async def execute_step(
|
|||
timeout=timeout,
|
||||
)
|
||||
except Exception as exc:
|
||||
return _result(step, "failed", started_at, attempts=max_retries + 1, error=_error(exc))
|
||||
return _result(
|
||||
step, "failed", started_at, attempts=max_retries + 1, error=_error(exc)
|
||||
)
|
||||
|
||||
return _result(step, "succeeded", started_at, attempts=attempts, result=result)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ from .plan_step import PlanStep
|
|||
class Execution(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
timeout_seconds: int = Field(default=600, gt=0, description="Wall-clock cap for the run.")
|
||||
timeout_seconds: int = Field(
|
||||
default=600, gt=0, description="Wall-clock cap for the run."
|
||||
)
|
||||
max_retries: int = Field(default=2, ge=0, description="Per-step retry budget.")
|
||||
retry_backoff: Literal["exponential", "linear", "none"] = "exponential"
|
||||
concurrency: Literal["drop_if_running", "queue", "always"] = "drop_if_running"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ class PlanStep(BaseModel):
|
|||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
step_id: str = Field(..., min_length=1, description="Unique within the plan.")
|
||||
action: str = Field(..., min_length=1, description="Action type; resolved via registry.")
|
||||
action: str = Field(
|
||||
..., min_length=1, description="Action type; resolved via registry."
|
||||
)
|
||||
when: str | None = Field(
|
||||
default=None,
|
||||
description="Optional predicate; step is skipped when falsy.",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ from pydantic import BaseModel, ConfigDict, Field
|
|||
class TriggerSpec(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
type: str = Field(..., min_length=1, description="Trigger type; resolved via registry.")
|
||||
type: str = Field(
|
||||
..., min_length=1, description="Trigger type; resolved via registry."
|
||||
)
|
||||
params: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Type-specific params; validated against the trigger's schema.",
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ from sqlalchemy import func, select
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.automations.persistence.enums.trigger_type import TriggerType
|
||||
from app.automations.persistence.models.automation import Automation
|
||||
from app.automations.persistence.models.trigger import AutomationTrigger
|
||||
from app.automations.schemas.api import (
|
||||
AutomationCreate,
|
||||
AutomationUpdate,
|
||||
TriggerCreate,
|
||||
)
|
||||
from app.automations.persistence.enums.trigger_type import TriggerType
|
||||
from app.automations.persistence.models.automation import Automation
|
||||
from app.automations.persistence.models.trigger import AutomationTrigger
|
||||
from app.automations.triggers import get_trigger
|
||||
from app.automations.triggers.schedule import compute_next_fire_at
|
||||
from app.db import Permission, User, get_async_session
|
||||
|
|
@ -34,7 +34,9 @@ class AutomationService:
|
|||
|
||||
async def create(self, payload: AutomationCreate) -> Automation:
|
||||
"""Create an automation and its initial triggers in one transaction."""
|
||||
await self._authorize(payload.search_space_id, Permission.AUTOMATIONS_CREATE.value)
|
||||
await self._authorize(
|
||||
payload.search_space_id, Permission.AUTOMATIONS_CREATE.value
|
||||
)
|
||||
|
||||
automation = Automation(
|
||||
search_space_id=payload.search_space_id,
|
||||
|
|
@ -67,22 +69,32 @@ class AutomationService:
|
|||
)
|
||||
|
||||
rows = (
|
||||
await self.session.execute(
|
||||
base.order_by(Automation.created_at.desc()).limit(limit).offset(offset)
|
||||
(
|
||||
await self.session.execute(
|
||||
base.order_by(Automation.created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
return list(rows), int(total or 0)
|
||||
|
||||
async def get(self, automation_id: int) -> Automation:
|
||||
"""Get an automation with its triggers loaded."""
|
||||
automation = await self._get_with_triggers_or_raise(automation_id)
|
||||
await self._authorize(automation.search_space_id, Permission.AUTOMATIONS_READ.value)
|
||||
await self._authorize(
|
||||
automation.search_space_id, Permission.AUTOMATIONS_READ.value
|
||||
)
|
||||
return automation
|
||||
|
||||
async def update(self, automation_id: int, patch: AutomationUpdate) -> Automation:
|
||||
"""Patch fields. Bumps ``version`` when ``definition`` changes."""
|
||||
automation = await self._get_with_triggers_or_raise(automation_id)
|
||||
await self._authorize(automation.search_space_id, Permission.AUTOMATIONS_UPDATE.value)
|
||||
await self._authorize(
|
||||
automation.search_space_id, Permission.AUTOMATIONS_UPDATE.value
|
||||
)
|
||||
|
||||
data = patch.model_dump(exclude_unset=True)
|
||||
|
||||
|
|
@ -93,7 +105,9 @@ class AutomationService:
|
|||
if "status" in data:
|
||||
automation.status = data["status"]
|
||||
if "definition" in data:
|
||||
automation.definition = patch.definition.model_dump(mode="json", by_alias=True)
|
||||
automation.definition = patch.definition.model_dump(
|
||||
mode="json", by_alias=True
|
||||
)
|
||||
automation.version += 1
|
||||
|
||||
await self.session.commit()
|
||||
|
|
@ -102,7 +116,9 @@ class AutomationService:
|
|||
async def delete(self, automation_id: int) -> None:
|
||||
"""Delete an automation; FK cascades remove triggers and runs."""
|
||||
automation = await self._get_or_raise(automation_id)
|
||||
await self._authorize(automation.search_space_id, Permission.AUTOMATIONS_DELETE.value)
|
||||
await self._authorize(
|
||||
automation.search_space_id, Permission.AUTOMATIONS_DELETE.value
|
||||
)
|
||||
await self.session.delete(automation)
|
||||
await self.session.commit()
|
||||
|
||||
|
|
@ -141,7 +157,9 @@ def _build_trigger(spec: TriggerCreate) -> AutomationTrigger:
|
|||
"""Validate trigger params via its registered Pydantic model and build the ORM row."""
|
||||
definition = get_trigger(spec.type.value)
|
||||
if definition is None:
|
||||
raise HTTPException(status_code=422, detail=f"unknown trigger type {spec.type.value!r}")
|
||||
raise HTTPException(
|
||||
status_code=422, detail=f"unknown trigger type {spec.type.value!r}"
|
||||
)
|
||||
|
||||
try:
|
||||
validated = definition.params_model.model_validate(spec.params)
|
||||
|
|
|
|||
|
|
@ -36,10 +36,16 @@ class RunService:
|
|||
)
|
||||
|
||||
rows = (
|
||||
await self.session.execute(
|
||||
base.order_by(AutomationRun.created_at.desc()).limit(limit).offset(offset)
|
||||
(
|
||||
await self.session.execute(
|
||||
base.order_by(AutomationRun.created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
)
|
||||
).scalars().all()
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
return list(rows), int(total or 0)
|
||||
|
||||
async def get(self, *, automation_id: int, run_id: int) -> AutomationRun:
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ from fastapi import Depends, HTTPException
|
|||
from pydantic import ValidationError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.automations.schemas.api import TriggerCreate, TriggerUpdate
|
||||
from app.automations.persistence.enums.trigger_type import TriggerType
|
||||
from app.automations.persistence.models.automation import Automation
|
||||
from app.automations.persistence.models.trigger import AutomationTrigger
|
||||
from app.automations.schemas.api import TriggerCreate, TriggerUpdate
|
||||
from app.automations.triggers import get_trigger
|
||||
from app.automations.triggers.schedule import compute_next_fire_at
|
||||
from app.db import Permission, User, get_async_session
|
||||
|
|
@ -40,7 +40,9 @@ class TriggerService:
|
|||
params=validated_params,
|
||||
static_inputs=payload.static_inputs,
|
||||
enabled=payload.enabled,
|
||||
next_fire_at=_initial_next_fire(payload.type, validated_params, payload.enabled),
|
||||
next_fire_at=_initial_next_fire(
|
||||
payload.type, validated_params, payload.enabled
|
||||
),
|
||||
)
|
||||
self.session.add(trigger)
|
||||
await self.session.commit()
|
||||
|
|
@ -54,7 +56,9 @@ class TriggerService:
|
|||
trigger_id: int,
|
||||
patch: TriggerUpdate,
|
||||
) -> AutomationTrigger:
|
||||
await self._authorize_automation(automation_id, Permission.AUTOMATIONS_UPDATE.value)
|
||||
await self._authorize_automation(
|
||||
automation_id, Permission.AUTOMATIONS_UPDATE.value
|
||||
)
|
||||
trigger = await self._get_trigger_or_raise(automation_id, trigger_id)
|
||||
|
||||
data = patch.model_dump(exclude_unset=True)
|
||||
|
|
@ -80,7 +84,9 @@ class TriggerService:
|
|||
return trigger
|
||||
|
||||
async def remove(self, *, automation_id: int, trigger_id: int) -> None:
|
||||
await self._authorize_automation(automation_id, Permission.AUTOMATIONS_UPDATE.value)
|
||||
await self._authorize_automation(
|
||||
automation_id, Permission.AUTOMATIONS_UPDATE.value
|
||||
)
|
||||
trigger = await self._get_trigger_or_raise(automation_id, trigger_id)
|
||||
await self.session.delete(trigger)
|
||||
await self.session.commit()
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ TASK_NAME = "automation_run_execute"
|
|||
|
||||
|
||||
@celery_app.task(name=TASK_NAME, bind=True)
|
||||
def automation_run_execute(self, run_id: int) -> None: # noqa: ARG001 — Celery bind
|
||||
def automation_run_execute(self, run_id: int) -> None:
|
||||
"""Execute one ``AutomationRun``. Idempotent: terminal runs no-op."""
|
||||
return run_async_celery_task(lambda: _impl(run_id))
|
||||
|
||||
|
|
|
|||
|
|
@ -103,9 +103,7 @@ async def _self_heal_null_next_fire(session: AsyncSession, *, now: datetime) ->
|
|||
await session.commit()
|
||||
|
||||
|
||||
async def _claim_due_triggers(
|
||||
session: AsyncSession, *, now: datetime
|
||||
) -> list[_Claim]:
|
||||
async def _claim_due_triggers(session: AsyncSession, *, now: datetime) -> list[_Claim]:
|
||||
"""Lock and advance due rows; return per-trigger fire context."""
|
||||
stmt = (
|
||||
select(AutomationTrigger)
|
||||
|
|
|
|||
|
|
@ -17,4 +17,4 @@ __all__ = [
|
|||
]
|
||||
|
||||
# Built-in triggers self-register at import time.
|
||||
from . import schedule # noqa: E402, F401
|
||||
from . import schedule # noqa: F401
|
||||
|
|
|
|||
|
|
@ -15,4 +15,4 @@ __all__ = [
|
|||
]
|
||||
|
||||
# Side-effect: register on the triggers store.
|
||||
from . import definition # noqa: E402, F401
|
||||
from . import definition # noqa: F401
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ def compute_next_fire_at(cron: str, timezone: str, *, after: datetime) -> dateti
|
|||
given timezone before evaluation so DST and IANA rules apply correctly.
|
||||
"""
|
||||
tz = ZoneInfo(timezone)
|
||||
base = after.astimezone(tz) if after.tzinfo else after.replace(tzinfo=UTC).astimezone(tz)
|
||||
base = (
|
||||
after.astimezone(tz)
|
||||
if after.tzinfo
|
||||
else after.replace(tzinfo=UTC).astimezone(tz)
|
||||
)
|
||||
nxt: datetime = croniter(cron, base).get_next(datetime)
|
||||
return nxt.astimezone(UTC)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ from .cron import InvalidCronError, validate_cron
|
|||
class ScheduleTriggerParams(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
cron: str = Field(..., description="Five-field cron expression.", examples=["0 9 * * 1-5"])
|
||||
cron: str = Field(
|
||||
..., description="Five-field cron expression.", examples=["0 9 * * 1-5"]
|
||||
)
|
||||
timezone: str = Field(..., description="IANA timezone.", examples=["Africa/Kigali"])
|
||||
|
||||
@model_validator(mode="after")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue