sync_ics2gcal/sync_ics2gcal/ical.py

202 lines
5.8 KiB
Python
Raw Permalink Normal View History

import datetime
import logging
2022-06-03 22:07:24 +00:00
from typing import Union, Dict, Callable, Optional, Mapping, TypedDict
from icalendar import Calendar, Event
from pytz import utc
2022-06-03 20:42:29 +00:00
from .gcal import (
EventData,
EventList,
EventDateOrDateTime,
EventDateTime,
EventDate,
EventDataKey,
)
2022-06-03 22:07:24 +00:00
DateDateTime = Union[datetime.date, datetime.datetime]
2021-05-19 09:55:54 +00:00
def format_datetime_utc(value: DateDateTime) -> str:
"""utc datetime as string from date or datetime value
2021-04-29 13:19:41 +00:00
Arguments:
value -- date or datetime value
Returns:
utc datetime value as string in iso format
"""
if not isinstance(value, datetime.datetime):
2022-03-08 15:46:21 +00:00
value = datetime.datetime(value.year, value.month, value.day, tzinfo=utc)
value = value.replace(microsecond=1)
2020-03-07 15:29:35 +00:00
2022-03-08 15:46:21 +00:00
return utc.normalize(value.astimezone(utc)).replace(tzinfo=None).isoformat() + "Z"
2022-03-08 15:46:21 +00:00
def gcal_date_or_datetime(
value: DateDateTime, check_value: Optional[DateDateTime] = None
2022-06-03 19:06:17 +00:00
) -> EventDateOrDateTime:
2022-02-24 09:20:07 +00:00
"""date or datetime to gcal (start or end dict)
2021-04-29 13:19:41 +00:00
Arguments:
2021-04-29 13:19:41 +00:00
value: date or datetime
check_value: optional for choose result type
Returns:
2021-04-29 13:19:41 +00:00
{ 'date': ... } or { 'dateTime': ... }
"""
if check_value is None:
check_value = value
2022-06-03 19:06:17 +00:00
result: EventDateOrDateTime
if isinstance(check_value, datetime.datetime):
2022-06-03 19:06:17 +00:00
result = EventDateTime(dateTime=format_datetime_utc(value))
else:
if isinstance(check_value, datetime.date):
if isinstance(value, datetime.datetime):
value = datetime.date(value.year, value.month, value.day)
2022-06-03 19:06:17 +00:00
result = EventDate(date=value.isoformat())
return result
2022-06-04 15:47:54 +00:00
class EventConverter(Event): # type: ignore
"""Convert icalendar event to google calendar resource
( https://developers.google.com/calendar/v3/reference/events#resource-representations )
"""
2021-04-29 13:19:41 +00:00
def _str_prop(self, prop: str) -> str:
"""decoded string property
Arguments:
2022-02-24 09:34:41 +00:00
prop - property name
Returns:
string value
"""
2022-06-04 15:47:54 +00:00
return str(self.decoded(prop).decode(encoding="utf-8"))
2021-04-29 13:19:41 +00:00
def _datetime_str_prop(self, prop: str) -> str:
"""utc datetime as string from property
Arguments:
prop -- property name
Returns:
utc datetime value as string in iso format
"""
return format_datetime_utc(self.decoded(prop))
2022-06-03 19:06:17 +00:00
def _gcal_start(self) -> EventDateOrDateTime:
2022-03-08 15:46:21 +00:00
"""event start dict from icalendar event
Raises:
ValueError -- if DTSTART not date or datetime
Returns:
dict
"""
2022-03-08 15:46:21 +00:00
value = self.decoded("DTSTART")
2022-02-24 09:20:07 +00:00
return gcal_date_or_datetime(value)
2022-06-03 19:06:17 +00:00
def _gcal_end(self) -> EventDateOrDateTime:
"""event end dict from icalendar event
Raises:
ValueError -- if no DTEND or DURATION
Returns:
dict
"""
2022-06-03 19:06:17 +00:00
result: EventDateOrDateTime
2022-03-08 15:46:21 +00:00
if "DTEND" in self:
value = self.decoded("DTEND")
2022-02-24 09:20:07 +00:00
result = gcal_date_or_datetime(value)
2022-03-08 15:46:21 +00:00
elif "DURATION" in self:
start_val = self.decoded("DTSTART")
duration = self.decoded("DURATION")
end_val = start_val + duration
2022-02-24 09:20:07 +00:00
result = gcal_date_or_datetime(end_val, check_value=start_val)
else:
2022-03-08 15:46:21 +00:00
raise ValueError("no DTEND or DURATION")
return result
2022-03-08 15:46:21 +00:00
def _put_to_gcal(
self,
gcal_event: EventData,
2022-06-03 20:42:29 +00:00
prop: EventDataKey,
2022-03-08 15:46:21 +00:00
func: Callable[[str], str],
ics_prop: Optional[str] = None,
2022-06-04 15:47:54 +00:00
) -> None:
2022-02-24 09:34:41 +00:00
"""get property from ical event if existed, and put to gcal event
Arguments:
2022-02-24 09:34:41 +00:00
gcal_event -- destination event
prop -- property name
func -- function to convert
ics_prop -- ical property name (default: {None})
"""
if not ics_prop:
ics_prop = prop
if ics_prop in self:
gcal_event[prop] = func(ics_prop)
2022-06-03 20:42:29 +00:00
def convert(self) -> EventData:
"""Convert
Returns:
dict - google calendar#event resource
"""
2022-06-03 20:42:29 +00:00
event: EventData = EventData(
iCalUID=self._str_prop("UID"),
start=self._gcal_start(),
end=self._gcal_end(),
)
2022-03-08 15:46:21 +00:00
self._put_to_gcal(event, "summary", self._str_prop)
self._put_to_gcal(event, "description", self._str_prop)
self._put_to_gcal(event, "location", self._str_prop)
self._put_to_gcal(event, "created", self._datetime_str_prop)
self._put_to_gcal(event, "updated", self._datetime_str_prop, "LAST-MODIFIED")
self._put_to_gcal(
2022-03-08 15:46:21 +00:00
event, "transparency", lambda prop: self._str_prop(prop).lower(), "TRANSP"
)
return event
2021-04-29 13:19:41 +00:00
class CalendarConverter:
2022-03-08 15:46:21 +00:00
"""Convert icalendar events to google calendar resources"""
2022-03-08 15:46:21 +00:00
logger = logging.getLogger("CalendarConverter")
2021-04-29 13:19:41 +00:00
def __init__(self, calendar: Optional[Calendar] = None):
self.calendar: Optional[Calendar] = calendar
2022-06-04 15:47:54 +00:00
def load(self, filename: str) -> None:
2022-03-08 15:46:21 +00:00
"""load calendar from ics file"""
with open(filename, "r", encoding="utf-8") as f:
self.calendar = Calendar.from_ical(f.read())
2022-03-08 15:46:21 +00:00
self.logger.info("%s loaded", filename)
2022-06-04 15:47:54 +00:00
def loads(self, string: str) -> None:
2022-03-08 15:46:21 +00:00
"""load calendar from ics string"""
self.calendar = Calendar.from_ical(string)
2021-05-19 09:55:54 +00:00
def events_to_gcal(self) -> EventList:
2022-03-08 15:46:21 +00:00
"""Convert events to google calendar resources"""
2022-06-03 19:06:17 +00:00
calendar: Calendar = self.calendar
ics_events = calendar.walk(name="VEVENT")
2022-03-08 15:46:21 +00:00
self.logger.info("%d events read", len(ics_events))
2022-06-03 20:42:29 +00:00
result = list(map(lambda event: EventConverter(event).convert(), ics_events))
2022-03-08 15:46:21 +00:00
self.logger.info("%d events converted", len(result))
return result