utp_service/backend/app/plugins/base.py

92 lines
2.7 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, Optional
from sqlalchemy.ext.asyncio import AsyncSession
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,
db: Optional[AsyncSession] = None,
) -> 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,
db: Optional[AsyncSession] = None,
) -> 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, db=db)