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
 
+[![PyPI version](https://badge.fury.io/py/sync-ics2gcal.svg)](https://badge.fury.io/py/sync-ics2gcal)
 [![Build Status](https://travis-ci.org/b4tman/sync_ics2gcal.svg?branch=master)](https://travis-ci.org/b4tman/sync_ics2gcal)
-[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=b4tman/sync_ics2gcal)](https://dependabot.com)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_shield)
+[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=b4tman/sync_ics2gcal)](https://dependabot.com)
+[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_shield)
 ![Python package status](https://github.com/b4tman/sync_ics2gcal/workflows/Python%20package/badge.svg)
 
 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
 
 ![How it works](how-it-works.png)

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
 
 ![How it works](how-it-works.png)
 
-
 ## License
+
 [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal.svg?type=large)](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
 
-[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_large)
\ No newline at end of file
+[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal.svg?type=large)](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 @@
 
 [![PyPI version](https://badge.fury.io/py/sync-ics2gcal.svg)](https://badge.fury.io/py/sync-ics2gcal)
 [![Build Status](https://travis-ci.org/b4tman/sync_ics2gcal.svg?branch=master)](https://travis-ci.org/b4tman/sync_ics2gcal)
-[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=b4tman/sync_ics2gcal)](https://dependabot.com)
-[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal?ref=badge_shield)
 ![Python package status](https://github.com/b4tman/sync_ics2gcal/workflows/Python%20package/badge.svg)
 
 Python scripts for sync .ics file with Google calendar
@@ -91,7 +89,3 @@ sync-ics2gcal
 ## How it works
 
 ![How it works](how-it-works.png)
-
-## License
-
-[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fb4tman%2Fsync_ics2gcal.svg?type=large)](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')