"""Calendar backend abstraction for Calendar TUI. This module defines the abstract interface that all calendar backends must implement, allowing the TUI to work with different calendar systems (khal, calcurse, etc.) """ from abc import ABC, abstractmethod from dataclasses import dataclass, field from datetime import datetime, date, time, timedelta from typing import Optional, List, Tuple @dataclass class Event: """Unified calendar event representation across backends.""" uid: str title: str start: datetime end: datetime location: str = "" description: str = "" calendar: str = "" all_day: bool = False recurring: bool = False organizer: str = "" url: str = "" categories: str = "" status: str = "" # CONFIRMED, TENTATIVE, CANCELLED @property def duration_minutes(self) -> int: """Get duration in minutes.""" delta = self.end - self.start return int(delta.total_seconds() / 60) @property def start_time(self) -> time: """Get start time.""" return self.start.time() @property def end_time(self) -> time: """Get end time.""" return self.end.time() @property def date(self) -> date: """Get the date of the event.""" return self.start.date() def overlaps(self, other: "Event") -> bool: """Check if this event overlaps with another.""" return self.start < other.end and self.end > other.start def get_row_span(self, minutes_per_row: int = 30) -> Tuple[int, int]: """Get the row range for this event in a grid. Args: minutes_per_row: Minutes each row represents (default 30) Returns: Tuple of (start_row, end_row) where rows are 0-indexed from midnight """ start_minutes = self.start.hour * 60 + self.start.minute end_minutes = self.end.hour * 60 + self.end.minute # Handle events ending at midnight (next day) if end_minutes == 0 and self.end.date() > self.start.date(): end_minutes = 24 * 60 start_row = start_minutes // minutes_per_row end_row = (end_minutes + minutes_per_row - 1) // minutes_per_row # Round up return start_row, end_row class CalendarBackend(ABC): """Abstract base class for calendar backends.""" @abstractmethod def get_events( self, start_date: date, end_date: date, calendar: Optional[str] = None, ) -> List[Event]: """Get events in a date range. Args: start_date: Start of range (inclusive) end_date: End of range (inclusive) calendar: Optional calendar name to filter by Returns: List of events in the range, sorted by start time """ pass @abstractmethod def get_event(self, uid: str) -> Optional[Event]: """Get a single event by UID. Args: uid: Event unique identifier Returns: Event if found, None otherwise """ pass @abstractmethod def get_calendars(self) -> List[str]: """Get list of available calendar names. Returns: List of calendar names """ pass @abstractmethod def create_event( self, title: str, start: datetime, end: datetime, calendar: Optional[str] = None, location: Optional[str] = None, description: Optional[str] = None, all_day: bool = False, ) -> Event: """Create a new event. Args: title: Event title start: Start datetime end: End datetime calendar: Calendar to add event to location: Event location description: Event description all_day: Whether this is an all-day event Returns: The created event """ pass @abstractmethod def delete_event(self, uid: str) -> bool: """Delete an event. Args: uid: Event unique identifier Returns: True if deleted successfully """ pass @abstractmethod def update_event( self, uid: str, title: Optional[str] = None, start: Optional[datetime] = None, end: Optional[datetime] = None, location: Optional[str] = None, description: Optional[str] = None, ) -> Optional[Event]: """Update an existing event. Args: uid: Event unique identifier title: New title (if provided) start: New start time (if provided) end: New end time (if provided) location: New location (if provided) description: New description (if provided) Returns: Updated event if successful, None otherwise """ pass def get_week_events( self, week_start: date, include_weekends: bool = True, ) -> dict[date, List[Event]]: """Get events for a week, grouped by date. Args: week_start: First day of the week include_weekends: Whether to include Saturday/Sunday Returns: Dict mapping dates to lists of events """ days = 7 if include_weekends else 5 end_date = week_start + timedelta(days=days - 1) events = self.get_events(week_start, end_date) # Group by date by_date: dict[date, List[Event]] = {} for i in range(days): d = week_start + timedelta(days=i) by_date[d] = [] for event in events: event_date = event.date if event_date in by_date: by_date[event_date].append(event) # Sort each day's events by start time for d in by_date: by_date[d].sort(key=lambda e: e.start) return by_date