1
0
mirror of https://github.com/b4tman/sync_ics2gcal synced 2026-08-14 04:52:54 +03:00
Files
sync_ics2gcal/sync_ics2gcal/sync.py
T

199 lines
6.6 KiB
Python
Raw Normal View History

2020-02-19 23:26:28 +03:00
import datetime
import logging
import operator
2021-05-19 12:55:54 +03:00
from typing import List, Dict, Set, Tuple, Union, Callable
2021-04-29 17:10:35 +03:00
import dateutil.parser
2020-02-19 23:26:28 +03:00
from pytz import utc
2021-05-19 12:55:54 +03:00
from .gcal import GoogleCalendar, EventData, EventList, EventTuple
from .ical import CalendarConverter, DateDateTime
2020-02-19 23:26:28 +03:00
2021-04-29 17:10:35 +03:00
class CalendarSync:
2022-03-08 10:46:21 -05:00
"""class for synchronize calendar with Google"""
2020-02-19 23:26:28 +03:00
2022-03-08 10:46:21 -05:00
logger = logging.getLogger("CalendarSync")
2020-02-19 23:26:28 +03:00
2021-04-29 17:10:35 +03:00
def __init__(self, gcalendar: GoogleCalendar, converter: CalendarConverter):
self.gcalendar: GoogleCalendar = gcalendar
self.converter: CalendarConverter = converter
2021-05-19 12:55:54 +03:00
self.to_insert: EventList = []
self.to_update: List[EventTuple] = []
self.to_delete: EventList = []
2020-02-19 23:26:28 +03:00
@staticmethod
2022-03-08 10:46:21 -05:00
def _events_list_compare(
items_src: EventList, items_dst: EventList, key: str = "iCalUID"
) -> Tuple[EventList, List[EventTuple], EventList]:
"""compare list of events by key
2020-02-19 23:26:28 +03:00
Arguments:
items_src {list of dict} -- source events
2022-02-24 12:34:41 +03:00
items_dst {list of dict} -- destination events
2020-02-19 23:26:28 +03:00
key {str} -- name of key to compare (default: {'iCalUID'})
Returns:
2020-03-07 18:34:31 +03:00
tuple -- (items_to_insert,
items_to_update,
2020-02-19 23:26:28 +03:00
items_to_delete)
"""
2022-03-08 10:46:21 -05:00
def get_key(item: EventData) -> str:
return item[key]
2020-02-19 23:26:28 +03:00
2021-04-29 17:10:35 +03:00
keys_src: Set[str] = set(map(get_key, items_src))
keys_dst: Set[str] = set(map(get_key, items_dst))
2020-02-19 23:26:28 +03:00
keys_to_insert = keys_src - keys_dst
keys_to_update = keys_src & keys_dst
keys_to_delete = keys_dst - keys_src
2022-03-08 10:46:21 -05:00
def items_by_keys(items: EventList, key_name: str, keys: Set[str]) -> EventList:
2020-02-19 23:26:28 +03:00
return list(filter(lambda item: item[key_name] in keys, items))
items_to_insert = items_by_keys(items_src, key, keys_to_insert)
items_to_delete = items_by_keys(items_dst, key, keys_to_delete)
to_upd_src = items_by_keys(items_src, key, keys_to_update)
to_upd_dst = items_by_keys(items_dst, key, keys_to_update)
to_upd_src.sort(key=get_key)
to_upd_dst.sort(key=get_key)
items_to_update = list(zip(to_upd_src, to_upd_dst))
return items_to_insert, items_to_update, items_to_delete
def _filter_events_to_update(self):
2022-03-08 10:46:21 -05:00
"""filter 'to_update' events by 'updated' datetime"""
2020-02-19 23:26:28 +03:00
2021-05-19 12:55:54 +03:00
def filter_updated(event_tuple: EventTuple) -> bool:
2020-02-19 23:26:28 +03:00
new, old = event_tuple
2022-03-08 10:46:21 -05:00
if "updated" not in new or "updated" not in old:
2022-02-22 10:15:05 +03:00
return True
2022-03-08 10:46:21 -05:00
new_date = dateutil.parser.parse(new["updated"])
old_date = dateutil.parser.parse(old["updated"])
2020-03-07 18:34:31 +03:00
return new_date > old_date
2020-02-19 23:26:28 +03:00
self.to_update = list(filter(filter_updated, self.to_update))
@staticmethod
2022-03-08 10:46:21 -05:00
def _filter_events_by_date(
events: EventList,
date: DateDateTime,
op: Callable[[DateDateTime, DateDateTime], bool],
) -> EventList:
"""filter events by start datetime
2020-02-19 23:26:28 +03:00
Arguments:
events -- events list
date {datetime} -- datetime to compare
2022-02-24 12:34:41 +03:00
op {operator} -- comparison operator
2020-02-19 23:26:28 +03:00
Returns:
2022-02-24 12:34:41 +03:00
list of filtered events
2020-02-19 23:26:28 +03:00
"""
2021-05-19 12:55:54 +03:00
def filter_by_date(event: EventData) -> bool:
2020-02-19 23:26:28 +03:00
date_cmp = date
2022-03-08 10:46:21 -05:00
event_start: Dict[str, str] = event["start"]
2021-05-19 12:55:54 +03:00
event_date: Union[DateDateTime, str, None] = None
2020-02-19 23:26:28 +03:00
compare_dates = False
2022-03-08 10:46:21 -05:00
if "date" in event_start:
event_date = event_start["date"]
2020-02-19 23:26:28 +03:00
compare_dates = True
2022-03-08 10:46:21 -05:00
elif "dateTime" in event_start:
event_date = event_start["dateTime"]
2020-03-07 18:34:31 +03:00
2020-02-19 23:26:28 +03:00
event_date = dateutil.parser.parse(event_date)
if compare_dates:
date_cmp = datetime.date(date.year, date.month, date.day)
2020-03-07 18:34:31 +03:00
event_date = datetime.date(
2022-03-08 10:46:21 -05:00
event_date.year, event_date.month, event_date.day
)
2020-02-19 23:26:28 +03:00
return op(event_date, date_cmp)
return list(filter(filter_by_date, events))
@staticmethod
2021-05-19 12:55:54 +03:00
def _tz_aware_datetime(date: DateDateTime) -> datetime.datetime:
2022-02-24 12:34:41 +03:00
"""make tz aware datetime from datetime/date (utc if no tz-info)
2020-02-19 23:26:28 +03:00
Arguments:
2022-02-24 12:34:41 +03:00
date - date or datetime / with or without tz-info
2020-02-19 23:26:28 +03:00
Returns:
2022-02-24 12:34:41 +03:00
datetime with tz-info
2020-02-19 23:26:28 +03:00
"""
if not isinstance(date, datetime.datetime):
date = datetime.datetime(date.year, date.month, date.day)
if date.tzinfo is None:
date = date.replace(tzinfo=utc)
return date
2021-05-19 12:55:54 +03:00
def prepare_sync(self, start_date: DateDateTime) -> None:
2022-02-24 12:34:41 +03:00
"""prepare sync lists by comparison of events
2020-02-19 23:26:28 +03:00
Arguments:
start_date -- date/datetime to start sync
"""
start_date = CalendarSync._tz_aware_datetime(start_date)
events_src = self.converter.events_to_gcal()
events_dst = self.gcalendar.list_events_from(start_date)
# divide source events by start datetime
events_src_pending = CalendarSync._filter_events_by_date(
2022-03-08 10:46:21 -05:00
events_src, start_date, operator.ge
)
2020-02-19 23:26:28 +03:00
events_src_past = CalendarSync._filter_events_by_date(
2022-03-08 10:46:21 -05:00
events_src, start_date, operator.lt
)
2020-02-19 23:26:28 +03:00
2022-02-24 12:34:41 +03:00
# first events comparison
2022-03-08 10:46:21 -05:00
(
self.to_insert,
self.to_update,
self.to_delete,
) = CalendarSync._events_list_compare(events_src_pending, events_dst)
2020-02-19 23:26:28 +03:00
# find in events 'to_delete' past events from source, for update (move to past)
_, add_to_update, self.to_delete = CalendarSync._events_list_compare(
2022-03-08 10:46:21 -05:00
events_src_past, self.to_delete
)
2020-02-19 23:26:28 +03:00
self.to_update.extend(add_to_update)
# find if events 'to_insert' exists in gcalendar, for update them
2022-03-08 10:46:21 -05:00
add_to_update, self.to_insert = self.gcalendar.find_exists(self.to_insert)
2020-02-19 23:26:28 +03:00
self.to_update.extend(add_to_update)
# exclude outdated events from 'to_update' list, by 'updated' field
self._filter_events_to_update()
2020-03-07 18:34:31 +03:00
self.logger.info(
2022-03-08 10:46:21 -05:00
"prepared to sync: ( insert: %d, update: %d, delete: %d )",
2020-03-07 18:34:31 +03:00
len(self.to_insert),
len(self.to_update),
2022-03-08 10:46:21 -05:00
len(self.to_delete),
2020-03-07 18:34:31 +03:00
)
2020-02-19 23:26:28 +03:00
2021-04-29 17:10:35 +03:00
def clear(self) -> None:
2022-03-08 10:46:21 -05:00
"""clear prepared sync lists (insert, update, delete)"""
2021-04-29 17:10:35 +03:00
self.to_insert.clear()
self.to_update.clear()
self.to_delete.clear()
def apply(self) -> None:
2022-03-08 10:46:21 -05:00
"""apply sync (insert, update, delete), using prepared lists of events"""
2020-02-19 23:26:28 +03:00
self.gcalendar.insert_events(self.to_insert)
self.gcalendar.update_events(self.to_update)
self.gcalendar.delete_events(self.to_delete)
2021-04-29 17:10:35 +03:00
self.clear()
2020-02-19 23:26:28 +03:00
2022-03-08 10:46:21 -05:00
self.logger.info("sync done")