mirror of
https://github.com/b4tman/sync_ics2gcal
synced 2025-01-21 07:28:24 +00:00
Merge branch 'develop'
* develop: rewrite callback for batch requests rewrite date/dateTime conversion func's performance fix in compare remove old test module fix sets in events_list_compare performance fix in get_item more items in comparison test
This commit is contained in:
commit
53115e5e34
@ -29,6 +29,24 @@ class GoogleCalendarService():
|
||||
return service
|
||||
|
||||
|
||||
def select_event_key(event):
|
||||
"""select event key for logging
|
||||
|
||||
Arguments:
|
||||
event -- event resource
|
||||
|
||||
Returns:
|
||||
key name or None if no key found
|
||||
"""
|
||||
|
||||
key = None
|
||||
if 'iCalUID' in event:
|
||||
key = 'iCalUID'
|
||||
elif 'id' in event:
|
||||
key = 'id'
|
||||
return key
|
||||
|
||||
|
||||
class GoogleCalendar():
|
||||
"""class to interact with calendar on google
|
||||
"""
|
||||
@ -39,6 +57,33 @@ class GoogleCalendar():
|
||||
self.service = service
|
||||
self.calendarId = calendarId
|
||||
|
||||
def _make_request_callback(self, action, events_by_req):
|
||||
"""make callback for log result of batch request
|
||||
|
||||
Arguments:
|
||||
action -- action name
|
||||
events_by_req -- list of events ordered by request id
|
||||
|
||||
Returns:
|
||||
callback function
|
||||
"""
|
||||
|
||||
def callback(request_id, response, exception):
|
||||
event = events_by_req[int(request_id)]
|
||||
key = select_event_key(event)
|
||||
|
||||
if exception is not None:
|
||||
self.logger.error('failed to %s event with %s: %s, exception: %s',
|
||||
action, key, event.get(key), str(exception))
|
||||
else:
|
||||
resp_key = select_event_key(response)
|
||||
if resp_key is not None:
|
||||
event = response
|
||||
key = resp_key
|
||||
self.logger.info('event %s ok, %s: %s',
|
||||
action, key, event.get(key))
|
||||
return callback
|
||||
|
||||
def list_events_from(self, start):
|
||||
""" list events from calendar, where start date >= start
|
||||
"""
|
||||
@ -110,15 +155,7 @@ class GoogleCalendar():
|
||||
fields = 'id'
|
||||
events_by_req = []
|
||||
|
||||
def insert_callback(request_id, response, exception):
|
||||
if exception is not None:
|
||||
event = events_by_req[int(request_id)]
|
||||
self.logger.error('failed to insert event with UID: %s, exception: %s', event.get(
|
||||
'UID'), str(exception))
|
||||
else:
|
||||
event = response
|
||||
self.logger.info('event created, id: %s', event.get('id'))
|
||||
|
||||
insert_callback = self._make_request_callback('insert', events_by_req)
|
||||
batch = self.service.new_batch_http_request(callback=insert_callback)
|
||||
i = 0
|
||||
for event in events:
|
||||
@ -138,15 +175,7 @@ class GoogleCalendar():
|
||||
fields = 'id'
|
||||
events_by_req = []
|
||||
|
||||
def patch_callback(request_id, response, exception):
|
||||
if exception is not None:
|
||||
event = events_by_req[int(request_id)]
|
||||
self.logger.error('failed to patch event with UID: %s, exception: %s', event.get(
|
||||
'UID'), str(exception))
|
||||
else:
|
||||
event = response
|
||||
self.logger.info('event patched, id: %s', event.get('id'))
|
||||
|
||||
patch_callback = self._make_request_callback('patch', events_by_req)
|
||||
batch = self.service.new_batch_http_request(callback=patch_callback)
|
||||
i = 0
|
||||
for event_new, event_old in event_tuples:
|
||||
@ -168,15 +197,7 @@ class GoogleCalendar():
|
||||
fields = 'id'
|
||||
events_by_req = []
|
||||
|
||||
def update_callback(request_id, response, exception):
|
||||
if exception is not None:
|
||||
event = events_by_req[int(request_id)]
|
||||
self.logger.error('failed to update event with UID: %s, exception: %s', event.get(
|
||||
'UID'), str(exception))
|
||||
else:
|
||||
event = response
|
||||
self.logger.info('event updated, id: %s', event.get('id'))
|
||||
|
||||
update_callback = self._make_request_callback('update', events_by_req)
|
||||
batch = self.service.new_batch_http_request(callback=update_callback)
|
||||
i = 0
|
||||
for event_new, event_old in event_tuples:
|
||||
@ -197,14 +218,7 @@ class GoogleCalendar():
|
||||
|
||||
events_by_req = []
|
||||
|
||||
def delete_callback(request_id, _, exception):
|
||||
event = events_by_req[int(request_id)]
|
||||
if exception is not None:
|
||||
self.logger.error('failed to delete event with UID: %s, exception: %s', event.get(
|
||||
'UID'), str(exception))
|
||||
else:
|
||||
self.logger.info('event deleted, id: %s', event.get('id'))
|
||||
|
||||
delete_callback = self._make_request_callback('delete', events_by_req)
|
||||
batch = self.service.new_batch_http_request(callback=delete_callback)
|
||||
i = 0
|
||||
for event in events:
|
||||
|
@ -1,8 +1,47 @@
|
||||
from icalendar import Calendar, Event
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from icalendar import Calendar, Event
|
||||
from pytz import utc
|
||||
|
||||
import datetime
|
||||
|
||||
def format_datetime_utc(value):
|
||||
"""utc datetime as string from date or datetime value
|
||||
Arguments:
|
||||
value -- date or datetime value
|
||||
|
||||
Returns:
|
||||
utc datetime value as string in iso format
|
||||
"""
|
||||
if not isinstance(value, datetime.datetime):
|
||||
value = datetime.datetime(
|
||||
value.year, value.month, value.day, tzinfo=utc)
|
||||
value = value.replace(microsecond=1)
|
||||
return utc.normalize(value.astimezone(utc)).replace(tzinfo=None).isoformat() + 'Z'
|
||||
|
||||
|
||||
def gcal_date_or_dateTime(value, check_value=None):
|
||||
"""date or dateTime to gcal (start or end dict)
|
||||
Arguments:
|
||||
value -- date or datetime value
|
||||
check_value - date or datetime to choise result type (if not None)
|
||||
|
||||
Returns:
|
||||
dict { 'date': ... } or { 'dateTime': ... }
|
||||
"""
|
||||
|
||||
if check_value is None:
|
||||
check_value = value
|
||||
|
||||
result = {}
|
||||
if isinstance(check_value, datetime.datetime):
|
||||
result['dateTime'] = format_datetime_utc(value)
|
||||
else:
|
||||
if isinstance(check_value, datetime.date):
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = datetime.date(value.year, value.month, value.day)
|
||||
result['date'] = value.isoformat()
|
||||
return result
|
||||
|
||||
|
||||
class EventConverter(Event):
|
||||
@ -32,12 +71,7 @@ class EventConverter(Event):
|
||||
utc datetime value as string in iso format
|
||||
"""
|
||||
|
||||
date = self.decoded(prop)
|
||||
if not isinstance(date, datetime.datetime):
|
||||
date = datetime.datetime(
|
||||
date.year, date.month, date.day, tzinfo=utc)
|
||||
date = date.replace(microsecond=1)
|
||||
return utc.normalize(date.astimezone(utc)).replace(tzinfo=None).isoformat() + 'Z'
|
||||
return format_datetime_utc(self.decoded(prop))
|
||||
|
||||
def _gcal_start(self):
|
||||
""" event start dict from icalendar event
|
||||
@ -49,58 +83,31 @@ class EventConverter(Event):
|
||||
dict
|
||||
"""
|
||||
|
||||
start_date = self.decoded('DTSTART')
|
||||
if isinstance(start_date, datetime.datetime):
|
||||
return {
|
||||
'dateTime': self._datetime_str_prop('DTSTART')
|
||||
}
|
||||
else:
|
||||
if isinstance(start_date, datetime.date):
|
||||
return {
|
||||
'date': start_date.isoformat()
|
||||
}
|
||||
raise ValueError('DTSTART must be date or datetime')
|
||||
value = self.decoded('DTSTART')
|
||||
return gcal_date_or_dateTime(value)
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
result = None
|
||||
if 'DTEND' in self:
|
||||
end_date = self.decoded('DTEND')
|
||||
if isinstance(end_date, datetime.datetime):
|
||||
return {
|
||||
'dateTime': self._datetime_str_prop('DTEND')
|
||||
}
|
||||
else:
|
||||
if isinstance(end_date, datetime.date):
|
||||
return {
|
||||
'date': end_date.isoformat()
|
||||
}
|
||||
raise ValueError('DTEND must be date or datetime')
|
||||
else:
|
||||
if 'DURATION' in self:
|
||||
start_date = self.decoded('DTSTART')
|
||||
value = self.decoded('DTEND')
|
||||
result = gcal_date_or_dateTime(value)
|
||||
elif 'DURATION' in self:
|
||||
start_val = self.decoded('DTSTART')
|
||||
duration = self.decoded('DURATION')
|
||||
end_date = start_date + duration
|
||||
end_val = start_val + duration
|
||||
|
||||
if isinstance(start_date, datetime.datetime):
|
||||
return {
|
||||
'dateTime': utc.normalize(end_date.astimezone(utc)).replace(tzinfo=None, microsecond=1).isoformat() + 'Z'
|
||||
}
|
||||
result = gcal_date_or_dateTime(end_val, check_value=start_val)
|
||||
else:
|
||||
if isinstance(start_date, datetime.date):
|
||||
return {
|
||||
'date': datetime.date(end_date.year, end_date.month, end_date.day).isoformat()
|
||||
}
|
||||
raise ValueError('no DTEND or DURATION')
|
||||
raise ValueError('end date/time not found')
|
||||
return result
|
||||
|
||||
def _put_to_gcal(self, gcal_event, prop, func, ics_prop=None):
|
||||
"""get property from ical event if exist, and put to gcal event
|
||||
|
@ -32,16 +32,12 @@ class CalendarSync():
|
||||
|
||||
def get_key(item): return item[key]
|
||||
|
||||
keys_src = list(map(get_key, items_src))
|
||||
keys_dst = list(map(get_key, items_dst))
|
||||
keys_src = set(map(get_key, items_src))
|
||||
keys_dst = set(map(get_key, items_dst))
|
||||
|
||||
keys_to_insert = set(keys_src) - set(keys_dst)
|
||||
keys_to_update = set(keys_src) & set(keys_dst)
|
||||
keys_to_delete = set(keys_dst) - set(keys_src)
|
||||
|
||||
def get_item(items, key_val):
|
||||
items = list(filter(lambda item: item[key] == key_val, items))
|
||||
return items[0]
|
||||
keys_to_insert = keys_src - keys_dst
|
||||
keys_to_update = keys_src & keys_dst
|
||||
keys_to_delete = keys_dst - keys_src
|
||||
|
||||
def items_by_keys(items, key_name, keys):
|
||||
return list(filter(lambda item: item[key_name] in keys, items))
|
||||
@ -49,9 +45,11 @@ class CalendarSync():
|
||||
items_to_insert = items_by_keys(items_src, key, keys_to_insert)
|
||||
items_to_delete = items_by_keys(items_dst, key, keys_to_delete)
|
||||
|
||||
items_to_update = []
|
||||
for key_val in keys_to_update:
|
||||
items_to_update.append( (get_item(items_src, key_val), get_item(items_dst, key_val)) )
|
||||
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
|
||||
|
||||
|
@ -1,42 +0,0 @@
|
||||
import hashlib
|
||||
|
||||
from pytz import UTC, timezone
|
||||
|
||||
import datetime
|
||||
|
||||
def sha1(string):
|
||||
''' Хеширование строки
|
||||
'''
|
||||
if isinstance(string, str):
|
||||
string = string.encode('utf8')
|
||||
h = hashlib.sha1()
|
||||
h.update(string)
|
||||
return h.hexdigest()
|
||||
|
||||
def genenerate(count=10):
|
||||
''' Создание тестовых событий
|
||||
'''
|
||||
msk = timezone('Europe/Moscow')
|
||||
now = UTC.localize(datetime.datetime.utcnow())
|
||||
msk_now = msk.normalize(now.astimezone(msk))
|
||||
|
||||
one_hour = datetime.datetime(1,1,1,2) - datetime.datetime(1,1,1,1)
|
||||
|
||||
start_time = msk_now - (one_hour * 3)
|
||||
for i in range(count):
|
||||
event_start = start_time + (one_hour * i)
|
||||
event_end = event_start + one_hour
|
||||
updated = UTC.normalize(event_start.astimezone(UTC)).replace(tzinfo=None)
|
||||
yield {
|
||||
'summary': 'test event __ {}'.format(i),
|
||||
'location': 'la la la {}'.format(i),
|
||||
'description': 'test TEST -- test event {}'.format(i),
|
||||
'start': {
|
||||
'dateTime': event_start.isoformat()
|
||||
},
|
||||
'end': {
|
||||
'dateTime': event_end.isoformat(),
|
||||
},
|
||||
"iCalUID": "{}@test-domain.ru".format(sha1("test - event {}".format(i))),
|
||||
"updated": updated.isoformat() + 'Z',
|
||||
"created": updated.isoformat() + 'Z'}
|
@ -50,7 +50,7 @@ class TestCalendarSync(unittest.TestCase):
|
||||
def gen_list_to_compare(start, stop):
|
||||
result = []
|
||||
for i in range(start, stop):
|
||||
result.append({'iCalUID': 'test{}'.format(i)})
|
||||
result.append({'iCalUID': 'test{:06d}'.format(i)})
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
@ -64,8 +64,11 @@ class TestCalendarSync(unittest.TestCase):
|
||||
return dateutil.parser.parse(start_date)
|
||||
|
||||
def test_compare(self):
|
||||
lst_src = TestCalendarSync.gen_list_to_compare(1, 11)
|
||||
lst_dst = TestCalendarSync.gen_list_to_compare(6, 16)
|
||||
part_len = 20
|
||||
# [1..2n]
|
||||
lst_src = TestCalendarSync.gen_list_to_compare(1, 1 + part_len * 2)
|
||||
# [n..3n]
|
||||
lst_dst = TestCalendarSync.gen_list_to_compare(1 + part_len, 1 + part_len * 3)
|
||||
|
||||
lst_src_rnd = deepcopy(lst_src)
|
||||
lst_dst_rnd = deepcopy(lst_dst)
|
||||
@ -76,16 +79,16 @@ class TestCalendarSync(unittest.TestCase):
|
||||
to_ins, to_upd, to_del = CalendarSync._events_list_compare(
|
||||
lst_src_rnd, lst_dst_rnd)
|
||||
|
||||
self.assertEqual(len(to_ins), 5)
|
||||
self.assertEqual(len(to_upd), 5)
|
||||
self.assertEqual(len(to_del), 5)
|
||||
self.assertEqual(len(to_ins), part_len)
|
||||
self.assertEqual(len(to_upd), part_len)
|
||||
self.assertEqual(len(to_del), part_len)
|
||||
|
||||
self.assertEqual(
|
||||
sorted(to_ins, key=lambda x: x['iCalUID']), lst_src[:5])
|
||||
sorted(to_ins, key=lambda x: x['iCalUID']), lst_src[:part_len])
|
||||
self.assertEqual(
|
||||
sorted(to_del, key=lambda x: x['iCalUID']), lst_dst[5:])
|
||||
sorted(to_del, key=lambda x: x['iCalUID']), lst_dst[part_len:])
|
||||
|
||||
to_upd_ok = list(zip(lst_src[5:], lst_dst[:5]))
|
||||
to_upd_ok = list(zip(lst_src[part_len:], lst_dst[:part_len]))
|
||||
self.assertEqual(len(to_upd), len(to_upd_ok))
|
||||
for item in to_upd_ok:
|
||||
self.assertIn(item, to_upd)
|
||||
|
Loading…
x
Reference in New Issue
Block a user