2018-04-05 17:25:17 +00:00
|
|
|
import datetime
|
2018-04-05 08:16:20 +00:00
|
|
|
import dateutil.parser
|
|
|
|
import logging
|
|
|
|
import operator
|
2018-04-05 11:08:51 +00:00
|
|
|
from pytz import utc
|
2018-04-05 08:16:20 +00:00
|
|
|
|
2018-04-05 17:25:17 +00:00
|
|
|
|
2018-04-05 08:16:20 +00:00
|
|
|
class CalendarSync():
|
2018-04-05 17:25:17 +00:00
|
|
|
"""class for syncronize calendar with google
|
|
|
|
"""
|
|
|
|
|
2018-04-05 08:16:20 +00:00
|
|
|
logger = logging.getLogger('CalendarSync')
|
|
|
|
|
|
|
|
def __init__(self, gcalendar, converter):
|
|
|
|
self.gcalendar = gcalendar
|
|
|
|
self.converter = converter
|
|
|
|
|
2018-04-05 17:25:17 +00:00
|
|
|
@staticmethod
|
|
|
|
def _events_list_compare(items_src, items_dst, key='iCalUID'):
|
2018-04-05 08:16:20 +00:00
|
|
|
""" compare list of events by key
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
items_src {list of dict} -- source events
|
|
|
|
items_dst {list of dict} -- dest events
|
|
|
|
key {str} -- name of key to compare (default: {'iCalUID'})
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
tuple -- (items_to_insert,
|
|
|
|
items_to_update,
|
|
|
|
items_to_delete)
|
|
|
|
"""
|
|
|
|
|
|
|
|
def get_key(item): return item[key]
|
|
|
|
|
2018-04-11 07:44:55 +00:00
|
|
|
keys_src = set(map(get_key, items_src))
|
|
|
|
keys_dst = set(map(get_key, items_dst))
|
2018-04-05 08:16:20 +00:00
|
|
|
|
2018-04-11 07:44:55 +00:00
|
|
|
keys_to_insert = keys_src - keys_dst
|
|
|
|
keys_to_update = keys_src & keys_dst
|
|
|
|
keys_to_delete = keys_dst - keys_src
|
2018-04-05 08:16:20 +00:00
|
|
|
|
|
|
|
def items_by_keys(items, key_name, keys):
|
|
|
|
return list(filter(lambda item: item[key_name] in keys, items))
|
2018-04-11 09:36:37 +00:00
|
|
|
|
2018-04-05 08:16:20 +00:00
|
|
|
items_to_insert = items_by_keys(items_src, key, keys_to_insert)
|
|
|
|
items_to_delete = items_by_keys(items_dst, key, keys_to_delete)
|
|
|
|
|
2018-04-11 09:36:37 +00:00
|
|
|
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))
|
|
|
|
|
2018-04-05 08:16:20 +00:00
|
|
|
return items_to_insert, items_to_update, items_to_delete
|
|
|
|
|
|
|
|
def _filter_events_to_update(self):
|
2018-04-05 17:25:17 +00:00
|
|
|
""" filter 'to_update' events by 'updated' datetime
|
2018-04-05 08:16:20 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
def filter_updated(event_tuple):
|
|
|
|
new, old = event_tuple
|
|
|
|
return dateutil.parser.parse(new['updated']) > dateutil.parser.parse(old['updated'])
|
|
|
|
|
|
|
|
self.to_update = list(filter(filter_updated, self.to_update))
|
|
|
|
|
2018-04-05 17:25:17 +00:00
|
|
|
@staticmethod
|
|
|
|
def _filter_events_by_date(events, date, op):
|
|
|
|
""" filter events by start datetime
|
2018-04-05 08:16:20 +00:00
|
|
|
|
|
|
|
Arguments:
|
2018-04-05 17:25:17 +00:00
|
|
|
events -- events list
|
|
|
|
date {datetime} -- datetime to compare
|
|
|
|
op {operator} -- comparsion operator
|
2018-04-05 08:16:20 +00:00
|
|
|
|
|
|
|
Returns:
|
2018-04-05 17:25:17 +00:00
|
|
|
list of filtred events
|
2018-04-05 08:16:20 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
def filter_by_date(event):
|
2018-04-11 20:38:12 +00:00
|
|
|
date_cmp = date
|
2018-04-05 17:25:17 +00:00
|
|
|
event_start = event['start']
|
|
|
|
event_date = None
|
2018-04-11 20:38:12 +00:00
|
|
|
compare_dates = False
|
|
|
|
|
2018-04-05 17:25:17 +00:00
|
|
|
if 'date' in event_start:
|
|
|
|
event_date = event_start['date']
|
2018-04-11 20:38:12 +00:00
|
|
|
compare_dates = True
|
|
|
|
elif 'dateTime' in event_start:
|
2018-04-05 17:25:17 +00:00
|
|
|
event_date = event_start['dateTime']
|
2018-04-11 20:38:12 +00:00
|
|
|
|
|
|
|
event_date = dateutil.parser.parse(event_date)
|
|
|
|
if compare_dates:
|
|
|
|
date_cmp = datetime.date(date.year, date.month, date.day)
|
|
|
|
event_date = datetime.date(event_date.year, event_date.month, event_date.day)
|
|
|
|
|
|
|
|
return op(event_date, date_cmp)
|
2018-04-05 08:16:20 +00:00
|
|
|
|
|
|
|
return list(filter(filter_by_date, events))
|
2018-04-05 17:25:17 +00:00
|
|
|
|
2018-04-05 11:08:51 +00:00
|
|
|
@staticmethod
|
|
|
|
def _tz_aware_datetime(date):
|
2018-04-05 17:25:17 +00:00
|
|
|
"""make tz aware datetime from datetime/date (utc if no tzinfo)
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
date - date or datetime / with or without tzinfo
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
datetime with tzinfo
|
|
|
|
"""
|
|
|
|
|
2018-04-05 11:08:51 +00: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
|
2018-04-05 17:25:17 +00:00
|
|
|
|
2018-04-05 08:16:20 +00:00
|
|
|
def prepare_sync(self, start_date):
|
2018-04-05 17:25:17 +00:00
|
|
|
"""prepare sync lists by comparsion of events
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
start_date -- date/datetime to start sync
|
|
|
|
"""
|
|
|
|
|
|
|
|
start_date = CalendarSync._tz_aware_datetime(start_date)
|
|
|
|
|
2018-04-05 08:16:20 +00:00
|
|
|
events_src = self.converter.events_to_gcal()
|
|
|
|
events_dst = self.gcalendar.list_events_from(start_date)
|
|
|
|
|
2018-04-05 17:25:17 +00:00
|
|
|
# divide source events by start datetime
|
|
|
|
events_src_pending = CalendarSync._filter_events_by_date(
|
2018-04-05 08:16:20 +00:00
|
|
|
events_src, start_date, operator.ge)
|
2018-04-05 17:25:17 +00:00
|
|
|
events_src_past = CalendarSync._filter_events_by_date(
|
2018-04-05 08:16:20 +00:00
|
|
|
events_src, start_date, operator.lt)
|
|
|
|
|
|
|
|
events_src = None
|
|
|
|
|
2018-04-05 17:25:17 +00:00
|
|
|
# first events comparsion
|
|
|
|
self.to_insert, self.to_update, self.to_delete = CalendarSync._events_list_compare(
|
2018-04-05 08:16:20 +00:00
|
|
|
events_src_pending, events_dst)
|
|
|
|
|
|
|
|
events_src_pending, events_dst = None, None
|
|
|
|
|
2018-04-05 17:25:17 +00: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(
|
2018-04-05 08:16:20 +00:00
|
|
|
events_src_past, self.to_delete)
|
|
|
|
self.to_update.extend(add_to_update)
|
|
|
|
|
|
|
|
events_src_past = None
|
|
|
|
|
2018-04-05 17:25:17 +00:00
|
|
|
# find if events 'to_insert' exists in gcalendar, for update them
|
2018-04-05 08:16:20 +00:00
|
|
|
add_to_update, self.to_insert = self.gcalendar.find_exists(
|
|
|
|
self.to_insert)
|
|
|
|
self.to_update.extend(add_to_update)
|
|
|
|
|
|
|
|
add_to_update = None
|
|
|
|
|
2018-04-05 17:25:17 +00:00
|
|
|
# exclude outdated events from 'to_update' list, by 'updated' field
|
2018-04-05 08:16:20 +00:00
|
|
|
self._filter_events_to_update()
|
|
|
|
|
|
|
|
self.logger.info('prepared to sync: ( insert: %d, update: %d, delete: %d )',
|
|
|
|
len(self.to_insert), len(self.to_update), len(self.to_delete))
|
|
|
|
|
|
|
|
def apply(self):
|
2018-04-05 17:25:17 +00:00
|
|
|
"""apply sync (insert, update, delete), using prepared lists of events
|
|
|
|
"""
|
|
|
|
|
2018-04-05 08:16:20 +00:00
|
|
|
self.gcalendar.insert_events(self.to_insert)
|
|
|
|
self.gcalendar.update_events(self.to_update)
|
|
|
|
self.gcalendar.delete_events(self.to_delete)
|
|
|
|
|
|
|
|
self.logger.info('sync done')
|
|
|
|
|
|
|
|
self.to_insert, self.to_update, self.to_delete = [], [], []
|