From a96050628a0ee4044c8f88dab9e2a1bb15e3b875 Mon Sep 17 00:00:00 2001 From: Dmitry Belyaev <b4tm4n@mail.ru> Date: Wed, 19 Feb 2020 23:26:28 +0300 Subject: [PATCH 01/65] 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 --- .git_archival.txt | 1 + .gitattributes | 1 + .github/workflows/pythonpublish.yml | 26 + .gitignore | 4 + MANIFEST.in | 7 + pyproject.toml | 3 + setup.cfg | 7 + setup.py | 44 ++ {gcal_sync => sync_ics2gcal}/__init__.py | 28 +- {gcal_sync => sync_ics2gcal}/gcal.py | 566 +++++++++--------- {gcal_sync => sync_ics2gcal}/ical.py | 368 ++++++------ .../manage_calendars.py | 208 +++---- {gcal_sync => sync_ics2gcal}/sync.py | 346 +++++------ .../sync_calendar.py | 105 ++-- tests/test_converter.py | 2 +- tests/test_sync.py | 2 +- 16 files changed, 905 insertions(+), 813 deletions(-) create mode 100644 .git_archival.txt create mode 100644 .gitattributes create mode 100644 .github/workflows/pythonpublish.yml create mode 100644 MANIFEST.in create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 setup.py rename {gcal_sync => sync_ics2gcal}/__init__.py (92%) rename {gcal_sync => sync_ics2gcal}/gcal.py (96%) rename {gcal_sync => sync_ics2gcal}/ical.py (96%) rename manage-calendars.py => sync_ics2gcal/manage_calendars.py (95%) rename {gcal_sync => sync_ics2gcal}/sync.py (97%) rename sync-calendar.py => sync_ics2gcal/sync_calendar.py (93%) diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 0000000..082d6c2 --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1 @@ +ref-names: $Format:%D$ \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..82bf71c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git_archival.txt export-subst \ No newline at end of file diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml new file mode 100644 index 0000000..9e383e8 --- /dev/null +++ b/.github/workflows/pythonpublish.yml @@ -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/* diff --git a/.gitignore b/.gitignore index 3fd4fef..d98c726 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ service-account.json *.pyc my-test*.ics .vscode/* +/dist/ +/*.egg-info/ +/build/ +/.eggs/ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..346766b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include pyproject.toml + +# Include the README +include *.md + +# Include the license file +include LICENSE diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..47626ae --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=40.8.0", "wheel", "setuptools_scm"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..5ed97a9 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[metadata] +license_files = LICENSE + +[options] +setup_requires = + setuptools_scm + setuptools_scm_git_archive \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f77adb1 --- /dev/null +++ b/setup.py @@ -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", + ] + } +) \ No newline at end of file diff --git a/gcal_sync/__init__.py b/sync_ics2gcal/__init__.py similarity index 92% rename from gcal_sync/__init__.py rename to sync_ics2gcal/__init__.py index 5c5fcb1..24b73f5 100644 --- a/gcal_sync/__init__.py +++ b/sync_ics2gcal/__init__.py @@ -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 +) diff --git a/gcal_sync/gcal.py b/sync_ics2gcal/gcal.py similarity index 96% rename from gcal_sync/gcal.py rename to sync_ics2gcal/gcal.py index 4d02e14..8481a08 100644 --- a/gcal_sync/gcal.py +++ b/sync_ics2gcal/gcal.py @@ -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() diff --git a/gcal_sync/ical.py b/sync_ics2gcal/ical.py similarity index 96% rename from gcal_sync/ical.py rename to sync_ics2gcal/ical.py index ec21b8d..10f8784 100644 --- a/gcal_sync/ical.py +++ b/sync_ics2gcal/ical.py @@ -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 diff --git a/manage-calendars.py b/sync_ics2gcal/manage_calendars.py similarity index 95% rename from manage-calendars.py rename to sync_ics2gcal/manage_calendars.py index e528c76..b1236a2 100644 --- a/manage-calendars.py +++ b/sync_ics2gcal/manage_calendars.py @@ -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() diff --git a/gcal_sync/sync.py b/sync_ics2gcal/sync.py similarity index 97% rename from gcal_sync/sync.py rename to sync_ics2gcal/sync.py index 6ca4fc8..de95836 100644 --- a/gcal_sync/sync.py +++ b/sync_ics2gcal/sync.py @@ -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 = [], [], [] diff --git a/sync-calendar.py b/sync_ics2gcal/sync_calendar.py similarity index 93% rename from sync-calendar.py rename to sync_ics2gcal/sync_calendar.py index 6df9a79..37f5805 100644 --- a/sync-calendar.py +++ b/sync_ics2gcal/sync_calendar.py @@ -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() diff --git a/tests/test_converter.py b/tests/test_converter.py index c5bb563..2aadde6 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -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 + """ diff --git a/tests/test_sync.py b/tests/test_sync.py index 52c2099..3201929 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -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): From 3ecd6695cfef4b0ae1dbbec4667693ff8d99fadf Mon Sep 17 00:00:00 2001 From: Dmitry Belyaev <b4tm4n@mail.ru> Date: Thu, 20 Feb 2020 12:39:53 +0300 Subject: [PATCH 02/65] update README --- README.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 79d7c92..8c192a7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,85 @@ # sync_ics2gcal +[](https://badge.fury.io/py/sync-ics2gcal) [](https://travis-ci.org/b4tman/sync_ics2gcal) -[](https://dependabot.com) [](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_shield) +[](https://dependabot.com) +[](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_shield)  Python scripts for sync .ics file with Google calendar +## Installation + +To install from [PyPI](https://pypi.org/project/sync-ics2gcal/) with [pip](https://pypi.python.org/pypi/pip), run: + +``` +pip install sync-ics2gcal +``` + + +Or download source code and install: + +``` +python setup.py install +``` + +## Configuration + +### Create application in Google API Console +1. Create a new project: https://console.developers.google.com/project +2. Choose the new project from the top right project dropdown (only if another project is selected) +3. In the project Dashboard, choose "Library" +4. Find and Enable "Google Calendar API" +5. In the project Dashboard, choose "Credentials" +6. In the "Service Accounts" group, click to "Manage service accounts" +7. Click "Create service account" +8. Choose service account name and ID +9. Go back to "Service Accounts" group in "Credentials" +10. Edit service account and click "Create key", choose JSON and download key file. + +### Create working directory +For example: `/home/user/myfolder`. +1. Save service account key in file `service-account.json`. +2. Download [sample config](https://github.com/b4tman/sync_ics2gcal/blob/develop/sample-config.yml) and save to file `config.yml`. For example: +``` +wget https://raw.githubusercontent.com/b4tman/sync_ics2gcal/develop/sample-config.yml -O config.yml +``` +3. *(Optional)* Place source `.ics` file, `my-calendar.ics` for example. + +### Configuration parameters +* `start_from` - start date: + * full format datetime, `2018-04-03T13:23:25.000001Z` for example + * or just `now` +* `service_account` - service account filename +* `google_id` - target google calendar id, `my-calendar@group.calendar.google.com` for example +* `source` - source `.ics` filename, `my-calendar.ics` for example + + +## Usage + +### Manage calendars + +``` +manage-ics2gcal <subcommand> [-h] [options] +``` + +subcomands: +* **list** - list calendars +* **create** - create calendar +* **add_owner** - add owner to calendar +* **remove** - remove calendar +* **rename** - rename calendar + +Use **-h** for more info. + +### Sync calendar + +just type: +``` +sync-ics2gcal +``` + + ## How it works  From d146eec7ae3a5e561d89142dd74d19bfa5f5c02d Mon Sep 17 00:00:00 2001 From: Dmitry Belyaev <b4tm4n@mail.ru> Date: Thu, 20 Feb 2020 17:37:31 +0300 Subject: [PATCH 03/65] + default credentials support https://googleapis.dev/python/google-auth/latest/reference/google.auth.html#google.auth.default https://developers.google.com/identity/protocols/application-default-credentials --- README.md | 3 ++- sync_ics2gcal/gcal.py | 35 +++++++++++++++++++++++++++++++ sync_ics2gcal/manage_calendars.py | 3 +-- sync_ics2gcal/sync_calendar.py | 3 +-- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8c192a7..b2b8b3b 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,8 @@ wget https://raw.githubusercontent.com/b4tman/sync_ics2gcal/develop/sample-confi * `start_from` - start date: * full format datetime, `2018-04-03T13:23:25.000001Z` for example * or just `now` -* `service_account` - service account filename +* *(Optional)* `service_account` - service account filename, remove it from config to use [default credentials](https://developers.google.com/identity/protocols/application-default-credentials) +* *(Optional)* `logging` - [config](https://docs.python.org/3.8/library/logging.config.html#dictionary-schema-details) to setup logging * `google_id` - target google calendar id, `my-calendar@group.calendar.google.com` for example * `source` - source `.ics` filename, `my-calendar.ics` for example diff --git a/sync_ics2gcal/gcal.py b/sync_ics2gcal/gcal.py index 8481a08..6bc9ed9 100644 --- a/sync_ics2gcal/gcal.py +++ b/sync_ics2gcal/gcal.py @@ -14,6 +14,21 @@ class GoogleCalendarService(): service Resource """ + @staticmethod + def default(): + """make service Resource from default credentials (authorize) + ( https://developers.google.com/identity/protocols/application-default-credentials ) + ( https://googleapis.dev/python/google-auth/latest/reference/google.auth.html#google.auth.default ) + + Returns: + service Resource + """ + + scopes = ['https://www.googleapis.com/auth/calendar'] + credentials, _ = google.auth.default(scopes=scopes) + service = discovery.build('calendar', 'v3', credentials=credentials) + return service + @staticmethod def from_srv_acc_file(service_account_file): """make service Resource from service account filename (authorize) @@ -27,6 +42,26 @@ class GoogleCalendarService(): scoped_credentials = credentials.with_scopes(scopes) service = discovery.build('calendar', 'v3', credentials=scoped_credentials) return service + + @staticmethod + def from_config(config): + """make service Resource from config dict + + Arguments: + config -- dict() config with keys: + (optional) service_account: - service account filename + if key not in dict then default credentials will be used + ( https://developers.google.com/identity/protocols/application-default-credentials ) + + Returns: + service Resource + """ + + if 'service_account' in config: + service = GoogleCalendarService.from_srv_acc_file(config['service_account']) + else: + service = GoogleCalendarService.default() + return service def select_event_key(event): """select event key for logging diff --git a/sync_ics2gcal/manage_calendars.py b/sync_ics2gcal/manage_calendars.py index b1236a2..f457294 100644 --- a/sync_ics2gcal/manage_calendars.py +++ b/sync_ics2gcal/manage_calendars.py @@ -86,8 +86,7 @@ def main(): if 'logging' in config: logging.config.dictConfig(config['logging']) - srv_acc_file = config['service_account'] - service = GoogleCalendarService.from_srv_acc_file(srv_acc_file) + service = GoogleCalendarService.from_config(config) if 'list' == args.command: list_calendars(service) diff --git a/sync_ics2gcal/sync_calendar.py b/sync_ics2gcal/sync_calendar.py index 37f5805..814d9d5 100644 --- a/sync_ics2gcal/sync_calendar.py +++ b/sync_ics2gcal/sync_calendar.py @@ -34,14 +34,13 @@ def main(): 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) + service = GoogleCalendarService.from_config(config) gcalendar = GoogleCalendar(service, calendarId) sync = CalendarSync(gcalendar, converter) From 5d37aa2a3375fd973646fe9c21ce5d210a4490f3 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Tue, 25 Feb 2020 22:43:45 +0300 Subject: [PATCH 04/65] disable cache_discovery to suppress errors in logs: file_cache is unavailable when using oauth2client >= 4.0.0 https://github.com/googleapis/google-api-python-client/issues/299 https://github.com/googleapis/google-api-python-client/issues/325 --- sync_ics2gcal/gcal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sync_ics2gcal/gcal.py b/sync_ics2gcal/gcal.py index 6bc9ed9..c6e817f 100644 --- a/sync_ics2gcal/gcal.py +++ b/sync_ics2gcal/gcal.py @@ -26,7 +26,7 @@ class GoogleCalendarService(): scopes = ['https://www.googleapis.com/auth/calendar'] credentials, _ = google.auth.default(scopes=scopes) - service = discovery.build('calendar', 'v3', credentials=credentials) + service = discovery.build('calendar', 'v3', credentials=credentials, cache_discovery=False) return service @staticmethod @@ -40,7 +40,7 @@ class GoogleCalendarService(): 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) + service = discovery.build('calendar', 'v3', credentials=scoped_credentials, cache_discovery=False) return service @staticmethod From 41cc6b4159c942d26e30b7f240b072aa83dafe85 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2020 02:18:46 +0000 Subject: [PATCH 05/65] Bump google-auth from 1.11.0 to 1.11.2 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.11.0 to 1.11.2. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.11.0...v1.11.2) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5369fe9..0e85f92 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.11.0 +google-auth==1.11.2 google-api-python-client==1.7.11 icalendar==4.0.4 pytz==2019.3 From 9aad7e1910d787e26d601d3fe6b7f9fecf0cf2ff Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 7 Mar 2020 16:14:47 +0300 Subject: [PATCH 06/65] make service even when config is None --- sync_ics2gcal/gcal.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sync_ics2gcal/gcal.py b/sync_ics2gcal/gcal.py index c6e817f..fd086ca 100644 --- a/sync_ics2gcal/gcal.py +++ b/sync_ics2gcal/gcal.py @@ -44,20 +44,21 @@ class GoogleCalendarService(): return service @staticmethod - def from_config(config): + def from_config(config=None): """make service Resource from config dict Arguments: config -- dict() config with keys: (optional) service_account: - service account filename if key not in dict then default credentials will be used - ( https://developers.google.com/identity/protocols/application-default-credentials ) + ( https://developers.google.com/identity/protocols/application-default-credentials ) + -- None: default credentials will be used Returns: service Resource """ - if 'service_account' in config: + if (not config is None) and 'service_account' in config: service = GoogleCalendarService.from_srv_acc_file(config['service_account']) else: service = GoogleCalendarService.default() From c17d3cd0ea4f8b6c9e2fd496f756d5fa3034453c Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 7 Mar 2020 16:21:48 +0300 Subject: [PATCH 07/65] manage_calendars: no config file required --- sync_ics2gcal/manage_calendars.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sync_ics2gcal/manage_calendars.py b/sync_ics2gcal/manage_calendars.py index f457294..dc2e3e9 100644 --- a/sync_ics2gcal/manage_calendars.py +++ b/sync_ics2gcal/manage_calendars.py @@ -44,8 +44,13 @@ def parse_args(): def load_config(): - with open('config.yml', 'r', encoding='utf-8') as f: - result = yaml.safe_load(f) + result = None + try: + with open('config.yml', 'r', encoding='utf-8') as f: + result = yaml.safe_load(f) + except FileNotFoundError: + pass + return result @@ -83,7 +88,7 @@ def main(): args = parse_args() config = load_config() - if 'logging' in config: + if (not config is None) and 'logging' in config: logging.config.dictConfig(config['logging']) service = GoogleCalendarService.from_config(config) From 55b67469be6763ed992d8a0b86f72d1e29172770 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 7 Mar 2020 17:41:05 +0300 Subject: [PATCH 08/65] + manage_calendars: get\set calendar properties --- sync_ics2gcal/manage_calendars.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/sync_ics2gcal/manage_calendars.py b/sync_ics2gcal/manage_calendars.py index dc2e3e9..6edb504 100644 --- a/sync_ics2gcal/manage_calendars.py +++ b/sync_ics2gcal/manage_calendars.py @@ -36,6 +36,20 @@ def parse_args(): 'id', action='store', help='calendar id') parser_rename.add_argument( 'summary', action='store', help='new summary') + parser_get = command_subparsers.add_parser( + 'get', help='get calendar property') + parser_get.add_argument( + 'id', action='store', help='calendar id') + parser_get.add_argument( + 'property', action='store', help='property key') + parser_get = command_subparsers.add_parser( + 'set', help='set calendar property') + parser_get.add_argument( + 'id', action='store', help='calendar id') + parser_get.add_argument( + 'property', action='store', help='property key') + parser_get.add_argument( + 'property_value', action='store', help='property value') args = parser.parse_args() if args.command is None: @@ -84,6 +98,15 @@ def rename_calendar(service, id, summary): service.calendars().patch(body=calendar, calendarId=id).execute() print('{}: {}'.format(summary, id)) +def get_calendar_property(service, id, property): + response = service.calendarList().get(calendarId=id, fields=property).execute() + print(response.get(property)) + +def set_calendar_property(service, id, property, property_value): + body = {property: property_value} + response = service.calendarList().patch(body=body, calendarId=id).execute() + print(response) + def main(): args = parse_args() config = load_config() @@ -103,6 +126,10 @@ def main(): remove_calendar(service, args.id) elif 'rename' == args.command: rename_calendar(service, args.id, args.summary) + elif 'get' == args.command: + get_calendar_property(service, args.id, args.property) + elif 'set' == args.command: + set_calendar_property(service, args.id, args.property, args.property_value) if __name__ == '__main__': main() From b0a39a1b8c0ecb7940c782193af42067274e5e6e Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 7 Mar 2020 17:54:16 +0300 Subject: [PATCH 09/65] manage_calendars: use nextPageToken for list By default maximum number of entries returned on one result page is 100 --- sync_ics2gcal/manage_calendars.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sync_ics2gcal/manage_calendars.py b/sync_ics2gcal/manage_calendars.py index 6edb504..c95b24d 100644 --- a/sync_ics2gcal/manage_calendars.py +++ b/sync_ics2gcal/manage_calendars.py @@ -69,8 +69,16 @@ def load_config(): def list_calendars(service): - response = service.calendarList().list(fields='items(id,summary)').execute() - for calendar in response.get('items'): + calendars = [] + page_token = None + while True: + response = service.calendarList().list(fields='nextPageToken,items(id,summary)', pageToken=page_token).execute() + if 'items' in response: + calendars.extend(response['items']) + page_token = response.get('nextPageToken') + if not page_token: + break + for calendar in calendars: print('{summary}: {id}'.format_map(calendar)) From ab00cb09c85f574a758045f7dc56562696ebeffb Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 7 Mar 2020 18:05:51 +0300 Subject: [PATCH 10/65] + manage_calendars: list hidden & deleted --- sync_ics2gcal/manage_calendars.py | 50 +++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/sync_ics2gcal/manage_calendars.py b/sync_ics2gcal/manage_calendars.py index c95b24d..99e4365 100644 --- a/sync_ics2gcal/manage_calendars.py +++ b/sync_ics2gcal/manage_calendars.py @@ -12,7 +12,13 @@ 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') + # list + parser_list = command_subparsers.add_parser('list', help='list calendars') + parser_list.add_argument( + '--show-hidden', default=False, action='store_true', help='show hidden calendars') + parser_list.add_argument( + '--show-deleted', default=False, action='store_true', help='show deleted calendars') + # create parser_create = command_subparsers.add_parser( 'create', help='create calendar') parser_create.add_argument( @@ -21,36 +27,41 @@ def parse_args(): default=None, required=False, help='new calendar timezone') parser_create.add_argument( '--public', default=False, action='store_true', help='make calendar public') + # add_owner 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') + # remove parser_remove = command_subparsers.add_parser( 'remove', help='remove calendar') parser_remove.add_argument( 'id', action='store', help='calendar id to remove') + # rename 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') + # get parser_get = command_subparsers.add_parser( 'get', help='get calendar property') parser_get.add_argument( 'id', action='store', help='calendar id') parser_get.add_argument( 'property', action='store', help='property key') - parser_get = command_subparsers.add_parser( + # set + parser_set = command_subparsers.add_parser( 'set', help='set calendar property') - parser_get.add_argument( + parser_set.add_argument( 'id', action='store', help='calendar id') - parser_get.add_argument( + parser_set.add_argument( 'property', action='store', help='property key') - parser_get.add_argument( + parser_set.add_argument( 'property_value', action='store', help='property value') - + args = parser.parse_args() if args.command is None: parser.print_usage() @@ -64,20 +75,23 @@ def load_config(): result = yaml.safe_load(f) except FileNotFoundError: pass - + return result -def list_calendars(service): +def list_calendars(service, show_hidden, show_deleted): calendars = [] page_token = None while True: - response = service.calendarList().list(fields='nextPageToken,items(id,summary)', pageToken=page_token).execute() + response = service.calendarList().list(fields='nextPageToken,items(id,summary)', + pageToken=page_token, + showHidden=show_hidden, + showDeleted=show_deleted).execute() if 'items' in response: - calendars.extend(response['items']) - page_token = response.get('nextPageToken') - if not page_token: - break + calendars.extend(response['items']) + page_token = response.get('nextPageToken') + if not page_token: + break for calendar in calendars: print('{summary}: {id}'.format_map(calendar)) @@ -101,20 +115,24 @@ def remove_calendar(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 get_calendar_property(service, id, property): response = service.calendarList().get(calendarId=id, fields=property).execute() print(response.get(property)) + def set_calendar_property(service, id, property, property_value): body = {property: property_value} response = service.calendarList().patch(body=body, calendarId=id).execute() print(response) + def main(): args = parse_args() config = load_config() @@ -125,7 +143,7 @@ def main(): service = GoogleCalendarService.from_config(config) if 'list' == args.command: - list_calendars(service) + list_calendars(service, args.show_hidden, args.show_deleted) elif 'create' == args.command: create_calendar(service, args.summary, args.timezone, args.public) elif 'add_owner' == args.command: @@ -137,7 +155,9 @@ def main(): elif 'get' == args.command: get_calendar_property(service, args.id, args.property) elif 'set' == args.command: - set_calendar_property(service, args.id, args.property, args.property_value) + set_calendar_property( + service, args.id, args.property, args.property_value) + if __name__ == '__main__': main() From 8d64869f062f459b15932d449c04410bf738d6e8 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 7 Mar 2020 18:14:34 +0300 Subject: [PATCH 11/65] lint manage_calendars --- sync_ics2gcal/manage_calendars.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/sync_ics2gcal/manage_calendars.py b/sync_ics2gcal/manage_calendars.py index 99e4365..ed3c6ef 100644 --- a/sync_ics2gcal/manage_calendars.py +++ b/sync_ics2gcal/manage_calendars.py @@ -1,9 +1,7 @@ import argparse -import datetime import logging.config import yaml -from pytz import utc from . import GoogleCalendar, GoogleCalendarService @@ -15,18 +13,22 @@ def parse_args(): # list parser_list = command_subparsers.add_parser('list', help='list calendars') parser_list.add_argument( - '--show-hidden', default=False, action='store_true', help='show hidden calendars') + '--show-hidden', default=False, + action='store_true', help='show hidden calendars') parser_list.add_argument( - '--show-deleted', default=False, action='store_true', help='show deleted calendars') + '--show-deleted', default=False, + action='store_true', help='show deleted calendars') # create 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') + default=None, required=False, + help='new calendar timezone') parser_create.add_argument( - '--public', default=False, action='store_true', help='make calendar public') + '--public', default=False, + action='store_true', help='make calendar public') # add_owner parser_add_owner = command_subparsers.add_parser( 'add_owner', help='add owner to calendar') @@ -80,13 +82,15 @@ def load_config(): def list_calendars(service, show_hidden, show_deleted): + fields = 'nextPageToken,items(id,summary)' calendars = [] page_token = None while True: - response = service.calendarList().list(fields='nextPageToken,items(id,summary)', + response = service.calendarList().list(fields=fields, pageToken=page_token, showHidden=show_hidden, - showDeleted=show_deleted).execute() + showDeleted=show_deleted + ).execute() if 'items' in response: calendars.extend(response['items']) page_token = response.get('nextPageToken') @@ -123,7 +127,8 @@ def rename_calendar(service, id, summary): def get_calendar_property(service, id, property): - response = service.calendarList().get(calendarId=id, fields=property).execute() + response = service.calendarList().get(calendarId=id, + fields=property).execute() print(response.get(property)) @@ -137,7 +142,7 @@ def main(): args = parse_args() config = load_config() - if (not config is None) and 'logging' in config: + if config is not None and 'logging' in config: logging.config.dictConfig(config['logging']) service = GoogleCalendarService.from_config(config) From 9e74772852ab740b00003b9be02af2484eaf3302 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 7 Mar 2020 18:26:37 +0300 Subject: [PATCH 12/65] lint gcal --- sync_ics2gcal/gcal.py | 83 +++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/sync_ics2gcal/gcal.py b/sync_ics2gcal/gcal.py index fd086ca..db6b831 100644 --- a/sync_ics2gcal/gcal.py +++ b/sync_ics2gcal/gcal.py @@ -1,5 +1,4 @@ import logging -import sys import google.auth from google.oauth2 import service_account @@ -26,9 +25,10 @@ class GoogleCalendarService(): scopes = ['https://www.googleapis.com/auth/calendar'] credentials, _ = google.auth.default(scopes=scopes) - service = discovery.build('calendar', 'v3', credentials=credentials, cache_discovery=False) + service = discovery.build( + 'calendar', 'v3', credentials=credentials, cache_discovery=False) return service - + @staticmethod def from_srv_acc_file(service_account_file): """make service Resource from service account filename (authorize) @@ -38,11 +38,14 @@ class GoogleCalendarService(): """ scopes = ['https://www.googleapis.com/auth/calendar'] - credentials = service_account.Credentials.from_service_account_file(service_account_file) + 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, cache_discovery=False) + service = discovery.build( + 'calendar', 'v3', credentials=scoped_credentials, + cache_discovery=False) return service - + @staticmethod def from_config(config=None): """make service Resource from config dict @@ -51,25 +54,27 @@ class GoogleCalendarService(): config -- dict() config with keys: (optional) service_account: - service account filename if key not in dict then default credentials will be used - ( https://developers.google.com/identity/protocols/application-default-credentials ) + ( https://developers.google.com/identity/protocols/application-default-credentials ) -- None: default credentials will be used Returns: service Resource """ - if (not config is None) and 'service_account' in config: - service = GoogleCalendarService.from_srv_acc_file(config['service_account']) + if config is not None and 'service_account' in config: + service = GoogleCalendarService.from_srv_acc_file( + config['service_account']) else: service = GoogleCalendarService.default() 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 """ @@ -94,11 +99,11 @@ class GoogleCalendar(): 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 """ @@ -108,8 +113,10 @@ class GoogleCalendar(): 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)) + 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: @@ -128,8 +135,11 @@ class GoogleCalendar(): 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() + 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') @@ -160,8 +170,9 @@ class GoogleCalendar(): if exception is None: found = ([] != response['items']) else: - self.logger.error('exception %s, while listing event with UID: %s', str( - exception), event['iCalUID']) + self.logger.error( + 'exception %s, while listing event with UID: %s', + str(exception), event['iCalUID']) if found: exists.append( (event, response['items'][0])) @@ -173,7 +184,12 @@ class GoogleCalendar(): 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)) + iCalUID=event['iCalUID'], + showDeleted=True, + fields=fields + ), + request_id=str(i) + ) i += 1 batch.execute() self.logger.info('%d events exists, %d not found', @@ -196,7 +212,9 @@ class GoogleCalendar(): 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)) + calendarId=self.calendarId, body=event, fields=fields), + request_id=str(i) + ) i += 1 batch.execute() @@ -218,7 +236,8 @@ class GoogleCalendar(): 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)) + calendarId=self.calendarId, eventId=event_old['id'], + body=event_new), fields=fields, request_id=str(i)) i += 1 batch.execute() @@ -240,7 +259,8 @@ class GoogleCalendar(): 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)) + calendarId=self.calendarId, eventId=event_old['id'], + body=event_new, fields=fields), request_id=str(i)) i += 1 batch.execute() @@ -259,7 +279,8 @@ class GoogleCalendar(): 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)) + calendarId=self.calendarId, + eventId=event['id']), request_id=str(i)) i += 1 batch.execute() @@ -280,7 +301,9 @@ class GoogleCalendar(): if timeZone is not None: calendar['timeZone'] = timeZone - created_calendar = self.service.calendars().insert(body=calendar).execute() + created_calendar = self.service.calendars().insert( + body=calendar + ).execute() self.calendarId = created_calendar['id'] return created_calendar @@ -300,7 +323,10 @@ class GoogleCalendar(): }, 'role': 'reader' } - return self.service.acl().insert(calendarId=self.calendarId, body=rule_public).execute() + return self.service.acl().insert( + calendarId=self.calendarId, + body=rule_public + ).execute() def add_owner(self, email): """add calendar owner by email @@ -316,4 +342,7 @@ class GoogleCalendar(): }, 'role': 'owner' } - return self.service.acl().insert(calendarId=self.calendarId, body=rule_owner).execute() + return self.service.acl().insert( + calendarId=self.calendarId, + body=rule_owner + ).execute() From 0161d65c1610632a68909064bd13537f9377fa3a Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 7 Mar 2020 18:29:35 +0300 Subject: [PATCH 13/65] lint ical --- sync_ics2gcal/ical.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sync_ics2gcal/ical.py b/sync_ics2gcal/ical.py index 10f8784..670ad75 100644 --- a/sync_ics2gcal/ical.py +++ b/sync_ics2gcal/ical.py @@ -17,7 +17,10 @@ def format_datetime_utc(value): 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' + + return utc.normalize( + value.astimezone(utc) + ).replace(tzinfo=None).isoformat() + 'Z' def gcal_date_or_dateTime(value, check_value=None): @@ -145,7 +148,9 @@ class EventConverter(Event): 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') + event, + 'transparency', + lambda prop: self._str_prop(prop).lower(), 'TRANSP') return event @@ -160,7 +165,7 @@ class CalendarConverter(): self.calendar = calendar def load(self, filename): - """ load calendar from ics file + """ load calendar from ics file """ with open(filename, 'r', encoding='utf-8') as f: self.calendar = Calendar.from_ical(f.read()) From 0f9a8d7a74a3d485df169973a931b8d96c128415 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 7 Mar 2020 18:34:31 +0300 Subject: [PATCH 14/65] lint sync --- sync_ics2gcal/sync.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/sync_ics2gcal/sync.py b/sync_ics2gcal/sync.py index de95836..bdc5b2f 100644 --- a/sync_ics2gcal/sync.py +++ b/sync_ics2gcal/sync.py @@ -25,8 +25,8 @@ class CalendarSync(): key {str} -- name of key to compare (default: {'iCalUID'}) Returns: - tuple -- (items_to_insert, - items_to_update, + tuple -- (items_to_insert, + items_to_update, items_to_delete) """ @@ -59,7 +59,9 @@ class CalendarSync(): def filter_updated(event_tuple): new, old = event_tuple - return dateutil.parser.parse(new['updated']) > dateutil.parser.parse(old['updated']) + new_date = dateutil.parser.parse(new['updated']) + old_date = dateutil.parser.parse(old['updated']) + return new_date > old_date self.to_update = list(filter(filter_updated, self.to_update)) @@ -87,11 +89,12 @@ class CalendarSync(): 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) + event_date = datetime.date( + event_date.year, event_date.month, event_date.day) return op(event_date, date_cmp) @@ -157,8 +160,12 @@ class CalendarSync(): # 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)) + 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 From 6e7c3cb7b2936ebb2988987e64c48ecb26ef1e09 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sun, 8 Mar 2020 13:10:42 +0300 Subject: [PATCH 15/65] fmt sync_calendar --- sync_ics2gcal/sync_calendar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sync_ics2gcal/sync_calendar.py b/sync_ics2gcal/sync_calendar.py index 814d9d5..cf39dc4 100644 --- a/sync_ics2gcal/sync_calendar.py +++ b/sync_ics2gcal/sync_calendar.py @@ -11,6 +11,7 @@ from . import ( CalendarSync ) + def load_config(): with open('config.yml', 'r', encoding='utf-8') as f: result = yaml.safe_load(f) @@ -18,7 +19,7 @@ def load_config(): def get_start_date(date_str): - result = datetime.datetime(1,1,1) + result = datetime.datetime(1, 1, 1) if 'now' == date_str: result = datetime.datetime.utcnow() else: @@ -47,5 +48,6 @@ def main(): sync.prepare_sync(start) sync.apply() + if __name__ == '__main__': main() From 51005fb29ed7b1f09b99c79869fc0c952767375d Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sun, 8 Mar 2020 13:17:54 +0300 Subject: [PATCH 16/65] fmt README --- README.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b2b8b3b..f2444a2 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,21 @@ Python scripts for sync .ics file with Google calendar To install from [PyPI](https://pypi.org/project/sync-ics2gcal/) with [pip](https://pypi.python.org/pypi/pip), run: -``` +```sh pip install sync-ics2gcal ``` - Or download source code and install: -``` +```sh python setup.py install ``` ## Configuration ### Create application in Google API Console -1. Create a new project: https://console.developers.google.com/project + +1. Create a new project: [console.developers.google.com/project](https://console.developers.google.com/project) 2. Choose the new project from the top right project dropdown (only if another project is selected) 3. In the project Dashboard, choose "Library" 4. Find and Enable "Google Calendar API" @@ -38,15 +38,20 @@ python setup.py install 10. Edit service account and click "Create key", choose JSON and download key file. ### Create working directory + For example: `/home/user/myfolder`. + 1. Save service account key in file `service-account.json`. 2. Download [sample config](https://github.com/b4tman/sync_ics2gcal/blob/develop/sample-config.yml) and save to file `config.yml`. For example: -``` + +```sh wget https://raw.githubusercontent.com/b4tman/sync_ics2gcal/develop/sample-config.yml -O config.yml ``` + 3. *(Optional)* Place source `.ics` file, `my-calendar.ics` for example. ### Configuration parameters + * `start_from` - start date: * full format datetime, `2018-04-03T13:23:25.000001Z` for example * or just `now` @@ -55,16 +60,16 @@ wget https://raw.githubusercontent.com/b4tman/sync_ics2gcal/develop/sample-confi * `google_id` - target google calendar id, `my-calendar@group.calendar.google.com` for example * `source` - source `.ics` filename, `my-calendar.ics` for example - ## Usage ### Manage calendars -``` +```sh manage-ics2gcal <subcommand> [-h] [options] ``` subcomands: + * **list** - list calendars * **create** - create calendar * **add_owner** - add owner to calendar @@ -76,15 +81,15 @@ Use **-h** for more info. ### Sync calendar just type: -``` + +```sh sync-ics2gcal ``` - ## How it works  - ## License + [](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_large) \ No newline at end of file From 8712b81a53d556ae39b1d8d88135ee759aed33c8 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sun, 8 Mar 2020 13:29:57 +0300 Subject: [PATCH 17/65] + get/set commands in README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f2444a2..839b4c4 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ subcomands: * **add_owner** - add owner to calendar * **remove** - remove calendar * **rename** - rename calendar +* **get** - get calendar property (see [CalendarList resource](https://developers.google.com/calendar/v3/reference/calendarList#resource)) +* **set** - set calendar property Use **-h** for more info. @@ -92,4 +94,4 @@ sync-ics2gcal ## License -[](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_large) \ No newline at end of file +[](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_large) From 694b91798e326f2013617431fb24372d9e3c0c5d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2020 02:42:16 +0000 Subject: [PATCH 18/65] Bump pyyaml from 5.3 to 5.3.1 Bumps [pyyaml](https://github.com/yaml/pyyaml) from 5.3 to 5.3.1. - [Release notes](https://github.com/yaml/pyyaml/releases) - [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES) - [Commits](https://github.com/yaml/pyyaml/compare/5.3...5.3.1) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0e85f92..4401d27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ google-auth==1.11.2 google-api-python-client==1.7.11 icalendar==4.0.4 pytz==2019.3 -PyYAML==5.3 +PyYAML==5.3.1 From c1d148c3f3954fd6568d7e2933a34ed4fa27a623 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2020 06:16:41 +0000 Subject: [PATCH 19/65] Bump icalendar from 4.0.4 to 4.0.5 Bumps [icalendar](https://github.com/collective/icalendar) from 4.0.4 to 4.0.5. - [Release notes](https://github.com/collective/icalendar/releases) - [Changelog](https://github.com/collective/icalendar/blob/master/CHANGES.rst) - [Commits](https://github.com/collective/icalendar/compare/4.0.4...4.0.5) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4401d27..ff30389 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.11.2 google-api-python-client==1.7.11 -icalendar==4.0.4 +icalendar==4.0.5 pytz==2019.3 PyYAML==5.3.1 From 9e027df34987fc598a4ed4a6ed88a8242ae5b877 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2020 06:21:42 +0000 Subject: [PATCH 20/65] Bump google-auth from 1.11.2 to 1.12.0 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.11.2 to 1.12.0. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.11.2...v1.12.0) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ff30389..4a5d195 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.11.2 +google-auth==1.12.0 google-api-python-client==1.7.11 icalendar==4.0.5 pytz==2019.3 From 9c086309314886389a0c4d8c1d4fd99f757d06cd Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2020 06:28:52 +0000 Subject: [PATCH 21/65] Bump google-api-python-client from 1.7.11 to 1.8.0 Bumps [google-api-python-client](https://github.com/google/google-api-python-client) from 1.7.11 to 1.8.0. - [Release notes](https://github.com/google/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/master/CHANGELOG) - [Commits](https://github.com/google/google-api-python-client/compare/v1.7.11...v1.8.0) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4a5d195..8ae5375 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.12.0 -google-api-python-client==1.7.11 +google-api-python-client==1.8.0 icalendar==4.0.5 pytz==2019.3 PyYAML==5.3.1 From 6058a3e5926818ac314a964a9cfb40a064fc7230 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 1 May 2020 02:32:03 +0000 Subject: [PATCH 22/65] Bump pytz from 2019.3 to 2020.1 Bumps [pytz](https://github.com/stub42/pytz) from 2019.3 to 2020.1. - [Release notes](https://github.com/stub42/pytz/releases) - [Commits](https://github.com/stub42/pytz/compare/release_2019.3...release_2020.1) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8ae5375..d4dc446 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.12.0 google-api-python-client==1.8.0 icalendar==4.0.5 -pytz==2019.3 +pytz==2020.1 PyYAML==5.3.1 From 283b164723e81bf6a645bc886602d5ccda4d6c9a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 1 May 2020 08:01:44 +0000 Subject: [PATCH 23/65] Bump google-auth from 1.12.0 to 1.14.1 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.12.0 to 1.14.1. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.12.0...v1.14.1) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d4dc446..cb7c1cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.12.0 +google-auth==1.14.1 google-api-python-client==1.8.0 icalendar==4.0.5 pytz==2020.1 From 2d00ae77c98172ea1e8e274638f3ad69d90740c7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 1 May 2020 09:38:21 +0000 Subject: [PATCH 24/65] Bump google-api-python-client from 1.8.0 to 1.8.2 Bumps [google-api-python-client](https://github.com/google/google-api-python-client) from 1.8.0 to 1.8.2. - [Release notes](https://github.com/google/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/google-api-python-client/compare/v1.8.0...v1.8.2) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cb7c1cc..c63cc94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.14.1 -google-api-python-client==1.8.0 +google-api-python-client==1.8.2 icalendar==4.0.5 pytz==2020.1 PyYAML==5.3.1 From 9ad544971ed6daf48934b2c9a0b09775d51ccec2 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2020 02:48:52 +0000 Subject: [PATCH 25/65] Bump google-auth from 1.14.1 to 1.16.0 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.14.1 to 1.16.0. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.14.1...v1.16.0) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c63cc94..3974938 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.14.1 +google-auth==1.16.0 google-api-python-client==1.8.2 icalendar==4.0.5 pytz==2020.1 From 38f7403b40314f979dd2131fd56897fb6e2ba479 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2020 06:01:28 +0000 Subject: [PATCH 26/65] Bump google-api-python-client from 1.8.2 to 1.8.4 Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 1.8.2 to 1.8.4. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v1.8.2...v1.8.4) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3974938..e4ae491 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.16.0 -google-api-python-client==1.8.2 +google-api-python-client==1.8.4 icalendar==4.0.5 pytz==2020.1 PyYAML==5.3.1 From a7164abb247f197fcf585e07f6caa44a4c27b0c1 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2020 07:38:39 +0000 Subject: [PATCH 27/65] Bump icalendar from 4.0.5 to 4.0.6 Bumps [icalendar](https://github.com/collective/icalendar) from 4.0.5 to 4.0.6. - [Release notes](https://github.com/collective/icalendar/releases) - [Changelog](https://github.com/collective/icalendar/blob/master/CHANGES.rst) - [Commits](https://github.com/collective/icalendar/compare/4.0.5...4.0.6) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e4ae491..711e65c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.16.0 google-api-python-client==1.8.4 -icalendar==4.0.5 +icalendar==4.0.6 pytz==2020.1 PyYAML==5.3.1 From 66f9de39805dc003f43ab822e9658ef968291cb6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2020 06:33:35 +0000 Subject: [PATCH 28/65] Create Dependabot config file --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..700d735 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: monthly + time: '02:00' + open-pull-requests-limit: 10 + target-branch: develop From 7664ca9e55ac6f16862f2ec968102314fcd11f82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2020 06:53:06 +0000 Subject: [PATCH 29/65] Bump google-api-python-client from 1.8.4 to 1.9.3 Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 1.8.4 to 1.9.3. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v1.8.4...v1.9.3) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 711e65c..28d964b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.16.0 -google-api-python-client==1.8.4 +google-api-python-client==1.9.3 icalendar==4.0.6 pytz==2020.1 PyYAML==5.3.1 From 05a47700713c191a3077052d37dd7207dba12dd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2020 07:13:29 +0000 Subject: [PATCH 30/65] Bump google-auth from 1.16.0 to 1.17.2 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.16.0 to 1.17.2. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.16.0...v1.17.2) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 28d964b..1b59501 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.16.0 +google-auth==1.17.2 google-api-python-client==1.9.3 icalendar==4.0.6 pytz==2020.1 From d8cb34555018e4975f304412bf66e65e28404c07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2020 02:09:53 +0000 Subject: [PATCH 31/65] Bump google-auth from 1.17.2 to 1.18.0 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.17.2 to 1.18.0. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.17.2...v1.18.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1b59501..cbaca2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.17.2 +google-auth==1.18.0 google-api-python-client==1.9.3 icalendar==4.0.6 pytz==2020.1 From fff533c0a897d2269de8974b813b83429dd9dc00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2020 02:02:46 +0000 Subject: [PATCH 32/65] Bump google-api-python-client from 1.9.3 to 1.10.0 Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 1.9.3 to 1.10.0. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v1.9.3...v1.10.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cbaca2a..9751a6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.18.0 -google-api-python-client==1.9.3 +google-api-python-client==1.10.0 icalendar==4.0.6 pytz==2020.1 PyYAML==5.3.1 From a5739cb64c21f51d5e2d04ef05bcf659137f981f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2020 10:32:08 +0000 Subject: [PATCH 33/65] Bump google-auth from 1.18.0 to 1.20.1 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.18.0 to 1.20.1. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.18.0...v1.20.1) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9751a6b..e3944ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.18.0 +google-auth==1.20.1 google-api-python-client==1.10.0 icalendar==4.0.6 pytz==2020.1 From 8f56ad426e22df80f587ccd6ada6a9c7a36f8f0e Mon Sep 17 00:00:00 2001 From: Dmitry Belyaev <b4tm4n@mail.ru> Date: Tue, 11 Aug 2020 00:22:53 +0300 Subject: [PATCH 34/65] Enabling code scanning https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/enabling-code-scanning --- .github/workflows/codeql-analysis.yml | 38 +++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..c72037a --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,38 @@ +name: "CodeQL" + +on: + push: + branches: [develop, ] + pull_request: + # The branches below must be a subset of the branches above + branches: [develop] + schedule: + - cron: '0 12 10 * *' + +jobs: + analyse: + name: Analyse + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + # Override language selection by uncommenting this and choosing your languages + with: + languages: python + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From ecb2f5a3d1d9cfd24fa9af07826bc6527ef57b0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2020 02:15:09 +0000 Subject: [PATCH 35/65] Bump google-api-python-client from 1.10.0 to 1.11.0 Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 1.10.0 to 1.11.0. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v1.10.0...v1.11.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e3944ac..f4b35e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.20.1 -google-api-python-client==1.10.0 +google-api-python-client==1.11.0 icalendar==4.0.6 pytz==2020.1 PyYAML==5.3.1 From fc490dcefe5311ea4db99e1765dbdbec83dd7593 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2020 06:43:49 +0000 Subject: [PATCH 36/65] Bump google-auth from 1.20.1 to 1.21.0 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.20.1 to 1.21.0. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.20.1...v1.21.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f4b35e4..535a221 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.20.1 +google-auth==1.21.0 google-api-python-client==1.11.0 icalendar==4.0.6 pytz==2020.1 From 5649a71da27448e0e603450d320efbca881d245b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2020 02:00:49 +0000 Subject: [PATCH 37/65] Bump icalendar from 4.0.6 to 4.0.7 Bumps [icalendar](https://github.com/collective/icalendar) from 4.0.6 to 4.0.7. - [Release notes](https://github.com/collective/icalendar/releases) - [Changelog](https://github.com/collective/icalendar/blob/master/CHANGES.rst) - [Commits](https://github.com/collective/icalendar/compare/4.0.6...4.0.7) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 535a221..b253063 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.21.0 google-api-python-client==1.11.0 -icalendar==4.0.6 +icalendar==4.0.7 pytz==2020.1 PyYAML==5.3.1 From 1bec98a53ec631990f4ae0686cb837b7a167de6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2020 07:09:38 +0000 Subject: [PATCH 38/65] Bump google-auth from 1.21.0 to 1.22.0 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.21.0 to 1.22.0. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.21.0...v1.22.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b253063..e93b970 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.21.0 +google-auth==1.22.0 google-api-python-client==1.11.0 icalendar==4.0.7 pytz==2020.1 From 55ee5002ccb1ad1427286c55293365c735134691 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2020 07:15:00 +0000 Subject: [PATCH 39/65] Bump google-api-python-client from 1.11.0 to 1.12.3 Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 1.11.0 to 1.12.3. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v1.11.0...v1.12.3) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e93b970..739a35b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.22.0 -google-api-python-client==1.11.0 +google-api-python-client==1.12.3 icalendar==4.0.7 pytz==2020.1 PyYAML==5.3.1 From eca648ee56fab978a96bde235ee260ea197f1e0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2020 02:00:27 +0000 Subject: [PATCH 40/65] Bump google-auth from 1.22.0 to 1.23.0 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.22.0 to 1.23.0. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.22.0...v1.23.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 739a35b..92ed7f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.22.0 +google-auth==1.23.0 google-api-python-client==1.12.3 icalendar==4.0.7 pytz==2020.1 From a10e62f806ec98d486366e572d5655081544782e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2020 09:26:54 +0000 Subject: [PATCH 41/65] Bump google-api-python-client from 1.12.3 to 1.12.5 Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 1.12.3 to 1.12.5. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v1.12.3...v1.12.5) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 92ed7f7..823bbe2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.23.0 -google-api-python-client==1.12.3 +google-api-python-client==1.12.5 icalendar==4.0.7 pytz==2020.1 PyYAML==5.3.1 From 38dd853436d301ae3bf526948a77e97d93086452 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Dec 2020 02:00:28 +0000 Subject: [PATCH 42/65] Bump google-api-python-client from 1.12.5 to 1.12.8 Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 1.12.5 to 1.12.8. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v1.12.5...v1.12.8) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 823bbe2..b5e9887 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.23.0 -google-api-python-client==1.12.5 +google-api-python-client==1.12.8 icalendar==4.0.7 pytz==2020.1 PyYAML==5.3.1 From c2a3a547831f86c49dd85fc5a138283da2a59ba5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Dec 2020 07:20:27 +0000 Subject: [PATCH 43/65] Bump pytz from 2020.1 to 2020.4 Bumps [pytz](https://github.com/stub42/pytz) from 2020.1 to 2020.4. - [Release notes](https://github.com/stub42/pytz/releases) - [Commits](https://github.com/stub42/pytz/compare/release_2020.1...release_2020.4) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b5e9887..5cf7752 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.23.0 google-api-python-client==1.12.8 icalendar==4.0.7 -pytz==2020.1 +pytz==2020.4 PyYAML==5.3.1 From 50f90925b8bd5a4e5ac976cff7800cc6fe5c40c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Jan 2021 02:00:24 +0000 Subject: [PATCH 44/65] Bump pytz from 2020.4 to 2020.5 Bumps [pytz](https://github.com/stub42/pytz) from 2020.4 to 2020.5. - [Release notes](https://github.com/stub42/pytz/releases) - [Commits](https://github.com/stub42/pytz/compare/release_2020.4...release_2020.5) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5cf7752..ca50e6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.23.0 google-api-python-client==1.12.8 icalendar==4.0.7 -pytz==2020.4 +pytz==2020.5 PyYAML==5.3.1 From e0b4e6c28a7ad77ae769107df53c1e0110bef682 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Fri, 1 Jan 2021 13:17:08 +0300 Subject: [PATCH 45/65] drop Python 3.5 support --- .github/workflows/pythonpackage.yml | 2 +- .travis.yml | 1 - setup.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 55c17b1..17f3578 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -17,7 +17,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.5, 3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8] steps: - uses: actions/checkout@v1 diff --git a/.travis.yml b/.travis.yml index f913379..8380fa3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - - "3.5" - "3.6" - "3.7" - "3.8" diff --git a/setup.py b/setup.py index f77adb1..449abd3 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,6 @@ setuptools.setup( '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', From 1beff774bcac0501b0296e4ebe2b2e868c1185f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Jan 2021 10:20:21 +0000 Subject: [PATCH 46/65] Bump google-auth from 1.23.0 to 1.24.0 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.23.0 to 1.24.0. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.23.0...v1.24.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ca50e6b..bbdb0d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.23.0 +google-auth==1.24.0 google-api-python-client==1.12.8 icalendar==4.0.7 pytz==2020.5 From 3ddc486614c763ab18607dbcc7534aa9638827cd Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Fri, 1 Jan 2021 13:34:04 +0300 Subject: [PATCH 47/65] add Python 3.9 support --- .github/workflows/pythonpackage.yml | 2 +- .travis.yml | 3 ++- setup.py | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 17f3578..30d2477 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -17,7 +17,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v1 diff --git a/.travis.yml b/.travis.yml index 8380fa3..09d1177 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,8 @@ language: python python: - "3.6" - "3.7" - - "3.8" + - "3.8" + - "3.9" script: - pytest -v diff --git a/setup.py b/setup.py index 449abd3..bd6dc4c 100644 --- a/setup.py +++ b/setup.py @@ -24,9 +24,10 @@ setuptools.setup( 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], - python_requires='>=3.5', + python_requires='>=3.6', install_requires = [ 'google-auth>=1.5.0', 'google-api-python-client>=1.7.0', From b7cd3847bccb7359f85b8a48230ccce32829219e Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Fri, 1 Jan 2021 15:14:24 +0300 Subject: [PATCH 48/65] remove Dependabot and FOSSA badges --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 839b4c4..baf3fd5 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ [](https://badge.fury.io/py/sync-ics2gcal) [](https://travis-ci.org/b4tman/sync_ics2gcal) -[](https://dependabot.com) -[](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_shield)  Python scripts for sync .ics file with Google calendar @@ -91,7 +89,3 @@ sync-ics2gcal ## How it works  - -## License - -[](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_large) From fef3586146d8262a6d9d6c7d8d018fd14a4fe98b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 02:00:26 +0000 Subject: [PATCH 49/65] Bump pyyaml from 5.3.1 to 5.4.1 Bumps [pyyaml](https://github.com/yaml/pyyaml) from 5.3.1 to 5.4.1. - [Release notes](https://github.com/yaml/pyyaml/releases) - [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES) - [Commits](https://github.com/yaml/pyyaml/compare/5.3.1...5.4.1) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bbdb0d6..235060f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ google-auth==1.24.0 google-api-python-client==1.12.8 icalendar==4.0.7 pytz==2020.5 -PyYAML==5.3.1 +PyYAML==5.4.1 From 12653df1bf7d7c056e7a563f54ac227c826352f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Mar 2021 02:00:27 +0000 Subject: [PATCH 50/65] Bump google-auth from 1.24.0 to 1.27.0 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.24.0 to 1.27.0. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.24.0...v1.27.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 235060f..62d0da8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.24.0 +google-auth==1.27.0 google-api-python-client==1.12.8 icalendar==4.0.7 pytz==2020.5 From a02775110d18c094def2e1aadc143abb80f9865b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Mar 2021 02:00:37 +0000 Subject: [PATCH 51/65] Bump pytz from 2020.5 to 2021.1 Bumps [pytz](https://github.com/stub42/pytz) from 2020.5 to 2021.1. - [Release notes](https://github.com/stub42/pytz/releases) - [Commits](https://github.com/stub42/pytz/compare/release_2020.5...release_2021.1) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 62d0da8..3e51fbb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.27.0 google-api-python-client==1.12.8 icalendar==4.0.7 -pytz==2020.5 +pytz==2021.1 PyYAML==5.4.1 From 41c2973646b4cd029efe33e143bb87a0e212bf95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Apr 2021 02:00:37 +0000 Subject: [PATCH 52/65] Bump google-auth from 1.27.0 to 1.28.0 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.27.0 to 1.28.0. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.27.0...v1.28.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3e51fbb..7f66d00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.27.0 +google-auth==1.28.0 google-api-python-client==1.12.8 icalendar==4.0.7 pytz==2021.1 From 18224ad5b4eb4051de257a006ef5370d338f0854 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Apr 2021 08:27:02 +0000 Subject: [PATCH 53/65] Bump google-api-python-client from 1.12.8 to 2.1.0 Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 1.12.8 to 2.1.0. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v1.12.8...v2.1.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f66d00..1e67753 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.28.0 -google-api-python-client==1.12.8 +google-api-python-client==2.1.0 icalendar==4.0.7 pytz==2021.1 PyYAML==5.4.1 From 77e2cdba3676e7a9ba21410b6ce8c4dd84d4e5bc Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Thu, 29 Apr 2021 15:22:27 +0300 Subject: [PATCH 54/65] ignore ide files and virtualenv --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d98c726..b5c5b91 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,10 @@ config.yml service-account.json *.pyc my-test*.ics -.vscode/* +.vscode/ +.idea/ /dist/ /*.egg-info/ /build/ /.eggs/ +venv/ From 8669aefabe61fc565f36cad4422a847522841425 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Thu, 29 Apr 2021 15:24:23 +0300 Subject: [PATCH 55/65] type annotations - gcal --- sync_ics2gcal/gcal.py | 55 +++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/sync_ics2gcal/gcal.py b/sync_ics2gcal/gcal.py index db6b831..f5226cd 100644 --- a/sync_ics2gcal/gcal.py +++ b/sync_ics2gcal/gcal.py @@ -4,9 +4,11 @@ import google.auth from google.oauth2 import service_account from googleapiclient import discovery from pytz import utc +from datetime import datetime +from typing import List, Dict, Any, Callable, Tuple, Optional -class GoogleCalendarService(): +class GoogleCalendarService: """class for make google calendar service Resource Returns: @@ -14,13 +16,10 @@ class GoogleCalendarService(): """ @staticmethod - def default(): + def default() -> discovery.Resource: """make service Resource from default credentials (authorize) ( https://developers.google.com/identity/protocols/application-default-credentials ) ( https://googleapis.dev/python/google-auth/latest/reference/google.auth.html#google.auth.default ) - - Returns: - service Resource """ scopes = ['https://www.googleapis.com/auth/calendar'] @@ -30,11 +29,8 @@ class GoogleCalendarService(): return service @staticmethod - def from_srv_acc_file(service_account_file): + def from_srv_acc_file(service_account_file: str) -> discovery.Resource: """make service Resource from service account filename (authorize) - - Returns: - service Resource """ scopes = ['https://www.googleapis.com/auth/calendar'] @@ -47,18 +43,15 @@ class GoogleCalendarService(): return service @staticmethod - def from_config(config=None): + def from_config(config: Optional[Dict[str, Optional[str]]] = None) -> discovery.Resource: """make service Resource from config dict Arguments: - config -- dict() config with keys: + config -- config with keys: (optional) service_account: - service account filename if key not in dict then default credentials will be used ( https://developers.google.com/identity/protocols/application-default-credentials ) -- None: default credentials will be used - - Returns: - service Resource """ if config is not None and 'service_account' in config: @@ -69,7 +62,7 @@ class GoogleCalendarService(): return service -def select_event_key(event): +def select_event_key(event: Dict[str, Any]) -> Optional[str]: """select event key for logging Arguments: @@ -87,17 +80,17 @@ def select_event_key(event): return key -class GoogleCalendar(): +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 __init__(self, service: discovery.Resource, calendarId: str): + self.service: discovery.Resource = service + self.calendarId: str = calendarId - def _make_request_callback(self, action, events_by_req): + def _make_request_callback(self, action: str, events_by_req: List[Dict[str, Any]]) -> Callable: """make callback for log result of batch request Arguments: @@ -126,7 +119,7 @@ class GoogleCalendar(): action, key, event.get(key)) return callback - def list_events_from(self, start): + def list_events_from(self, start: datetime) -> List[Any]: """ list events from calendar, where start date >= start """ fields = 'nextPageToken,items(id,iCalUID,updated)' @@ -148,7 +141,7 @@ class GoogleCalendar(): self.logger.info('%d events listed', len(events)) return events - def find_exists(self, events): + def find_exists(self, events: List) -> Tuple[List[Tuple[Any, Any]], List[Any]]: """ find existing events from list, by 'iCalUID' field Arguments: @@ -166,16 +159,16 @@ class GoogleCalendar(): def list_callback(request_id, response, exception): found = False - event = events_by_req[int(request_id)] + cur_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']) + str(exception), cur_event['iCalUID']) if found: exists.append( - (event, response['items'][0])) + (cur_event, response['items'][0])) else: not_found.append(events_by_req[int(request_id)]) @@ -196,7 +189,7 @@ class GoogleCalendar(): len(exists), len(not_found)) return exists, not_found - def insert_events(self, events): + def insert_events(self, events: List[Any]): """ insert list of events Arguments: @@ -218,7 +211,7 @@ class GoogleCalendar(): i += 1 batch.execute() - def patch_events(self, event_tuples): + def patch_events(self, event_tuples: List[Tuple[Any, Any]]): """ patch (update) events Arguments: @@ -241,7 +234,7 @@ class GoogleCalendar(): i += 1 batch.execute() - def update_events(self, event_tuples): + def update_events(self, event_tuples: List[Tuple[Any, Any]]): """ update events Arguments: @@ -264,7 +257,7 @@ class GoogleCalendar(): i += 1 batch.execute() - def delete_events(self, events): + def delete_events(self, events: List[Any]): """ delete events Arguments: @@ -284,7 +277,7 @@ class GoogleCalendar(): i += 1 batch.execute() - def create(self, summary, timeZone=None): + def create(self, summary: str, timeZone: Optional[str] = None) -> Any: """create calendar Arguments: @@ -328,7 +321,7 @@ class GoogleCalendar(): body=rule_public ).execute() - def add_owner(self, email): + def add_owner(self, email: str): """add calendar owner by email Arguments: From a6474ee984e5d3ae328e19ff068d0910ee70b030 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Thu, 29 Apr 2021 16:19:41 +0300 Subject: [PATCH 56/65] type annotations - ical --- sync_ics2gcal/ical.py | 50 ++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/sync_ics2gcal/ical.py b/sync_ics2gcal/ical.py index 670ad75..678c471 100644 --- a/sync_ics2gcal/ical.py +++ b/sync_ics2gcal/ical.py @@ -1,12 +1,14 @@ import datetime import logging +from typing import Union, Dict, Any, Callable, Optional, List from icalendar import Calendar, Event from pytz import utc -def format_datetime_utc(value): +def format_datetime_utc(value: Union[datetime.date, datetime.datetime]) -> str: """utc datetime as string from date or datetime value + Arguments: value -- date or datetime value @@ -23,20 +25,23 @@ def format_datetime_utc(value): ).replace(tzinfo=None).isoformat() + 'Z' -def gcal_date_or_dateTime(value, check_value=None): +def gcal_date_or_dateTime(value: Union[datetime.date, datetime.datetime], + check_value: Union[datetime.date, datetime.datetime, None] = None)\ + -> Dict[str, str]: """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) + value: date or datetime + check_value: optional for choose result type Returns: - dict { 'date': ... } or { 'dateTime': ... } + { 'date': ... } or { 'dateTime': ... } """ if check_value is None: check_value = value - result = {} + result: Dict[str, str] = {} if isinstance(check_value, datetime.datetime): result['dateTime'] = format_datetime_utc(value) else: @@ -52,7 +57,7 @@ class EventConverter(Event): ( https://developers.google.com/calendar/v3/reference/events#resource-representations ) """ - def _str_prop(self, prop): + def _str_prop(self, prop: str) -> str: """decoded string property Arguments: @@ -64,7 +69,7 @@ class EventConverter(Event): return self.decoded(prop).decode(encoding='utf-8') - def _datetime_str_prop(self, prop): + def _datetime_str_prop(self, prop: str) -> str: """utc datetime as string from property Arguments: @@ -76,7 +81,7 @@ class EventConverter(Event): return format_datetime_utc(self.decoded(prop)) - def _gcal_start(self): + def _gcal_start(self) -> Dict[str, str]: """ event start dict from icalendar event Raises: @@ -89,7 +94,7 @@ class EventConverter(Event): value = self.decoded('DTSTART') return gcal_date_or_dateTime(value) - def _gcal_end(self): + def _gcal_end(self) -> Dict[str, str]: """event end dict from icalendar event Raises: @@ -112,7 +117,9 @@ class EventConverter(Event): raise ValueError('no DTEND or DURATION') return result - def _put_to_gcal(self, gcal_event, prop, func, ics_prop=None): + def _put_to_gcal(self, gcal_event: Dict[str, Any], + prop: str, func: Callable[[str], str], + ics_prop: Optional[str] = None): """get property from ical event if exist, and put to gcal event Arguments: @@ -127,7 +134,7 @@ class EventConverter(Event): if ics_prop in self: gcal_event[prop] = func(ics_prop) - def to_gcal(self): + def to_gcal(self) -> Dict[str, Any]: """Convert Returns: @@ -135,12 +142,11 @@ class EventConverter(Event): """ event = { - 'iCalUID': self._str_prop('UID') + 'iCalUID': self._str_prop('UID'), + 'start': self._gcal_start(), + 'end': self._gcal_end() } - 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) @@ -155,28 +161,28 @@ class EventConverter(Event): return event -class CalendarConverter(): +class CalendarConverter: """Convert icalendar events to google calendar resources """ logger = logging.getLogger('CalendarConverter') - def __init__(self, calendar=None): - self.calendar = calendar + def __init__(self, calendar: Optional[Calendar] = None): + self.calendar: Optional[Calendar] = calendar - def load(self, filename): + def load(self, filename: str): """ 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): + def loads(self, string: str): """ load calendar from ics string """ self.calendar = Calendar.from_ical(string) - def events_to_gcal(self): + def events_to_gcal(self) -> List[Dict[str, Any]]: """Convert events to google calendar resources """ From 6c571df7bcd95c4a1a0448147eb54ec3e1071544 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Thu, 29 Apr 2021 17:10:35 +0300 Subject: [PATCH 57/65] type annotations - sync --- sync_ics2gcal/gcal.py | 18 +++++------ sync_ics2gcal/sync.py | 74 ++++++++++++++++++++++++++----------------- 2 files changed, 54 insertions(+), 38 deletions(-) diff --git a/sync_ics2gcal/gcal.py b/sync_ics2gcal/gcal.py index f5226cd..e786e11 100644 --- a/sync_ics2gcal/gcal.py +++ b/sync_ics2gcal/gcal.py @@ -16,7 +16,7 @@ class GoogleCalendarService: """ @staticmethod - def default() -> discovery.Resource: + def default(): """make service Resource from default credentials (authorize) ( https://developers.google.com/identity/protocols/application-default-credentials ) ( https://googleapis.dev/python/google-auth/latest/reference/google.auth.html#google.auth.default ) @@ -29,7 +29,7 @@ class GoogleCalendarService: return service @staticmethod - def from_srv_acc_file(service_account_file: str) -> discovery.Resource: + def from_srv_acc_file(service_account_file: str): """make service Resource from service account filename (authorize) """ @@ -43,7 +43,7 @@ class GoogleCalendarService: return service @staticmethod - def from_config(config: Optional[Dict[str, Optional[str]]] = None) -> discovery.Resource: + def from_config(config: Optional[Dict[str, Optional[str]]] = None): """make service Resource from config dict Arguments: @@ -119,7 +119,7 @@ class GoogleCalendar: action, key, event.get(key)) return callback - def list_events_from(self, start: datetime) -> List[Any]: + def list_events_from(self, start: datetime) -> List[Dict[str, Any]]: """ list events from calendar, where start date >= start """ fields = 'nextPageToken,items(id,iCalUID,updated)' @@ -141,7 +141,7 @@ class GoogleCalendar: self.logger.info('%d events listed', len(events)) return events - def find_exists(self, events: List) -> Tuple[List[Tuple[Any, Any]], List[Any]]: + def find_exists(self, events: List) -> Tuple[List[Tuple[Dict[str, Any], Dict[str, Any]]], List[Dict[str, Any]]]: """ find existing events from list, by 'iCalUID' field Arguments: @@ -189,7 +189,7 @@ class GoogleCalendar: len(exists), len(not_found)) return exists, not_found - def insert_events(self, events: List[Any]): + def insert_events(self, events: List[Dict[str, Any]]): """ insert list of events Arguments: @@ -211,7 +211,7 @@ class GoogleCalendar: i += 1 batch.execute() - def patch_events(self, event_tuples: List[Tuple[Any, Any]]): + def patch_events(self, event_tuples: List[Tuple[Dict[str, Any], Dict[str, Any]]]): """ patch (update) events Arguments: @@ -234,7 +234,7 @@ class GoogleCalendar: i += 1 batch.execute() - def update_events(self, event_tuples: List[Tuple[Any, Any]]): + def update_events(self, event_tuples: List[Tuple[Dict[str, Any], Dict[str, Any]]]): """ update events Arguments: @@ -257,7 +257,7 @@ class GoogleCalendar: i += 1 batch.execute() - def delete_events(self, events: List[Any]): + def delete_events(self, events: List[Dict[str, Any]]): """ delete events Arguments: diff --git a/sync_ics2gcal/sync.py b/sync_ics2gcal/sync.py index bdc5b2f..2d77749 100644 --- a/sync_ics2gcal/sync.py +++ b/sync_ics2gcal/sync.py @@ -1,22 +1,33 @@ import datetime -import dateutil.parser import logging import operator +from typing import List, Any, Dict, Set, Tuple, Union, Callable + +import dateutil.parser from pytz import utc +from .gcal import GoogleCalendar +from .ical import CalendarConverter -class CalendarSync(): + +class CalendarSync: """class for syncronize calendar with google """ logger = logging.getLogger('CalendarSync') - def __init__(self, gcalendar, converter): - self.gcalendar = gcalendar - self.converter = converter + def __init__(self, gcalendar: GoogleCalendar, converter: CalendarConverter): + self.gcalendar: GoogleCalendar = gcalendar + self.converter: CalendarConverter = converter + self.to_insert: List[Dict[str, Any]] = [] + self.to_update: List[Tuple[Dict[str, Any], Dict[str, Any]]] = [] + self.to_delete: List[Dict[str, Any]] = [] @staticmethod - def _events_list_compare(items_src, items_dst, key='iCalUID'): + def _events_list_compare(items_src: List[Dict[str, Any]], + items_dst: List[Dict[str, Any]], + key: str = 'iCalUID') \ + -> Tuple[List[Dict[str, Any]], List[Tuple[Dict[str, Any], Dict[str, Any]]], List[Dict[str, Any]]]: """ compare list of events by key Arguments: @@ -30,16 +41,18 @@ class CalendarSync(): items_to_delete) """ - def get_key(item): return item[key] + def get_key(item: Dict[str, Any]) -> str: return item[key] - keys_src = set(map(get_key, items_src)) - keys_dst = set(map(get_key, items_dst)) + keys_src: Set[str] = set(map(get_key, items_src)) + keys_dst: Set[str] = 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): + def items_by_keys(items: List[Dict[str, Any]], + key_name: str, + keys: Set[str]) -> List[Dict[str, Any]]: return list(filter(lambda item: item[key_name] in keys, items)) items_to_insert = items_by_keys(items_src, key, keys_to_insert) @@ -57,7 +70,7 @@ class CalendarSync(): """ filter 'to_update' events by 'updated' datetime """ - def filter_updated(event_tuple): + def filter_updated(event_tuple: Tuple[Dict[str, Any], Dict[str, Any]]) -> bool: new, old = event_tuple new_date = dateutil.parser.parse(new['updated']) old_date = dateutil.parser.parse(old['updated']) @@ -66,7 +79,10 @@ class CalendarSync(): self.to_update = list(filter(filter_updated, self.to_update)) @staticmethod - def _filter_events_by_date(events, date, op): + def _filter_events_by_date(events: List[Dict[str, Any]], + date: Union[datetime.date, datetime.datetime], + op: Callable[[Union[datetime.date, datetime.datetime], + Union[datetime.date, datetime.datetime]], bool]) -> List[Dict[str, Any]]: """ filter events by start datetime Arguments: @@ -78,10 +94,10 @@ class CalendarSync(): list of filtred events """ - def filter_by_date(event): + def filter_by_date(event: Dict[str, Any]) -> bool: date_cmp = date - event_start = event['start'] - event_date = None + event_start: Dict[str, str] = event['start'] + event_date: Union[datetime.date, datetime.datetime, str, None] = None compare_dates = False if 'date' in event_start: @@ -101,7 +117,8 @@ class CalendarSync(): return list(filter(filter_by_date, events)) @staticmethod - def _tz_aware_datetime(date): + def _tz_aware_datetime(date: Union[datetime.date, datetime.datetime]) \ + -> Union[datetime.date, datetime.datetime]: """make tz aware datetime from datetime/date (utc if no tzinfo) Arguments: @@ -117,7 +134,7 @@ class CalendarSync(): date = date.replace(tzinfo=utc) return date - def prepare_sync(self, start_date): + def prepare_sync(self, start_date: Union[datetime.date, datetime.datetime]) -> None: """prepare sync lists by comparsion of events Arguments: @@ -135,28 +152,20 @@ class CalendarSync(): 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() @@ -167,14 +176,21 @@ class CalendarSync(): len(self.to_delete) ) - def apply(self): - """apply sync (insert, update, delete), using prepared lists of events + def clear(self) -> None: + """ clear prepared sync lists (insert, update, delete) + """ + self.to_insert.clear() + self.to_update.clear() + self.to_delete.clear() + + def apply(self) -> None: + """ 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.clear() - self.to_insert, self.to_update, self.to_delete = [], [], [] + self.logger.info('sync done') From 19192d1641a583d275c2b5c6bc59e239a856978d Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Fri, 30 Apr 2021 11:08:56 +0300 Subject: [PATCH 58/65] fix _tz_aware_datetime return type --- sync_ics2gcal/sync.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sync_ics2gcal/sync.py b/sync_ics2gcal/sync.py index 2d77749..475ad95 100644 --- a/sync_ics2gcal/sync.py +++ b/sync_ics2gcal/sync.py @@ -117,8 +117,7 @@ class CalendarSync: return list(filter(filter_by_date, events)) @staticmethod - def _tz_aware_datetime(date: Union[datetime.date, datetime.datetime]) \ - -> Union[datetime.date, datetime.datetime]: + def _tz_aware_datetime(date: Union[datetime.date, datetime.datetime]) -> datetime.datetime: """make tz aware datetime from datetime/date (utc if no tzinfo) Arguments: From e5064eeaed553c1e8213cef6e52d556c8a32f69a Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Fri, 30 Apr 2021 11:45:05 +0300 Subject: [PATCH 59/65] cfg optional --- sync_ics2gcal/gcal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sync_ics2gcal/gcal.py b/sync_ics2gcal/gcal.py index e786e11..6c96cbd 100644 --- a/sync_ics2gcal/gcal.py +++ b/sync_ics2gcal/gcal.py @@ -86,7 +86,7 @@ class GoogleCalendar: logger = logging.getLogger('GoogleCalendar') - def __init__(self, service: discovery.Resource, calendarId: str): + def __init__(self, service: discovery.Resource, calendarId: Optional[str]): self.service: discovery.Resource = service self.calendarId: str = calendarId From 9dab3c5709f7a0564ed0cc274603694d154d4e8d Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Fri, 30 Apr 2021 11:46:10 +0300 Subject: [PATCH 60/65] type annotations - scripts --- sync_ics2gcal/manage_calendars.py | 39 ++++++++++++++++--------------- sync_ics2gcal/sync_calendar.py | 11 +++++---- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/sync_ics2gcal/manage_calendars.py b/sync_ics2gcal/manage_calendars.py index ed3c6ef..a2c10a5 100644 --- a/sync_ics2gcal/manage_calendars.py +++ b/sync_ics2gcal/manage_calendars.py @@ -1,5 +1,6 @@ import argparse import logging.config +from typing import Optional, Dict, Any import yaml @@ -70,7 +71,7 @@ def parse_args(): return args -def load_config(): +def load_config() -> Optional[Dict[str, Any]]: result = None try: with open('config.yml', 'r', encoding='utf-8') as f: @@ -81,7 +82,7 @@ def load_config(): return result -def list_calendars(service, show_hidden, show_deleted): +def list_calendars(service, show_hidden: bool, show_deleted: bool) -> None: fields = 'nextPageToken,items(id,summary)' calendars = [] page_token = None @@ -100,7 +101,7 @@ def list_calendars(service, show_hidden, show_deleted): print('{summary}: {id}'.format_map(calendar)) -def create_calendar(service, summary, timezone, public): +def create_calendar(service, summary: str, timezone: str, public: bool) -> None: calendar = GoogleCalendar(service, None) calendar.create(summary, timezone) if public: @@ -108,33 +109,33 @@ def create_calendar(service, summary, timezone, public): print('{}: {}'.format(summary, calendar.calendarId)) -def add_owner(service, id, owner_email): - calendar = GoogleCalendar(service, id) +def add_owner(service, calendar_id: str, owner_email: str) -> None: + calendar = GoogleCalendar(service, calendar_id) calendar.add_owner(owner_email) - print('to {} added owner: {}'.format(id, owner_email)) + print('to {} added owner: {}'.format(calendar_id, owner_email)) -def remove_calendar(service, id): - calendar = GoogleCalendar(service, id) +def remove_calendar(service, calendar_id: str) -> None: + calendar = GoogleCalendar(service, calendar_id) calendar.delete() - print('removed: {}'.format(id)) + print('removed: {}'.format(calendar_id)) -def rename_calendar(service, id, summary): +def rename_calendar(service, calendar_id: str, summary: str) -> None: calendar = {'summary': summary} - service.calendars().patch(body=calendar, calendarId=id).execute() - print('{}: {}'.format(summary, id)) + service.calendars().patch(body=calendar, calendarId=calendar_id).execute() + print('{}: {}'.format(summary, calendar_id)) -def get_calendar_property(service, id, property): - response = service.calendarList().get(calendarId=id, - fields=property).execute() - print(response.get(property)) +def get_calendar_property(service, calendar_id: str, property_name: str) -> None: + response = service.calendarList().get(calendarId=calendar_id, + fields=property_name).execute() + print(response.get(property_name)) -def set_calendar_property(service, id, property, property_value): - body = {property: property_value} - response = service.calendarList().patch(body=body, calendarId=id).execute() +def set_calendar_property(service, calendar_id: str, property_name: str, property_value: str) -> None: + body = {property_name: property_value} + response = service.calendarList().patch(body=body, calendarId=calendar_id).execute() print(response) diff --git a/sync_ics2gcal/sync_calendar.py b/sync_ics2gcal/sync_calendar.py index cf39dc4..2e90b6a 100644 --- a/sync_ics2gcal/sync_calendar.py +++ b/sync_ics2gcal/sync_calendar.py @@ -1,3 +1,5 @@ +from typing import Dict, Any + import yaml import dateutil.parser @@ -12,14 +14,13 @@ from . import ( ) -def load_config(): +def load_config() -> Dict[str, Any]: 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) +def get_start_date(date_str: str) -> datetime.datetime: if 'now' == date_str: result = datetime.datetime.utcnow() else: @@ -33,8 +34,8 @@ def main(): if 'logging' in config: logging.config.dictConfig(config['logging']) - calendarId = config['calendar']['google_id'] - ics_filepath = config['calendar']['source'] + calendarId: str = config['calendar']['google_id'] + ics_filepath: str = config['calendar']['source'] start = get_start_date(config['start_from']) From c41b3a4dbd852fb458241451b316495fb825ec67 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 1 May 2021 13:47:02 +0300 Subject: [PATCH 61/65] type annotations - tests --- tests/test_converter.py | 8 +++++--- tests/test_sync.py | 45 ++++++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/tests/test_converter.py b/tests/test_converter.py index 2aadde6..37a9f97 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -1,3 +1,5 @@ +from typing import Tuple + import pytest from sync_ics2gcal import CalendarConverter @@ -26,11 +28,11 @@ LAST-MODIFIED:20180326T120235Z """ -def ics_test_cal(content): +def ics_test_cal(content: str) -> str: return "BEGIN:VCALENDAR\r\n{}END:VCALENDAR\r\n".format(content) -def ics_test_event(content): +def ics_test_event(content: str) -> str: return ics_test_cal("BEGIN:VEVENT\r\n{}END:VEVENT\r\n".format(content)) @@ -68,7 +70,7 @@ def param_events_start_end(request): return request.param -def test_event_start_end(param_events_start_end): +def test_event_start_end(param_events_start_end: Tuple[str, str, str, str]): (date_type, ics_str, start, end) = param_events_start_end converter = CalendarConverter() converter.loads(ics_str) diff --git a/tests/test_sync.py b/tests/test_sync.py index 3201929..f480260 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -3,6 +3,7 @@ import hashlib import operator from copy import deepcopy from random import shuffle +from typing import Union, List, Dict, Optional import dateutil.parser import pytest @@ -11,7 +12,7 @@ from pytz import timezone, utc from sync_ics2gcal import CalendarSync -def sha1(string): +def sha1(string: Union[str, bytes]) -> str: if isinstance(string, str): string = string.encode('utf8') h = hashlib.sha1() @@ -19,55 +20,57 @@ def sha1(string): return h.hexdigest() -def gen_events(start, stop, start_time, no_time=False): +def gen_events(start: int, + stop: int, + start_time: Union[datetime.datetime, datetime.date], + no_time: bool = False) -> List[Dict[str, Union[str, Dict[str, str]]]]: if no_time: start_time = datetime.date( start_time.year, start_time.month, start_time.day) - duration = datetime.date(1, 1, 2) - datetime.date(1, 1, 1) - date_key = "date" - suff = '' + duration: datetime.timedelta = datetime.date(1, 1, 2) - datetime.date(1, 1, 1) + date_key: str = "date" + date_end: str = '' else: start_time = utc.normalize( start_time.astimezone(utc)).replace(tzinfo=None) - duration = datetime.datetime( - 1, 1, 1, 2) - datetime.datetime(1, 1, 1, 1) - date_key = "dateTime" - suff = 'Z' + duration: datetime.timedelta = datetime.datetime(1, 1, 1, 2) - datetime.datetime(1, 1, 1, 1) + date_key: str = "dateTime" + date_end: str = 'Z' - result = [] + result: List[Dict[str, Union[str, Dict[str, str]]]] = [] for i in range(start, stop): event_start = start_time + (duration * i) event_end = event_start + duration - updated = event_start + updated: Union[datetime.datetime, datetime.date] = event_start if no_time: updated = datetime.datetime( updated.year, updated.month, updated.day, 0, 0, 0, 1, tzinfo=utc) - event = { + event: Dict[str, Union[str, Dict[str, str]]] = { 'summary': 'test event __ {}'.format(i), 'location': 'la la la {}'.format(i), 'description': 'test TEST -- test event {}'.format(i), "iCalUID": "{}@test.com".format(sha1("test - event {}".format(i))), "updated": updated.isoformat() + 'Z', - "created": updated.isoformat() + 'Z' + "created": updated.isoformat() + 'Z', + 'start': {date_key: event_start.isoformat() + date_end}, + 'end': {date_key: event_end.isoformat() + date_end} } - event['start'] = {date_key: event_start.isoformat() + suff} - event['end'] = {date_key: event_end.isoformat() + suff} result.append(event) return result -def gen_list_to_compare(start, stop): - result = [] +def gen_list_to_compare(start: int, stop: int) -> List[Dict[str, str]]: + result: List[Dict[str, str]] = [] for i in range(start, stop): result.append({'iCalUID': 'test{:06d}'.format(i)}) return result -def get_start_date(event): - event_start = event['start'] - start_date = None +def get_start_date(event: Dict[str, Union[str, Dict[str, str]]]) -> Union[datetime.datetime, datetime.date]: + event_start: Dict[str, str] = event['start'] + start_date: Optional[str] = None is_date = False if 'date' in event_start: start_date = event_start['date'] @@ -113,7 +116,7 @@ def test_compare(): @pytest.mark.parametrize("no_time", [True, False], ids=['date', 'dateTime']) -def test_filter_events_by_date(no_time): +def test_filter_events_by_date(no_time: bool): msk = timezone('Europe/Moscow') now = utc.localize(datetime.datetime.utcnow()) msk_now = msk.normalize(now.astimezone(msk)) From 1cdf1da6eea8cfa9dc93443aa0385f9509187fa9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 May 2021 02:01:52 +0000 Subject: [PATCH 62/65] Bump google-auth from 1.28.0 to 1.30.0 Bumps [google-auth](https://github.com/googleapis/google-auth-library-python) from 1.28.0 to 1.30.0. - [Release notes](https://github.com/googleapis/google-auth-library-python/releases) - [Changelog](https://github.com/googleapis/google-auth-library-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-auth-library-python/compare/v1.28.0...v1.30.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1e67753..3958fbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -google-auth==1.28.0 +google-auth==1.30.0 google-api-python-client==2.1.0 icalendar==4.0.7 pytz==2021.1 From 97614ae21d8aae74cbda16d7224b3d11b83f9648 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 May 2021 12:08:53 +0000 Subject: [PATCH 63/65] Bump google-api-python-client from 2.1.0 to 2.3.0 Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 2.1.0 to 2.3.0. - [Release notes](https://github.com/googleapis/google-api-python-client/releases) - [Changelog](https://github.com/googleapis/google-api-python-client/blob/master/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-api-python-client/compare/v2.1.0...v2.3.0) Signed-off-by: dependabot[bot] <support@github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3958fbd..bf22b57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ google-auth==1.30.0 -google-api-python-client==2.1.0 +google-api-python-client==2.3.0 icalendar==4.0.7 pytz==2021.1 PyYAML==5.4.1 From 3b0de9d636e92820e850c15c800302ceac0b17e3 Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 1 May 2021 17:31:14 +0300 Subject: [PATCH 64/65] fire instead of argparse --- requirements.txt | 1 + sync_ics2gcal/manage_calendars.py | 235 +++++++++++++----------------- 2 files changed, 102 insertions(+), 134 deletions(-) diff --git a/requirements.txt b/requirements.txt index bf22b57..08659f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ google-api-python-client==2.3.0 icalendar==4.0.7 pytz==2021.1 PyYAML==5.4.1 +fire==0.4.0 \ No newline at end of file diff --git a/sync_ics2gcal/manage_calendars.py b/sync_ics2gcal/manage_calendars.py index a2c10a5..dfd0e2d 100644 --- a/sync_ics2gcal/manage_calendars.py +++ b/sync_ics2gcal/manage_calendars.py @@ -1,80 +1,16 @@ -import argparse import logging.config -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List +import fire import yaml 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') - # list - parser_list = command_subparsers.add_parser('list', help='list calendars') - parser_list.add_argument( - '--show-hidden', default=False, - action='store_true', help='show hidden calendars') - parser_list.add_argument( - '--show-deleted', default=False, - action='store_true', help='show deleted calendars') - # create - 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') - # add_owner - 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') - # remove - parser_remove = command_subparsers.add_parser( - 'remove', help='remove calendar') - parser_remove.add_argument( - 'id', action='store', help='calendar id to remove') - # rename - 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') - # get - parser_get = command_subparsers.add_parser( - 'get', help='get calendar property') - parser_get.add_argument( - 'id', action='store', help='calendar id') - parser_get.add_argument( - 'property', action='store', help='property key') - # set - parser_set = command_subparsers.add_parser( - 'set', help='set calendar property') - parser_set.add_argument( - 'id', action='store', help='calendar id') - parser_set.add_argument( - 'property', action='store', help='property key') - parser_set.add_argument( - 'property_value', action='store', help='property value') - - args = parser.parse_args() - if args.command is None: - parser.print_usage() - return args - - -def load_config() -> Optional[Dict[str, Any]]: +def load_config(filename: str) -> Optional[Dict[str, Any]]: result = None try: - with open('config.yml', 'r', encoding='utf-8') as f: + with open(filename, 'r', encoding='utf-8') as f: result = yaml.safe_load(f) except FileNotFoundError: pass @@ -82,87 +18,118 @@ def load_config() -> Optional[Dict[str, Any]]: return result -def list_calendars(service, show_hidden: bool, show_deleted: bool) -> None: - fields = 'nextPageToken,items(id,summary)' - calendars = [] - page_token = None - while True: - response = service.calendarList().list(fields=fields, - pageToken=page_token, - showHidden=show_hidden, - showDeleted=show_deleted - ).execute() - if 'items' in response: - calendars.extend(response['items']) - page_token = response.get('nextPageToken') - if not page_token: - break - for calendar in calendars: - print('{summary}: {id}'.format_map(calendar)) +class Commands: + """ manage google calendars in service account """ + def __init__(self, config: str = 'config.yml'): + """ -def create_calendar(service, summary: str, timezone: str, public: bool) -> None: - calendar = GoogleCalendar(service, None) - calendar.create(summary, timezone) - if public: - calendar.make_public() - print('{}: {}'.format(summary, calendar.calendarId)) + Args: + config(str): config filename + """ + self._config: Optional[Dict[str, Any]] = load_config(config) + if self._config is not None and 'logging' in self._config: + logging.config.dictConfig(self._config['logging']) + self._service = GoogleCalendarService.from_config(self._config) + def list(self, show_hidden: bool = False, show_deleted: bool = False) -> None: + """ list calendars -def add_owner(service, calendar_id: str, owner_email: str) -> None: - calendar = GoogleCalendar(service, calendar_id) - calendar.add_owner(owner_email) - print('to {} added owner: {}'.format(calendar_id, owner_email)) + Args: + show_hidden: show hidden calendars + show_deleted: show deleted calendars + """ + fields: str = 'nextPageToken,items(id,summary)' + calendars: List[Dict[str, Any]] = [] + page_token: Optional[str] = None + while True: + calendars_api = self._service.calendarList() + response = calendars_api.list(fields=fields, + pageToken=page_token, + showHidden=show_hidden, + showDeleted=show_deleted + ).execute() + if 'items' in response: + calendars.extend(response['items']) + page_token = response.get('nextPageToken') + if page_token is None: + break + for calendar in calendars: + print('{summary}: {id}'.format_map(calendar)) -def remove_calendar(service, calendar_id: str) -> None: - calendar = GoogleCalendar(service, calendar_id) - calendar.delete() - print('removed: {}'.format(calendar_id)) + def create(self, summary: str, timezone: Optional[str] = None, public: bool = False) -> None: + """ create calendar + Args: + summary: new calendar summary + timezone: new calendar timezone + public: make calendar public + """ + calendar = GoogleCalendar(self._service, None) + calendar.create(summary, timezone) + if public: + calendar.make_public() + print('{}: {}'.format(summary, calendar.calendarId)) -def rename_calendar(service, calendar_id: str, summary: str) -> None: - calendar = {'summary': summary} - service.calendars().patch(body=calendar, calendarId=calendar_id).execute() - print('{}: {}'.format(summary, calendar_id)) + def add_owner(self, calendar_id: str, email: str) -> None: + """ add owner to calendar + Args: + calendar_id: calendar id + email: new owner email + """ + calendar = GoogleCalendar(self._service, calendar_id) + calendar.add_owner(email) + print('to {} added owner: {}'.format(calendar_id, email)) -def get_calendar_property(service, calendar_id: str, property_name: str) -> None: - response = service.calendarList().get(calendarId=calendar_id, - fields=property_name).execute() - print(response.get(property_name)) + def remove(self, calendar_id: str) -> None: + """ remove calendar + Args: + calendar_id: calendar id + """ + calendar = GoogleCalendar(self._service, calendar_id) + calendar.delete() + print('removed: {}'.format(calendar_id)) -def set_calendar_property(service, calendar_id: str, property_name: str, property_value: str) -> None: - body = {property_name: property_value} - response = service.calendarList().patch(body=body, calendarId=calendar_id).execute() - print(response) + def rename(self, calendar_id: str, summary: str) -> None: + """ rename calendar + + Args: + calendar_id: calendar id + summary: + """ + calendar = {'summary': summary} + self._service.calendars().patch(body=calendar, calendarId=calendar_id).execute() + print('{}: {}'.format(summary, calendar_id)) + + def get(self, calendar_id: str, property_name: str) -> None: + """ get calendar property + + Args: + calendar_id: calendar id + property_name: property key + """ + response = self._service.calendarList().get(calendarId=calendar_id, + fields=property_name).execute() + print(response.get(property_name)) + + def set(self, calendar_id: str, property_name: str, property_value: str) -> None: + """ set calendar property + + Args: + calendar_id: calendar id + property_name: property key + property_value: property value + """ + body = {property_name: property_value} + response = self._service.calendarList().patch(body=body, calendarId=calendar_id).execute() + print(response) def main(): - args = parse_args() - config = load_config() - - if config is not None and 'logging' in config: - logging.config.dictConfig(config['logging']) - - service = GoogleCalendarService.from_config(config) - - if 'list' == args.command: - list_calendars(service, args.show_hidden, args.show_deleted) - 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) - elif 'get' == args.command: - get_calendar_property(service, args.id, args.property) - elif 'set' == args.command: - set_calendar_property( - service, args.id, args.property, args.property_value) + fire.Fire(Commands, name='manage-ics2gcal') if __name__ == '__main__': From c3bdd25d5a08b7a670607e39246bbed864899b6e Mon Sep 17 00:00:00 2001 From: Dmitry <b4tm4n@mail.ru> Date: Sat, 1 May 2021 17:58:30 +0300 Subject: [PATCH 65/65] cli group for property commands --- README.md | 13 +++++--- sync_ics2gcal/manage_calendars.py | 54 ++++++++++++++++++------------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index baf3fd5..90dd467 100644 --- a/README.md +++ b/README.md @@ -63,18 +63,23 @@ wget https://raw.githubusercontent.com/b4tman/sync_ics2gcal/develop/sample-confi ### Manage calendars ```sh -manage-ics2gcal <subcommand> [-h] [options] +manage-ics2gcal GROUP | COMMAND ``` -subcomands: +**GROUPS**: + +* **property** - get/set properties (see [CalendarList resource](https://developers.google.com/calendar/v3/reference/calendarList#resource)), subcommands: + - **get** - get calendar property + - **set** - set calendar property + +**COMMANDS**: * **list** - list calendars * **create** - create calendar * **add_owner** - add owner to calendar * **remove** - remove calendar * **rename** - rename calendar -* **get** - get calendar property (see [CalendarList resource](https://developers.google.com/calendar/v3/reference/calendarList#resource)) -* **set** - set calendar property + Use **-h** for more info. diff --git a/sync_ics2gcal/manage_calendars.py b/sync_ics2gcal/manage_calendars.py index dfd0e2d..f3e9cdc 100644 --- a/sync_ics2gcal/manage_calendars.py +++ b/sync_ics2gcal/manage_calendars.py @@ -18,6 +18,36 @@ def load_config(filename: str) -> Optional[Dict[str, Any]]: return result +class PropertyCommands: + """ get/set google calendar properties """ + + def __init__(self, _service): + self._service = _service + + def get(self, calendar_id: str, property_name: str) -> None: + """ get calendar property + + Args: + calendar_id: calendar id + property_name: property key + """ + response = self._service.calendarList().get(calendarId=calendar_id, + fields=property_name).execute() + print(response.get(property_name)) + + def set(self, calendar_id: str, property_name: str, property_value: str) -> None: + """ set calendar property + + Args: + calendar_id: calendar id + property_name: property key + property_value: property value + """ + body = {property_name: property_value} + response = self._service.calendarList().patch(body=body, calendarId=calendar_id).execute() + print(response) + + class Commands: """ manage google calendars in service account """ @@ -31,6 +61,7 @@ class Commands: if self._config is not None and 'logging' in self._config: logging.config.dictConfig(self._config['logging']) self._service = GoogleCalendarService.from_config(self._config) + self.property = PropertyCommands(self._service) def list(self, show_hidden: bool = False, show_deleted: bool = False) -> None: """ list calendars @@ -104,29 +135,6 @@ class Commands: self._service.calendars().patch(body=calendar, calendarId=calendar_id).execute() print('{}: {}'.format(summary, calendar_id)) - def get(self, calendar_id: str, property_name: str) -> None: - """ get calendar property - - Args: - calendar_id: calendar id - property_name: property key - """ - response = self._service.calendarList().get(calendarId=calendar_id, - fields=property_name).execute() - print(response.get(property_name)) - - def set(self, calendar_id: str, property_name: str, property_value: str) -> None: - """ set calendar property - - Args: - calendar_id: calendar id - property_name: property key - property_value: property value - """ - body = {property_name: property_value} - response = self._service.calendarList().patch(body=body, calendarId=calendar_id).execute() - print(response) - def main(): fire.Fire(Commands, name='manage-ics2gcal')