schedule_entry.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920
  1. """
  2. Schedule Entry Module for Trixy Scheduler System
  3. This module provides the ScheduleEntry class that represents individual schedule entries
  4. with unique names, triggers, actions, and validation. Each schedule entry can have
  5. multiple triggers and actions, enabling complex scheduling scenarios.
  6. Features:
  7. - Unique name validation and constraint enforcement
  8. - Multiple trigger support (date, time, event, weekday, interval, cron)
  9. - Multiple action support (events, ML training, functions)
  10. - Enable/disable functionality
  11. - Execution history tracking
  12. - Thread-safe operations
  13. - Comprehensive validation and error handling
  14. - Integration with Trixy event system
  15. Usage:
  16. from trixy_core.scheduler.schedule_entry import ScheduleEntry
  17. from trixy_core.scheduler.triggers import DateTrigger
  18. from trixy_core.scheduler.actions import EventAction
  19. # Create a schedule entry
  20. entry = ScheduleEntry(
  21. name="daily_backup",
  22. description="Daily backup at 2 AM",
  23. enabled=True
  24. )
  25. # Add triggers and actions
  26. entry.add_trigger(DateTrigger(hour=2, minute=0))
  27. entry.add_action(EventAction("system_backup", {"type": "full"}))
  28. # Check if schedule should execute
  29. if entry.should_execute():
  30. entry.execute()
  31. """
  32. import threading
  33. import time
  34. import uuid
  35. from datetime import datetime, timezone
  36. from typing import List, Dict, Any, Optional, Set, Callable, Union
  37. from dataclasses import dataclass, field
  38. from enum import Enum
  39. import json
  40. from pprint import pformat
  41. class ScheduleStatus(Enum):
  42. """Status of a schedule entry."""
  43. ENABLED = "enabled"
  44. DISABLED = "disabled"
  45. EXECUTING = "executing"
  46. ERROR = "error"
  47. COMPLETED = "completed"
  48. class ExecutionResult(Enum):
  49. """Result of schedule execution."""
  50. SUCCESS = "success"
  51. FAILURE = "failure"
  52. PARTIAL = "partial"
  53. SKIPPED = "skipped"
  54. TIMEOUT = "timeout"
  55. @dataclass
  56. class ExecutionHistory:
  57. """History entry for schedule execution."""
  58. execution_id: str
  59. timestamp: float
  60. duration_seconds: float
  61. result: ExecutionResult
  62. triggers_fired: List[str] = field(default_factory=list)
  63. actions_executed: List[str] = field(default_factory=list)
  64. actions_failed: List[str] = field(default_factory=list)
  65. error_message: Optional[str] = None
  66. metadata: Dict[str, Any] = field(default_factory=dict)
  67. def to_dict(self) -> Dict[str, Any]:
  68. """Convert execution history to dictionary."""
  69. return {
  70. 'execution_id': self.execution_id,
  71. 'timestamp': self.timestamp,
  72. 'duration_seconds': self.duration_seconds,
  73. 'result': self.result.value,
  74. 'triggers_fired': self.triggers_fired,
  75. 'actions_executed': self.actions_executed,
  76. 'actions_failed': self.actions_failed,
  77. 'error_message': self.error_message,
  78. 'metadata': self.metadata
  79. }
  80. @classmethod
  81. def from_dict(cls, data: Dict[str, Any]) -> 'ExecutionHistory':
  82. """Create execution history from dictionary."""
  83. return cls(
  84. execution_id=data['execution_id'],
  85. timestamp=data['timestamp'],
  86. duration_seconds=data['duration_seconds'],
  87. result=ExecutionResult(data['result']),
  88. triggers_fired=data.get('triggers_fired', []),
  89. actions_executed=data.get('actions_executed', []),
  90. actions_failed=data.get('actions_failed', []),
  91. error_message=data.get('error_message'),
  92. metadata=data.get('metadata', {})
  93. )
  94. class ScheduleEntryError(Exception):
  95. """Base exception for schedule entry errors."""
  96. pass
  97. class NameConflictError(ScheduleEntryError):
  98. """Raised when schedule name conflicts with existing entry."""
  99. pass
  100. class ValidationError(ScheduleEntryError):
  101. """Raised when schedule validation fails."""
  102. pass
  103. class ExecutionError(ScheduleEntryError):
  104. """Raised when schedule execution fails."""
  105. pass
  106. def pprint(message: str) -> None:
  107. """
  108. Schedule entry logging function that adapts based on mode.
  109. Uses the same pattern as specified in CLAUDE.md.
  110. """
  111. print(f"[SCHEDULER.ENTRY] {message}")
  112. class ScheduleEntry:
  113. """
  114. Represents a single schedule entry with unique name, triggers, and actions.
  115. A schedule entry is the fundamental unit of the Trixy scheduler system.
  116. Each entry has:
  117. - A unique name that serves as the primary identifier
  118. - One or more triggers that determine when the schedule should execute
  119. - One or more actions that are performed when triggers fire
  120. - Configuration options for execution behavior
  121. - History tracking for monitoring and debugging
  122. The schedule entry is thread-safe and can be safely accessed from multiple
  123. threads, making it suitable for use in the multi-satellite Trixy environment.
  124. """
  125. # Class-level registry to enforce unique names
  126. _name_registry: Set[str] = set()
  127. _registry_lock = threading.RLock()
  128. def __init__(
  129. self,
  130. name: str,
  131. description: str = "",
  132. enabled: bool = True,
  133. max_history: int = 100,
  134. execution_timeout: float = 300.0, # 5 minutes default
  135. allow_concurrent: bool = False,
  136. priority: int = 0,
  137. tags: Optional[List[str]] = None,
  138. metadata: Optional[Dict[str, Any]] = None
  139. ):
  140. """
  141. Initialize a new schedule entry.
  142. Args:
  143. name: Unique name for the schedule entry
  144. description: Human-readable description
  145. enabled: Whether the schedule is enabled
  146. max_history: Maximum number of execution history entries to keep
  147. execution_timeout: Maximum execution time in seconds
  148. allow_concurrent: Whether to allow concurrent executions
  149. priority: Execution priority (higher values execute first)
  150. tags: Optional tags for categorization
  151. metadata: Additional metadata for the schedule
  152. Raises:
  153. NameConflictError: If name is already in use
  154. ValidationError: If parameters are invalid
  155. """
  156. # Validate and register name
  157. self._register_name(name)
  158. # Basic properties
  159. self._name = name
  160. self._description = description
  161. self._enabled = enabled
  162. self._max_history = max_history
  163. self._execution_timeout = execution_timeout
  164. self._allow_concurrent = allow_concurrent
  165. self._priority = priority
  166. self._tags = tags or []
  167. self._metadata = metadata or {}
  168. # Internal state
  169. self._status = ScheduleStatus.ENABLED if enabled else ScheduleStatus.DISABLED
  170. self._created_at = time.time()
  171. self._last_modified = self._created_at
  172. self._last_executed = None
  173. self._next_execution = None
  174. self._execution_count = 0
  175. self._error_count = 0
  176. # Thread safety
  177. self._lock = threading.RLock()
  178. self._currently_executing = False
  179. self._execution_thread = None
  180. # Triggers and actions storage
  181. self._triggers = []
  182. self._actions = []
  183. # Execution history
  184. self._execution_history: List[ExecutionHistory] = []
  185. # Event handlers (will be set by scheduler)
  186. self._on_execution_start: Optional[Callable] = None
  187. self._on_execution_complete: Optional[Callable] = None
  188. self._on_execution_error: Optional[Callable] = None
  189. pprint(f"Created schedule entry: {self._name}")
  190. def _register_name(self, name: str) -> None:
  191. """
  192. Register the schedule name to ensure uniqueness.
  193. Args:
  194. name: Name to register
  195. Raises:
  196. NameConflictError: If name is already in use
  197. ValidationError: If name is invalid
  198. """
  199. if not name or not isinstance(name, str):
  200. raise ValidationError("Schedule name must be a non-empty string")
  201. if len(name) > 255:
  202. raise ValidationError("Schedule name must be 255 characters or less")
  203. # Check for valid characters (alphanumeric, underscore, hyphen, dot)
  204. if not all(c.isalnum() or c in '_-.' for c in name):
  205. raise ValidationError("Schedule name contains invalid characters")
  206. with self._registry_lock:
  207. if name in self._name_registry:
  208. raise NameConflictError(f"Schedule name '{name}' is already in use")
  209. self._name_registry.add(name)
  210. def _unregister_name(self) -> None:
  211. """Unregister the schedule name when entry is destroyed."""
  212. with self._registry_lock:
  213. self._name_registry.discard(self._name)
  214. @property
  215. def name(self) -> str:
  216. """Get the schedule name."""
  217. return self._name
  218. @property
  219. def description(self) -> str:
  220. """Get the schedule description."""
  221. return self._description
  222. @description.setter
  223. def description(self, value: str) -> None:
  224. """Set the schedule description."""
  225. with self._lock:
  226. self._description = value
  227. self._last_modified = time.time()
  228. @property
  229. def enabled(self) -> bool:
  230. """Check if the schedule is enabled."""
  231. return self._enabled
  232. @enabled.setter
  233. def enabled(self, value: bool) -> None:
  234. """Enable or disable the schedule."""
  235. with self._lock:
  236. if self._enabled != value:
  237. self._enabled = value
  238. self._status = ScheduleStatus.ENABLED if value else ScheduleStatus.DISABLED
  239. self._last_modified = time.time()
  240. pprint(f"Schedule '{self._name}' {'enabled' if value else 'disabled'}")
  241. @property
  242. def status(self) -> ScheduleStatus:
  243. """Get the current schedule status."""
  244. return self._status
  245. @property
  246. def priority(self) -> int:
  247. """Get the schedule priority."""
  248. return self._priority
  249. @priority.setter
  250. def priority(self, value: int) -> None:
  251. """Set the schedule priority."""
  252. with self._lock:
  253. self._priority = value
  254. self._last_modified = time.time()
  255. @property
  256. def tags(self) -> List[str]:
  257. """Get the schedule tags."""
  258. return self._tags.copy()
  259. def add_tag(self, tag: str) -> None:
  260. """Add a tag to the schedule."""
  261. with self._lock:
  262. if tag not in self._tags:
  263. self._tags.append(tag)
  264. self._last_modified = time.time()
  265. def remove_tag(self, tag: str) -> None:
  266. """Remove a tag from the schedule."""
  267. with self._lock:
  268. if tag in self._tags:
  269. self._tags.remove(tag)
  270. self._last_modified = time.time()
  271. @property
  272. def metadata(self) -> Dict[str, Any]:
  273. """Get the schedule metadata."""
  274. return self._metadata.copy()
  275. def set_metadata(self, key: str, value: Any) -> None:
  276. """Set metadata value."""
  277. with self._lock:
  278. self._metadata[key] = value
  279. self._last_modified = time.time()
  280. def get_metadata(self, key: str, default: Any = None) -> Any:
  281. """Get metadata value."""
  282. return self._metadata.get(key, default)
  283. @property
  284. def created_at(self) -> float:
  285. """Get creation timestamp."""
  286. return self._created_at
  287. @property
  288. def last_modified(self) -> float:
  289. """Get last modification timestamp."""
  290. return self._last_modified
  291. @property
  292. def last_executed(self) -> Optional[float]:
  293. """Get last execution timestamp."""
  294. return self._last_executed
  295. @property
  296. def next_execution(self) -> Optional[float]:
  297. """Get next scheduled execution timestamp."""
  298. return self._next_execution
  299. @property
  300. def execution_count(self) -> int:
  301. """Get total execution count."""
  302. return self._execution_count
  303. @property
  304. def error_count(self) -> int:
  305. """Get total error count."""
  306. return self._error_count
  307. @property
  308. def is_executing(self) -> bool:
  309. """Check if schedule is currently executing."""
  310. return self._currently_executing
  311. def add_trigger(self, trigger) -> None:
  312. """
  313. Add a trigger to the schedule.
  314. Args:
  315. trigger: Trigger instance to add
  316. Raises:
  317. ValidationError: If trigger is invalid
  318. """
  319. if not hasattr(trigger, 'should_fire') or not callable(trigger.should_fire):
  320. raise ValidationError("Trigger must have a 'should_fire' method")
  321. with self._lock:
  322. self._triggers.append(trigger)
  323. self._last_modified = time.time()
  324. pprint(f"Added trigger to schedule '{self._name}': {type(trigger).__name__}")
  325. def remove_trigger(self, trigger) -> bool:
  326. """
  327. Remove a trigger from the schedule.
  328. Args:
  329. trigger: Trigger instance to remove
  330. Returns:
  331. True if trigger was removed, False if not found
  332. """
  333. with self._lock:
  334. try:
  335. self._triggers.remove(trigger)
  336. self._last_modified = time.time()
  337. pprint(f"Removed trigger from schedule '{self._name}': {type(trigger).__name__}")
  338. return True
  339. except ValueError:
  340. return False
  341. def get_triggers(self) -> List:
  342. """Get all triggers for the schedule."""
  343. with self._lock:
  344. return self._triggers.copy()
  345. def clear_triggers(self) -> None:
  346. """Remove all triggers from the schedule."""
  347. with self._lock:
  348. self._triggers.clear()
  349. self._last_modified = time.time()
  350. pprint(f"Cleared all triggers for schedule '{self._name}'")
  351. def add_action(self, action) -> None:
  352. """
  353. Add an action to the schedule.
  354. Args:
  355. action: Action instance to add
  356. Raises:
  357. ValidationError: If action is invalid
  358. """
  359. if not hasattr(action, 'execute') or not callable(action.execute):
  360. raise ValidationError("Action must have an 'execute' method")
  361. with self._lock:
  362. self._actions.append(action)
  363. self._last_modified = time.time()
  364. pprint(f"Added action to schedule '{self._name}': {type(action).__name__}")
  365. def remove_action(self, action) -> bool:
  366. """
  367. Remove an action from the schedule.
  368. Args:
  369. action: Action instance to remove
  370. Returns:
  371. True if action was removed, False if not found
  372. """
  373. with self._lock:
  374. try:
  375. self._actions.remove(action)
  376. self._last_modified = time.time()
  377. pprint(f"Removed action from schedule '{self._name}': {type(action).__name__}")
  378. return True
  379. except ValueError:
  380. return False
  381. def get_actions(self) -> List:
  382. """Get all actions for the schedule."""
  383. with self._lock:
  384. return self._actions.copy()
  385. def clear_actions(self) -> None:
  386. """Remove all actions from the schedule."""
  387. with self._lock:
  388. self._actions.clear()
  389. self._last_modified = time.time()
  390. pprint(f"Cleared all actions for schedule '{self._name}'")
  391. def should_execute(self, current_time: Optional[float] = None) -> bool:
  392. """
  393. Check if the schedule should execute based on its triggers.
  394. Args:
  395. current_time: Current timestamp (defaults to now)
  396. Returns:
  397. True if any trigger indicates execution should occur
  398. """
  399. if not self._enabled or self._status != ScheduleStatus.ENABLED:
  400. return False
  401. if not self._triggers:
  402. return False
  403. if self._currently_executing and not self._allow_concurrent:
  404. return False
  405. current_time = current_time or time.time()
  406. # Check if any trigger should fire
  407. with self._lock:
  408. for trigger in self._triggers:
  409. try:
  410. if trigger.should_fire(current_time):
  411. return True
  412. except Exception as e:
  413. pprint(f"Error checking trigger in schedule '{self._name}': {e}")
  414. return False
  415. def get_next_execution_time(self, after_time: Optional[float] = None) -> Optional[float]:
  416. """
  417. Get the next scheduled execution time.
  418. Args:
  419. after_time: Time after which to find next execution (defaults to now)
  420. Returns:
  421. Next execution timestamp or None if no future execution
  422. """
  423. if not self._enabled or not self._triggers:
  424. return None
  425. after_time = after_time or time.time()
  426. next_times = []
  427. with self._lock:
  428. for trigger in self._triggers:
  429. try:
  430. if hasattr(trigger, 'get_next_fire_time'):
  431. next_time = trigger.get_next_fire_time(after_time)
  432. if next_time:
  433. next_times.append(next_time)
  434. except Exception as e:
  435. pprint(f"Error getting next fire time from trigger in schedule '{self._name}': {e}")
  436. return min(next_times) if next_times else None
  437. def execute(self, execution_context: Optional[Dict[str, Any]] = None) -> ExecutionResult:
  438. """
  439. Execute the schedule's actions.
  440. Args:
  441. execution_context: Optional context data for execution
  442. Returns:
  443. ExecutionResult indicating the outcome
  444. Raises:
  445. ExecutionError: If execution fails catastrophically
  446. """
  447. if not self._enabled:
  448. return ExecutionResult.SKIPPED
  449. if self._currently_executing and not self._allow_concurrent:
  450. pprint(f"Schedule '{self._name}' is already executing and concurrent execution is disabled")
  451. return ExecutionResult.SKIPPED
  452. execution_id = str(uuid.uuid4())
  453. start_time = time.time()
  454. execution_context = execution_context or {}
  455. # Create execution history entry
  456. history_entry = ExecutionHistory(
  457. execution_id=execution_id,
  458. timestamp=start_time,
  459. duration_seconds=0.0,
  460. result=ExecutionResult.SUCCESS,
  461. metadata=execution_context.copy()
  462. )
  463. try:
  464. with self._lock:
  465. if self._currently_executing and not self._allow_concurrent:
  466. return ExecutionResult.SKIPPED
  467. self._currently_executing = True
  468. self._status = ScheduleStatus.EXECUTING
  469. self._execution_count += 1
  470. pprint(f"Executing schedule '{self._name}' (execution_id: {execution_id})")
  471. # Call execution start handler
  472. if self._on_execution_start:
  473. try:
  474. self._on_execution_start(self, execution_id, execution_context)
  475. except Exception as e:
  476. pprint(f"Error in execution start handler: {e}")
  477. # Record which triggers fired
  478. fired_triggers = []
  479. current_time = time.time()
  480. with self._lock:
  481. for i, trigger in enumerate(self._triggers):
  482. try:
  483. if trigger.should_fire(current_time):
  484. trigger_name = getattr(trigger, 'name', f"trigger_{i}")
  485. fired_triggers.append(trigger_name)
  486. except Exception as e:
  487. pprint(f"Error checking trigger firing: {e}")
  488. history_entry.triggers_fired = fired_triggers
  489. # Execute actions
  490. actions_executed = []
  491. actions_failed = []
  492. overall_result = ExecutionResult.SUCCESS
  493. with self._lock:
  494. actions_to_execute = self._actions.copy()
  495. for i, action in enumerate(actions_to_execute):
  496. action_name = getattr(action, 'name', f"action_{i}")
  497. try:
  498. pprint(f"Executing action '{action_name}' in schedule '{self._name}'")
  499. # Execute with timeout
  500. action_start = time.time()
  501. result = action.execute(execution_context)
  502. action_duration = time.time() - action_start
  503. actions_executed.append(action_name)
  504. pprint(f"Action '{action_name}' completed in {action_duration:.2f}s")
  505. except Exception as e:
  506. actions_failed.append(action_name)
  507. overall_result = ExecutionResult.PARTIAL if actions_executed else ExecutionResult.FAILURE
  508. pprint(f"Action '{action_name}' failed: {e}")
  509. # Check if this should be a complete failure
  510. if hasattr(action, 'is_critical') and action.is_critical:
  511. overall_result = ExecutionResult.FAILURE
  512. break
  513. history_entry.actions_executed = actions_executed
  514. history_entry.actions_failed = actions_failed
  515. history_entry.result = overall_result
  516. # Update execution statistics
  517. with self._lock:
  518. self._last_executed = start_time
  519. if overall_result in [ExecutionResult.FAILURE, ExecutionResult.PARTIAL]:
  520. self._error_count += 1
  521. pprint(f"Schedule '{self._name}' execution completed with result: {overall_result.value}")
  522. return overall_result
  523. except Exception as e:
  524. # Handle catastrophic execution failure
  525. overall_result = ExecutionResult.FAILURE
  526. history_entry.result = overall_result
  527. history_entry.error_message = str(e)
  528. with self._lock:
  529. self._error_count += 1
  530. self._status = ScheduleStatus.ERROR
  531. pprint(f"Schedule '{self._name}' execution failed catastrophically: {e}")
  532. # Call error handler
  533. if self._on_execution_error:
  534. try:
  535. self._on_execution_error(self, execution_id, e)
  536. except Exception as handler_error:
  537. pprint(f"Error in execution error handler: {handler_error}")
  538. raise ExecutionError(f"Schedule execution failed: {e}") from e
  539. finally:
  540. # Finalize execution
  541. end_time = time.time()
  542. history_entry.duration_seconds = end_time - start_time
  543. with self._lock:
  544. self._currently_executing = False
  545. if self._status == ScheduleStatus.EXECUTING:
  546. self._status = ScheduleStatus.ENABLED if self._enabled else ScheduleStatus.DISABLED
  547. # Add to history
  548. self._execution_history.append(history_entry)
  549. # Trim history if needed
  550. if len(self._execution_history) > self._max_history:
  551. self._execution_history = self._execution_history[-self._max_history:]
  552. # Call completion handler
  553. if self._on_execution_complete:
  554. try:
  555. self._on_execution_complete(self, execution_id, overall_result)
  556. except Exception as e:
  557. pprint(f"Error in execution complete handler: {e}")
  558. def get_execution_history(self, limit: Optional[int] = None) -> List[ExecutionHistory]:
  559. """
  560. Get execution history for the schedule.
  561. Args:
  562. limit: Maximum number of entries to return
  563. Returns:
  564. List of execution history entries (most recent first)
  565. """
  566. with self._lock:
  567. history = self._execution_history.copy()
  568. history.reverse() # Most recent first
  569. if limit:
  570. history = history[:limit]
  571. return history
  572. def get_last_execution_result(self) -> Optional[ExecutionResult]:
  573. """Get the result of the last execution."""
  574. with self._lock:
  575. if self._execution_history:
  576. return self._execution_history[-1].result
  577. return None
  578. def clear_execution_history(self) -> None:
  579. """Clear all execution history."""
  580. with self._lock:
  581. self._execution_history.clear()
  582. pprint(f"Cleared execution history for schedule '{self._name}'")
  583. def set_event_handlers(
  584. self,
  585. on_start: Optional[Callable] = None,
  586. on_complete: Optional[Callable] = None,
  587. on_error: Optional[Callable] = None
  588. ) -> None:
  589. """
  590. Set event handlers for execution lifecycle events.
  591. Args:
  592. on_start: Called when execution starts
  593. on_complete: Called when execution completes
  594. on_error: Called when execution encounters an error
  595. """
  596. with self._lock:
  597. self._on_execution_start = on_start
  598. self._on_execution_complete = on_complete
  599. self._on_execution_error = on_error
  600. def validate(self) -> List[str]:
  601. """
  602. Validate the schedule configuration.
  603. Returns:
  604. List of validation errors (empty if valid)
  605. """
  606. errors = []
  607. # Check basic properties
  608. if not self._name:
  609. errors.append("Schedule name is required")
  610. if not self._triggers:
  611. errors.append("At least one trigger is required")
  612. if not self._actions:
  613. errors.append("At least one action is required")
  614. # Validate triggers
  615. for i, trigger in enumerate(self._triggers):
  616. if not hasattr(trigger, 'should_fire'):
  617. errors.append(f"Trigger {i} missing 'should_fire' method")
  618. if hasattr(trigger, 'validate'):
  619. try:
  620. trigger_errors = trigger.validate()
  621. for error in trigger_errors:
  622. errors.append(f"Trigger {i}: {error}")
  623. except Exception as e:
  624. errors.append(f"Trigger {i} validation failed: {e}")
  625. # Validate actions
  626. for i, action in enumerate(self._actions):
  627. if not hasattr(action, 'execute'):
  628. errors.append(f"Action {i} missing 'execute' method")
  629. if hasattr(action, 'validate'):
  630. try:
  631. action_errors = action.validate()
  632. for error in action_errors:
  633. errors.append(f"Action {i}: {error}")
  634. except Exception as e:
  635. errors.append(f"Action {i} validation failed: {e}")
  636. return errors
  637. def to_dict(self) -> Dict[str, Any]:
  638. """
  639. Convert schedule entry to dictionary for serialization.
  640. Returns:
  641. Dictionary representation of the schedule
  642. """
  643. with self._lock:
  644. return {
  645. 'name': self._name,
  646. 'description': self._description,
  647. 'enabled': self._enabled,
  648. 'status': self._status.value,
  649. 'priority': self._priority,
  650. 'tags': self._tags.copy(),
  651. 'metadata': self._metadata.copy(),
  652. 'created_at': self._created_at,
  653. 'last_modified': self._last_modified,
  654. 'last_executed': self._last_executed,
  655. 'execution_count': self._execution_count,
  656. 'error_count': self._error_count,
  657. 'max_history': self._max_history,
  658. 'execution_timeout': self._execution_timeout,
  659. 'allow_concurrent': self._allow_concurrent,
  660. 'triggers': [
  661. trigger.to_dict() if hasattr(trigger, 'to_dict') else str(trigger)
  662. for trigger in self._triggers
  663. ],
  664. 'actions': [
  665. action.to_dict() if hasattr(action, 'to_dict') else str(action)
  666. for action in self._actions
  667. ],
  668. 'execution_history': [
  669. entry.to_dict() for entry in self._execution_history
  670. ]
  671. }
  672. def get_info(self) -> Dict[str, Any]:
  673. """
  674. Get summary information about the schedule entry.
  675. Returns:
  676. Dictionary with schedule information
  677. """
  678. with self._lock:
  679. next_exec = self.get_next_execution_time()
  680. last_result = self.get_last_execution_result()
  681. return {
  682. 'name': self._name,
  683. 'description': self._description,
  684. 'enabled': self._enabled,
  685. 'status': self._status.value,
  686. 'priority': self._priority,
  687. 'tags': len(self._tags),
  688. 'triggers': len(self._triggers),
  689. 'actions': len(self._actions),
  690. 'execution_count': self._execution_count,
  691. 'error_count': self._error_count,
  692. 'last_executed': self._last_executed,
  693. 'last_result': last_result.value if last_result else None,
  694. 'next_execution': next_exec,
  695. 'is_executing': self._currently_executing,
  696. 'uptime_hours': (time.time() - self._created_at) / 3600,
  697. 'success_rate': (
  698. (self._execution_count - self._error_count) / self._execution_count * 100
  699. if self._execution_count > 0 else 0
  700. )
  701. }
  702. def __str__(self) -> str:
  703. """String representation of the schedule entry."""
  704. return f"ScheduleEntry(name='{self._name}', enabled={self._enabled}, triggers={len(self._triggers)}, actions={len(self._actions)})"
  705. def __repr__(self) -> str:
  706. """Detailed representation of the schedule entry."""
  707. return (
  708. f"ScheduleEntry("
  709. f"name='{self._name}', "
  710. f"description='{self._description}', "
  711. f"enabled={self._enabled}, "
  712. f"status={self._status.value}, "
  713. f"triggers={len(self._triggers)}, "
  714. f"actions={len(self._actions)}, "
  715. f"executions={self._execution_count})"
  716. )
  717. def __del__(self):
  718. """Cleanup when schedule entry is destroyed."""
  719. try:
  720. self._unregister_name()
  721. except:
  722. pass # Ignore errors during cleanup
  723. # Utility functions for name management
  724. def get_registered_names() -> Set[str]:
  725. """Get all currently registered schedule names."""
  726. with ScheduleEntry._registry_lock:
  727. return ScheduleEntry._name_registry.copy()
  728. def is_name_available(name: str) -> bool:
  729. """Check if a schedule name is available."""
  730. with ScheduleEntry._registry_lock:
  731. return name not in ScheduleEntry._name_registry
  732. def clear_name_registry() -> None:
  733. """Clear the name registry (for testing purposes)."""
  734. with ScheduleEntry._registry_lock:
  735. ScheduleEntry._name_registry.clear()
  736. # Module exports
  737. __all__ = [
  738. 'ScheduleEntry',
  739. 'ScheduleStatus',
  740. 'ExecutionResult',
  741. 'ExecutionHistory',
  742. 'ScheduleEntryError',
  743. 'NameConflictError',
  744. 'ValidationError',
  745. 'ExecutionError',
  746. 'get_registered_names',
  747. 'is_name_available',
  748. 'clear_name_registry',
  749. 'pprint'
  750. ]