""" BaseAction — interface every action plugin must implement. execute() receives the device, its parent object, params from the request, and an emit() callback to write progress messages to the SSE stream. """ from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any, Awaitable, Callable from app.models.device import Device from app.models.object import Object # Type alias for the progress callback Emitter = Callable[[str, str], Awaitable[None]] # (message, level) @dataclass class ActionResult: success: bool stdout: str = "" stderr: str = "" exit_code: int = 0 data: dict[str, Any] = field(default_factory=dict) error: str = "" class BaseAction(ABC): # Unique identifier used in POST /api/v1/tasks {"type": ""} name: str # Human-readable name shown in UI display_name: str # Which device categories this action supports. Empty list = all categories. compatible_categories: list[str] = [] # Parameter definitions for UI form generation params_schema: list[dict] = [] @abstractmethod async def execute( self, device: Device, obj: Object, params: dict, emit: Emitter, ) -> ActionResult: ... def is_compatible(self, category: str) -> bool: return not self.compatible_categories or category in self.compatible_categories class CompositeAction(BaseAction): """Dispatches to different sub-actions based on device category. Define routing as {category: action_instance}. The composite action's compatible_categories is derived automatically from the routing keys. Devices with a category not in routing are skipped with an error result. """ def __init__( self, name: str, display_name: str, routing: dict[str, "BaseAction"], params_schema: list[dict] | None = None, ) -> None: self.name = name self.display_name = display_name self._routing = routing self.compatible_categories = list(routing.keys()) self.params_schema = params_schema or [] async def execute( self, device: Device, obj: Object, params: dict, emit: Emitter, ) -> ActionResult: action = self._routing.get(device.category) if action is None: msg = f"[{device.ip}] Нет обработчика для категории '{device.category}'" await emit(msg, "error") return ActionResult(success=False, error=msg) return await action.execute(device, obj, params, emit)