This commit is contained in:
Dmitry Belyaev 2018-04-05 20:25:17 +03:00
parent cc75f522d4
commit 26e35c1b68
Signed by: b4tman
GPG Key ID: 014E87EC54B77673
3 changed files with 145 additions and 46 deletions

View File

@ -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',

View File

@ -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
"""

View File

@ -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)