Files
luk/src/calendar/backend.py
Bendt a41d59e529 WIP
2025-12-18 22:11:47 -05:00

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