mirror of
https://github.com/b4tman/sync_ics2gcal
synced 2024-12-05 02:06:53 +00:00
Initial commit
This commit is contained in:
commit
3ef89e6d32
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
config.yml
|
||||||
|
service-account.json
|
||||||
|
*.pyc
|
||||||
|
my-test*.ics
|
||||||
|
.vscode/*
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017 Dmitry Belyaev
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# sync_ics2gcal
|
||||||
|
|
||||||
|
Python script for sync .ics file with Google calendar
|
14
gcal_sync/__init__.py
Normal file
14
gcal_sync/__init__.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
from .ical import (
|
||||||
|
CalendarConverter,
|
||||||
|
EventConverter
|
||||||
|
)
|
||||||
|
|
||||||
|
from .gcal import (
|
||||||
|
GoogleCalendarService,
|
||||||
|
GoogleCalendar
|
||||||
|
)
|
||||||
|
|
||||||
|
from .sync import (
|
||||||
|
CalendarSync
|
||||||
|
)
|
215
gcal_sync/gcal.py
Normal file
215
gcal_sync/gcal.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
from apiclient import discovery
|
||||||
|
import httplib2
|
||||||
|
import logging
|
||||||
|
from oauth2client import service_account
|
||||||
|
from pytz import utc
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleCalendarService():
|
||||||
|
@staticmethod
|
||||||
|
def from_srv_acc_file(service_account_file):
|
||||||
|
scopes = 'https://www.googleapis.com/auth/calendar'
|
||||||
|
credentials = service_account.ServiceAccountCredentials.from_json_keyfile_name(
|
||||||
|
service_account_file, scopes=scopes)
|
||||||
|
http = credentials.authorize(httplib2.Http())
|
||||||
|
service = discovery.build('calendar', 'v3', http=http)
|
||||||
|
return service
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleCalendar():
|
||||||
|
logger = logging.getLogger('GoogleCalendar')
|
||||||
|
|
||||||
|
def __init__(self, service, calendarId):
|
||||||
|
self.service = service
|
||||||
|
self.calendarId = calendarId
|
||||||
|
|
||||||
|
def list_events_from(self, start):
|
||||||
|
''' Получение списка событий из GCAL начиная с даты start
|
||||||
|
'''
|
||||||
|
events = []
|
||||||
|
page_token = None
|
||||||
|
timeMin = utc.normalize(start.astimezone(utc)).replace(
|
||||||
|
tzinfo=None).isoformat() + 'Z'
|
||||||
|
while True:
|
||||||
|
response = self.service.events().list(calendarId=self.calendarId, pageToken=page_token,
|
||||||
|
singleEvents=True, timeMin=timeMin, fields='id,iCalUID,updated').execute()
|
||||||
|
if 'items' in response:
|
||||||
|
events.extend(response['items'])
|
||||||
|
page_token = response.get('nextPageToken')
|
||||||
|
if not page_token:
|
||||||
|
break
|
||||||
|
self.logger.info('%d events listed', len(events))
|
||||||
|
return events
|
||||||
|
|
||||||
|
def find_exists(self, events):
|
||||||
|
""" Поиск уже существующих в GCAL событий, из списка событий к вставке
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
events {list} -- list of events
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple -- (events_exist, events_not_found)
|
||||||
|
events_exist - list of tuples: (new_event, exists_event)
|
||||||
|
"""
|
||||||
|
|
||||||
|
events_by_req = []
|
||||||
|
exists = []
|
||||||
|
not_found = []
|
||||||
|
|
||||||
|
def list_callback(request_id, response, exception):
|
||||||
|
found = False
|
||||||
|
event = events_by_req[int(request_id)]
|
||||||
|
if exception is None:
|
||||||
|
found = ([] != response['items'])
|
||||||
|
else:
|
||||||
|
self.logger.error('exception %s, while listing event with UID: %s', str(
|
||||||
|
exception), event['iCalUID'])
|
||||||
|
if found:
|
||||||
|
exists.append(
|
||||||
|
(event, response['items'][0]))
|
||||||
|
else:
|
||||||
|
not_found.append(events_by_req[int(request_id)])
|
||||||
|
|
||||||
|
batch = self.service.new_batch_http_request(callback=list_callback)
|
||||||
|
i = 0
|
||||||
|
for event in events:
|
||||||
|
events_by_req.append(event)
|
||||||
|
batch.add(self.service.events().list(calendarId=self.calendarId,
|
||||||
|
iCalUID=event['iCalUID'], showDeleted=True, fields='id,iCalUID,updated'), request_id=str(i))
|
||||||
|
i += 1
|
||||||
|
batch.execute()
|
||||||
|
self.logger.info('%d events exists, %d not found',
|
||||||
|
len(exists), len(not_found))
|
||||||
|
return exists, not_found
|
||||||
|
|
||||||
|
def insert_events(self, events):
|
||||||
|
""" Вставка событий в GCAL
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
events -- список событий
|
||||||
|
"""
|
||||||
|
|
||||||
|
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'))
|
||||||
|
|
||||||
|
batch = self.service.new_batch_http_request(callback=insert_callback)
|
||||||
|
i = 0
|
||||||
|
for event in events:
|
||||||
|
events_by_req.append(event)
|
||||||
|
batch.add(self.service.events().insert(
|
||||||
|
calendarId=self.calendarId, body=event, fields='id,iCalUID,updated'), request_id=str(i))
|
||||||
|
i += 1
|
||||||
|
batch.execute()
|
||||||
|
|
||||||
|
def patch_events(self, event_tuples):
|
||||||
|
""" Обновление (патч) событий в GCAL
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
calendarId -- ИД календаря
|
||||||
|
event_tuples -- список кортежей событий (новое, старое)
|
||||||
|
"""
|
||||||
|
|
||||||
|
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'))
|
||||||
|
|
||||||
|
batch = self.service.new_batch_http_request(callback=patch_callback)
|
||||||
|
i = 0
|
||||||
|
for event_new, event_old in event_tuples:
|
||||||
|
if 'id' not in event_old:
|
||||||
|
continue
|
||||||
|
events_by_req.append(event_new)
|
||||||
|
batch.add(self.service.events().patch(
|
||||||
|
calendarId=self.calendarId, eventId=event_old['id'], body=event_new), fields='id,iCalUID,updated', request_id=str(i))
|
||||||
|
i += 1
|
||||||
|
batch.execute()
|
||||||
|
|
||||||
|
def update_events(self, event_tuples):
|
||||||
|
""" Обновление событий в GCAL
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
event_tuples -- список кортежей событий (новое, старое)
|
||||||
|
"""
|
||||||
|
|
||||||
|
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'))
|
||||||
|
|
||||||
|
batch = self.service.new_batch_http_request(callback=update_callback)
|
||||||
|
i = 0
|
||||||
|
for event_new, event_old in event_tuples:
|
||||||
|
if 'id' not in event_old:
|
||||||
|
continue
|
||||||
|
events_by_req.append(event_new)
|
||||||
|
batch.add(self.service.events().update(
|
||||||
|
calendarId=self.calendarId, eventId=event_old['id'], body=event_new, fields='id,iCalUID,updated'), request_id=str(i))
|
||||||
|
i += 1
|
||||||
|
batch.execute()
|
||||||
|
|
||||||
|
def delete_events(self, events):
|
||||||
|
""" Удаление событий в GCAL
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
events -- список событий
|
||||||
|
"""
|
||||||
|
|
||||||
|
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'))
|
||||||
|
|
||||||
|
batch = self.service.new_batch_http_request(callback=delete_callback)
|
||||||
|
i = 0
|
||||||
|
for event in events:
|
||||||
|
events_by_req.append(event)
|
||||||
|
batch.add(self.service.events().delete(
|
||||||
|
calendarId=self.calendarId, eventId=event['id']), request_id=str(i))
|
||||||
|
i += 1
|
||||||
|
batch.execute()
|
||||||
|
|
||||||
|
def make_public(self):
|
||||||
|
rule_public = {
|
||||||
|
'scope': {
|
||||||
|
'type': 'default',
|
||||||
|
},
|
||||||
|
'role': 'reader'
|
||||||
|
}
|
||||||
|
return self.service.acl().insert(calendarId=self.calendarId, body=rule_public).execute()
|
||||||
|
|
||||||
|
def add_owner(self, email):
|
||||||
|
rule_owner = {
|
||||||
|
'scope': {
|
||||||
|
'type': 'user',
|
||||||
|
'value': email,
|
||||||
|
},
|
||||||
|
'role': 'owner'
|
||||||
|
}
|
||||||
|
return self.service.acl().insert(calendarId=self.calendarId, body=rule_owner).execute()
|
126
gcal_sync/ical.py
Normal file
126
gcal_sync/ical.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
from icalendar import Calendar, Event
|
||||||
|
import logging
|
||||||
|
from pytz import utc
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class EventConverter(Event):
|
||||||
|
"""Convert icalendar event to google calendar resource
|
||||||
|
( https://developers.google.com/calendar/v3/reference/events#resource-representations )
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _str_prop(self, prop):
|
||||||
|
return self.decoded(prop).decode(encoding='utf-8')
|
||||||
|
|
||||||
|
def _datetime_str_prop(self, prop):
|
||||||
|
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'
|
||||||
|
|
||||||
|
def _gcal_start(self):
|
||||||
|
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')
|
||||||
|
|
||||||
|
def _gcal_end(self):
|
||||||
|
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')
|
||||||
|
duration = self.decoded('DURATION')
|
||||||
|
end_date = start_date + duration
|
||||||
|
|
||||||
|
if isinstance(start_date, datetime.datetime):
|
||||||
|
return {
|
||||||
|
'dateTime': utc.normalize(end_date.astimezone(utc)).replace(tzinfo=None, microsecond=1).isoformat() + 'Z'
|
||||||
|
}
|
||||||
|
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')
|
||||||
|
|
||||||
|
def _put_to_gcal(self, gcal_event, prop, func, ics_prop=None):
|
||||||
|
if not ics_prop:
|
||||||
|
ics_prop = prop
|
||||||
|
if ics_prop in self:
|
||||||
|
gcal_event[prop] = func(ics_prop)
|
||||||
|
|
||||||
|
def to_gcal(self):
|
||||||
|
"""Convert
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict - google calendar#event resource
|
||||||
|
"""
|
||||||
|
|
||||||
|
event = {
|
||||||
|
'iCalUID': self._str_prop('UID')
|
||||||
|
}
|
||||||
|
|
||||||
|
event['start'] = self._gcal_start()
|
||||||
|
event['end'] = self._gcal_end()
|
||||||
|
|
||||||
|
self._put_to_gcal(event, 'summary', self._str_prop)
|
||||||
|
self._put_to_gcal(event, 'description', self._str_prop)
|
||||||
|
self._put_to_gcal(event, 'location', self._str_prop)
|
||||||
|
self._put_to_gcal(event, 'created', self._datetime_str_prop)
|
||||||
|
self._put_to_gcal(
|
||||||
|
event, 'updated', self._datetime_str_prop, 'LAST-MODIFIED')
|
||||||
|
self._put_to_gcal(
|
||||||
|
event, 'transparency', lambda prop: self._str_prop(prop).lower(), 'TRANSP')
|
||||||
|
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarConverter():
|
||||||
|
"""Convert icalendar events to google calendar resources
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger = logging.getLogger('CalendarConverter')
|
||||||
|
|
||||||
|
def __init__(self, calendar=None):
|
||||||
|
self.calendar = calendar
|
||||||
|
|
||||||
|
def load(self, filename):
|
||||||
|
""" load calendar from ics file
|
||||||
|
"""
|
||||||
|
with open(filename, 'r', encoding='utf-8') as f:
|
||||||
|
self.calendar = Calendar.from_ical(f.read())
|
||||||
|
self.logger.info('%s loaded', filename)
|
||||||
|
|
||||||
|
def events_to_gcal(self):
|
||||||
|
"""Convert events to google calendar resources
|
||||||
|
"""
|
||||||
|
|
||||||
|
ics_events = self.calendar.walk(name='VEVENT')
|
||||||
|
self.logger.info('%d events readed', len(ics_events))
|
||||||
|
|
||||||
|
result = list(
|
||||||
|
map(lambda event: EventConverter(event).to_gcal(), ics_events))
|
||||||
|
self.logger.info('%d events converted', len(result))
|
||||||
|
return result
|
124
gcal_sync/sync.py
Normal file
124
gcal_sync/sync.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import dateutil.parser
|
||||||
|
import logging
|
||||||
|
import operator
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarSync():
|
||||||
|
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'):
|
||||||
|
""" 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]
|
||||||
|
|
||||||
|
keys_src = list(map(get_key, items_src))
|
||||||
|
keys_dst = list(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 items_by_keys(items, key_name, keys):
|
||||||
|
return list(filter(lambda item: item[key_name] in keys, items))
|
||||||
|
|
||||||
|
items_to_insert = items_by_keys(items_src, key, keys_to_insert)
|
||||||
|
items_to_update = list(zip(items_by_keys(
|
||||||
|
items_src, key, keys_to_update), items_by_keys(items_dst, key, keys_to_update)))
|
||||||
|
items_to_delete = items_by_keys(items_dst, key, keys_to_delete)
|
||||||
|
|
||||||
|
return items_to_insert, items_to_update, items_to_delete
|
||||||
|
|
||||||
|
def _filter_events_to_update(self):
|
||||||
|
""" Отбор событий к обновлению, по дате обновления
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
events -- список кортежей к обновлению (новое, старое)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
список кортежей к обновлению (новое, старое)
|
||||||
|
"""
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
def _filter_events_by_date(self, events, date, op):
|
||||||
|
""" Отбор событий по дате обновления
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
events -- список событий к обновлению
|
||||||
|
date {datetime} -- дата для сравнения
|
||||||
|
op {operator} -- оператор сравнения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
список событий
|
||||||
|
"""
|
||||||
|
|
||||||
|
def filter_by_date(event):
|
||||||
|
return op(dateutil.parser.parse(event['updated']), date)
|
||||||
|
|
||||||
|
return list(filter(filter_by_date, events))
|
||||||
|
|
||||||
|
def prepare_sync(self, 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(
|
||||||
|
events_src, start_date, operator.ge)
|
||||||
|
events_src_past = self._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(
|
||||||
|
events_src_pending, events_dst)
|
||||||
|
|
||||||
|
events_src_pending, events_dst = None, None
|
||||||
|
|
||||||
|
# сравнение списка на удаление со списком прошлых событий, для определения доп событий к обновлению
|
||||||
|
_, add_to_update, self.to_delete = self._events_list_compare(
|
||||||
|
events_src_past, self.to_delete)
|
||||||
|
self.to_update.extend(add_to_update)
|
||||||
|
|
||||||
|
events_src_past = None
|
||||||
|
|
||||||
|
# проверка списка к вставке и перемещение доп. элементов в список к обновлению
|
||||||
|
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')
|
||||||
|
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):
|
||||||
|
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 = [], [], []
|
42
gcal_sync/test-events.py
Normal file
42
gcal_sync/test-events.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
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'}
|
7
sample-config.yml
Normal file
7
sample-config.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
#start_from: 2018-04-03T13:23:25.000001Z
|
||||||
|
start_from: now
|
||||||
|
service_account: service-account.json
|
||||||
|
calendar:
|
||||||
|
google_id: google-calendar-id@group.calendar.google.com
|
||||||
|
source: my-test.ics
|
53
sync-calendar.py
Normal file
53
sync-calendar.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import yaml
|
||||||
|
|
||||||
|
import dateutil.parser
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
from gcal_sync import (
|
||||||
|
CalendarConverter,
|
||||||
|
GoogleCalendarService,
|
||||||
|
GoogleCalendar,
|
||||||
|
CalendarSync
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
with open('config.yml', 'r', encoding='utf-8') as f:
|
||||||
|
result = yaml.load(f)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_start_date(date_str):
|
||||||
|
result = datetime.datetime(1,1,1)
|
||||||
|
if 'now' == date_str:
|
||||||
|
result = datetime.datetime.utcnow()
|
||||||
|
else:
|
||||||
|
result = dateutil.parser.parse(date_str)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config = load_config()
|
||||||
|
|
||||||
|
if 'logging' in config:
|
||||||
|
logging.config.dictConfig(config['logging'])
|
||||||
|
|
||||||
|
calendarId = config['calendar']['google_id']
|
||||||
|
ics_filepath = config['calendar']['source']
|
||||||
|
srv_acc_file = config['service_account']
|
||||||
|
|
||||||
|
start = get_start_date(config['start_from'])
|
||||||
|
|
||||||
|
converter = CalendarConverter()
|
||||||
|
converter.load(ics_filepath)
|
||||||
|
|
||||||
|
service = GoogleCalendarService.from_srv_acc_file(srv_acc_file)
|
||||||
|
gcalendar = GoogleCalendar(service, calendarId)
|
||||||
|
|
||||||
|
sync = CalendarSync(gcalendar, converter)
|
||||||
|
sync.prepare_sync(start)
|
||||||
|
sync.apply()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Loading…
Reference in New Issue
Block a user