feat(automations): static_inputs on triggers + vertical-slice api/services

This commit is contained in:
CREDO23 2026-05-27 21:21:43 +02:00
parent 84d99f19a2
commit 27ab367a13
27 changed files with 915 additions and 356 deletions

View file

@ -9,8 +9,7 @@ from .params import ManualTriggerParams
MANUAL_TRIGGER = TriggerDefinition(
type="manual",
description="Fire on a user-initiated 'Run now' invocation.",
params_schema=ManualTriggerParams.model_json_schema(),
payload_schema={"type": "object"},
params_model=ManualTriggerParams,
)
register_trigger(MANUAL_TRIGGER)

View file

@ -19,9 +19,14 @@ async def dispatch_manual_run(
*,
session: AsyncSession,
automation_id: int,
payload: dict[str, Any] | None,
runtime_inputs: dict[str, Any] | None,
) -> AutomationRun:
"""Find the automation + its enabled manual trigger, then run the generic dispatch."""
"""Find the automation + its enabled manual trigger, then run the generic dispatch.
``runtime_inputs`` is the caller-supplied payload (e.g. an HTTP body for a
"Run now" API call); it is merged with the trigger's ``static_inputs`` by
the generic dispatcher, with static winning on key collision.
"""
automation = await _load_automation(session, automation_id)
if automation is None:
raise DispatchError(f"automation {automation_id} not found")
@ -41,7 +46,7 @@ async def dispatch_manual_run(
session=session,
automation=automation,
trigger=trigger,
payload=payload,
runtime_inputs=runtime_inputs,
)

View file

@ -9,12 +9,7 @@ from .params import ScheduleTriggerParams
SCHEDULE_TRIGGER = TriggerDefinition(
type="schedule",
description="Fire on a cron schedule in a given timezone.",
params_schema=ScheduleTriggerParams.model_json_schema(),
payload_schema={
"type": "object",
"additionalProperties": False,
"properties": {},
},
params_model=ScheduleTriggerParams,
)
register_trigger(SCHEDULE_TRIGGER)

View file

@ -2,6 +2,8 @@
from __future__ import annotations
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@ -16,9 +18,18 @@ async def dispatch_schedule_run(
*,
session: AsyncSession,
trigger: AutomationTrigger,
fired_at: datetime,
scheduled_for: datetime,
previous_last_fired_at: datetime | None,
) -> AutomationRun:
"""Fire one scheduled run for ``trigger``.
Emits calendar context as runtime inputs:
- ``fired_at`` actual fire time
- ``scheduled_for`` cron-derived target time for this fire
- ``last_fired_at`` fire time of the previous run, or null on first fire
The caller (the schedule tick) is responsible for selecting due triggers
and advancing ``next_fire_at`` / ``last_fired_at`` before invoking this.
"""
@ -33,11 +44,19 @@ async def dispatch_schedule_run(
f"automation {trigger.automation_id} is {automation.status.value}, not active"
)
runtime_inputs = {
"fired_at": fired_at.isoformat(),
"scheduled_for": scheduled_for.isoformat(),
"last_fired_at": (
previous_last_fired_at.isoformat() if previous_last_fired_at else None
),
}
return await dispatch_run(
session=session,
automation=automation,
trigger=trigger,
payload=None,
runtime_inputs=runtime_inputs,
)

View file

@ -5,10 +5,16 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from pydantic import BaseModel
@dataclass(frozen=True, slots=True)
class TriggerDefinition:
type: str
description: str
params_schema: dict[str, Any]
payload_schema: dict[str, Any]
params_model: type[BaseModel]
@property
def params_schema(self) -> dict[str, Any]:
"""JSON Schema (draft 2020-12) derived from ``params_model``."""
return self.params_model.model_json_schema()