mirror of
				https://github.com/b4tman/sync_ics2gcal
				synced 2025-11-04 04:38:26 +00:00 
			
		
		
		
	fix docs
This commit is contained in:
		@@ -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',
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        """
 | 
			
		||||
 
 | 
			
		||||
@@ -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,25 +59,41 @@ 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:
 | 
			
		||||
@@ -85,46 +101,55 @@ class CalendarSync():
 | 
			
		||||
        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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user