ovdashboard/api/ovdashboard_api/dav_calendar.py

193 lines
4.9 KiB
Python
Raw Normal View History

2022-09-05 12:54:02 +00:00
"""
Definition of an asyncio compatible CalDAV calendar.
Caches events using `timed_alru_cache`.
"""
2022-09-04 18:48:46 +00:00
from dataclasses import dataclass
from datetime import datetime, timedelta
2022-09-04 20:59:45 +00:00
from functools import total_ordering
2022-09-07 00:29:32 +00:00
from logging import getLogger
2023-10-16 18:06:55 +00:00
from typing import Annotated, Iterator
2022-09-04 18:48:46 +00:00
2022-10-28 11:06:02 +00:00
from cache import AsyncTTL
2022-09-04 18:48:46 +00:00
from caldav import Calendar
2022-09-04 21:41:40 +00:00
from caldav.lib.error import ReportError
2023-10-16 18:06:55 +00:00
from pydantic import AfterValidator, BaseModel
2022-09-08 00:24:36 +00:00
from vobject.base import Component
2022-09-04 18:48:46 +00:00
2022-10-28 11:06:02 +00:00
from .async_helpers import run_in_executor
2022-09-06 22:20:01 +00:00
from .config import Config
2022-09-04 22:30:40 +00:00
from .dav_common import caldav_principal
2022-09-04 23:41:51 +00:00
from .settings import SETTINGS
2022-09-04 18:48:46 +00:00
2022-09-07 00:29:32 +00:00
_logger = getLogger(__name__)
2023-10-16 18:06:55 +00:00
StrippedStr = Annotated[str, AfterValidator(lambda s: s.strip())]
2022-09-04 20:59:45 +00:00
@total_ordering
2022-09-04 18:48:46 +00:00
class CalEvent(BaseModel):
2022-09-05 12:54:02 +00:00
"""
A CalDAV calendar event.
Properties are to be named as in the EVENT component of
RFC5545 (iCalendar).
https://icalendar.org/iCalendar-RFC-5545/3-6-1-event-component.html
"""
2023-10-16 18:06:55 +00:00
summary: StrippedStr = ""
description: StrippedStr = ""
2022-09-04 21:41:40 +00:00
dtstart: datetime = datetime.utcnow()
dtend: datetime = datetime.utcnow()
class Config:
frozen = True
2022-09-04 18:48:46 +00:00
2022-09-04 20:59:45 +00:00
def __lt__(self, other: "CalEvent") -> bool:
2022-09-05 12:54:02 +00:00
"""
Order Events by start time.
"""
2022-09-04 20:59:45 +00:00
return self.dtstart < other.dtstart
def __eq__(self, other: "CalEvent") -> bool:
2022-09-05 12:54:02 +00:00
"""
Compare all properties.
"""
2023-10-17 12:45:56 +00:00
return self.model_dump() == other.model_dump()
2022-09-04 20:59:45 +00:00
2022-09-04 21:41:40 +00:00
@classmethod
2022-09-08 00:24:36 +00:00
def from_vevent(cls, event: Component) -> "CalEvent":
2022-09-05 12:54:02 +00:00
"""
Create a CalEvent instance from a `VObject.VEvent` object.
"""
2022-09-04 21:41:40 +00:00
data = {}
2022-09-19 12:55:49 +00:00
keys = ("summary", "description", "dtstart", "dtend", "duration")
2022-09-04 21:41:40 +00:00
2022-09-19 12:55:49 +00:00
for key in keys:
2022-09-04 21:41:40 +00:00
try:
2022-09-08 00:24:36 +00:00
data[key] = event.contents[key][0].value # type: ignore
2022-09-04 21:41:40 +00:00
except KeyError:
pass
2022-09-19 12:55:49 +00:00
if "dtend" not in data:
data["dtend"] = data["dtstart"]
if "duration" in data:
try:
2022-09-19 13:22:32 +00:00
data["dtend"] += data["duration"]
2022-09-19 12:55:49 +00:00
except (ValueError, TypeError, AttributeError):
_logger.warn(
"Could not add duration %s to %s",
repr(data["duration"]),
repr(data["dtstart"]),
)
2022-09-19 13:22:32 +00:00
del data["duration"]
2022-09-19 12:55:49 +00:00
2023-10-17 12:45:56 +00:00
return cls.model_validate(data)
2022-09-04 21:41:40 +00:00
2022-09-04 18:48:46 +00:00
2022-10-28 11:06:02 +00:00
@AsyncTTL(time_to_live=SETTINGS.cache_time, maxsize=SETTINGS.cache_size)
2022-09-04 18:48:46 +00:00
async def _get_calendar(
calendar_name: str,
) -> Calendar:
2022-09-05 12:54:02 +00:00
"""
Get a calendar by name using the CalDAV principal object.
"""
2022-09-04 18:48:46 +00:00
@run_in_executor
2022-09-04 20:50:24 +00:00
def _inner() -> Calendar:
2022-09-04 20:50:11 +00:00
return caldav_principal().calendar(calendar_name)
2022-09-04 20:50:24 +00:00
return await _inner()
2022-09-04 20:50:11 +00:00
2022-10-28 11:06:02 +00:00
@AsyncTTL(time_to_live=SETTINGS.cache_time, maxsize=SETTINGS.cache_size)
2022-09-04 20:50:11 +00:00
async def _get_calendar_events(
calendar_name: str,
) -> list[CalEvent]:
2022-09-05 12:54:02 +00:00
"""
Get a sorted list of events by CalDAV calendar name.
Do not return an iterator here - this result is cached and
an iterator would get consumed.
"""
2022-09-04 20:50:11 +00:00
cfg = await Config.get()
search_span = timedelta(days=cfg.calendar.future_days)
2022-09-04 20:50:11 +00:00
@run_in_executor
2022-09-08 00:24:36 +00:00
def _inner() -> Iterator[Component]:
2022-09-05 12:54:02 +00:00
"""
Get events by CalDAV calendar name.
This can return an iterator - only the outer function is
cached.
"""
2022-09-06 22:55:23 +00:00
_logger.info(f"downloading {calendar_name!r} ...")
2022-09-04 18:48:46 +00:00
2022-09-04 20:50:11 +00:00
calendar = caldav_principal().calendar(calendar_name)
2022-09-04 21:41:40 +00:00
date_start = datetime.utcnow().date()
time_min = datetime.min.time()
dt_start = datetime.combine(date_start, time_min)
dt_end = dt_start + search_span
2022-09-04 21:41:40 +00:00
try:
search_result = calendar.date_search(
start=dt_start,
end=dt_end,
2022-09-04 20:50:11 +00:00
expand=True,
2022-09-04 21:41:40 +00:00
verify_expand=True,
)
except ReportError:
2022-09-07 00:17:55 +00:00
_logger.warning("CalDAV server does not support expanded search")
2022-09-04 21:41:40 +00:00
search_result = calendar.date_search(
start=dt_start,
end=dt_end,
expand=False,
2022-09-04 20:50:11 +00:00
)
2022-09-04 21:41:40 +00:00
2022-09-08 00:24:36 +00:00
for event in search_result:
vobject: Component = event.vobject_instance # type: ignore
yield from vobject.vevent_list
2022-09-04 18:48:46 +00:00
2023-10-16 18:06:55 +00:00
return sorted([CalEvent.from_vevent(vevent) for vevent in await _inner()])
2022-09-04 18:48:46 +00:00
@dataclass(frozen=True)
class DavCalendar:
2022-09-05 12:54:02 +00:00
"""
Object representation of a CalDAV calendar.
"""
2022-09-04 18:48:46 +00:00
calendar_name: str
@property
async def calendar(self) -> Calendar:
2022-09-05 12:54:02 +00:00
"""
Calendar as `caldav` library representation.
"""
2022-09-04 18:48:46 +00:00
return await _get_calendar(
calendar_name=self.calendar_name,
)
@property
2022-09-04 20:50:11 +00:00
async def events(self) -> list[CalEvent]:
2022-09-05 12:54:02 +00:00
"""
Calendar events in object representation.
"""
2022-09-04 20:50:11 +00:00
return await _get_calendar_events(
calendar_name=self.calendar_name,
2022-09-04 18:48:46 +00:00
)