219 lines
5.8 KiB
Python
219 lines
5.8 KiB
Python
"""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
|