base.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. # -*- coding: utf-8 -*-
  2. """
  3. Base Action - Abstrakte Basis für alle Actions.
  4. """
  5. from abc import ABC, abstractmethod
  6. from dataclasses import dataclass, field
  7. from datetime import datetime
  8. from typing import Any, TYPE_CHECKING
  9. if TYPE_CHECKING:
  10. from trixy_core.scheduler.form_fields import FormField, FormValidationError
  11. @dataclass
  12. class ActionResult:
  13. """Ergebnis einer Action-Ausführung."""
  14. success: bool
  15. action_name: str
  16. start_time: datetime
  17. end_time: datetime
  18. result: Any = None
  19. error: str | None = None
  20. metadata: dict[str, Any] = field(default_factory=dict)
  21. @property
  22. def duration_ms(self) -> float:
  23. """Ausführungsdauer in Millisekunden."""
  24. return (self.end_time - self.start_time).total_seconds() * 1000
  25. def to_dict(self) -> dict:
  26. """Konvertiert zu Dictionary."""
  27. return {
  28. "success": self.success,
  29. "action_name": self.action_name,
  30. "start_time": self.start_time.isoformat(),
  31. "end_time": self.end_time.isoformat(),
  32. "duration_ms": self.duration_ms,
  33. "error": self.error,
  34. "metadata": self.metadata,
  35. }
  36. class Action(ABC):
  37. """
  38. Abstrakte Basis für Actions.
  39. Eine Action wird ausgeführt wenn ein Job triggert.
  40. """
  41. def __init__(
  42. self,
  43. name: str | None = None,
  44. timeout_seconds: float = 60.0,
  45. retry_on_failure: bool = False,
  46. max_retries: int = 3,
  47. ):
  48. """
  49. Initialisiert die Action.
  50. Args:
  51. name: Action-Name
  52. timeout_seconds: Timeout für Ausführung
  53. retry_on_failure: Bei Fehler wiederholen
  54. max_retries: Maximale Wiederholungen
  55. """
  56. self._name = name or self.__class__.__name__
  57. self._timeout_seconds = timeout_seconds
  58. self._retry_on_failure = retry_on_failure
  59. self._max_retries = max_retries
  60. self._enabled = True
  61. self._execution_count = 0
  62. self._last_result: ActionResult | None = None
  63. @property
  64. def name(self) -> str:
  65. """Action-Name."""
  66. return self._name
  67. @property
  68. def timeout_seconds(self) -> float:
  69. """Timeout in Sekunden."""
  70. return self._timeout_seconds
  71. @property
  72. def is_enabled(self) -> bool:
  73. """Prüft ob Action aktiv ist."""
  74. return self._enabled
  75. @property
  76. def execution_count(self) -> int:
  77. """Anzahl Ausführungen."""
  78. return self._execution_count
  79. @property
  80. def last_result(self) -> ActionResult | None:
  81. """Letztes Ergebnis."""
  82. return self._last_result
  83. def enable(self) -> None:
  84. """Aktiviert die Action."""
  85. self._enabled = True
  86. def disable(self) -> None:
  87. """Deaktiviert die Action."""
  88. self._enabled = False
  89. @abstractmethod
  90. async def _execute(self, context: dict) -> Any:
  91. """
  92. Interne Ausführung.
  93. Args:
  94. context: Ausführungs-Kontext
  95. Returns:
  96. Ergebnis der Ausführung
  97. """
  98. pass
  99. async def execute(self, context: dict | None = None) -> ActionResult:
  100. """
  101. Führt die Action aus.
  102. Args:
  103. context: Ausführungs-Kontext
  104. Returns:
  105. ActionResult mit Ergebnis
  106. """
  107. start_time = datetime.now()
  108. ctx = context or {}
  109. retries = 0
  110. while True:
  111. try:
  112. result = await self._execute(ctx)
  113. end_time = datetime.now()
  114. action_result = ActionResult(
  115. success=True,
  116. action_name=self._name,
  117. start_time=start_time,
  118. end_time=end_time,
  119. result=result,
  120. )
  121. self._execution_count += 1
  122. self._last_result = action_result
  123. return action_result
  124. except Exception as e:
  125. retries += 1
  126. if self._retry_on_failure and retries < self._max_retries:
  127. # Retry
  128. import asyncio
  129. await asyncio.sleep(1.0 * retries) # Exponential backoff
  130. continue
  131. end_time = datetime.now()
  132. action_result = ActionResult(
  133. success=False,
  134. action_name=self._name,
  135. start_time=start_time,
  136. end_time=end_time,
  137. error=str(e),
  138. metadata={"retries": retries},
  139. )
  140. self._execution_count += 1
  141. self._last_result = action_result
  142. return action_result
  143. @abstractmethod
  144. def to_dict(self) -> dict:
  145. """Konvertiert zu Dictionary."""
  146. pass
  147. def _base_dict(self) -> dict:
  148. """Basis-Dictionary für Serialisierung."""
  149. return {
  150. "type": self.__class__.__name__,
  151. "name": self._name,
  152. "timeout_seconds": self._timeout_seconds,
  153. "retry_on_failure": self._retry_on_failure,
  154. "max_retries": self._max_retries,
  155. "enabled": self._enabled,
  156. }
  157. @classmethod
  158. def form_fields(cls) -> list["FormField"]:
  159. """Gibt die Formular-Felder fuer die TUI zurueck."""
  160. return []
  161. @classmethod
  162. def validate_form(cls, data: dict) -> list["FormValidationError"]:
  163. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  164. return []
  165. @classmethod
  166. def display_name(cls) -> str:
  167. """Anzeigename fuer die TUI."""
  168. return cls.__name__
  169. def __repr__(self) -> str:
  170. return f"{self.__class__.__name__}(name={self._name})"