diff --git a/gcal_sync/gcal.py b/gcal_sync/gcal.py index 68c87f7..fd51423 100644 --- a/gcal_sync/gcal.py +++ b/gcal_sync/gcal.py @@ -7,8 +7,20 @@ import sys class GoogleCalendarService(): + """class for make google calendar service Resource + + Returns: + service Resource + """ + @staticmethod def from_srv_acc_file(service_account_file): + """make service Resource from service account filename (authorize) + + Returns: + service Resource + """ + scopes = 'https://www.googleapis.com/auth/calendar' credentials = service_account.ServiceAccountCredentials.from_json_keyfile_name( service_account_file, scopes=scopes) @@ -18,6 +30,9 @@ class GoogleCalendarService(): class GoogleCalendar(): + """class to interact with calendar on google + """ + logger = logging.getLogger('GoogleCalendar') def __init__(self, service, calendarId): @@ -25,9 +40,9 @@ class GoogleCalendar(): self.calendarId = calendarId def list_events_from(self, start): - ''' Получение списка событий из GCAL начиная с даты start - ''' - fields='nextPageToken,items(id,iCalUID,updated)' + """ list events from calendar, where start date >= start + """ + fields = 'nextPageToken,items(id,iCalUID,updated)' events = [] page_token = None timeMin = utc.normalize(start.astimezone(utc)).replace( @@ -44,7 +59,7 @@ class GoogleCalendar(): return events def find_exists(self, events): - """ Поиск уже существующих в GCAL событий, из списка событий к вставке + """ find existing events from list, by 'iCalUID' field Arguments: events {list} -- list of events @@ -54,7 +69,7 @@ class GoogleCalendar(): events_exist - list of tuples: (new_event, exists_event) """ - fields='items(id,iCalUID,updated)' + fields = 'items(id,iCalUID,updated)' events_by_req = [] exists = [] not_found = [] @@ -86,13 +101,13 @@ class GoogleCalendar(): return exists, not_found def insert_events(self, events): - """ Вставка событий в GCAL + """ insert list of events Arguments: - events -- список событий + events - events list """ - fields='id' + fields = 'id' events_by_req = [] def insert_callback(request_id, response, exception): @@ -114,14 +129,13 @@ class GoogleCalendar(): batch.execute() def patch_events(self, event_tuples): - """ Обновление (патч) событий в GCAL + """ patch (update) events Arguments: - calendarId -- ИД календаря - event_tuples -- список кортежей событий (новое, старое) + event_tuples -- list of tuples: (new_event, exists_event) """ - fields='id' + fields = 'id' events_by_req = [] def patch_callback(request_id, response, exception): @@ -145,13 +159,13 @@ class GoogleCalendar(): batch.execute() def update_events(self, event_tuples): - """ Обновление событий в GCAL + """ update events Arguments: - event_tuples -- список кортежей событий (новое, старое) + event_tuples -- list of tuples: (new_event, exists_event) """ - - fields='id' + + fields = 'id' events_by_req = [] def update_callback(request_id, response, exception): @@ -175,10 +189,10 @@ class GoogleCalendar(): batch.execute() def delete_events(self, events): - """ Удаление событий в GCAL + """ delete events Arguments: - events -- список событий + events -- list of events """ events_by_req = [] @@ -201,6 +215,9 @@ class GoogleCalendar(): batch.execute() def make_public(self): + """make calendar puplic + """ + rule_public = { 'scope': { 'type': 'default', @@ -210,6 +227,12 @@ class GoogleCalendar(): return self.service.acl().insert(calendarId=self.calendarId, body=rule_public).execute() def add_owner(self, email): + """add calendar owner by email + + Arguments: + email -- email to add + """ + rule_owner = { 'scope': { 'type': 'user', diff --git a/gcal_sync/ical.py b/gcal_sync/ical.py index 26c3e5f..1a71ac1 100644 --- a/gcal_sync/ical.py +++ b/gcal_sync/ical.py @@ -11,9 +11,27 @@ class EventConverter(Event): """ def _str_prop(self, prop): + """decoded string property + + Arguments: + prop - propperty name + + Returns: + string value + """ + return self.decoded(prop).decode(encoding='utf-8') def _datetime_str_prop(self, prop): + """utc datetime as string from property + + Arguments: + prop -- property name + + Returns: + utc datetime value as string in iso format + """ + date = self.decoded(prop) if not isinstance(date, datetime.datetime): date = datetime.datetime( @@ -22,6 +40,15 @@ class EventConverter(Event): return utc.normalize(date.astimezone(utc)).replace(tzinfo=None).isoformat() + 'Z' def _gcal_start(self): + """ event start dict from icalendar event + + Raises: + ValueError -- if DTSTART not date or datetime + + Returns: + dict + """ + start_date = self.decoded('DTSTART') if isinstance(start_date, datetime.datetime): return { @@ -35,6 +62,16 @@ class EventConverter(Event): raise ValueError('DTSTART must be date or datetime') def _gcal_end(self): + """event end dict from icalendar event + + Raises: + ValueError -- if DTEND not date or datetime + ValueError -- if no DTEND or DURATION + ValueError -- if end date/datetime not found + Returns: + dict + """ + if 'DTEND' in self: end_date = self.decoded('DTEND') if isinstance(end_date, datetime.datetime): @@ -66,6 +103,15 @@ class EventConverter(Event): raise ValueError('end date/time not found') def _put_to_gcal(self, gcal_event, prop, func, ics_prop=None): + """get property from ical event if exist, and put to gcal event + + Arguments: + gcal_event -- dest 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: @@ -113,6 +159,11 @@ class CalendarConverter(): self.calendar = Calendar.from_ical(f.read()) self.logger.info('%s loaded', filename) + def loads(self, string): + """ load calendar from ics string + """ + self.calendar = Calendar.from_ical(string) + def events_to_gcal(self): """Convert events to google calendar resources """ diff --git a/gcal_sync/sync.py b/gcal_sync/sync.py index fbd2f67..27e13f3 100644 --- a/gcal_sync/sync.py +++ b/gcal_sync/sync.py @@ -1,16 +1,22 @@ +import datetime import dateutil.parser import logging import operator from pytz import utc + class CalendarSync(): + """class for syncronize calendar with google + """ + logger = logging.getLogger('CalendarSync') def __init__(self, gcalendar, converter): self.gcalendar = gcalendar self.converter = converter - def _events_list_compare(self, items_src, items_dst, key='iCalUID'): + @staticmethod + def _events_list_compare(items_src, items_dst, key='iCalUID'): """ compare list of events by key Arguments: @@ -44,13 +50,7 @@ class CalendarSync(): return items_to_insert, items_to_update, items_to_delete def _filter_events_to_update(self): - """ Отбор событий к обновлению, по дате обновления - - Arguments: - events -- список кортежей к обновлению (новое, старое) - - Returns: - список кортежей к обновлению (новое, старое) + """ filter 'to_update' events by 'updated' datetime """ def filter_updated(event_tuple): @@ -59,72 +59,97 @@ class CalendarSync(): self.to_update = list(filter(filter_updated, self.to_update)) - def _filter_events_by_date(self, events, date, op): - """ Отбор событий по дате обновления + @staticmethod + def _filter_events_by_date(events, date, op): + """ filter events by start datetime Arguments: - events -- список событий к обновлению - date {datetime} -- дата для сравнения - op {operator} -- оператор сравнения + events -- events list + date {datetime} -- datetime to compare + op {operator} -- comparsion operator Returns: - список событий + list of filtred events """ def filter_by_date(event): - return op(dateutil.parser.parse(event['updated']), date) + event_start = event['start'] + event_date = None + if 'date' in event_start: + event_date = event_start['date'] + if 'dateTime' in event_start: + event_date = event_start['dateTime'] + return op(dateutil.parser.parse(event_date), date) return list(filter(filter_by_date, events)) - + @staticmethod def _tz_aware_datetime(date): + """make tz aware datetime from datetime/date (utc if no tzinfo) + + Arguments: + date - date or datetime / with or without tzinfo + + Returns: + datetime with tzinfo + """ + 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 - + def prepare_sync(self, start_date): - start_date = _tz_aware_datetime(start_date) - + """prepare sync lists by comparsion of events + + 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) - # разбитие тестовых событий на будующие и прошлые - events_src_pending = self._filter_events_by_date( + # divide source events by start datetime + events_src_pending = CalendarSync._filter_events_by_date( events_src, start_date, operator.ge) - events_src_past = self._filter_events_by_date( + events_src_past = CalendarSync._filter_events_by_date( events_src, start_date, operator.lt) events_src = None - # первоначальное сравнение списков - self.to_insert, self.to_update, self.to_delete = self._events_list_compare( + # first events comparsion + self.to_insert, self.to_update, self.to_delete = CalendarSync._events_list_compare( events_src_pending, events_dst) events_src_pending, events_dst = None, None - # сравнение списка на удаление со списком прошлых событий, для определения доп событий к обновлению - _, add_to_update, self.to_delete = self._events_list_compare( + # find in events 'to_delete' past events from source, for update (move to past) + _, add_to_update, self.to_delete = CalendarSync._events_list_compare( events_src_past, self.to_delete) self.to_update.extend(add_to_update) events_src_past = None - # проверка списка к вставке и перемещение доп. элементов в список к обновлению + # find if events 'to_insert' exists in gcalendar, for update them add_to_update, self.to_insert = self.gcalendar.find_exists( self.to_insert) self.to_update.extend(add_to_update) add_to_update = None - # отбор событий требующих обновления (по полю 'updated') + # exclude outdated events from 'to_update' list, by 'updated' field 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): + """apply sync (insert, update, delete), using prepared lists of events + """ + self.gcalendar.insert_events(self.to_insert) self.gcalendar.update_events(self.to_update) self.gcalendar.delete_events(self.to_delete)