mirror of
https://github.com/b4tman/sync_ics2gcal
synced 2025-01-21 07:28:24 +00:00
Feature: setup (#15)
* add files for setup * ! package rename * move scripts * + setuptools_scm_git_archive * + fallback_version * + setuptools_scm_git_archive to setup.cfg * bdist_wheel universal * ignore build/ and .eggs/ * don't use version from setuptools_scm * Revert "don't use version from setuptools_scm" This reverts commit 7ad0b4d3d856e4f4d23ddb24209bfea6a2ac3f6d. * Revert "bdist_wheel universal" This reverts commit 5027866b3904f5765a1a0681c987f6b1f0431edb. * no-local-version * +workflow: Upload Python Package
This commit is contained in:
parent
fcba8f07ef
commit
a96050628a
1
.git_archival.txt
Normal file
1
.git_archival.txt
Normal file
@ -0,0 +1 @@
|
||||
ref-names: $Format:%D$
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
.git_archival.txt export-subst
|
26
.github/workflows/pythonpublish.yml
vendored
Normal file
26
.github/workflows/pythonpublish.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install setuptools setuptools_scm setuptools_scm_git_archive wheel twine
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.pypi_token }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,3 +3,7 @@ service-account.json
|
||||
*.pyc
|
||||
my-test*.ics
|
||||
.vscode/*
|
||||
/dist/
|
||||
/*.egg-info/
|
||||
/build/
|
||||
/.eggs/
|
||||
|
7
MANIFEST.in
Normal file
7
MANIFEST.in
Normal file
@ -0,0 +1,7 @@
|
||||
include pyproject.toml
|
||||
|
||||
# Include the README
|
||||
include *.md
|
||||
|
||||
# Include the license file
|
||||
include LICENSE
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=40.8.0", "wheel", "setuptools_scm"]
|
||||
build-backend = "setuptools.build_meta"
|
7
setup.cfg
Normal file
7
setup.cfg
Normal file
@ -0,0 +1,7 @@
|
||||
[metadata]
|
||||
license_files = LICENSE
|
||||
|
||||
[options]
|
||||
setup_requires =
|
||||
setuptools_scm
|
||||
setuptools_scm_git_archive
|
44
setup.py
Normal file
44
setup.py
Normal file
@ -0,0 +1,44 @@
|
||||
import setuptools
|
||||
|
||||
with open('README.md', 'r') as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setuptools.setup(
|
||||
name='sync-ics2gcal',
|
||||
author='Dmitry Belyaev',
|
||||
author_email='b4tm4n@mail.ru',
|
||||
license='MIT',
|
||||
description='Sync ics file with Google calendar',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
url='https://github.com/b4tman/sync_ics2gcal',
|
||||
use_scm_version={
|
||||
'fallback_version': '0.1',
|
||||
'local_scheme': 'no-local-version'
|
||||
},
|
||||
setup_requires=['setuptools_scm', 'setuptools_scm_git_archive'],
|
||||
packages=setuptools.find_packages(exclude=['tests']),
|
||||
classifiers=[
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
],
|
||||
python_requires='>=3.5',
|
||||
install_requires = [
|
||||
'google-auth>=1.5.0',
|
||||
'google-api-python-client>=1.7.0',
|
||||
'icalendar>=4.0.1',
|
||||
'pytz',
|
||||
'PyYAML>=3.13'
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"sync-ics2gcal = sync_ics2gcal.sync_calendar:main",
|
||||
"manage-ics2gcal = sync_ics2gcal.manage_calendars:main",
|
||||
]
|
||||
}
|
||||
)
|
@ -1,14 +1,14 @@
|
||||
|
||||
from .ical import (
|
||||
CalendarConverter,
|
||||
EventConverter
|
||||
)
|
||||
|
||||
from .gcal import (
|
||||
GoogleCalendarService,
|
||||
GoogleCalendar
|
||||
)
|
||||
|
||||
from .sync import (
|
||||
CalendarSync
|
||||
)
|
||||
|
||||
from .ical import (
|
||||
CalendarConverter,
|
||||
EventConverter
|
||||
)
|
||||
|
||||
from .gcal import (
|
||||
GoogleCalendarService,
|
||||
GoogleCalendar
|
||||
)
|
||||
|
||||
from .sync import (
|
||||
CalendarSync
|
||||
)
|
@ -1,283 +1,283 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import google.auth
|
||||
from google.oauth2 import service_account
|
||||
from googleapiclient import discovery
|
||||
from pytz import utc
|
||||
|
||||
|
||||
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.Credentials.from_service_account_file(service_account_file)
|
||||
scoped_credentials = credentials.with_scopes(scopes)
|
||||
service = discovery.build('calendar', 'v3', credentials=scoped_credentials)
|
||||
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
|
||||
"""
|
||||
|
||||
logger = logging.getLogger('GoogleCalendar')
|
||||
|
||||
def __init__(self, service, calendarId):
|
||||
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
|
||||
"""
|
||||
fields = 'nextPageToken,items(id,iCalUID,updated)'
|
||||
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=fields).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):
|
||||
""" find existing events from list, by 'iCalUID' field
|
||||
|
||||
Arguments:
|
||||
events {list} -- list of events
|
||||
|
||||
Returns:
|
||||
tuple -- (events_exist, events_not_found)
|
||||
events_exist - list of tuples: (new_event, exists_event)
|
||||
"""
|
||||
|
||||
fields = 'items(id,iCalUID,updated)'
|
||||
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=fields), 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):
|
||||
""" insert list of events
|
||||
|
||||
Arguments:
|
||||
events - events list
|
||||
"""
|
||||
|
||||
fields = 'id'
|
||||
events_by_req = []
|
||||
|
||||
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:
|
||||
events_by_req.append(event)
|
||||
batch.add(self.service.events().insert(
|
||||
calendarId=self.calendarId, body=event, fields=fields), request_id=str(i))
|
||||
i += 1
|
||||
batch.execute()
|
||||
|
||||
def patch_events(self, event_tuples):
|
||||
""" patch (update) events
|
||||
|
||||
Arguments:
|
||||
event_tuples -- list of tuples: (new_event, exists_event)
|
||||
"""
|
||||
|
||||
fields = 'id'
|
||||
events_by_req = []
|
||||
|
||||
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:
|
||||
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=fields, request_id=str(i))
|
||||
i += 1
|
||||
batch.execute()
|
||||
|
||||
def update_events(self, event_tuples):
|
||||
""" update events
|
||||
|
||||
Arguments:
|
||||
event_tuples -- list of tuples: (new_event, exists_event)
|
||||
"""
|
||||
|
||||
fields = 'id'
|
||||
events_by_req = []
|
||||
|
||||
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:
|
||||
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=fields), request_id=str(i))
|
||||
i += 1
|
||||
batch.execute()
|
||||
|
||||
def delete_events(self, events):
|
||||
""" delete events
|
||||
|
||||
Arguments:
|
||||
events -- list of events
|
||||
"""
|
||||
|
||||
events_by_req = []
|
||||
|
||||
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:
|
||||
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 create(self, summary, timeZone=None):
|
||||
"""create calendar
|
||||
|
||||
Arguments:
|
||||
summary -- new calendar summary
|
||||
|
||||
Keyword Arguments:
|
||||
timeZone -- new calendar timezone as string (optional)
|
||||
|
||||
Returns:
|
||||
calendar Resource
|
||||
"""
|
||||
|
||||
calendar = {'summary': summary}
|
||||
if timeZone is not None:
|
||||
calendar['timeZone'] = timeZone
|
||||
|
||||
created_calendar = self.service.calendars().insert(body=calendar).execute()
|
||||
self.calendarId = created_calendar['id']
|
||||
return created_calendar
|
||||
|
||||
def delete(self):
|
||||
"""delete calendar
|
||||
"""
|
||||
|
||||
self.service.calendars().delete(calendarId=self.calendarId).execute()
|
||||
|
||||
def make_public(self):
|
||||
"""make calendar puplic
|
||||
"""
|
||||
|
||||
rule_public = {
|
||||
'scope': {
|
||||
'type': 'default',
|
||||
},
|
||||
'role': 'reader'
|
||||
}
|
||||
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',
|
||||
'value': email,
|
||||
},
|
||||
'role': 'owner'
|
||||
}
|
||||
return self.service.acl().insert(calendarId=self.calendarId, body=rule_owner).execute()
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import google.auth
|
||||
from google.oauth2 import service_account
|
||||
from googleapiclient import discovery
|
||||
from pytz import utc
|
||||
|
||||
|
||||
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.Credentials.from_service_account_file(service_account_file)
|
||||
scoped_credentials = credentials.with_scopes(scopes)
|
||||
service = discovery.build('calendar', 'v3', credentials=scoped_credentials)
|
||||
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
|
||||
"""
|
||||
|
||||
logger = logging.getLogger('GoogleCalendar')
|
||||
|
||||
def __init__(self, service, calendarId):
|
||||
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
|
||||
"""
|
||||
fields = 'nextPageToken,items(id,iCalUID,updated)'
|
||||
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=fields).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):
|
||||
""" find existing events from list, by 'iCalUID' field
|
||||
|
||||
Arguments:
|
||||
events {list} -- list of events
|
||||
|
||||
Returns:
|
||||
tuple -- (events_exist, events_not_found)
|
||||
events_exist - list of tuples: (new_event, exists_event)
|
||||
"""
|
||||
|
||||
fields = 'items(id,iCalUID,updated)'
|
||||
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=fields), 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):
|
||||
""" insert list of events
|
||||
|
||||
Arguments:
|
||||
events - events list
|
||||
"""
|
||||
|
||||
fields = 'id'
|
||||
events_by_req = []
|
||||
|
||||
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:
|
||||
events_by_req.append(event)
|
||||
batch.add(self.service.events().insert(
|
||||
calendarId=self.calendarId, body=event, fields=fields), request_id=str(i))
|
||||
i += 1
|
||||
batch.execute()
|
||||
|
||||
def patch_events(self, event_tuples):
|
||||
""" patch (update) events
|
||||
|
||||
Arguments:
|
||||
event_tuples -- list of tuples: (new_event, exists_event)
|
||||
"""
|
||||
|
||||
fields = 'id'
|
||||
events_by_req = []
|
||||
|
||||
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:
|
||||
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=fields, request_id=str(i))
|
||||
i += 1
|
||||
batch.execute()
|
||||
|
||||
def update_events(self, event_tuples):
|
||||
""" update events
|
||||
|
||||
Arguments:
|
||||
event_tuples -- list of tuples: (new_event, exists_event)
|
||||
"""
|
||||
|
||||
fields = 'id'
|
||||
events_by_req = []
|
||||
|
||||
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:
|
||||
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=fields), request_id=str(i))
|
||||
i += 1
|
||||
batch.execute()
|
||||
|
||||
def delete_events(self, events):
|
||||
""" delete events
|
||||
|
||||
Arguments:
|
||||
events -- list of events
|
||||
"""
|
||||
|
||||
events_by_req = []
|
||||
|
||||
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:
|
||||
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 create(self, summary, timeZone=None):
|
||||
"""create calendar
|
||||
|
||||
Arguments:
|
||||
summary -- new calendar summary
|
||||
|
||||
Keyword Arguments:
|
||||
timeZone -- new calendar timezone as string (optional)
|
||||
|
||||
Returns:
|
||||
calendar Resource
|
||||
"""
|
||||
|
||||
calendar = {'summary': summary}
|
||||
if timeZone is not None:
|
||||
calendar['timeZone'] = timeZone
|
||||
|
||||
created_calendar = self.service.calendars().insert(body=calendar).execute()
|
||||
self.calendarId = created_calendar['id']
|
||||
return created_calendar
|
||||
|
||||
def delete(self):
|
||||
"""delete calendar
|
||||
"""
|
||||
|
||||
self.service.calendars().delete(calendarId=self.calendarId).execute()
|
||||
|
||||
def make_public(self):
|
||||
"""make calendar puplic
|
||||
"""
|
||||
|
||||
rule_public = {
|
||||
'scope': {
|
||||
'type': 'default',
|
||||
},
|
||||
'role': 'reader'
|
||||
}
|
||||
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',
|
||||
'value': email,
|
||||
},
|
||||
'role': 'owner'
|
||||
}
|
||||
return self.service.acl().insert(calendarId=self.calendarId, body=rule_owner).execute()
|
@ -1,184 +1,184 @@
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from icalendar import Calendar, Event
|
||||
from pytz import utc
|
||||
|
||||
|
||||
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):
|
||||
"""Convert icalendar event to google calendar resource
|
||||
( https://developers.google.com/calendar/v3/reference/events#resource-representations )
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
return format_datetime_utc(self.decoded(prop))
|
||||
|
||||
def _gcal_start(self):
|
||||
""" event start dict from icalendar event
|
||||
|
||||
Raises:
|
||||
ValueError -- if DTSTART not date or datetime
|
||||
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
|
||||
value = self.decoded('DTSTART')
|
||||
return gcal_date_or_dateTime(value)
|
||||
|
||||
def _gcal_end(self):
|
||||
"""event end dict from icalendar event
|
||||
|
||||
Raises:
|
||||
ValueError -- if no DTEND or DURATION
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
|
||||
result = None
|
||||
if 'DTEND' in self:
|
||||
value = self.decoded('DTEND')
|
||||
result = gcal_date_or_dateTime(value)
|
||||
elif 'DURATION' in self:
|
||||
start_val = self.decoded('DTSTART')
|
||||
duration = self.decoded('DURATION')
|
||||
end_val = start_val + duration
|
||||
|
||||
result = gcal_date_or_dateTime(end_val, check_value=start_val)
|
||||
else:
|
||||
raise ValueError('no DTEND or DURATION')
|
||||
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
|
||||
|
||||
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:
|
||||
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 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
|
||||
"""
|
||||
|
||||
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
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from icalendar import Calendar, Event
|
||||
from pytz import utc
|
||||
|
||||
|
||||
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):
|
||||
"""Convert icalendar event to google calendar resource
|
||||
( https://developers.google.com/calendar/v3/reference/events#resource-representations )
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
return format_datetime_utc(self.decoded(prop))
|
||||
|
||||
def _gcal_start(self):
|
||||
""" event start dict from icalendar event
|
||||
|
||||
Raises:
|
||||
ValueError -- if DTSTART not date or datetime
|
||||
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
|
||||
value = self.decoded('DTSTART')
|
||||
return gcal_date_or_dateTime(value)
|
||||
|
||||
def _gcal_end(self):
|
||||
"""event end dict from icalendar event
|
||||
|
||||
Raises:
|
||||
ValueError -- if no DTEND or DURATION
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
|
||||
result = None
|
||||
if 'DTEND' in self:
|
||||
value = self.decoded('DTEND')
|
||||
result = gcal_date_or_dateTime(value)
|
||||
elif 'DURATION' in self:
|
||||
start_val = self.decoded('DTSTART')
|
||||
duration = self.decoded('DURATION')
|
||||
end_val = start_val + duration
|
||||
|
||||
result = gcal_date_or_dateTime(end_val, check_value=start_val)
|
||||
else:
|
||||
raise ValueError('no DTEND or DURATION')
|
||||
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
|
||||
|
||||
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:
|
||||
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 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
|
||||
"""
|
||||
|
||||
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
|
@ -1,104 +1,104 @@
|
||||
import argparse
|
||||
import datetime
|
||||
import logging.config
|
||||
|
||||
import yaml
|
||||
from pytz import utc
|
||||
|
||||
from gcal_sync import GoogleCalendar, GoogleCalendarService
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="manage google calendars in service account")
|
||||
command_subparsers = parser.add_subparsers(help='command', dest='command')
|
||||
command_subparsers.add_parser('list', help='list calendars')
|
||||
parser_create = command_subparsers.add_parser(
|
||||
'create', help='create calendar')
|
||||
parser_create.add_argument(
|
||||
'summary', action='store', help='new calendar summary')
|
||||
parser_create.add_argument('--timezone', action='store',
|
||||
default=None, required=False, help='new calendar timezone')
|
||||
parser_create.add_argument(
|
||||
'--public', default=False, action='store_true', help='make calendar public')
|
||||
parser_add_owner = command_subparsers.add_parser(
|
||||
'add_owner', help='add owner to calendar')
|
||||
parser_add_owner.add_argument('id', action='store', help='calendar id')
|
||||
parser_add_owner.add_argument(
|
||||
'owner_email', action='store', help='new owner email')
|
||||
parser_remove = command_subparsers.add_parser(
|
||||
'remove', help='remove calendar')
|
||||
parser_remove.add_argument(
|
||||
'id', action='store', help='calendar id to remove')
|
||||
parser_rename = command_subparsers.add_parser(
|
||||
'rename', help='rename calendar')
|
||||
parser_rename.add_argument(
|
||||
'id', action='store', help='calendar id')
|
||||
parser_rename.add_argument(
|
||||
'summary', action='store', help='new summary')
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.command is None:
|
||||
parser.print_usage()
|
||||
return args
|
||||
|
||||
|
||||
def load_config():
|
||||
with open('config.yml', 'r', encoding='utf-8') as f:
|
||||
result = yaml.safe_load(f)
|
||||
return result
|
||||
|
||||
|
||||
def list_calendars(service):
|
||||
response = service.calendarList().list(fields='items(id,summary)').execute()
|
||||
for calendar in response.get('items'):
|
||||
print('{summary}: {id}'.format_map(calendar))
|
||||
|
||||
|
||||
def create_calendar(service, summary, timezone, public):
|
||||
calendar = GoogleCalendar(service, None)
|
||||
calendar.create(summary, timezone)
|
||||
if public:
|
||||
calendar.make_public()
|
||||
print('{}: {}'.format(summary, calendar.calendarId))
|
||||
|
||||
|
||||
def add_owner(service, id, owner_email):
|
||||
calendar = GoogleCalendar(service, id)
|
||||
calendar.add_owner(owner_email)
|
||||
print('to {} added owner: {}'.format(id, owner_email))
|
||||
|
||||
|
||||
def remove_calendar(service, id):
|
||||
calendar = GoogleCalendar(service, id)
|
||||
calendar.delete()
|
||||
print('removed: {}'.format(id))
|
||||
|
||||
def rename_calendar(service, id, summary):
|
||||
calendar = {'summary': summary}
|
||||
service.calendars().patch(body=calendar, calendarId=id).execute()
|
||||
print('{}: {}'.format(summary, id))
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
config = load_config()
|
||||
|
||||
if 'logging' in config:
|
||||
logging.config.dictConfig(config['logging'])
|
||||
|
||||
srv_acc_file = config['service_account']
|
||||
service = GoogleCalendarService.from_srv_acc_file(srv_acc_file)
|
||||
|
||||
if 'list' == args.command:
|
||||
list_calendars(service)
|
||||
elif 'create' == args.command:
|
||||
create_calendar(service, args.summary, args.timezone, args.public)
|
||||
elif 'add_owner' == args.command:
|
||||
add_owner(service, args.id, args.owner_email)
|
||||
elif 'remove' == args.command:
|
||||
remove_calendar(service, args.id)
|
||||
elif 'rename' == args.command:
|
||||
rename_calendar(service, args.id, args.summary)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
import argparse
|
||||
import datetime
|
||||
import logging.config
|
||||
|
||||
import yaml
|
||||
from pytz import utc
|
||||
|
||||
from . import GoogleCalendar, GoogleCalendarService
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="manage google calendars in service account")
|
||||
command_subparsers = parser.add_subparsers(help='command', dest='command')
|
||||
command_subparsers.add_parser('list', help='list calendars')
|
||||
parser_create = command_subparsers.add_parser(
|
||||
'create', help='create calendar')
|
||||
parser_create.add_argument(
|
||||
'summary', action='store', help='new calendar summary')
|
||||
parser_create.add_argument('--timezone', action='store',
|
||||
default=None, required=False, help='new calendar timezone')
|
||||
parser_create.add_argument(
|
||||
'--public', default=False, action='store_true', help='make calendar public')
|
||||
parser_add_owner = command_subparsers.add_parser(
|
||||
'add_owner', help='add owner to calendar')
|
||||
parser_add_owner.add_argument('id', action='store', help='calendar id')
|
||||
parser_add_owner.add_argument(
|
||||
'owner_email', action='store', help='new owner email')
|
||||
parser_remove = command_subparsers.add_parser(
|
||||
'remove', help='remove calendar')
|
||||
parser_remove.add_argument(
|
||||
'id', action='store', help='calendar id to remove')
|
||||
parser_rename = command_subparsers.add_parser(
|
||||
'rename', help='rename calendar')
|
||||
parser_rename.add_argument(
|
||||
'id', action='store', help='calendar id')
|
||||
parser_rename.add_argument(
|
||||
'summary', action='store', help='new summary')
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.command is None:
|
||||
parser.print_usage()
|
||||
return args
|
||||
|
||||
|
||||
def load_config():
|
||||
with open('config.yml', 'r', encoding='utf-8') as f:
|
||||
result = yaml.safe_load(f)
|
||||
return result
|
||||
|
||||
|
||||
def list_calendars(service):
|
||||
response = service.calendarList().list(fields='items(id,summary)').execute()
|
||||
for calendar in response.get('items'):
|
||||
print('{summary}: {id}'.format_map(calendar))
|
||||
|
||||
|
||||
def create_calendar(service, summary, timezone, public):
|
||||
calendar = GoogleCalendar(service, None)
|
||||
calendar.create(summary, timezone)
|
||||
if public:
|
||||
calendar.make_public()
|
||||
print('{}: {}'.format(summary, calendar.calendarId))
|
||||
|
||||
|
||||
def add_owner(service, id, owner_email):
|
||||
calendar = GoogleCalendar(service, id)
|
||||
calendar.add_owner(owner_email)
|
||||
print('to {} added owner: {}'.format(id, owner_email))
|
||||
|
||||
|
||||
def remove_calendar(service, id):
|
||||
calendar = GoogleCalendar(service, id)
|
||||
calendar.delete()
|
||||
print('removed: {}'.format(id))
|
||||
|
||||
def rename_calendar(service, id, summary):
|
||||
calendar = {'summary': summary}
|
||||
service.calendars().patch(body=calendar, calendarId=id).execute()
|
||||
print('{}: {}'.format(summary, id))
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
config = load_config()
|
||||
|
||||
if 'logging' in config:
|
||||
logging.config.dictConfig(config['logging'])
|
||||
|
||||
srv_acc_file = config['service_account']
|
||||
service = GoogleCalendarService.from_srv_acc_file(srv_acc_file)
|
||||
|
||||
if 'list' == args.command:
|
||||
list_calendars(service)
|
||||
elif 'create' == args.command:
|
||||
create_calendar(service, args.summary, args.timezone, args.public)
|
||||
elif 'add_owner' == args.command:
|
||||
add_owner(service, args.id, args.owner_email)
|
||||
elif 'remove' == args.command:
|
||||
remove_calendar(service, args.id)
|
||||
elif 'rename' == args.command:
|
||||
rename_calendar(service, args.id, args.summary)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,173 +1,173 @@
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _events_list_compare(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 = set(map(get_key, items_src))
|
||||
keys_dst = set(map(get_key, items_dst))
|
||||
|
||||
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))
|
||||
|
||||
items_to_insert = items_by_keys(items_src, key, keys_to_insert)
|
||||
items_to_delete = items_by_keys(items_dst, key, keys_to_delete)
|
||||
|
||||
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
|
||||
|
||||
def _filter_events_to_update(self):
|
||||
""" filter 'to_update' events by 'updated' datetime
|
||||
"""
|
||||
|
||||
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))
|
||||
|
||||
@staticmethod
|
||||
def _filter_events_by_date(events, date, op):
|
||||
""" filter events by start datetime
|
||||
|
||||
Arguments:
|
||||
events -- events list
|
||||
date {datetime} -- datetime to compare
|
||||
op {operator} -- comparsion operator
|
||||
|
||||
Returns:
|
||||
list of filtred events
|
||||
"""
|
||||
|
||||
def filter_by_date(event):
|
||||
date_cmp = date
|
||||
event_start = event['start']
|
||||
event_date = None
|
||||
compare_dates = False
|
||||
|
||||
if 'date' in event_start:
|
||||
event_date = event_start['date']
|
||||
compare_dates = True
|
||||
elif 'dateTime' in event_start:
|
||||
event_date = event_start['dateTime']
|
||||
|
||||
event_date = dateutil.parser.parse(event_date)
|
||||
if compare_dates:
|
||||
date_cmp = datetime.date(date.year, date.month, date.day)
|
||||
event_date = datetime.date(event_date.year, event_date.month, event_date.day)
|
||||
|
||||
return op(event_date, date_cmp)
|
||||
|
||||
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):
|
||||
"""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)
|
||||
|
||||
# divide source events by start datetime
|
||||
events_src_pending = CalendarSync._filter_events_by_date(
|
||||
events_src, start_date, operator.ge)
|
||||
events_src_past = CalendarSync._filter_events_by_date(
|
||||
events_src, start_date, operator.lt)
|
||||
|
||||
events_src = None
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
|
||||
self.logger.info('sync done')
|
||||
|
||||
self.to_insert, self.to_update, self.to_delete = [], [], []
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _events_list_compare(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 = set(map(get_key, items_src))
|
||||
keys_dst = set(map(get_key, items_dst))
|
||||
|
||||
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))
|
||||
|
||||
items_to_insert = items_by_keys(items_src, key, keys_to_insert)
|
||||
items_to_delete = items_by_keys(items_dst, key, keys_to_delete)
|
||||
|
||||
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
|
||||
|
||||
def _filter_events_to_update(self):
|
||||
""" filter 'to_update' events by 'updated' datetime
|
||||
"""
|
||||
|
||||
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))
|
||||
|
||||
@staticmethod
|
||||
def _filter_events_by_date(events, date, op):
|
||||
""" filter events by start datetime
|
||||
|
||||
Arguments:
|
||||
events -- events list
|
||||
date {datetime} -- datetime to compare
|
||||
op {operator} -- comparsion operator
|
||||
|
||||
Returns:
|
||||
list of filtred events
|
||||
"""
|
||||
|
||||
def filter_by_date(event):
|
||||
date_cmp = date
|
||||
event_start = event['start']
|
||||
event_date = None
|
||||
compare_dates = False
|
||||
|
||||
if 'date' in event_start:
|
||||
event_date = event_start['date']
|
||||
compare_dates = True
|
||||
elif 'dateTime' in event_start:
|
||||
event_date = event_start['dateTime']
|
||||
|
||||
event_date = dateutil.parser.parse(event_date)
|
||||
if compare_dates:
|
||||
date_cmp = datetime.date(date.year, date.month, date.day)
|
||||
event_date = datetime.date(event_date.year, event_date.month, event_date.day)
|
||||
|
||||
return op(event_date, date_cmp)
|
||||
|
||||
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):
|
||||
"""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)
|
||||
|
||||
# divide source events by start datetime
|
||||
events_src_pending = CalendarSync._filter_events_by_date(
|
||||
events_src, start_date, operator.ge)
|
||||
events_src_past = CalendarSync._filter_events_by_date(
|
||||
events_src, start_date, operator.lt)
|
||||
|
||||
events_src = None
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
|
||||
self.logger.info('sync done')
|
||||
|
||||
self.to_insert, self.to_update, self.to_delete = [], [], []
|
@ -1,53 +1,52 @@
|
||||
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.safe_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()
|
||||
import yaml
|
||||
|
||||
import dateutil.parser
|
||||
import datetime
|
||||
import logging
|
||||
import logging.config
|
||||
from . import (
|
||||
CalendarConverter,
|
||||
GoogleCalendarService,
|
||||
GoogleCalendar,
|
||||
CalendarSync
|
||||
)
|
||||
|
||||
def load_config():
|
||||
with open('config.yml', 'r', encoding='utf-8') as f:
|
||||
result = yaml.safe_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()
|
@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from gcal_sync import CalendarConverter
|
||||
from sync_ics2gcal import CalendarConverter
|
||||
|
||||
uid = "UID:uisgtr8tre93wewe0yr8wqy@test.com"
|
||||
only_start_date = uid + """
|
||||
|
@ -8,7 +8,7 @@ import dateutil.parser
|
||||
import pytest
|
||||
from pytz import timezone, utc
|
||||
|
||||
from gcal_sync import CalendarSync
|
||||
from sync_ics2gcal import CalendarSync
|
||||
|
||||
|
||||
def sha1(string):
|
||||
|
Loading…
x
Reference in New Issue
Block a user