| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019 |
- """
- Main Scheduler System for Trixy Application
- This module provides the main Scheduler class that manages schedule entries,
- handles execution timing, integrates with the event system, and provides
- a comprehensive scheduling solution for the Trixy application.
- Key Features:
- - Multiple schedule entry management with unique name validation
- - Thread-safe operations for multi-satellite environments
- - Integration with Trixy event system and application container
- - Automatic schedule persistence and backup
- - Comprehensive monitoring and execution history
- - Advanced querying and filtering capabilities
- - Graceful startup, shutdown, and error handling
- - Cron expression support with advanced scheduling
- - Multiple trigger and action types
- Components:
- - Scheduler: Main scheduler class managing all schedules
- - ScheduleManager: Advanced schedule management with querying
- - ScheduleQuery: Query builder for finding schedules
- - SchedulerConfig: Configuration for scheduler behavior
- Usage:
- from trixy_core.scheduler import Scheduler, CronTrigger, EventAction
-
- # Create and configure scheduler
- scheduler = Scheduler(
- storage_path="/config/schedules.json",
- auto_start=True,
- check_interval=60
- )
-
- # Add schedule with cron trigger
- scheduler.add_schedule(
- name="daily_report",
- triggers=[CronTrigger("0 9 * * 1-5")], # 9 AM weekdays
- actions=[EventAction("generate_report", {"type": "daily"})],
- description="Generate daily reports on weekdays"
- )
-
- # Start scheduler
- scheduler.start()
-
- # Add more schedules dynamically
- scheduler.add_cron_schedule(
- name="backup",
- cron_expression="0 2 * * *", # Daily at 2 AM
- actions=[EventAction("system_backup", {"type": "full"})]
- )
- """
- import time
- import threading
- import traceback
- from datetime import datetime, timedelta
- from typing import List, Dict, Any, Optional, Union, Set, Callable
- from dataclasses import dataclass, field
- from enum import Enum
- from concurrent.futures import ThreadPoolExecutor, Future
- import uuid
- import re
- from .schedule_entry import (
- ScheduleEntry, ScheduleStatus, ExecutionResult,
- ScheduleEntryError, NameConflictError, ValidationError,
- get_registered_names, is_name_available
- )
- from .triggers import (
- BaseTrigger, CronTrigger, DateTrigger, TimeTrigger,
- EventTrigger, WeekdayTrigger, IntervalTrigger, ManualTrigger,
- TriggerFactory
- )
- from .actions import (
- BaseAction, EventAction, MLTrainingAction, FunctionAction,
- MultiAction, ConditionalAction, ActionFactory
- )
- from .persistence import SchedulePersistence, PersistenceConfig, BackupPolicy
- from .cron_parser import parse_cron_expression, validate_cron_expression
- def pprint(message: str) -> None:
- """
- Scheduler logging function that adapts based on mode.
- Uses the same pattern as specified in CLAUDE.md.
- """
- print(f"[SCHEDULER] {message}")
- class SchedulerError(Exception):
- """Base exception for scheduler-related errors."""
- pass
- class SchedulerStatus(Enum):
- """Status of the scheduler."""
- STOPPED = "stopped"
- STARTING = "starting"
- RUNNING = "running"
- STOPPING = "stopping"
- ERROR = "error"
- @dataclass
- class SchedulerConfig:
- """Configuration for the scheduler."""
- check_interval_seconds: float = 60.0
- max_concurrent_executions: int = 10
- enable_persistence: bool = True
- auto_save_interval_seconds: float = 300.0 # 5 minutes
- storage_path: str = "config/schedules.json"
- backup_policy: BackupPolicy = BackupPolicy.DAILY
- max_backups: int = 30
- startup_delay_seconds: float = 5.0
- shutdown_timeout_seconds: float = 30.0
- enable_event_integration: bool = True
- validate_on_load: bool = True
-
- def __post_init__(self):
- """Post-initialization validation."""
- if self.check_interval_seconds <= 0:
- raise ValueError("Check interval must be positive")
-
- if self.max_concurrent_executions <= 0:
- raise ValueError("Max concurrent executions must be positive")
-
- if self.auto_save_interval_seconds <= 0:
- raise ValueError("Auto save interval must be positive")
- class ScheduleQuery:
- """
- Query builder for finding schedules based on various criteria.
-
- Provides a fluent interface for building complex schedule queries.
- """
-
- def __init__(self, schedules: List[ScheduleEntry]):
- """
- Initialize query with schedule list.
-
- Args:
- schedules: List of schedules to query
- """
- self._schedules = schedules
- self._filters: List[Callable[[ScheduleEntry], bool]] = []
-
- def by_name(self, name: str) -> 'ScheduleQuery':
- """Filter by exact name match."""
- self._filters.append(lambda s: s.name == name)
- return self
-
- def by_name_pattern(self, pattern: str) -> 'ScheduleQuery':
- """Filter by name pattern (regex)."""
- regex = re.compile(pattern)
- self._filters.append(lambda s: bool(regex.search(s.name)))
- return self
-
- def by_status(self, status: ScheduleStatus) -> 'ScheduleQuery':
- """Filter by schedule status."""
- self._filters.append(lambda s: s.status == status)
- return self
-
- def by_enabled(self, enabled: bool = True) -> 'ScheduleQuery':
- """Filter by enabled status."""
- self._filters.append(lambda s: s.enabled == enabled)
- return self
-
- def by_tag(self, tag: str) -> 'ScheduleQuery':
- """Filter by tag."""
- self._filters.append(lambda s: tag in s.tags)
- return self
-
- def by_trigger_type(self, trigger_type: type) -> 'ScheduleQuery':
- """Filter by trigger type."""
- self._filters.append(
- lambda s: any(isinstance(t, trigger_type) for t in s.get_triggers())
- )
- return self
-
- def by_action_type(self, action_type: type) -> 'ScheduleQuery':
- """Filter by action type."""
- self._filters.append(
- lambda s: any(isinstance(a, action_type) for a in s.get_actions())
- )
- return self
-
- def by_execution_count(self, min_count: int = 0, max_count: Optional[int] = None) -> 'ScheduleQuery':
- """Filter by execution count range."""
- def filter_func(s):
- count = s.execution_count
- if count < min_count:
- return False
- if max_count is not None and count > max_count:
- return False
- return True
-
- self._filters.append(filter_func)
- return self
-
- def by_last_execution(self, hours_ago: float) -> 'ScheduleQuery':
- """Filter by last execution time (within N hours ago)."""
- cutoff_time = time.time() - (hours_ago * 3600)
- self._filters.append(
- lambda s: s.last_executed is not None and s.last_executed >= cutoff_time
- )
- return self
-
- def by_next_execution(self, within_hours: float) -> 'ScheduleQuery':
- """Filter by next execution time (within N hours)."""
- future_time = time.time() + (within_hours * 3600)
-
- def filter_func(s):
- next_exec = s.get_next_execution_time()
- return next_exec is not None and next_exec <= future_time
-
- self._filters.append(filter_func)
- return self
-
- def custom_filter(self, filter_func: Callable[[ScheduleEntry], bool]) -> 'ScheduleQuery':
- """Add custom filter function."""
- self._filters.append(filter_func)
- return self
-
- def execute(self) -> List[ScheduleEntry]:
- """Execute the query and return matching schedules."""
- result = self._schedules
-
- for filter_func in self._filters:
- result = [s for s in result if filter_func(s)]
-
- return result
-
- def first(self) -> Optional[ScheduleEntry]:
- """Get first matching schedule."""
- result = self.execute()
- return result[0] if result else None
-
- def count(self) -> int:
- """Get count of matching schedules."""
- return len(self.execute())
-
- def exists(self) -> bool:
- """Check if any schedules match."""
- return self.count() > 0
- class ScheduleManager:
- """
- Advanced schedule management with querying capabilities.
-
- Provides high-level operations for managing collections of schedules.
- """
-
- def __init__(self, schedules: Optional[List[ScheduleEntry]] = None):
- """
- Initialize schedule manager.
-
- Args:
- schedules: Initial list of schedules (optional)
- """
- self._schedules: List[ScheduleEntry] = schedules or []
- self._lock = threading.RLock()
-
- def add_schedule(self, schedule: ScheduleEntry) -> None:
- """Add a schedule to the manager."""
- with self._lock:
- self._schedules.append(schedule)
-
- def remove_schedule(self, name: str) -> bool:
- """Remove a schedule by name."""
- with self._lock:
- for i, schedule in enumerate(self._schedules):
- if schedule.name == name:
- del self._schedules[i]
- return True
- return False
-
- def get_schedule(self, name: str) -> Optional[ScheduleEntry]:
- """Get a schedule by name."""
- with self._lock:
- for schedule in self._schedules:
- if schedule.name == name:
- return schedule
- return None
-
- def get_all_schedules(self) -> List[ScheduleEntry]:
- """Get all schedules."""
- with self._lock:
- return self._schedules.copy()
-
- def query(self) -> ScheduleQuery:
- """Create a new query builder."""
- with self._lock:
- return ScheduleQuery(self._schedules.copy())
-
- def get_schedules_due(self, current_time: Optional[float] = None) -> List[ScheduleEntry]:
- """Get schedules that should execute now."""
- current_time = current_time or time.time()
-
- with self._lock:
- return [s for s in self._schedules if s.should_execute(current_time)]
-
- def get_next_execution_time(self) -> Optional[float]:
- """Get the next execution time across all schedules."""
- current_time = time.time()
- next_times = []
-
- with self._lock:
- for schedule in self._schedules:
- next_time = schedule.get_next_execution_time(current_time)
- if next_time:
- next_times.append(next_time)
-
- return min(next_times) if next_times else None
-
- def get_statistics(self) -> Dict[str, Any]:
- """Get statistics about managed schedules."""
- with self._lock:
- total_schedules = len(self._schedules)
- enabled_schedules = sum(1 for s in self._schedules if s.enabled)
- disabled_schedules = total_schedules - enabled_schedules
-
- total_executions = sum(s.execution_count for s in self._schedules)
- total_errors = sum(s.error_count for s in self._schedules)
-
- # Status breakdown
- status_counts = {}
- for status in ScheduleStatus:
- count = sum(1 for s in self._schedules if s.status == status)
- status_counts[status.value] = count
-
- # Trigger type breakdown
- trigger_counts = {}
- for schedule in self._schedules:
- for trigger in schedule.get_triggers():
- trigger_type = type(trigger).__name__
- trigger_counts[trigger_type] = trigger_counts.get(trigger_type, 0) + 1
-
- # Action type breakdown
- action_counts = {}
- for schedule in self._schedules:
- for action in schedule.get_actions():
- action_type = type(action).__name__
- action_counts[action_type] = action_counts.get(action_type, 0) + 1
-
- return {
- 'total_schedules': total_schedules,
- 'enabled_schedules': enabled_schedules,
- 'disabled_schedules': disabled_schedules,
- 'total_executions': total_executions,
- 'total_errors': total_errors,
- 'success_rate': (total_executions - total_errors) / total_executions * 100 if total_executions > 0 else 0,
- 'status_breakdown': status_counts,
- 'trigger_type_breakdown': trigger_counts,
- 'action_type_breakdown': action_counts,
- 'next_execution': self.get_next_execution_time()
- }
-
- def validate_all(self) -> Dict[str, List[str]]:
- """Validate all schedules and return errors."""
- validation_results = {}
-
- with self._lock:
- for schedule in self._schedules:
- errors = schedule.validate()
- if errors:
- validation_results[schedule.name] = errors
-
- return validation_results
-
- def clear_all(self) -> None:
- """Remove all schedules."""
- with self._lock:
- self._schedules.clear()
- class Scheduler:
- """
- Main scheduler class for the Trixy application.
-
- Manages schedule entries, handles execution timing, integrates with
- the event system, and provides comprehensive scheduling capabilities.
- """
-
- def __init__(
- self,
- config: Optional[SchedulerConfig] = None,
- storage_path: Optional[str] = None,
- event_handler=None,
- application=None
- ):
- """
- Initialize the scheduler.
-
- Args:
- config: Scheduler configuration (optional)
- storage_path: Path for schedule storage (optional)
- event_handler: Event handler for integration (optional)
- application: Application container for integration (optional)
- """
- # Configuration
- if config is None:
- config = SchedulerConfig()
-
- if storage_path:
- config.storage_path = storage_path
-
- self.config = config
-
- # Core components
- self.schedule_manager = ScheduleManager()
- self._status = SchedulerStatus.STOPPED
- self._lock = threading.RLock()
-
- # Threading
- self._scheduler_thread: Optional[threading.Thread] = None
- self._auto_save_thread: Optional[threading.Thread] = None
- self._stop_event = threading.Event()
- self._executor = ThreadPoolExecutor(max_workers=config.max_concurrent_executions)
-
- # Persistence
- if config.enable_persistence:
- persistence_config = PersistenceConfig(
- storage_path=config.storage_path,
- backup_policy=config.backup_policy,
- max_backups=config.max_backups,
- validate_on_load=config.validate_on_load
- )
- self.persistence = SchedulePersistence(config.storage_path, config=persistence_config)
- else:
- self.persistence = None
-
- # Integration
- self._event_handler = event_handler
- self._application = application
-
- # Statistics
- self._start_time: Optional[float] = None
- self._execution_count = 0
- self._error_count = 0
- self._last_check_time: Optional[float] = None
-
- # Running futures for tracking executions
- self._running_executions: Dict[str, Future] = {}
-
- pprint(f"Scheduler initialized with {len(self.schedule_manager.get_all_schedules())} schedules")
-
- def set_event_handler(self, event_handler) -> None:
- """Set the event handler for integration."""
- with self._lock:
- self._event_handler = event_handler
-
- # Update event handlers for existing schedules
- for schedule in self.schedule_manager.get_all_schedules():
- for action in schedule.get_actions():
- if hasattr(action, 'set_event_handler'):
- action.set_event_handler(event_handler)
-
- for trigger in schedule.get_triggers():
- if hasattr(trigger, 'set_event_handler'):
- trigger.set_event_handler(event_handler)
-
- pprint("Event handler connected to scheduler")
-
- def set_application(self, application) -> None:
- """Set the application container for integration."""
- with self._lock:
- self._application = application
- pprint("Application container connected to scheduler")
-
- def start(self) -> None:
- """Start the scheduler."""
- with self._lock:
- if self._status != SchedulerStatus.STOPPED:
- raise SchedulerError(f"Cannot start scheduler in status: {self._status.value}")
-
- self._status = SchedulerStatus.STARTING
- pprint("Starting scheduler...")
-
- try:
- # Load schedules from persistence
- if self.persistence:
- self._load_schedules()
-
- # Start scheduler thread
- self._stop_event.clear()
- self._scheduler_thread = threading.Thread(target=self._scheduler_loop, daemon=True)
- self._scheduler_thread.start()
-
- # Start auto-save thread if enabled
- if self.persistence and self.config.auto_save_interval_seconds > 0:
- self._auto_save_thread = threading.Thread(target=self._auto_save_loop, daemon=True)
- self._auto_save_thread.start()
-
- self._start_time = time.time()
- self._status = SchedulerStatus.RUNNING
-
- # Trigger startup event
- if self._event_handler and self.config.enable_event_integration:
- self._event_handler.trigger_event(
- "scheduler_started",
- scheduler_id=id(self),
- schedule_count=len(self.schedule_manager.get_all_schedules()),
- config=self.config.__dict__
- )
-
- pprint(f"Scheduler started successfully with {len(self.schedule_manager.get_all_schedules())} schedules")
-
- except Exception as e:
- self._status = SchedulerStatus.ERROR
- pprint(f"Failed to start scheduler: {e}")
- raise SchedulerError(f"Failed to start scheduler: {e}")
-
- def stop(self, timeout: Optional[float] = None) -> None:
- """Stop the scheduler."""
- with self._lock:
- if self._status not in [SchedulerStatus.RUNNING, SchedulerStatus.ERROR]:
- pprint(f"Scheduler not running (status: {self._status.value})")
- return
-
- self._status = SchedulerStatus.STOPPING
- pprint("Stopping scheduler...")
-
- timeout = timeout or self.config.shutdown_timeout_seconds
-
- try:
- # Signal threads to stop
- self._stop_event.set()
-
- # Wait for scheduler thread
- if self._scheduler_thread and self._scheduler_thread.is_alive():
- self._scheduler_thread.join(timeout=timeout / 2)
- if self._scheduler_thread.is_alive():
- pprint("Warning: Scheduler thread did not stop gracefully")
-
- # Wait for auto-save thread
- if self._auto_save_thread and self._auto_save_thread.is_alive():
- self._auto_save_thread.join(timeout=timeout / 2)
- if self._auto_save_thread.is_alive():
- pprint("Warning: Auto-save thread did not stop gracefully")
-
- # Wait for running executions
- if self._running_executions:
- pprint(f"Waiting for {len(self._running_executions)} running executions to complete...")
-
- for execution_id, future in list(self._running_executions.items()):
- try:
- future.result(timeout=min(5.0, timeout / len(self._running_executions)))
- except Exception:
- future.cancel()
- finally:
- self._running_executions.pop(execution_id, None)
-
- # Shutdown executor
- self._executor.shutdown(wait=True)
-
- # Save schedules one final time
- if self.persistence:
- try:
- self._save_schedules()
- except Exception as e:
- pprint(f"Warning: Failed to save schedules during shutdown: {e}")
-
- self._status = SchedulerStatus.STOPPED
-
- # Trigger shutdown event
- if self._event_handler and self.config.enable_event_integration:
- self._event_handler.trigger_event(
- "scheduler_stopped",
- scheduler_id=id(self),
- uptime_seconds=time.time() - self._start_time if self._start_time else 0,
- execution_count=self._execution_count
- )
-
- pprint("Scheduler stopped successfully")
-
- except Exception as e:
- self._status = SchedulerStatus.ERROR
- pprint(f"Error during scheduler shutdown: {e}")
- raise SchedulerError(f"Failed to stop scheduler: {e}")
-
- def _scheduler_loop(self) -> None:
- """Main scheduler loop that checks and executes schedules."""
- pprint("Scheduler loop started")
-
- # Initial startup delay
- if self.config.startup_delay_seconds > 0:
- pprint(f"Startup delay: {self.config.startup_delay_seconds} seconds")
- if self._stop_event.wait(self.config.startup_delay_seconds):
- return
-
- while not self._stop_event.is_set():
- try:
- self._check_and_execute_schedules()
- self._cleanup_completed_executions()
-
- # Wait for next check or stop signal
- self._stop_event.wait(self.config.check_interval_seconds)
-
- except Exception as e:
- pprint(f"Error in scheduler loop: {e}")
- pprint(f"Traceback: {traceback.format_exc()}")
-
- # Brief pause before continuing to avoid rapid error loops
- self._stop_event.wait(1.0)
-
- pprint("Scheduler loop stopped")
-
- def _check_and_execute_schedules(self) -> None:
- """Check schedules and execute those that are due."""
- current_time = time.time()
- self._last_check_time = current_time
-
- # Get schedules that should execute
- due_schedules = self.schedule_manager.get_schedules_due(current_time)
-
- if due_schedules:
- pprint(f"Found {len(due_schedules)} schedules due for execution")
-
- for schedule in due_schedules:
- try:
- self._execute_schedule(schedule)
- except Exception as e:
- pprint(f"Error executing schedule '{schedule.name}': {e}")
- self._error_count += 1
-
- def _execute_schedule(self, schedule: ScheduleEntry) -> None:
- """Execute a schedule asynchronously."""
- execution_id = str(uuid.uuid4())
-
- pprint(f"Scheduling execution of '{schedule.name}' (execution_id: {execution_id})")
-
- # Create execution context
- context = {
- 'execution_id': execution_id,
- 'scheduler_id': id(self),
- 'schedule_name': schedule.name,
- 'execution_time': time.time(),
- 'check_interval': self.config.check_interval_seconds
- }
-
- # Submit execution to thread pool
- future = self._executor.submit(self._execute_schedule_sync, schedule, context)
- self._running_executions[execution_id] = future
-
- # Add completion callback
- future.add_done_callback(lambda f: self._on_execution_complete(execution_id, f))
-
- self._execution_count += 1
-
- def _execute_schedule_sync(self, schedule: ScheduleEntry, context: Dict[str, Any]) -> None:
- """Execute a schedule synchronously."""
- try:
- # Trigger schedule_triggered event
- if self._event_handler and self.config.enable_event_integration:
- self._event_handler.trigger_event(
- "schedule_triggered",
- schedule_name=schedule.name,
- execution_id=context['execution_id'],
- schedule_info=schedule.get_info()
- )
-
- # Execute the schedule
- result = schedule.execute(context)
-
- pprint(f"Schedule '{schedule.name}' executed with result: {result.value}")
-
- except Exception as e:
- pprint(f"Schedule '{schedule.name}' execution failed: {e}")
- self._error_count += 1
- raise
-
- def _on_execution_complete(self, execution_id: str, future: Future) -> None:
- """Handle completion of schedule execution."""
- try:
- future.result() # This will raise any exception that occurred
- except Exception as e:
- pprint(f"Execution {execution_id} failed: {e}")
- finally:
- self._running_executions.pop(execution_id, None)
-
- def _cleanup_completed_executions(self) -> None:
- """Clean up completed executions from tracking."""
- completed_ids = []
-
- for execution_id, future in self._running_executions.items():
- if future.done():
- completed_ids.append(execution_id)
-
- for execution_id in completed_ids:
- self._running_executions.pop(execution_id, None)
-
- def _auto_save_loop(self) -> None:
- """Auto-save loop for schedule persistence."""
- pprint("Auto-save loop started")
-
- while not self._stop_event.is_set():
- if self._stop_event.wait(self.config.auto_save_interval_seconds):
- break
-
- try:
- self._save_schedules()
- except Exception as e:
- pprint(f"Auto-save failed: {e}")
-
- pprint("Auto-save loop stopped")
-
- def _load_schedules(self) -> None:
- """Load schedules from persistence."""
- if not self.persistence:
- return
-
- try:
- schedules = self.persistence.load_schedules()
-
- # Clear current schedules and add loaded ones
- self.schedule_manager.clear_all()
-
- for schedule in schedules:
- # Set up event handlers for actions and triggers
- if self._event_handler:
- for action in schedule.get_actions():
- if hasattr(action, 'set_event_handler'):
- action.set_event_handler(self._event_handler)
-
- for trigger in schedule.get_triggers():
- if hasattr(trigger, 'set_event_handler'):
- trigger.set_event_handler(self._event_handler)
-
- self.schedule_manager.add_schedule(schedule)
-
- pprint(f"Loaded {len(schedules)} schedules from storage")
-
- except Exception as e:
- pprint(f"Failed to load schedules: {e}")
-
- def _save_schedules(self) -> None:
- """Save schedules to persistence."""
- if not self.persistence:
- return
-
- try:
- schedules = self.schedule_manager.get_all_schedules()
- self.persistence.save_schedules(schedules)
- pprint(f"Saved {len(schedules)} schedules to storage")
-
- except Exception as e:
- pprint(f"Failed to save schedules: {e}")
- raise
-
- # Public API methods
-
- def add_schedule(
- self,
- name: str,
- triggers: List[BaseTrigger],
- actions: List[BaseAction],
- description: str = "",
- enabled: bool = True,
- **kwargs
- ) -> ScheduleEntry:
- """
- Add a new schedule.
-
- Args:
- name: Unique name for the schedule
- triggers: List of triggers for the schedule
- actions: List of actions for the schedule
- description: Schedule description
- enabled: Whether schedule is enabled
- **kwargs: Additional ScheduleEntry parameters
-
- Returns:
- ScheduleEntry: Created schedule entry
-
- Raises:
- SchedulerError: If schedule cannot be added
- """
- try:
- schedule = ScheduleEntry(
- name=name,
- description=description,
- enabled=enabled,
- **kwargs
- )
-
- # Add triggers
- for trigger in triggers:
- schedule.add_trigger(trigger)
-
- # Add actions
- for action in actions:
- # Set up event handler if available
- if self._event_handler and hasattr(action, 'set_event_handler'):
- action.set_event_handler(self._event_handler)
-
- schedule.add_action(action)
-
- # Add to manager
- self.schedule_manager.add_schedule(schedule)
-
- pprint(f"Added schedule '{name}' with {len(triggers)} triggers and {len(actions)} actions")
-
- # Auto-save if scheduler is running
- if self._status == SchedulerStatus.RUNNING and self.persistence:
- try:
- self._save_schedules()
- except Exception as e:
- pprint(f"Failed to auto-save after adding schedule: {e}")
-
- return schedule
-
- except Exception as e:
- raise SchedulerError(f"Failed to add schedule '{name}': {e}")
-
- def add_cron_schedule(
- self,
- name: str,
- cron_expression: str,
- actions: List[BaseAction],
- description: str = "",
- enabled: bool = True,
- **kwargs
- ) -> ScheduleEntry:
- """
- Add a schedule with cron trigger.
-
- Args:
- name: Unique name for the schedule
- cron_expression: Standard cron expression
- actions: List of actions for the schedule
- description: Schedule description
- enabled: Whether schedule is enabled
- **kwargs: Additional parameters
-
- Returns:
- ScheduleEntry: Created schedule entry
- """
- # Validate cron expression
- if not validate_cron_expression(cron_expression):
- raise SchedulerError(f"Invalid cron expression: {cron_expression}")
-
- # Create cron trigger
- cron_trigger = CronTrigger(cron_expression)
-
- return self.add_schedule(
- name=name,
- triggers=[cron_trigger],
- actions=actions,
- description=description,
- enabled=enabled,
- **kwargs
- )
-
- def remove_schedule(self, name: str) -> bool:
- """
- Remove a schedule by name.
-
- Args:
- name: Name of schedule to remove
-
- Returns:
- bool: True if schedule was removed
- """
- removed = self.schedule_manager.remove_schedule(name)
-
- if removed:
- pprint(f"Removed schedule '{name}'")
-
- # Auto-save if scheduler is running
- if self._status == SchedulerStatus.RUNNING and self.persistence:
- try:
- self._save_schedules()
- except Exception as e:
- pprint(f"Failed to auto-save after removing schedule: {e}")
-
- return removed
-
- def get_schedule(self, name: str) -> Optional[ScheduleEntry]:
- """Get a schedule by name."""
- return self.schedule_manager.get_schedule(name)
-
- def get_all_schedules(self) -> List[ScheduleEntry]:
- """Get all schedules."""
- return self.schedule_manager.get_all_schedules()
-
- def query_schedules(self) -> ScheduleQuery:
- """Create a schedule query builder."""
- return self.schedule_manager.query()
-
- def enable_schedule(self, name: str) -> bool:
- """Enable a schedule by name."""
- schedule = self.get_schedule(name)
- if schedule:
- schedule.enabled = True
- pprint(f"Enabled schedule '{name}'")
- return True
- return False
-
- def disable_schedule(self, name: str) -> bool:
- """Disable a schedule by name."""
- schedule = self.get_schedule(name)
- if schedule:
- schedule.enabled = False
- pprint(f"Disabled schedule '{name}'")
- return True
- return False
-
- def trigger_schedule_manually(self, name: str) -> bool:
- """Manually trigger a schedule execution."""
- schedule = self.get_schedule(name)
- if not schedule:
- return False
-
- try:
- self._execute_schedule(schedule)
- pprint(f"Manually triggered schedule '{name}'")
- return True
- except Exception as e:
- pprint(f"Failed to manually trigger schedule '{name}': {e}")
- return False
-
- def get_status(self) -> SchedulerStatus:
- """Get current scheduler status."""
- return self._status
-
- def get_statistics(self) -> Dict[str, Any]:
- """Get comprehensive scheduler statistics."""
- with self._lock:
- schedule_stats = self.schedule_manager.get_statistics()
-
- uptime = 0.0
- if self._start_time and self._status == SchedulerStatus.RUNNING:
- uptime = time.time() - self._start_time
-
- return {
- 'status': self._status.value,
- 'uptime_seconds': uptime,
- 'uptime_hours': uptime / 3600,
- 'total_executions': self._execution_count,
- 'total_errors': self._error_count,
- 'success_rate': (self._execution_count - self._error_count) / self._execution_count * 100 if self._execution_count > 0 else 0,
- 'running_executions': len(self._running_executions),
- 'last_check_time': self._last_check_time,
- 'next_check_in': max(0, self.config.check_interval_seconds - (time.time() - self._last_check_time)) if self._last_check_time else 0,
- 'config': {
- 'check_interval_seconds': self.config.check_interval_seconds,
- 'max_concurrent_executions': self.config.max_concurrent_executions,
- 'storage_path': self.config.storage_path if self.persistence else None,
- 'persistence_enabled': self.config.enable_persistence,
- 'event_integration_enabled': self.config.enable_event_integration
- },
- 'schedules': schedule_stats
- }
-
- def validate_all_schedules(self) -> Dict[str, List[str]]:
- """Validate all schedules and return validation errors."""
- return self.schedule_manager.validate_all()
-
- def create_backup(self, backup_name: Optional[str] = None) -> Optional[str]:
- """Create a backup of current schedules."""
- if not self.persistence:
- return None
-
- try:
- backup_path = self.persistence.create_backup(backup_name)
- pprint(f"Created schedule backup: {backup_path}")
- return backup_path
- except Exception as e:
- pprint(f"Failed to create backup: {e}")
- return None
-
- def restore_from_backup(self, backup_path: str) -> bool:
- """Restore schedules from backup."""
- if not self.persistence:
- return False
-
- try:
- self.persistence.restore_from_backup(backup_path)
-
- # Reload schedules if scheduler is running
- if self._status == SchedulerStatus.RUNNING:
- self._load_schedules()
-
- pprint(f"Restored schedules from backup: {backup_path}")
- return True
- except Exception as e:
- pprint(f"Failed to restore from backup: {e}")
- return False
-
- def list_backups(self):
- """List available backups."""
- if not self.persistence:
- return []
-
- return self.persistence.list_backups()
-
- def __enter__(self):
- """Context manager entry."""
- self.start()
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- """Context manager exit."""
- self.stop()
- # Module exports
- __all__ = [
- 'Scheduler',
- 'SchedulerError',
- 'SchedulerStatus',
- 'SchedulerConfig',
- 'ScheduleManager',
- 'ScheduleQuery',
- 'pprint'
- ]
|