88 lines
2.6 KiB
Python
88 lines
2.6 KiB
Python
"""
|
|
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>"}
|
|
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)
|