Repository: Ravio1i/notion-gcal-sync (language: python)

Issue:
Add automatic setup
## Description

Currently the setup takes too much manual effort. Let's automate some of it 🥳 

## Possible solution

Running the script detects the config file aswell as the credentials and lets the user input the wanted configurations


Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..6a64bea
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,3 @@
+[flake8]
+max-line-length = 128
+max-complexity = 18
diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yml
similarity index 50%
rename from .github/workflows/test.yml
rename to .github/workflows/ci.yml
index 36b8ac0..b1d29ff 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/ci.yml
@@ -11,15 +11,20 @@ on:
 
 jobs:
   unit_test:
-
     runs-on: ubuntu-latest
-
     steps:
     - uses: actions/checkout@v2
-    - name: Set up Python 3.9
-      uses: actions/setup-python@v2
+    - uses: actions/setup-python@v2
       with:
         python-version: 3.9
+    - name: Install Python Poetry
+      uses: abatilo/actions-poetry@v2.1.0
+      with:
+        poetry-version: 1.1.11
+    - name: Configure poetry
+      shell: bash
+      run: python -m poetry config virtualenvs.in-project true
+
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
@@ -32,4 +37,21 @@ jobs:
         flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
     - name: Test with pytest
       run: |
-        pytest tests/unit --cov=.
+        python -m poetry run python -m pytest -v tests
+
+  Release:
+    needs: Quality
+    # https://github.community/t/how-do-i-specify-job-dependency-running-in-another-workflow/16482
+    if: github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, 'chore(release):')
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: 3.9
+      - name: Semantic Release
+        run: |
+          pip install python-semantic-release
+          git config user.name github-actions
+          git config user.email github-actions@github.com
+          semantic-release publish
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 77d1858..ad2e48d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,4 +1,4 @@
-name: Create and publish a Docker image
+name: Release
 
 on:
   push:
@@ -9,7 +9,7 @@ env:
   IMAGE_NAME: ${{ github.repository }}
 
 jobs:
-  build-and-push-image:
+  docker:
     runs-on: ubuntu-latest
     permissions:
       contents: read
@@ -39,3 +39,10 @@ jobs:
           push: true
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
+
+  pip:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+      with:
+        ref: ${{ github.event.inputs.branch }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..82ce223
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,19 @@
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.0.1
+    hooks:
+      - id: check-yaml
+      - id: trailing-whitespace
+        args: [--markdown-linebreak-ext=md]
+      - id: end-of-file-fixer
+      - id: double-quote-string-fixer
+      - id: no-commit-to-branch
+        args : [--branch, main, --branch, release]
+      - id: mixed-line-ending
+      - id: check-toml
+      - id: check-merge-conflict
+  - repo: https://github.com/PyCQA/flake8
+    rev: 4.0.1
+    hooks:
+      - id: flake8
+        additional_dependencies: [ flake8-typing-imports==1.7.0 ]
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index c75cfe8..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,10 +0,0 @@
-include README.md
-include LICENSE
-include notion_gcal_sync/config.default.yml
-
-
-exclude notion_gcal_sync/client-credentials.json
-exclude notion_gcal_sync/token.json
-exclude notion_gcal_sync/config.yml
-
-recursive-exclude tests/ *
diff --git a/README.md b/README.md
index c4023f4..a653bc3 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,20 @@
 [![Unit Tests](https://github.com/Ravio1i/notion-gcal-sync/actions/workflows/test.yml/badge.svg)](https://github.com/Ravio1i/notion-gcal-sync/actions/workflows/test.yml)
+[![Create and publish a Docker image](https://github.com/Ravio1i/notion-gcal-sync/actions/workflows/release.yml/badge.svg?branch=main&event=deployment)](https://github.com/Ravio1i/notion-gcal-sync/actions/workflows/release.yml)
+[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
+
 # Notion-GCal-Sync
 
 Notion-GCal-Sync is a python application to bidirectional synchronize calendar events within notion and google calendar.
 
-## Setup 
-
-With [pip](https://pypi.org/project/notion-gcal-sync/)
-
-```bash
-pip install notion-gcal-sync
-```
-
-With docker
+## Getting started
 
-```yaml 
-docker pull notion-gcal-sync
-```
-
-Keep following [these instructions](docs/setup.md).
+Follow [these instructions](https://github.com/Ravio1i/notion-gcal-sync/blob/main/docs/setup.md).
 
 ## Usage
 
-**IMPORTANT:** Make sure you followed the [setup](docs/setup.md) and configured the `config.yml` with your notion token and page for Notion API and gathered and setup credentials `client_secret.json` for Google Calendar API.
+**IMPORTANT:** Make sure you followed the [setup](https://github.com/Ravio1i/notion-gcal-sync/blob/main/docs/setup.md) and
+configured the `config.yml` with your notion token and page for Notion API and gathered and setup
+credentials `client_secret.json` for Google Calendar API.
 
 From pip and running directly
 
@@ -38,29 +31,30 @@ docker run --net=host -it \
     notion-gcal-sync
 ```
 
-On first run or when token is old you will be asked to authorize the application. Follow the link and authorize with your account. After authorization the application will continue.
+On first run or when token is old you will be asked to authorize the application. Follow the link and authorize with your
+account. After authorization the application will continue.
 
 ```bash
 $ notion-gcal-sync
 ...
-Please visit this URL to authorize this application: 
+Please visit this URL to authorize this application:
 https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=***
 ```
 
-
 ## Notes
 
 BE AWARE OF THE FOLLOWING:
-* This sync will update your source links in gcal. Links to mail etc. will get overwritten with a link to the notion page.
-  The original links will be put on top of the description
-* This sync will update all your invites from other calendars not specified to your default calendar. There is a button on gcal to restore 
-  back
-* Goals defined from calendar apps are skipped.
-* Recurrent original events are skipped. Recurrent occurrences of events are created one by one in notion. 
-  Changing in notion will change only an occurrence in GCal.
 
+* This sync will update your source links in gcal. Links to mail etc. will get overwritten with a link to the notion page. The
+  original links will be put on top of the description
+* This sync will update all your invites from other calendars not specified to your default calendar. There is a button on gcal
+  to restore back
+* Goals defined from calendar apps are skipped.
+* Recurrent original events are skipped. Recurrent occurrences of events are created one by one in notion. Changing in notion
+  will change only an occurrence in GCal.
 
 ## Notes
 
 With around ~2500 events in gcal the sync:
-* to get all events took ~1min
\ No newline at end of file
+
+* to get all events took ~1min
diff --git a/docs/setup.md b/docs/setup.md
index 79758ca..d8c53a6 100644
--- a/docs/setup.md
+++ b/docs/setup.md
@@ -4,24 +4,27 @@ This document describes how to set up and configure your notion-gcal-sync.
 
 ## Installation
 
+With [pip](https://pypi.org/project/notion-gcal-sync/)
+
 ```bash
 pip install notion-gcal-sync
 ```
 
-With docker (Not the mounting of `client_secret.json` and `config.yml`)
+With [docker](https://github.com/Ravio1i/notion-gcal-sync/pkgs/container/notion-gcal-sync)
 
 ```yaml
-docker pull notion-gcal-sync
+docker pull ghrc.io/ravio1i/notion-gcal-sync
 ```
 
 ## Configuration
 
 Additionally, to installing the python package you will need to properly configure and authenticate. This requires the 2 files:
+
 * `config.yml`
 * `client_secret.yml`
 
-The default `config.yml` is included in the installation and can be found in its python library directory.
-This file will be configured in the following setup. To get the location of your `config.yml`:
+The default `config.yml` is included in the installation and can be found in its python library directory. This file will be
+configured in the following setup. To get the location of your `config.yml`:
 
 * Linux (or WSL):
     ```bash
@@ -29,7 +32,7 @@ This file will be configured in the following setup. To get the location of your
     ls $NOTION_GCAL_SYNC_LIB
     ```
 
-* Windows: 
+* Windows:
 
   Search where your Python library got installed.
     ```powershell
@@ -38,7 +41,8 @@ This file will be configured in the following setup. To get the location of your
 
 ## Setup credentials for Google Calendar
 
-The following is a summary of [this](https://developers.google.com/workspace/guides/create-credentials) and will get you the credentials for authenticating against your Google calendar.
+The following is a summary of [this](https://developers.google.com/workspace/guides/create-credentials) and will get you the
+credentials for authenticating against your Google calendar.
 
 1. New Project and call it something e.g `notion`. (Note: Make sure to select the newly created project)
 2. Navigate to the [API Library](https://console.cloud.google.com/apis/).
@@ -46,44 +50,48 @@ The following is a summary of [this](https://developers.google.com/workspace/gui
 4. Navigate to the [API Credentials Page](https://console.cloud.google.com/apis/credentials).
 5. Click `+ Create Credentials` > `OAuth client ID`
 6. If Consent Screen > `CONFIGURE CONSENT SCREEN`
-   1. OAuth consent Screen
-      1. Set User Type to `External`
-      2. For App information use any App name, like `notion-gcal-sync` and for support email use your email.
-      3. For Developer contact information use your email as well.
-      4. `Save and continue`
-   2. Scopes
-      1. Click `Add or Remove Scopes`
-      2. Select `Google Calendar API` with Scope `.../auth/calendar` and click `Update`
-      3. `Save and Continue`
-   3. Test users
-      1. `+ Add users` and add your email
-      2. `Save and continue` (The error `Ineligible accounts not added` can be ignored)
+    1. OAuth consent Screen
+        1. Set User Type to `External`
+        2. For App information use any App name, like `notion-gcal-sync` and for support email use your email.
+        3. For Developer contact information use your email as well.
+        4. `Save and continue`
+    2. Scopes
+        1. Click `Add or Remove Scopes`
+        2. Select `Google Calendar API` with Scope `.../auth/calendar` and click `Update`
+        3. `Save and Continue`
+    3. Test users
+        1. `+ Add users` and add your email
+        2. `Save and continue` (The error `Ineligible accounts not added` can be ignored)
 7. Navigate to the [API Credentials Page](https://console.cloud.google.com/apis/credentials).
 8. Click `+ Create Credentials` > `OAuth client ID`
-   1. Select `Desktop App` with any name, like `notion-gcal-sync`
-   2. `Create` 
-   3. In the new prompt `DOWNLOAD JSON` or on the just created credentials on `Actions` click the `Download OAuth client` icon.
+    1. Select `Desktop App` with any name, like `notion-gcal-sync`
+    2. `Create`
+    3. In the new prompt `DOWNLOAD JSON` or on the just created credentials on `Actions` click the `Download OAuth client`
+       icon.
 
-The `token.json` will store the user's access and refresh tokens as json and is created automatically when the authorization flow completes for the first time.
+The `token.json` will store the user's access and refresh tokens as json and is created automatically when the authorization
+flow completes for the first time.
 
 10. Move your credentials into this folder as `client_secret.json`.
-    
+
     **Linux (or WSL)**
     ```bash
     NOTION_GCAL_SYNC_LIB="$(pip list -v | grep notion-gcal-sync | awk '{print $3}')/notion_gcal_sync"
     cp client_secret_*.apps.googleusercontent.com.json "$NOTION_GCAL_SYNC_LIB/client_secret.json"
     ```
-    
+
     **Windows**
 
-    Search where your Python library got installed and copy your `client_secret_*.apps.googleusercontent.com.json` as `client_secret.json`. inside
+    Search where your Python library got installed and copy your `client_secret_*.apps.googleusercontent.com.json`
+    as `client_secret.json`. inside
     ```powershell
     # TODO
     ```
 
 ## Setup credentials for Notion
 
-The following is a summary of [this](https://developers.notion.com/docs/authorization) and will set up your notion integration to authenticate this application for notion.
+The following is a summary of [this](https://developers.notion.com/docs/authorization) and will set up your notion integration
+to authenticate this application for notion.
 
 1. Navigate to [Notion Integrations](https://www.notion.so/my-integrations).
 2. Click `Create new integration` and give it name, like `notion-gcal-sync`.
@@ -98,14 +106,17 @@ The following is a summary of [this](https://developers.notion.com/docs/authoriz
 
 ### Set up your Notion page
 
-1. Duplicate [this page](https://virtuose.notion.site/130c26a74ca44da585506be9e8af678d?v=f76cc35334204f5abf0cd749134dc047) to your notion. 
+1. Duplicate [this page](https://virtuose.notion.site/130c26a74ca44da585506be9e8af678d?v=f76cc35334204f5abf0cd749134dc047) to
+   your notion.
+
+   Don't be overwhelmed by the amount of properties. You can hide or [rename columns](#Columns) you don't want to see, **but do
+   not remove them!**
 
-    Don't be overwhelmed by the amount of properties. You can hide or [rename columns](#Columns) you don't want to see, **but do not remove them!**
+2. Click `Share` in the top right corner of your duplicated page and `Invite`
+   your [Notion Integration](#setup-credentials-for-notion) with `Can edit` .
 
-2. Click `Share` in the top right corner of your duplicated page and `Invite` your [Notion Integration](#setup-credentials-for-notion) with `Can edit` .
+3. Copy the URL of your duplicated page and add it to your `config.yml`.
 
-3. Copy the URL of your duplicated page and add it to your `config.yml`. 
-   
    **Append a `&p=` to the URL.**
 
     ```yaml
@@ -120,12 +131,12 @@ The following is a summary of [this](https://developers.notion.com/docs/authoriz
 1. Navigate to [Google Calendar](https://calendar.google.com) and go to Settings.
 2. Click on your calendar you want to have synced with notion. and click `Integrate Calendar`
 3. Copy the `Calendar-ID`
-   1. For your default calendar this is something like `***@gmail.com`
-   2. For other calendars this is something like: `***@group.calendar.google.com`
+    1. For your default calendar this is something like `***@gmail.com`
+    2. For other calendars this is something like: `***@group.calendar.google.com`
 4. Add the `Calendar-ID` to your `config.yml` and specify your default calendar as `default_calendar_id`.
 
-   Add any amount of additional calendars to the `calendars`.
-   Make sure your default calendar is specified in the calendars section as well.
+   Add any amount of additional calendars to the `calendars`. Make sure your default calendar is specified in the calendars
+   section as well.
     ```yaml
    ...
     gcal:
@@ -145,11 +156,14 @@ Automatic: You should not change these values yourself!
 * `Description: (text)`: The description of the event.
 * `Location: (text)`: Location field, e.g. place in Google Maps.
 * `Calendar (select)`: The Name of the calendar specified as a key in `calendars` section.
-* `Delete? (checkbox)`: If you select this, the event will be removed from Google Calendar and the `Deleted` column will be set.
+* `Delete? (checkbox)`: If you select this, the event will be removed from Google Calendar and the `Deleted` column will be
+  set.
 * `Delete (checkbox) [Automatic]`:  Indicates whether an event got deleted from Google Calendar.
-* `Read Only (checkbox) (Automatic)`: This gets selected automatically by events which are from other calendars than yours. This is for example if you accept a meeting from someone. Because you cannot change this event this is marked as read only. 
+* `Read Only (checkbox) (Automatic)`: This gets selected automatically by events which are from other calendars than yours.
+  This is for example if you accept a meeting from someone. Because you cannot change this event this is marked as read only.
 * `GCal event url (url) [Automatic]`: Link to your Google Calendar event.
-* `GCal Recurrence (text) [Automatic]`: If specified, indicates the url of the event with recurrence in Google Calendar. Changing an event with recurrence will only change one occurrence.
+* `GCal Recurrence (text) [Automatic]`: If specified, indicates the url of the event with recurrence in Google Calendar.
+  Changing an event with recurrence will only change one occurrence.
 * `GCal Calendar Id (select) [Automatic]`: The Calendar id. One of the calendar ids specified in calendar section.
 * `Last Updated (last_edited_time) [Automatic]`: Indicates if you changed something in notion.
 * `Last Synced (text) [Automatic]`: Indicates as timestamp as text when the last sync changed a row.
@@ -160,4 +174,4 @@ Automatic: You should not change these values yourself!
 
 * `no_date_action`: What to do with items that don't have a date. Defaults to `skip`.
     * `skip`: Won't sync this item
-    * `today`: Updates the items `date` to today's date and synchronizes.
\ No newline at end of file
+    * `today`: Updates the items `date` to today's date and synchronizes.
diff --git a/environment.yml b/environment.yml
new file mode 100644
index 0000000..2875cec
--- /dev/null
+++ b/environment.yml
@@ -0,0 +1,5 @@
+name: notion-gcal-sync
+channels:
+  - defaults
+dependencies:
+  - python==3.9
diff --git a/install.sh b/install.sh
deleted file mode 100755
index e2032b7..0000000
--- a/install.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/bash
-
-CONFIG_FILE="notion_gcal_sync/config.yml"
-CLIENT_CREDENTIALS="notion_gcal_sync/client_credentials.json"
-CLIENT_TOKEN="notion_gcal_sync/token.json"
-
-pip install .
-
-NOTION_GCAL_SYNC_LIB="$(pip list -v | grep notion-gcal-sync | awk '{print $3}')/notion_gcal_sync"
-cp $CONFIG_FILE "$NOTION_GCAL_SYNC_LIB/config.yml"
-cp $CLIENT_CREDENTIALS "$NOTION_GCAL_SYNC_LIB/client_credentials.json"
-cp $CLIENT_TOKEN "$NOTION_GCAL_SYNC_LIB/token.json"
diff --git a/notion_gcal_sync/__init__.py b/notion_gcal_sync/__init__.py
index e69de29..1f356cc 100644
--- a/notion_gcal_sync/__init__.py
+++ b/notion_gcal_sync/__init__.py
@@ -0,0 +1,1 @@
+__version__ = '1.0.0'
diff --git a/notion_gcal_sync/__main__.py b/notion_gcal_sync/__main__.py
index e9ee777..24c9ffc 100644
--- a/notion_gcal_sync/__main__.py
+++ b/notion_gcal_sync/__main__.py
@@ -3,16 +3,23 @@
 Invoke as `notion-gcal-sync' or `python -m notion-gcal-sync'
 or 'python -m notion_gcal_sync.__main__
 """
-import os
+import logging
 
-current_dir = os.path.dirname(__file__)
+from notion_gcal_sync.install import configure
+
+logging.basicConfig(
+    level=logging.INFO,
+    format='%(asctime)s [%(levelname)s] %(message)s',
+    handlers=[logging.FileHandler('debug.log'), logging.StreamHandler()],
+)
 
 
 def main():
-    from notion_gcal_sync.core import main
-    main()
+    from notion_gcal_sync.core import sync
+
+    cfg = configure()
+    sync(cfg)
 
 
 if __name__ == '__main__':
-    import sys
-    sys.exit(main())
+    main()
diff --git a/notion_gcal_sync/clients/GCalClient.py b/notion_gcal_sync/clients/GCalClient.py
index ba37990..1bd34fa 100644
--- a/notion_gcal_sync/clients/GCalClient.py
+++ b/notion_gcal_sync/clients/GCalClient.py
@@ -2,42 +2,45 @@
 import os
 from typing import List
 
-from google_auth_oauthlib.flow import InstalledAppFlow
-from googleapiclient.discovery import build
-
 from google.auth.transport.requests import Request
 from google.oauth2.credentials import Credentials
+from google_auth_oauthlib.flow import InstalledAppFlow
+from googleapiclient.discovery import build
 
-from ..config import Config
-from ..events.GCalEvent import GCalEvent
+from notion_gcal_sync.config import Config
+from notion_gcal_sync.events.GCalEvent import GCalEvent
 
-current_dir = os.path.dirname(__file__)
-root_dir = os.path.dirname(current_dir)
+CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.notion-gcal-sync')
 
 
 class GCalClient:
     """The client class used to perform requests against google api"""
+
     def __init__(self, cfg: Config):
         self.cfg = cfg
-        self.scopes = ['https://www.googleapis.com/auth/calendar']
+        self.credentials = self.get_credentials()
+        self.service = build('calendar', 'v3', credentials=self.credentials, cache_discovery=False)
+        self.calendar = self.service.calendars().get(calendarId=self.cfg.gcal_default_calendar_id).execute()
+
+    @staticmethod
+    def get_credentials():
+        scopes = ['https://www.googleapis.com/auth/calendar']
         credentials = None
-        token_path = os.path.join(root_dir, 'token.json')
-        if os.path.exists(os.path.join(root_dir, token_path)):
-            credentials = Credentials.from_authorized_user_file(token_path, self.scopes)
+        token_path = os.path.join(CONFIG_PATH, 'token.json')
+        if os.path.exists(os.path.join(CONFIG_PATH, token_path)):
+            credentials = Credentials.from_authorized_user_file(token_path, scopes)
         # If there are no (valid) credentials available, let the user log in.
         if not credentials or not credentials.valid:
             if credentials and credentials.expired and credentials.refresh_token:
                 credentials.refresh(Request())
             else:
-                credentials_path = os.path.join(root_dir, 'client_credentials.json')
-                flow = InstalledAppFlow.from_client_secrets_file(credentials_path, self.scopes)
+                credentials_path = os.path.join(CONFIG_PATH, 'client_credentials.json')
+                flow = InstalledAppFlow.from_client_secrets_file(credentials_path, scopes)
                 credentials = flow.run_local_server(port=0)
             # Save the credentials for the next run
             with open(token_path, 'w') as token:
                 token.write(credentials.to_json())
-
-        self.service = build('calendar', 'v3', credentials=credentials, cache_discovery=False)
-        self.calendar = self.service.calendars().get(calendarId=self.cfg.default_calendar_id).execute()
+        return credentials
 
     def get_event(self, gcal_calendar_id: str, gcal_event_id: str):
         """
@@ -57,21 +60,27 @@ def list_events(self, calendar_id: str) -> List[dict]:
         max_results = 2500
         gcal_event_items = []
         gcal_event_count = 0
+
         logging.info('Fetching events from calendar: {}'.format(self.cfg.get_calendar_name(calendar_id)))
         while True:
-            gcal_events_res = self.service.events().list(
-                calendarId=calendar_id, pageToken=page_token, timeZone=self.cfg.time.timezone_name, maxResults=max_results
-            ).execute()
+            gcal_events_res = (
+                self.service.events()
+                    .list(calendar_id, page_token, timeZone=self.cfg.time.timezone_name, maxResults=max_results, )
+                    .execute()
+            )
             gcal_event_count += len(gcal_events_res['items'])
             print('Found {} events'.format(gcal_event_count), end='\r')
+
             for event in gcal_events_res['items']:
+
                 if event['status'] == 'cancelled':
                     logging.debug('Event "{}" is  cancelled. Skipping...'.format(event.get('id', '')))
                     continue
+
                 if not event.get('summary'):
-                    logging.error('Event "{}" at "{}" does not have a name. Skipping...'
-                                  .format(event.get('id', ''), event['start']))
+                    logging.error('Event "{}" at "{}" has no name. Skipping...'.format(event.get('id', ''), event['start']))
                     continue
+
                 if event.get('recurrence'):
                     logging.debug('Event {} is recurrent source .Skipping...'.format(event['summary']))
                     continue
@@ -85,7 +94,9 @@ def list_events(self, calendar_id: str) -> List[dict]:
                     logging.debug('Using gcal event link for "{}" as recurrence reference'.format(gcal_event.name))
                     gcal_res = self.get_event(gcal_event.gcal_calendar_id, gcal_event.gcal_event_id)
                     gcal_event.recurrent_event = gcal_res['htmlLink']
-                gcal_event_items.append(gcal_event.dict_from_class())
+
+                gcal_event_items.append(gcal_event.to_dict())
+
             page_token = gcal_events_res.get('nextPageToken')
             if not page_token:
                 break
@@ -99,7 +110,7 @@ def create_event(self, gcal_event: GCalEvent):
         :param gcal_event: GCalEvent
         :return: dict: response object from google calendar update
         """
-        res = self.service.events().insert(calendarId=gcal_event.gcal_calendar_id, body=gcal_event.body()).execute()
+        res = self.service.events().insert(calendarId=gcal_event.gcal_calendar_id, body=gcal_event.body).execute()
         return res
 
     def update_event(self, gcal_event: GCalEvent) -> dict or None:
@@ -112,8 +123,11 @@ def update_event(self, gcal_event: GCalEvent) -> dict or None:
         if gcal_event.read_only:
             logging.info('Not updating in gcal read only event "{}"'.format(gcal_event.name))
             return
-        return self.service.events().update(calendarId=gcal_event.gcal_calendar_id, eventId=gcal_event.gcal_event_id,
-                                            body=gcal_event.body()).execute()
+        return (
+            self.service.events()
+                .update(calendarId=gcal_event.gcal_calendar_id, eventId=gcal_event.gcal_event_id, body=gcal_event.body, )
+                .execute()
+        )
         # TODO: what to do about forbidden
         # except:
         #    return None
diff --git a/notion_gcal_sync/clients/NotionClient.py b/notion_gcal_sync/clients/NotionClient.py
index 0bb01a2..f773027 100644
--- a/notion_gcal_sync/clients/NotionClient.py
+++ b/notion_gcal_sync/clients/NotionClient.py
@@ -3,14 +3,14 @@
 
 from notion_client import Client
 
-from ..config import Config
-from ..events.NotionEvent import NotionEvent
+from notion_gcal_sync.config import Config
+from notion_gcal_sync.events.NotionEvent import NotionEvent
 
 
 class NotionClient:
     def __init__(self, cfg: Config):
         self.cfg = cfg
-        self.client = Client(auth=self.cfg.token)
+        self.client = Client(auth=self.cfg.notion_token)
 
     def list_events(self, delete: bool = False):
         cursor = None
@@ -35,39 +35,31 @@ def list_events(self, delete: bool = False):
                     logging.warning('Event {} with no date set to today'.format(notion_event.name))
                     notion_event.time_start = notion_event.time_end = date.today()
 
-                notion_event_items.append(notion_event.dict_from_class())
+                notion_event_items.append(notion_event.to_dict())
             if not notion_event_res['has_more']:
                 break
             cursor = notion_event_res['next_cursor']
 
-        logging.info("Found {} event(s) in Notion".format(len(notion_event_items)))
+        logging.info('Found {} event(s) in Notion'.format(len(notion_event_items)))
         return notion_event_items
 
     def query_items(self, delete: bool = False, cursor: str = None):
         body = {
-                'database_id': self.cfg.database_id,
-                'filter': {
-                    'and': [
-                        {
-                            'property': self.cfg.col_to_delete,
-                            'checkbox': {
-                                'equals': delete
-                            }
-                        }, {
-                            'property': self.cfg.col_deleted,
-                            'checkbox': {
-                                'equals': False
-                            }
-                        }]
-                }
-            }
+            'database_id': self.cfg.notion_database_id,
+            'filter': {
+                'and': [
+                    {'property': self.cfg.notion_columns['to_delete'], 'checkbox': {'equals': delete}},
+                    {'property': self.cfg.notion_columns['deleted'], 'checkbox': {'equals': False}},
+                ]
+            },
+        }
         if cursor:
             body['start_cursor'] = cursor
 
         return self.client.databases.query(**body)
 
     def create_event(self, notion_event: NotionEvent) -> dict:
-        header = {"parent": {"database_id": self.cfg.database_id}}
+        header = {'parent': {'database_id': self.cfg.notion_database_id}}
         return self.client.pages.create(**header, **notion_event.body())
 
     def update_event(self, notion_event: NotionEvent) -> dict:
@@ -76,30 +68,19 @@ def update_event(self, notion_event: NotionEvent) -> dict:
 
     def delete_event(self, notion_event: NotionEvent) -> dict:
         """Currently only puts match to True as notion api does not support delete"""
-        return self.client.pages.update(notion_event.notion_id, **{
-            "properties": {
-                self.cfg.col_deleted: {
-                    'checkbox': True
-                }
-            }
-        })
+        return self.client.pages.update(
+            notion_event.notion_id, **{'properties': {self.cfg.notion_columns['deleted']: {'checkbox': True}}}
+        )
 
     def set_sync_error(self, notion_event: NotionEvent) -> dict:
-        return self.client.pages.update(notion_event.notion_id, **{
-            "properties": {
-                self.cfg.col_last_synced_time: {
-                    "rich_text": [{
-                        "text": {
-                            "content": 'ERROR'
-                        }
-                    }]
-                }
-            }
-        })
+        return self.client.pages.update(
+            notion_event.notion_id,
+            **{'properties': {self.cfg.notion_columns['last_synced_time']: {'rich_text': [{'text': {'content': 'ERROR'}}]}}},
+        )
 
     def update_gcal_link(self, notion_event: NotionEvent, gcal_gcal_page_url: str):
-        if gcal_gcal_page_url == notion_event.gcal_page_url.replace('&ctz=' + self.cfg.time.timezone_name, ''):
+        if gcal_gcal_page_url == notion_event.gcal_page_url.replace('&ctz=' + self.cfg.timezone_name, ''):
             return
         logging.info('- Updating gcal page url for event "{}" in Notion'.format(notion_event.name))
-        notion_event.gcal_page_url = gcal_gcal_page_url + '&ctz=' + self.cfg.time.timezone_name
+        notion_event.gcal_page_url = gcal_gcal_page_url + '&ctz=' + self.cfg.timezone_name
         self.update_event(notion_event)
diff --git a/notion_gcal_sync/config.default.yml b/notion_gcal_sync/config.default.yml
index bc5b4e7..76015a8 100644
--- a/notion_gcal_sync/config.default.yml
+++ b/notion_gcal_sync/config.default.yml
@@ -1,38 +1,31 @@
 ---
-# !!! KEEP THE ORDER OF THE CONFIGURATION !!!
 
-defaults:
-  default_event_length: 60
-  no_date_action: 'skip' # ['skip', 'today']
+default_event_length: 60
+no_date_action: 'skip' # ['skip', 'today']
 
-time:
-  timezone_name: 'Europe/Berlin'
-  timezone_diff: '+02:00'
+timezone_name: 'Europe/Berlin'
+timezone_diff: '+02:00'
 
-notion:
-  # Page url with ?v and &p= 
-  database_url: 'https://www.notion.so/*/***?v=***&p='
-  token: '***'
-  columns:
-    name: 'Name'
-    date: 'Date'
-    tags: 'Tags'
-    description: 'Description'
-    location: 'Location'
-    last_updated_time: 'Last Updated'
-    last_synced_time: 'Last Synced'
-    gcal_event_id: 'GCal event Id'
-    recurrent_event: 'GCal Recurrence'
-    gcal_event_url: 'GCal event url'
-    gcal_calendar_name: 'Calendar'
-    gcal_calendar_id: 'GCal calendar Id'
-    to_delete: 'Delete?'
-    deleted: 'Deleted'
-    read_only: 'Read Only'
+gcal_calendars:
+  Default: '***@gmail.com'
+  Work: '***@group.calendar.google.com'
+gcal_default_calendar_name: "Default"
 
-gcal:
-  default_calendar_id: '***@gmail.com'
-  default_calendar_name: General
-  calendars:
-    General: '***@gmail.com' # Should match default_calendar_name and default_calendar_id
-    Work: '***@group.calendar.google.com'
+notion_database_url: 'https://www.notion.so/*/***?v=***&p='
+notion_token: '***'
+notion_columns:
+  name: 'Name'
+  date: 'Date'
+  tags: 'Tags'
+  description: 'Description'
+  location: 'Location'
+  last_updated_time: 'Last Updated'
+  last_synced_time: 'Last Synced'
+  gcal_event_id: 'GCal event Id'
+  recurrent_event: 'GCal Recurrence'
+  gcal_event_url: 'GCal event url'
+  gcal_calendar_name: 'Calendar'
+  gcal_calendar_id: 'GCal calendar Id'
+  to_delete: 'Delete?'
+  deleted: 'Deleted'
+  read_only: 'Read Only'
diff --git a/notion_gcal_sync/config.py b/notion_gcal_sync/config.py
index ad918cf..7e309d3 100644
--- a/notion_gcal_sync/config.py
+++ b/notion_gcal_sync/config.py
@@ -1,54 +1,85 @@
 import logging
+import os
 
-from .utils import Time
+import yaml
+
+from notion_gcal_sync.utils import Time
+
+CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.notion-gcal-sync')
+CONFIG_FILE = os.path.join(CONFIG_PATH, 'config.yml')
 
 
 class Config:
-    def __init__(self, default_event_length: int, no_date_action: str, default_calendar_id: str, default_calendar_name: str,
-                 calendars: dict, database_url: str, token: str,  columns: dict,  time: Time):
-        # GENERAL
+    default_notion_columns = {
+        'name': 'Name',
+        'date': 'Date',
+        'tags': 'Tags',
+        'description': 'Description',
+        'location': 'Location',
+        'last_updated_time': 'Last Updated',
+        'last_synced_time': 'Last Synced',
+        'gcal_event_id': 'GCal event Id',
+        'recurrent_event': 'GCal Recurrence',
+        'gcal_event_url': 'GCal event url',
+        'gcal_calendar_name': 'Calendar',
+        'gcal_calendar_id': 'GCal calendar Id',
+        'to_delete': 'To Delete',
+        'deleted': 'Deleted',
+        'read_only': 'Read Only',
+    }
+
+    def __init__(
+            self,
+            default_event_length: int = 60,
+            no_date_action: str = 'skip',
+            gcal_calendars=None,
+            gcal_default_calendar_name: str = 'Default',
+            notion_database_url: str = None,
+            notion_token: str = None,
+            notion_columns: dict = None,
+            timezone_name: str = 'Europe/Berlin',
+            timezone_diff: str = '+02:00',
+    ):
         self.default_event_length = default_event_length
         self.no_date_action = no_date_action
-
+        self.timezone_name = timezone_name
+        self.timezone_diff = timezone_diff
+        self.time = Time(timezone_name, timezone_diff)
         # GCAL
-        self.default_calendar_id = default_calendar_id
-        self.default_calendar_name = default_calendar_name
-        self.calendars = calendars
-
+        self.gcal_calendars = gcal_calendars if gcal_calendars else {}
+        self.gcal_default_calendar_name = gcal_default_calendar_name
+        self.gcal_default_calendar_id = self.get_calendar_id(self.gcal_default_calendar_name)
         # NOTION
-        self.database_url = database_url
-        self.token = token
-        self.col_name = columns['name']
-        self.col_date = columns['date']
-        self.col_recurrent_event = columns['recurrent_event']
-        self.col_tags = columns['tags']
-        self.col_description = columns['description']
-        self.col_location = columns['location']
-        self.col_last_updated_time = columns['last_updated_time']
-        self.col_last_synced_time = columns['last_synced_time']
-        self.col_gcal_event_id = columns['gcal_event_id']
-        self.col_gcal_event_url = columns['gcal_event_url']
-        self.col_gcal_calendar_name = columns['gcal_calendar_name']
-        self.col_gcal_calendar_id = columns['gcal_calendar_id']
-        self.col_to_delete = columns['to_delete']
-        self.col_deleted = columns['deleted']
-        self.col_read_only = columns['read_only']
-
-        # TIME
-        self.time = time
+        self.notion_database_url = notion_database_url
+        self.notion_token = notion_token
+
+        self.notion_columns = notion_columns if notion_columns else self.default_notion_columns
+        if not len([key for key in self.default_notion_columns.keys() if key in self.notion_columns.keys()]) == len(
+                self.default_notion_columns.keys()
+        ):
+            raise ValueError
 
     @property
-    def database_url(self):
-        return self._database_url
-
-    @database_url.setter
-    def database_url(self, value):
-        self._database_url = value
-        if "?v=" in value and value.startswith("https://www.notion.so/") and value.endswith("&p="):
-            self.database_id = self.database_url[:self.database_url.index('?v=')].split('/')[-1]
+    def notion_database_url(self):
+        return self._notion_database_url
+
+    @notion_database_url.setter
+    def notion_database_url(self, value):
+        if not value or not value.startswith('https://www.notion.so/') or '?v=' not in value or value.endswith('?v='):
+            logging.error(
+                'Invalid database url: '
+                "Should start with 'https://www.notion.so/'. "
+                "Should contain '?v='. "
+                "Should not end with '?v='. "
+            )
+            self._notion_database_url = None
+            self.notion_database_id = None
             return
-        logging.error("Invalid database url. Cannot get database id")
-        self.database_id = None
+
+        if not value.endswith('&p='):
+            value += '&p='
+        self._notion_database_url = value
+        self.notion_database_id = value[: value.index('?v=')].split('/')[-1]
 
     @property
     def default_event_length(self):
@@ -57,7 +88,7 @@ def default_event_length(self):
     @default_event_length.setter
     def default_event_length(self, value):
         if type(value) != int:
-            logging.error("Invalid value {} for default_event_length. Defaulting to 60 minutes...".format(value))
+            logging.error('Invalid value {} for default_event_length. Defaulting to 60 minutes...'.format(value))
             value = 60
         self._default_event_length = value
 
@@ -68,23 +99,46 @@ def no_date_action(self):
     @no_date_action.setter
     def no_date_action(self, value):
         if value not in ['skip', 'today']:
-            logging.error("Invalid no_date_action {}. Defaulting to skip".format(value))
+            logging.error('Invalid no_date_action {}. Defaulting to skip'.format(value))
             value = 'skip'
         self._no_date_action = value
 
+    @property
+    def gcal_default_calendar_id(self):
+        return self._gcal_default_calendar_id
+
+    @gcal_default_calendar_id.setter
+    def gcal_default_calendar_id(self, value):
+        self._gcal_default_calendar_id = value
+
     def get_calendar_id(self, calendar_name: str) -> str:
-        return self.calendars.get(calendar_name)
+        return self.gcal_calendars.get(calendar_name)
 
     def get_calendar_name(self, calendar_id: str) -> str or None:
         try:
-            return list(self.calendars.keys())[list(self.calendars.values()).index(calendar_id)]
+            return list(self.gcal_calendars.keys())[list(self.gcal_calendars.values()).index(calendar_id)]
         except ValueError:
             return None
 
     def is_valid_calendar_name(self, calendar_name: str) -> bool:
-        calendar_name = self.calendars.get(calendar_name)
+        calendar_name = self.gcal_calendars.get(calendar_name)
         return True if calendar_name else False
 
     def is_valid_calendar_id(self, calendar_id: str) -> bool:
         calendar_id = self.get_calendar_name(calendar_id)
         return True if calendar_id else False
+
+    def to_dict(self):
+        config_dict = dict(
+            (key.replace('_', '', 1), value) if key.startswith('_') else (key, value)
+            for (key, value) in self.__dict__.items()
+            if not key.startswith('__') and key != 'cfg'
+        )
+        del config_dict['time']
+        del config_dict['notion_database_id']
+        del config_dict['gcal_default_calendar_id']
+        return config_dict
+
+    def to_yaml(self):
+        with open(CONFIG_FILE, 'w') as file:
+            yaml.dump(self.to_dict(), file)
diff --git a/notion_gcal_sync/core.py b/notion_gcal_sync/core.py
index ee66a9c..4f3b3e6 100644
--- a/notion_gcal_sync/core.py
+++ b/notion_gcal_sync/core.py
@@ -1,38 +1,27 @@
 import logging
-import os
 
 import pandas as pd
-import yaml
-
-from .clients.GCalClient import GCalClient
-from .clients.NotionClient import NotionClient
-from .config import Config
-from .events.GCalEvent import GCalEvent
-from .events.NotionEvent import NotionEvent
-from .utils import Time
-
-logging.basicConfig(
-    level=logging.INFO,
-    format='%(asctime)s [%(levelname)s] %(message)s',
-    handlers=[
-        logging.FileHandler('debug.log'),
-        logging.StreamHandler()
-    ]
-)
 
-current_dir = os.path.dirname(__file__)
+from notion_gcal_sync.clients.GCalClient import GCalClient
+from notion_gcal_sync.clients.NotionClient import NotionClient
+from notion_gcal_sync.events.GCalEvent import GCalEvent
+from notion_gcal_sync.events.NotionEvent import NotionEvent
 
 
-def create_gcal_events(df: pd.DataFrame, gcal_client: GCalClient, notion_client: NotionClient, notion_df: pd.DataFrame,
-                       notion_specific_columns: list):
+def create_gcal_events(
+        df: pd.DataFrame,
+        gcal_client: GCalClient,
+        notion_client: NotionClient,
+        notion_df: pd.DataFrame,
+        notion_specific_columns: list,
+):
     logging.info('== CREATING EVENTS IN GCAL ' + '=' * 73)
     # left only indicates that the events are only present in Notion
     notion_only_df = df.loc[df['_merge'] == 'left_only']
     # use notion_columns as columns for events to be created
     notion_only_df.columns = [
-        x.replace('_notion', '')
-        if any(k in x for k in notion_only_df.columns)
-        else x for x in notion_only_df]
+        x.replace('_notion', '') if any(k in x for k in notion_only_df.columns) else x for x in notion_only_df
+    ]
     # drop all other columns
     notion_only_df = notion_only_df.loc[:, notion_df.columns]
 
@@ -56,8 +45,10 @@ def create_gcal_events(df: pd.DataFrame, gcal_client: GCalClient, notion_client:
         gcal_client.update_notion_link(gcal_event, notion_event_res['url'])
 
 
-def create_notion_events(df: pd.DataFrame, gcal_client: GCalClient, notion_client: NotionClient, gcal_df: pd.DataFrame,
-                         gcal_specific_columns: list):
+def create_notion_events(
+        df: pd.DataFrame, gcal_client: GCalClient, notion_client: NotionClient, gcal_df: pd.DataFrame,
+        gcal_specific_columns: list,
+):
     logging.info('== CREATING EVENTS IN NOTION ' + '=' * 71)
     # right only indicates that the events are only present in Notion
     gcal_only_df = df.loc[df['_merge'] == 'right_only']
@@ -72,7 +63,7 @@ def create_notion_events(df: pd.DataFrame, gcal_client: GCalClient, notion_clien
 
         logging.info('- Synchronize event "{}" in GCal'.format(el['name']))
         gcal_event = GCalEvent(**el.to_dict(), cfg=gcal_client.cfg)
-        gcal_event.notion_page_url = notion_client.cfg.database_url + notion_event_res['id'].replace('-', '')
+        gcal_event.notion_page_url = notion_client.cfg.notion_database_url + notion_event_res['id'].replace('-', '')
         gcal_event_res = gcal_client.update_event(gcal_event)
         if not gcal_event_res:
             return
@@ -80,39 +71,45 @@ def create_notion_events(df: pd.DataFrame, gcal_client: GCalClient, notion_clien
         notion_client.update_gcal_link(notion_event, gcal_event_res['htmlLink'])
 
 
-def update_events(df: pd.DataFrame, gcal_client: GCalClient, gcal_df: pd.DataFrame, gcal_specific_columns: list,
-                  notion_client: NotionClient, notion_df: pd.DataFrame, notion_specific_columns: list):
+def update_events(
+        df: pd.DataFrame,
+        gcal_client: GCalClient,
+        gcal_df: pd.DataFrame,
+        gcal_specific_columns: list,
+        notion_client: NotionClient,
+        notion_df: pd.DataFrame,
+        notion_specific_columns: list,
+):
     logging.info('== UPDATING EVENTS ' + '=' * 81)
     notion_values_df = df.loc[df['_merge'] == 'both']
     gcal_values_df = notion_values_df.copy()
 
     # Take these values if notion is newer
     notion_values_df.columns = [
-        x.replace('_notion', '')
-        if any(k in x for k in notion_values_df.columns)
-        else x for x in notion_values_df]
+        x.replace('_notion', '') if any(k in x for k in notion_values_df.columns) else x for x in notion_values_df
+    ]
     notion_values_df = notion_values_df[notion_df.columns]
 
     # Take these values if gcal is newer
     gcal_values_df.columns = [
-        x.replace('_gcal', '')
-        if any(k in x for k in gcal_values_df.columns)
-        else x for x in gcal_values_df]
+        x.replace('_gcal', '') if any(k in x for k in gcal_values_df.columns) else x for x in gcal_values_df
+    ]
     gcal_values_df = gcal_values_df[gcal_df.columns]
 
     # Comparing the notion values to the gcal values
-    diff_df = notion_values_df.drop(notion_specific_columns, axis=1) \
-        .compare(gcal_values_df.drop(gcal_specific_columns, axis=1), keep_shape=True, keep_equal=False)
+    diff_df = notion_values_df.drop(notion_specific_columns, axis=1).compare(
+        gcal_values_df.drop(gcal_specific_columns, axis=1), keep_shape=True, keep_equal=False,
+    )
     diff_df = diff_df.dropna(axis=0, how='all').astype(object).where(pd.notnull(diff_df), '')
-    diff_df.to_csv("diff.csv")
+    diff_df.to_csv('diff.csv')
 
     logging.info('Found {} event(s) to be updated'.format(len(diff_df)))
     for idx, el in diff_df.iterrows():
         # Get the values from the notion values
         gcal_updates = gcal_values_df.iloc[idx]
         # Nothing is newer but somewhere this is a difference
-        if gcal_updates['read_only'] == "True":
-            logging.info("Skipping read only event {} with update".format(gcal_updates["name"]))
+        if gcal_updates['read_only'] == 'True':
+            logging.info('Skipping read only event {} with update'.format(gcal_updates['name']))
             continue
 
         notion_updates = notion_values_df.iloc[idx]
@@ -120,7 +117,7 @@ def update_events(df: pd.DataFrame, gcal_client: GCalClient, gcal_df: pd.DataFra
             logging.error('The event "{}" is synced, but still there is a difference'.format(notion_updates['name']))
             logging.error(el.loc[:, ~el.isin([''])].to_dict())
             notion_event = NotionEvent(**notion_updates.to_dict(), cfg=notion_client.cfg)
-            logging.error("Setting sync status to ERROR")
+            logging.error('Setting sync status to ERROR')
             notion_client.set_sync_error(notion_event)
             continue
 
@@ -153,8 +150,11 @@ def update_events(df: pd.DataFrame, gcal_client: GCalClient, gcal_df: pd.DataFra
 
             logging.info('Event "{}" has an update in GCal'.format(gcal_updates['name']))
             logging.info('- Updating event "{}" in Notion'.format(gcal_updates['name']))
-            notion_event = NotionEvent(**gcal_updates.drop(gcal_specific_columns).to_dict(),
-                                       notion_id=notion_updates['notion_id'], cfg=notion_client.cfg)
+            notion_event = NotionEvent(
+                **gcal_updates.drop(gcal_specific_columns).to_dict(),
+                notion_id=notion_updates['notion_id'],
+                cfg=notion_client.cfg,
+            )
             notion_event_res = notion_client.update_event(notion_event)
             if not notion_event_res:
                 continue
@@ -189,32 +189,8 @@ def delete_gcal_events(notion_client: NotionClient, gcal_client: GCalClient, not
         notion_client.delete_event(notion_event)
 
 
-def validate_paths():
-    config_file = os.path.join(current_dir, 'config.yml')
-    config_default_file = os.path.join(current_dir, 'config.default.yml')
-    token_file = os.path.join(current_dir, 'token.json')
-    credentials_files = os.path.join(current_dir, 'client_credentials.json')
-    if not os.path.exists(config_file):
-        logging.error("{} does not exist. Make sure the config.yml exists with the right keys".format(config_file))
-        if os.path.exists(config_default_file):
-            logging.error("Only the default {} exists. Make sure to edit it and rename it to config.yml"
-                          .format(config_default_file))
-        raise FileNotFoundError
-    if not os.path.exists(token_file) and not os.path.exists(credentials_files):
-        logging.error("{} or {} does not exist. Follow the README to obtain a client credential from Google API."
-                      .format(token_file, credentials_files))
-        raise FileNotFoundError
-
-
-def main():
+def sync(cfg):
     logging.info('=' * 100)
-    validate_paths()
-
-    with open(os.path.join(current_dir, 'config.yml'), 'r') as yaml_file:
-        yaml_cfg = yaml.safe_load(yaml_file)
-
-    cfg = Config(**yaml_cfg['defaults'], **yaml_cfg['gcal'], **yaml_cfg['notion'], time=Time(**yaml_cfg['time']))
-
     ###########################################################################
     # Fetching Notion Events
     ###########################################################################
@@ -223,24 +199,23 @@ def main():
     notion_client = NotionClient(cfg)
     notion_events = notion_client.list_events()
     notion_df = pd.DataFrame(notion_events).astype(str)
-    # notion_df.to_csv('notion.csv')
 
     ###########################################################################
     # Fetching GCal Events
     ###########################################################################
     logging.info('Fetching events from Google Calendar')
     gcal_client = GCalClient(cfg)
-    gcal_events = sum([gcal_client.list_events(cfg.calendars[calendar]) for calendar in cfg.calendars], [])
+    gcal_events = sum([gcal_client.list_events(cfg.gcal_calendars[calendar]) for calendar in cfg.gcal_calendars], [], )
     gcal_df = pd.DataFrame(gcal_events).astype(str)
-    # gcal_df.to_csv('gcal.csv')
 
     ###########################################################################
     # Creating Dataframe with all information
     ###########################################################################
     df = None
     if not notion_df.empty and not gcal_df.empty:
-        df = notion_df.merge(gcal_df, on='gcal_event_id', how='outer', indicator=True, suffixes=('_notion', '_gcal'))\
-            .drop_duplicates()
+        df = notion_df.merge(
+            gcal_df, on='gcal_event_id', how='outer', indicator=True, suffixes=('_notion', '_gcal'),
+        ).drop_duplicates()
     # TODO: MAKE THIS BOOTSTRAP AUTOMATIC ITS UGLY
     elif gcal_df.empty and not notion_df.empty:
         df = notion_df.copy()
@@ -269,6 +244,8 @@ def main():
     # GCAL events not in Notion Events
     create_notion_events(df, gcal_client, notion_client, gcal_df, gcal_specific_columns)
     # GCAL or Notion events requiring update
-    update_events(df, gcal_client, gcal_df, gcal_specific_columns, notion_client, notion_df, notion_specific_columns)
+    update_events(
+        df, gcal_client, gcal_df, gcal_specific_columns, notion_client, notion_df, notion_specific_columns,
+    )
     # GCAL events to be deleted
     delete_gcal_events(notion_client, gcal_client, notion_specific_columns)
diff --git a/notion_gcal_sync/events/Event.py b/notion_gcal_sync/events/Event.py
index 5395714..7f2b3f5 100644
--- a/notion_gcal_sync/events/Event.py
+++ b/notion_gcal_sync/events/Event.py
@@ -1,16 +1,29 @@
 import logging
 from datetime import datetime, timedelta
 
-from ..config import Config
+from notion_gcal_sync.config import Config
 
 
 class Event:
-    def __init__(self, name: str = None, description: str = None, location: str = None, gcal_event_id: str = None,
-                 gcal_calendar_name: str = None, gcal_calendar_id: str = None, time_start: str or datetime = None,
-                 time_end: str or datetime = None, recurrent_event: str = None, time_last_updated: datetime = None,
-                 time_last_synced: str = None, notion_page_url: str = None, gcal_page_url: str = None, read_only: bool = None,
-                 cfg: Config = None):
-        self.cfg = cfg
+    def __init__(
+            self,
+            name: str = None,
+            description: str = None,
+            location: str = None,
+            gcal_event_id: str = None,
+            gcal_calendar_name: str = None,
+            gcal_calendar_id: str = None,
+            time_start: str or datetime = None,
+            time_end: str or datetime = None,
+            recurrent_event: str = None,
+            time_last_updated: datetime = None,
+            time_last_synced: str = None,
+            notion_page_url: str = None,
+            gcal_page_url: str = None,
+            read_only: bool = None,
+            cfg: Config = None,
+    ):
+        self.cfg = cfg if cfg else Config()
         # Properties
         self.name = name
         self.description = description
@@ -22,12 +35,16 @@ def __init__(self, name: str = None, description: str = None, location: str = No
         if self.gcal_calendar_name != self.cfg.get_calendar_name(self.gcal_calendar_id):
             self.gcal_calendar_id = self.cfg.get_calendar_id(self.gcal_calendar_name)
 
-        self.time_start = cfg.time.to_datetime(time_start)
-        self.time_end = cfg.time.to_datetime(time_end)
+        self.time_start = self.cfg.time.to_datetime(time_start)
+        self.time_end = self.cfg.time.to_datetime(time_end)
         # Apply default length when no end is specified but a time is given
-        if self.time_start and self.time_end \
-                and not self.cfg.time.is_date(self.time_start) and not self.cfg.time.is_date(self.time_end) \
-                and self.time_start == self.time_end:
+        if (
+                self.time_start
+                and self.time_end
+                and not self.cfg.time.is_date(self.time_start)
+                and not self.cfg.time.is_date(self.time_end)
+                and self.time_start == self.time_end
+        ):
             self.time_end = self.time_start + timedelta(minutes=self.cfg.default_event_length)
 
         self.recurrent_event = recurrent_event
@@ -37,11 +54,11 @@ def __init__(self, name: str = None, description: str = None, location: str = No
         self.gcal_page_url = gcal_page_url
         self.read_only = read_only
 
-    def dict_from_class(self):
+    def to_dict(self):
         return dict(
             (key.replace('_', '', 1), value) if key.startswith('_') else (key, value)
             for (key, value) in self.__dict__.items()
-            if not key.startswith('__') and key != "cfg"
+            if not key.startswith('__') and key != 'cfg'
         )
 
     @property
@@ -60,6 +77,8 @@ def set_calendar(self, gcal_calendar_id: str, gcal_calendar_name: str) -> (str,
         :return: gcal_calendar_id, gcal_calendar_name
         """
         # Valid calendar name and valid calendar id
+        if not gcal_calendar_id and not gcal_calendar_name:
+            return self.cfg.gcal_default_calendar_id, self.cfg.gcal_default_calendar_name
         if self.cfg.is_valid_calendar_name(gcal_calendar_name) and self.cfg.is_valid_calendar_id(gcal_calendar_id):
             return gcal_calendar_id, gcal_calendar_name
         # Invalid calendar name but valid calendar id -> derive calendar name from id
@@ -72,6 +91,9 @@ def set_calendar(self, gcal_calendar_id: str, gcal_calendar_name: str) -> (str,
         if gcal_calendar_name == 'Google Calendar':
             return 'skip', 'skip'
 
-        logging.debug('Different organizer calendar "{}" for event "{}". Using default calendar {}'
-                      .format(gcal_calendar_id, self.name, self.cfg.default_calendar_id))
-        return self.cfg.default_calendar_id, self.cfg.default_calendar_name
+        logging.debug(
+            'Different organizer calendar "{}" for event "{}". Using default calendar {}'.format(
+                gcal_calendar_id, self.name, self.cfg.get_calendar_id(self.cfg.gcal_default_calendar_name),
+            )
+        )
+        return self.cfg.gcal_default_calendar_id, self.cfg.gcal_default_calendar_name
diff --git a/notion_gcal_sync/events/GCalEvent.py b/notion_gcal_sync/events/GCalEvent.py
index ada13b4..1bcf3a1 100644
--- a/notion_gcal_sync/events/GCalEvent.py
+++ b/notion_gcal_sync/events/GCalEvent.py
@@ -1,21 +1,48 @@
 import logging
 from datetime import datetime, timedelta
-from typing import Union, Optional, Any, TypedDict
 
-from ..config import Config
-from ..events.Event import Event
-from ..utils import Time
+from notion_gcal_sync.config import Config
+from notion_gcal_sync.events.Event import Event
+from notion_gcal_sync.utils import Time
 
 
 class GCalEvent(Event):
-    def __init__(self, name: str = None, description: str = None, location: str = None, gcal_event_id: str = None,
-                 gcal_calendar_name: str = None, gcal_calendar_id: str = None, time_start: datetime = None,
-                 time_end: datetime = None, recurrent_event: str = None, time_last_updated: datetime = None,
-                 time_last_synced: str = None, notion_page_url: str = None, gcal_page_url: str = None, color_id: str = None,
-                 read_only: bool = None, cfg: Config = None):
-        super().__init__(name, description, location, gcal_event_id, gcal_calendar_name, gcal_calendar_id, time_start,
-                         time_end, recurrent_event, time_last_updated, time_last_synced, notion_page_url, gcal_page_url,
-                         read_only, cfg)
+    def __init__(
+            self,
+            name: str = None,
+            description: str = None,
+            location: str = None,
+            gcal_event_id: str = None,
+            gcal_calendar_name: str = None,
+            gcal_calendar_id: str = None,
+            time_start: datetime = None,
+            time_end: datetime = None,
+            recurrent_event: str = None,
+            time_last_updated: datetime = None,
+            time_last_synced: str = None,
+            notion_page_url: str = None,
+            gcal_page_url: str = None,
+            color_id: str = None,
+            read_only: bool = None,
+            cfg: Config = None,
+    ):
+        super().__init__(
+            name,
+            description,
+            location,
+            gcal_event_id,
+            gcal_calendar_name,
+            gcal_calendar_id,
+            time_start,
+            time_end,
+            recurrent_event,
+            time_last_updated,
+            time_last_synced,
+            notion_page_url,
+            gcal_page_url,
+            read_only,
+            cfg,
+        )
         self.color_id = color_id
 
     @classmethod
@@ -30,9 +57,24 @@ def from_api(cls, obj, cfg: Config, time: Time):
         gcal_page_url = obj.get('htmlLink', '')
         color_id = obj.get('colorId', '')
         read_only = obj.get('privateCopy', False)
-        return cls(name, description, location, gcal_event_id, gcal_calendar_name, gcal_calendar_id, time_start, time_end,
-                   recurrent_event, time_last_updated, time_last_synced, notion_page_url, gcal_page_url, color_id, read_only,
-                   cfg)
+        return cls(
+            name,
+            description,
+            location,
+            gcal_event_id,
+            gcal_calendar_name,
+            gcal_calendar_id,
+            time_start,
+            time_end,
+            recurrent_event,
+            time_last_updated,
+            time_last_synced,
+            notion_page_url,
+            gcal_page_url,
+            color_id,
+            read_only,
+            cfg,
+        )
 
     @classmethod
     def get_meta(cls, obj: dict) -> (str, str):
@@ -44,7 +86,7 @@ def get_meta(cls, obj: dict) -> (str, str):
         source_url = source.get('url')
         source_title = source.get('title')
         if not source_title.startswith('Notion at'):
-            description = "Source: {}\n{}".format(source_url, description)
+            description = 'Source: {}\n{}'.format(source_url, description)
             source_url = source_title = ''
         return source_url, source_title.replace('Notion at ', ''), description
 
@@ -73,22 +115,20 @@ def get_recurrent_event(cls, obj: dict) -> str:
         """Get event id of original event if this event is occurrence of recurrence"""
         return obj.get('recurringEventId', '')
 
+    @property
     def body(self):
-        body: dict[str, Union[str, None, dict[str, Optional[str]], dict[str, Union[dict[str, Any], Any]], TypedDict]] = {
+        body = {
             'summary': self.name,
             'description': self.description,
             'location': self.location,
-            'source': {
-                'title': 'Notion at ' + self.time_last_synced,
-                'url': self.notion_page_url,
-            }
+            'source': {'title': 'Notion at ' + self.time_last_synced, 'url': self.notion_page_url},
         }
 
         time_end = self.time_end
         # utils is just a date
         if Time.is_date(self.time_start) and Time.is_date(self.time_end):
             logging.debug('Updating end of date of "{}" by one day to get all day event'.format(self.name))
-            time_end = (self.time_end + timedelta(days=1))
+            time_end = self.time_end + timedelta(days=1)
 
         if self.cfg.time.is_date(self.time_start) and self.cfg.time.is_date(self.time_end):
             body['start'] = {
diff --git a/notion_gcal_sync/events/NotionEvent.py b/notion_gcal_sync/events/NotionEvent.py
index 6cfc560..c585e97 100644
--- a/notion_gcal_sync/events/NotionEvent.py
+++ b/notion_gcal_sync/events/NotionEvent.py
@@ -1,20 +1,48 @@
 import logging
 from datetime import datetime
 
-from ..config import Config
-from ..events.Event import Event
-from ..utils import Time
+from notion_gcal_sync.config import Config
+from notion_gcal_sync.events.Event import Event
+from notion_gcal_sync.utils import Time
 
 
 class NotionEvent(Event):
-    def __init__(self, name: str = None, description: str = None, location: str = None, gcal_event_id: str = None,
-                 gcal_calendar_name: str = None, gcal_calendar_id: str = None, time_start: datetime = None,
-                 time_end: datetime = None, recurrent_event: str = None, time_last_updated: datetime = None,
-                 time_last_synced: str = None, notion_page_url: str = None,  gcal_page_url: str = None, notion_id: str = None,
-                 read_only: bool = None, cfg: Config = None):
-        super().__init__(name, description, location, gcal_event_id, gcal_calendar_name, gcal_calendar_id, time_start,
-                         time_end, recurrent_event, time_last_updated, time_last_synced, notion_page_url, gcal_page_url,
-                         read_only, cfg)
+    def __init__(
+            self,
+            name: str = None,
+            description: str = None,
+            location: str = None,
+            gcal_event_id: str = None,
+            gcal_calendar_name: str = None,
+            gcal_calendar_id: str = None,
+            time_start: datetime = None,
+            time_end: datetime = None,
+            recurrent_event: str = None,
+            time_last_updated: datetime = None,
+            time_last_synced: str = None,
+            notion_page_url: str = None,
+            gcal_page_url: str = None,
+            notion_id: str = None,
+            read_only: bool = None,
+            cfg: Config = None,
+    ):
+        super().__init__(
+            name,
+            description,
+            location,
+            gcal_event_id,
+            gcal_calendar_name,
+            gcal_calendar_id,
+            time_start,
+            time_end,
+            recurrent_event,
+            time_last_updated,
+            time_last_synced,
+            notion_page_url,
+            gcal_page_url,
+            read_only,
+            cfg,
+        )
         self.notion_id = notion_id
         # self.tags = tags
 
@@ -23,22 +51,37 @@ def from_api(cls, obj: dict, cfg: Config):
         props = obj['properties']
         notion_id = obj['id']
         notion_page_url = obj['url']
-        name = cls.get_name(props, cfg.col_name)
-        location = cls.get_text(props, cfg.col_location)
-        time_start, time_end = cls.get_time(props, cfg.col_date)
-        recurrent_event = cls.get_text(props, cfg.col_recurrent_event)
-        time_last_updated = cls.get_last_edited_time(props, cfg.col_last_updated_time, cfg.time)
-        time_last_synced = cls.get_text(props, cfg.col_last_synced_time)
-        description = cls.get_text(props, cfg.col_description)
-        gcal_event_id = cls.get_text(props, cfg.col_gcal_event_id)
-        gcal_page_url = cls.get_url(props, cfg.col_gcal_event_url)
-        gcal_calendar_name = cls.get_select(props, cfg.col_gcal_calendar_name)
-        gcal_calendar_id = cls.get_select(props, cfg.col_gcal_calendar_id)
-        read_only = cls.get_checkbox(props, cfg.col_read_only)
+        name = cls.get_name(props, cfg.notion_columns['name'])
+        location = cls.get_text(props, cfg.notion_columns['location'])
+        time_start, time_end = cls.get_time(props, cfg.notion_columns['date'])
+        recurrent_event = cls.get_text(props, cfg.notion_columns['recurrent_event'])
+        time_last_updated = cls.get_last_edited_time(props, cfg.notion_columns['last_updated_time'], cfg.time)
+        time_last_synced = cls.get_text(props, cfg.notion_columns['last_synced_time'])
+        description = cls.get_text(props, cfg.notion_columns['description'])
+        gcal_event_id = cls.get_text(props, cfg.notion_columns['gcal_event_id'])
+        gcal_page_url = cls.get_url(props, cfg.notion_columns['gcal_event_url'])
+        gcal_calendar_name = cls.get_select(props, cfg.notion_columns['gcal_calendar_name'])
+        gcal_calendar_id = cls.get_select(props, cfg.notion_columns['gcal_calendar_id'])
+        read_only = cls.get_checkbox(props, cfg.notion_columns['read_only'])
         # tags = cls.get_multiselect(props, cfg.col_tags)
-        return cls(name, description, location, gcal_event_id, gcal_calendar_name, gcal_calendar_id, time_start, time_end,
-                   recurrent_event, time_last_updated, time_last_synced, notion_page_url, gcal_page_url, notion_id, read_only,
-                   cfg)
+        return cls(
+            name,
+            description,
+            location,
+            gcal_event_id,
+            gcal_calendar_name,
+            gcal_calendar_id,
+            time_start,
+            time_end,
+            recurrent_event,
+            time_last_updated,
+            time_last_synced,
+            notion_page_url,
+            gcal_page_url,
+            notion_id,
+            read_only,
+            cfg,
+        )
 
     @classmethod
     def get_name(cls, properties: dict, column: str) -> str:
@@ -109,71 +152,18 @@ def body(self) -> dict:
             time_end = None
 
         body = {
-            "properties": {
-                self.cfg.col_name: {
-                    "title": [{
-                        "text": {
-                            "content": self.name
-                        }
-                    }]
-                },
-                self.cfg.col_date: {
-                    "date": {
-                        "start": time_start,
-                        "end": time_end
-                    }
-                },
-                self.cfg.col_recurrent_event: {
-                    "rich_text": [{
-                        "text": {
-                            "content": self.recurrent_event
-                        }
-                    }]
-                },
-                self.cfg.col_description: {
-                    "rich_text": [{
-                        "text": {
-                            "content": self.description
-                        }
-                    }]
-                },
-                self.cfg.col_gcal_calendar_id: {
-                    "select": {
-                        "name": self.gcal_calendar_id
-                    }
-                },
-                self.cfg.col_gcal_calendar_name: {
-                    "select": {
-                        "name": self.gcal_calendar_name
-                    }
-                },
-                self.cfg.col_location: {
-                    "rich_text": [{
-                        "text": {
-                            "content": self.location
-                        }
-                    }]
-                },
-                self.cfg.col_gcal_event_id: {
-                    "rich_text": [{
-                        "text": {
-                            "content": self.gcal_event_id
-                        }
-                    }]
-                },
-                self.cfg.col_last_synced_time: {
-                    "rich_text": [{
-                        "text": {
-                            "content": self.cfg.time.now(),
-                        }
-                    }]
-                },
-                self.cfg.col_gcal_event_url: {
-                    "url": self.gcal_page_url
-                },
-                self.cfg.col_read_only: {
-                    "checkbox": bool(self.read_only)
-                }
+            'properties': {
+                self.cfg.notion_columns['name']: {'title': [{'text': {'content': self.name}}]},
+                self.cfg.notion_columns['date']: {'date': {'start': time_start, 'end': time_end}},
+                self.cfg.notion_columns['recurrent_event']: {'rich_text': [{'text': {'content': self.recurrent_event}}]},
+                self.cfg.notion_columns['description']: {'rich_text': [{'text': {'content': self.description}}]},
+                self.cfg.notion_columns['gcal_calendar_id']: {'select': {'name': self.gcal_calendar_id}},
+                self.cfg.notion_columns['gcal_calendar_name']: {'select': {'name': self.gcal_calendar_name}},
+                self.cfg.notion_columns['location']: {'rich_text': [{'text': {'content': self.location}}]},
+                self.cfg.notion_columns['gcal_event_id']: {'rich_text': [{'text': {'content': self.gcal_event_id}}]},
+                self.cfg.notion_columns['last_synced_time']: {'rich_text': [{'text': {'content': self.cfg.time.now()}}]},
+                self.cfg.notion_columns['gcal_event_url']: {'url': self.gcal_page_url},
+                self.cfg.notion_columns['read_only']: {'checkbox': bool(self.read_only)},
             },
         }
         logging.info(body)
diff --git a/notion_gcal_sync/install.py b/notion_gcal_sync/install.py
new file mode 100644
index 0000000..7f02ae2
--- /dev/null
+++ b/notion_gcal_sync/install.py
@@ -0,0 +1,78 @@
+import logging
+import os
+import sys
+
+import click as click
+import yaml
+
+from notion_gcal_sync.clients.GCalClient import GCalClient
+from notion_gcal_sync.config import Config
+
+CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.notion-gcal-sync')
+CONFIG_FILE = os.path.join(CONFIG_PATH, 'config.yml')
+CONFIG_DEFAULT_FILE = os.path.join(CONFIG_PATH, 'config.default.yml')
+TOKEN_FILE = os.path.join(CONFIG_PATH, 'token.json')
+CREDENTIALS_FILE = os.path.join(CONFIG_PATH, 'client_credentials.json')
+
+
+def confirm_path(path):
+    logging.info('{} does not exist'.format(path))
+    if click.confirm('Create non-existing {}?'.format(path), default=True):
+        return
+    logging.info('Exiting...')
+    sys.exit()
+
+
+def confirm_config_path():
+    if os.path.exists(CONFIG_PATH):
+        return
+    confirm_path(CONFIG_PATH)
+    logging.info('Creating {}'.format(CONFIG_PATH))
+    os.mkdir(CONFIG_PATH)
+
+
+def confirm_config_file():
+    if os.path.exists(CONFIG_FILE):
+        return
+    confirm_path(CONFIG_FILE)
+    logging.info('Configuring {}'.format(CONFIG_FILE))
+    config_dict = {}
+    for key, val in Config().to_dict().items():
+        if key in ['gcal_calendars', 'notion_columns']:
+            continue
+        if key == 'gcal_default_calendar_name':
+            gcal_mail = click.prompt('google_mail (e.g name@gmail.com)')
+            config_dict['gcal_default_calendar_name'] = 'Default'
+            config_dict['gcal_calendars'] = {'Default': gcal_mail}
+            continue
+        config_dict[key] = click.prompt('{}'.format(key), default=val)
+
+    logging.info('Writing configured values to {}'.format(CONFIG_FILE))
+    Config(**config_dict).to_yaml()
+    logging.info('Open {} to configure additional values.'.format(CONFIG_FILE))
+
+
+def confirm_credentials_file():
+    if not os.path.exists(CREDENTIALS_FILE) and not os.path.exists(TOKEN_FILE):
+        logging.error('{} nor {} exist'.format(CREDENTIALS_FILE, TOKEN_FILE))
+        logging.info(
+            'Please follow the instructions on setting up the client_credentials.json: '
+            'https://github.com/Ravio1i/notion-gcal-sync/blob/main/docs/setup.md#setup-credentials-for-google-calendar'
+        )
+        logging.info('Exiting...')
+        sys.exit()
+    if not os.path.exists(TOKEN_FILE):
+        logging.info('{} does not exist'.format(TOKEN_FILE))
+        logging.info('Generating token file {}'.format(TOKEN_FILE))
+        GCalClient.get_credentials()
+
+
+def configure():
+    confirm_config_path()
+    confirm_config_file()
+    confirm_credentials_file()
+
+    with open(CONFIG_FILE, 'r') as yaml_file:
+        yaml_cfg = yaml.safe_load(yaml_file)
+
+    return Config(**yaml_cfg)
diff --git a/notion_gcal_sync/utils.py b/notion_gcal_sync/utils.py
index f46b144..ec0ee76 100644
--- a/notion_gcal_sync/utils.py
+++ b/notion_gcal_sync/utils.py
@@ -1,6 +1,6 @@
-from datetime import datetime, timedelta, timezone
-import re
 import logging
+import re
+from datetime import datetime, timedelta, timezone
 
 
 class Time:
@@ -18,14 +18,14 @@ def is_date(dt: datetime or str) -> bool or None:
             return True if date_match else False
         if type(dt) == datetime:
             return dt.hour == 0 and dt.minute == 0 if not dt.tzinfo else False
-        logging.error("Date {} is in some other format".format(dt))
+        logging.error('Date {} is in some other format'.format(dt))
         return None
 
     @staticmethod
     def datetime_to_str(dt: datetime) -> str or None:
         if type(dt) != datetime:
             return None
-        return dt.isoformat('T', "seconds")
+        return dt.isoformat('T', 'seconds')
 
     @staticmethod
     def datetime_to_str_date(dt: datetime) -> str or None:
@@ -35,14 +35,14 @@ def datetime_to_str_date(dt: datetime) -> str or None:
 
     @staticmethod
     def now() -> str:
-        return datetime.now().isoformat('T', "minutes")
+        return datetime.now().isoformat('T', 'minutes')
 
     def to_datetime(self, dt: str or datetime) -> datetime or None:
         if type(dt) == datetime:
             return dt
         if type(dt) == str:
             return self.str_to_datetime(dt)
-        logging.error("Date {} is in some other format".format(dt))
+        logging.error('Date {} is in some other format'.format(dt))
         return None
 
     def str_to_datetime(self, date_str: str) -> datetime or None:
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..d1d66ca
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,1916 @@
+[[package]]
+name = "anyio"
+version = "3.3.4"
+description = "High level compatibility layer for multiple asynchronous event loop implementations"
+category = "main"
+optional = false
+python-versions = ">=3.6.2"
+
+[package.dependencies]
+idna = ">=2.8"
+sniffio = ">=1.1"
+
+[package.extras]
+doc = ["sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"]
+test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"]
+trio = ["trio (>=0.16)"]
+
+[[package]]
+name = "atomicwrites"
+version = "1.4.0"
+description = "Atomic file writes."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "attrs"
+version = "21.2.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
+docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
+
+[[package]]
+name = "backports.entry-points-selectable"
+version = "1.1.0"
+description = "Compatibility shim providing selectable entry points for older implementations"
+category = "dev"
+optional = false
+python-versions = ">=2.7"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"]
+
+[[package]]
+name = "bleach"
+version = "4.1.0"
+description = "An easy safelist-based HTML-sanitizing tool."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+packaging = "*"
+six = ">=1.9.0"
+webencodings = "*"
+
+[[package]]
+name = "cachecontrol"
+version = "0.12.6"
+description = "httplib2 caching for requests"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+lockfile = {version = ">=0.9", optional = true, markers = "extra == \"filecache\""}
+msgpack = ">=0.5.2"
+requests = "*"
+
+[package.extras]
+filecache = ["lockfile (>=0.9)"]
+redis = ["redis (>=2.10.5)"]
+
+[[package]]
+name = "cachetools"
+version = "4.2.4"
+description = "Extensible memoizing collections and decorators"
+category = "main"
+optional = false
+python-versions = "~=3.5"
+
+[[package]]
+name = "cachy"
+version = "0.3.0"
+description = "Cachy provides a simple yet effective caching library."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.extras]
+redis = ["redis (>=3.3.6,<4.0.0)"]
+memcached = ["python-memcached (>=1.59,<2.0)"]
+msgpack = ["msgpack-python (>=0.5,<0.6)"]
+
+[[package]]
+name = "certifi"
+version = "2021.10.8"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "cffi"
+version = "1.15.0"
+description = "Foreign Function Interface for Python calling C code."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.0.7"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.5.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
+[[package]]
+name = "cleo"
+version = "0.8.1"
+description = "Cleo allows you to create beautiful and testable command-line interfaces."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+clikit = ">=0.6.0,<0.7.0"
+
+[[package]]
+name = "click"
+version = "8.0.3"
+description = "Composable command line interface toolkit"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "click-log"
+version = "0.3.2"
+description = "Logging integration for Click"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+click = "*"
+
+[[package]]
+name = "clikit"
+version = "0.6.2"
+description = "CliKit is a group of utilities to build beautiful and testable command line interfaces."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""}
+pastel = ">=0.2.0,<0.3.0"
+pylev = ">=1.3,<2.0"
+
+[[package]]
+name = "colorama"
+version = "0.4.4"
+description = "Cross-platform colored terminal text."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "coverage"
+version = "6.0.2"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "crashtest"
+version = "0.3.1"
+description = "Manage Python errors with ease"
+category = "dev"
+optional = false
+python-versions = ">=3.6,<4.0"
+
+[[package]]
+name = "cryptography"
+version = "35.0.0"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+cffi = ">=1.12"
+
+[package.extras]
+docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
+docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
+pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
+sdist = ["setuptools_rust (>=0.11.4)"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
+
+[[package]]
+name = "distlib"
+version = "0.3.3"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "docutils"
+version = "0.17.1"
+description = "Docutils -- Python Documentation Utilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "dotty-dict"
+version = "1.3.0"
+description = "Dictionary wrapper for quick access to deeply nested keys."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+setuptools_scm = "*"
+
+[[package]]
+name = "filelock"
+version = "3.3.1"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
+testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
+
+[[package]]
+name = "flake8"
+version = "4.0.1"
+description = "the modular source code checker: pep8 pyflakes and co"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+mccabe = ">=0.6.0,<0.7.0"
+pycodestyle = ">=2.8.0,<2.9.0"
+pyflakes = ">=2.4.0,<2.5.0"
+
+[[package]]
+name = "gitdb"
+version = "4.0.8"
+description = "Git Object Database"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+smmap = ">=3.0.1,<6"
+
+[[package]]
+name = "gitpython"
+version = "3.1.24"
+description = "GitPython is a python library used to interact with Git repositories"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+gitdb = ">=4.0.1,<5"
+typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""}
+
+[[package]]
+name = "google-api-core"
+version = "2.1.1"
+description = "Google API client core library"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+google-auth = ">=1.25.0,<3.0dev"
+googleapis-common-protos = ">=1.6.0,<2.0dev"
+protobuf = ">=3.12.0"
+requests = ">=2.18.0,<3.0.0dev"
+
+[package.extras]
+grpc = ["grpcio (>=1.33.2,<2.0dev)"]
+grpcgcp = ["grpcio-gcp (>=0.2.2)"]
+grpcio-gcp = ["grpcio-gcp (>=0.2.2)"]
+
+[[package]]
+name = "google-api-python-client"
+version = "2.23.0"
+description = "Google API Client Library for Python"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+google-api-core = ">=1.21.0,<3.0.0dev"
+google-auth = ">=1.16.0,<3.0.0dev"
+google-auth-httplib2 = ">=0.1.0"
+httplib2 = ">=0.15.0,<1dev"
+uritemplate = ">=3.0.0,<4dev"
+
+[[package]]
+name = "google-auth"
+version = "2.3.0"
+description = "Google Authentication Library"
+category = "main"
+optional = false
+python-versions = ">= 3.6"
+
+[package.dependencies]
+cachetools = ">=2.0.0,<5.0"
+pyasn1-modules = ">=0.2.1"
+rsa = ">=3.1.4,<5"
+
+[package.extras]
+aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "requests (>=2.20.0,<3.0.0dev)"]
+pyopenssl = ["pyopenssl (>=20.0.0)"]
+reauth = ["pyu2f (>=0.1.5)"]
+
+[[package]]
+name = "google-auth-httplib2"
+version = "0.1.0"
+description = "Google Authentication Library: httplib2 transport"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+google-auth = "*"
+httplib2 = ">=0.15.0"
+six = "*"
+
+[[package]]
+name = "google-auth-oauthlib"
+version = "0.4.6"
+description = "Google Authentication Library"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+google-auth = ">=1.0.0"
+requests-oauthlib = ">=0.7.0"
+
+[package.extras]
+tool = ["click (>=6.0.0)"]
+
+[[package]]
+name = "googleapis-common-protos"
+version = "1.53.0"
+description = "Common protobufs used in Google APIs"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+protobuf = ">=3.12.0"
+
+[package.extras]
+grpc = ["grpcio (>=1.0.0)"]
+
+[[package]]
+name = "h11"
+version = "0.12.0"
+description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "html5lib"
+version = "1.1"
+description = "HTML parser based on the WHATWG HTML specification"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+six = ">=1.9"
+webencodings = "*"
+
+[package.extras]
+all = ["genshi", "chardet (>=2.2)", "lxml"]
+chardet = ["chardet (>=2.2)"]
+genshi = ["genshi"]
+lxml = ["lxml"]
+
+[[package]]
+name = "httpcore"
+version = "0.13.7"
+description = "A minimal low-level HTTP client."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+anyio = ">=3.0.0,<4.0.0"
+h11 = ">=0.11,<0.13"
+sniffio = ">=1.0.0,<2.0.0"
+
+[package.extras]
+http2 = ["h2 (>=3,<5)"]
+
+[[package]]
+name = "httplib2"
+version = "0.20.1"
+description = "A comprehensive HTTP client library."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+pyparsing = ">=2.4.2,<3"
+
+[[package]]
+name = "httpx"
+version = "0.20.0"
+description = "The next generation HTTP client."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+certifi = "*"
+charset-normalizer = "*"
+httpcore = ">=0.13.3,<0.14.0"
+rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
+sniffio = "*"
+
+[package.extras]
+brotli = ["brotlicffi", "brotli"]
+cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"]
+http2 = ["h2 (>=3,<5)"]
+
+[[package]]
+name = "identify"
+version = "2.3.1"
+description = "File identification library for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.extras]
+license = ["editdistance-s"]
+
+[[package]]
+name = "idna"
+version = "3.3"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "importlib-metadata"
+version = "4.8.1"
+description = "Read metadata from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+perf = ["ipython"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "invoke"
+version = "1.6.0"
+description = "Pythonic task execution"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "jeepney"
+version = "0.7.1"
+description = "Low-level, pure Python DBus protocol wrapper."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"]
+trio = ["trio", "async-generator"]
+
+[[package]]
+name = "keyring"
+version = "21.8.0"
+description = "Store and access your passwords safely."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
+pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""}
+SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
+[[package]]
+name = "lockfile"
+version = "0.12.2"
+description = "Platform-independent file locking module"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "mccabe"
+version = "0.6.1"
+description = "McCabe checker, plugin for flake8"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "msgpack"
+version = "1.0.2"
+description = "MessagePack (de)serializer."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "nodeenv"
+version = "1.6.0"
+description = "Node.js virtual environment builder"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "notion-client"
+version = "0.7.0"
+description = "Python client for the official Notion API"
+category = "main"
+optional = false
+python-versions = ">=3.7, <4"
+
+[package.dependencies]
+httpx = ">=0.15.0"
+
+[[package]]
+name = "numpy"
+version = "1.21.1"
+description = "NumPy is the fundamental package for array computing with Python."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "oauthlib"
+version = "3.1.1"
+description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+rsa = ["cryptography (>=3.0.0,<4)"]
+signals = ["blinker (>=1.4.0)"]
+signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"]
+
+[[package]]
+name = "packaging"
+version = "20.9"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+pyparsing = ">=2.0.2"
+
+[[package]]
+name = "pandas"
+version = "1.3.3"
+description = "Powerful data structures for data analysis, time series, and statistics"
+category = "main"
+optional = false
+python-versions = ">=3.7.1"
+
+[package.dependencies]
+numpy = ">=1.17.3"
+python-dateutil = ">=2.7.3"
+pytz = ">=2017.3"
+
+[package.extras]
+test = ["hypothesis (>=3.58)", "pytest (>=6.0)", "pytest-xdist"]
+
+[[package]]
+name = "pastel"
+version = "0.2.1"
+description = "Bring colors to your terminal."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pexpect"
+version = "4.8.0"
+description = "Pexpect allows easy control of interactive console applications."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+ptyprocess = ">=0.5"
+
+[[package]]
+name = "pkginfo"
+version = "1.7.1"
+description = "Query metadatdata from sdists / bdists / installed packages."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+testing = ["nose", "coverage"]
+
+[[package]]
+name = "platformdirs"
+version = "2.4.0"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
+test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
+
+[[package]]
+name = "pluggy"
+version = "0.13.1"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+
+[[package]]
+name = "poetry"
+version = "1.1.11"
+description = "Python dependency management and packaging made easy."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+cachecontrol = {version = ">=0.12.4,<0.13.0", extras = ["filecache"]}
+cachy = ">=0.3.0,<0.4.0"
+cleo = ">=0.8.1,<0.9.0"
+clikit = ">=0.6.2,<0.7.0"
+crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""}
+html5lib = ">=1.0,<2.0"
+keyring = {version = ">=21.2.0,<22.0.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""}
+packaging = ">=20.4,<21.0"
+pexpect = ">=4.7.0,<5.0.0"
+pkginfo = ">=1.4,<2.0"
+poetry-core = ">=1.0.7,<1.1.0"
+requests = ">=2.18,<3.0"
+requests-toolbelt = ">=0.9.1,<0.10.0"
+shellingham = ">=1.1,<2.0"
+tomlkit = ">=0.7.0,<1.0.0"
+virtualenv = ">=20.0.26,<21.0.0"
+
+[[package]]
+name = "poetry-core"
+version = "1.0.7"
+description = "Poetry PEP 517 Build Backend"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "poetry2setup"
+version = "1.0.0"
+description = "Convert python-poetry to setup.py"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+poetry-core = ">=1.0.0,<2.0.0"
+
+[[package]]
+name = "pre-commit"
+version = "2.15.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+category = "dev"
+optional = false
+python-versions = ">=3.6.1"
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+toml = "*"
+virtualenv = ">=20.0.8"
+
+[[package]]
+name = "protobuf"
+version = "3.19.0"
+description = "Protocol Buffers"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+description = "Run a subprocess in a pseudo terminal"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "py"
+version = "1.10.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pyasn1"
+version = "0.4.8"
+description = "ASN.1 types and codecs"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyasn1-modules"
+version = "0.2.8"
+description = "A collection of ASN.1-based protocols modules."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pyasn1 = ">=0.4.6,<0.5.0"
+
+[[package]]
+name = "pycodestyle"
+version = "2.8.0"
+description = "Python style guide checker"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "pycparser"
+version = "2.20"
+description = "C parser in Python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pyflakes"
+version = "2.4.0"
+description = "passive checker of Python programs"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pygments"
+version = "2.10.0"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "pylev"
+version = "1.4.0"
+description = "A pure Python Levenshtein implementation that's not freaking GPL'd."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyparsing"
+version = "2.4.7"
+description = "Python parsing module"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "pytest"
+version = "6.2.4"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<1.0.0a1"
+py = ">=1.8.2"
+toml = "*"
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "3.0.0"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.2"
+description = "Extensions to the standard Python datetime module"
+category = "main"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "python-gitlab"
+version = "2.10.1"
+description = "Interact with GitLab API"
+category = "dev"
+optional = false
+python-versions = ">=3.6.0"
+
+[package.dependencies]
+requests = ">=2.25.0"
+requests-toolbelt = ">=0.9.1"
+
+[package.extras]
+autocompletion = ["argcomplete (>=1.10.0,<2)"]
+yaml = ["PyYaml (>=5.2)"]
+
+[[package]]
+name = "python-semantic-release"
+version = "7.19.2"
+description = "Automatic Semantic Versioning for Python projects"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+click = ">=7,<9"
+click-log = ">=0.3,<1"
+dotty-dict = ">=1.3.0,<2"
+gitpython = ">=3.0.8,<4"
+invoke = ">=1.4.1,<2"
+python-gitlab = ">=1.10,<3"
+requests = ">=2.25,<3"
+semver = ">=2.10,<3"
+tomlkit = "0.7.0"
+twine = ">=3,<4"
+
+[package.extras]
+dev = ["tox", "isort", "black"]
+docs = ["Sphinx (==1.3.6)"]
+mypy = ["mypy", "types-requests"]
+test = ["coverage (>=5,<6)", "pytest (>=5,<6)", "pytest-xdist (>=1,<2)", "pytest-mock (>=2,<3)", "responses (==0.13.3)", "mock (==1.3.0)"]
+
+[[package]]
+name = "pytz"
+version = "2021.3"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pywin32-ctypes"
+version = "0.2.0"
+description = ""
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pyyaml"
+version = "5.4.1"
+description = "YAML parser and emitter for Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[[package]]
+name = "readme-renderer"
+version = "30.0"
+description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+bleach = ">=2.1.0"
+docutils = ">=0.13.1"
+Pygments = ">=2.5.1"
+
+[package.extras]
+md = ["cmarkgfm (>=0.5.0,<0.7.0)"]
+
+[[package]]
+name = "requests"
+version = "2.26.0"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
+idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+
+[[package]]
+name = "requests-oauthlib"
+version = "1.3.0"
+description = "OAuthlib authentication support for Requests."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+oauthlib = ">=3.0.0"
+requests = ">=2.0.0"
+
+[package.extras]
+rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
+
+[[package]]
+name = "requests-toolbelt"
+version = "0.9.1"
+description = "A utility belt for advanced users of python-requests"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+requests = ">=2.0.1,<3.0.0"
+
+[[package]]
+name = "rfc3986"
+version = "1.5.0"
+description = "Validating URI References per RFC 3986"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
+
+[package.extras]
+idna2008 = ["idna"]
+
+[[package]]
+name = "rsa"
+version = "4.7.2"
+description = "Pure-Python RSA implementation"
+category = "main"
+optional = false
+python-versions = ">=3.5, <4"
+
+[package.dependencies]
+pyasn1 = ">=0.1.3"
+
+[[package]]
+name = "secretstorage"
+version = "3.3.1"
+description = "Python bindings to FreeDesktop.org Secret Service API"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+cryptography = ">=2.0"
+jeepney = ">=0.6"
+
+[[package]]
+name = "semver"
+version = "2.13.0"
+description = "Python helper for Semantic Versioning (http://semver.org/)"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "setuptools-scm"
+version = "6.3.2"
+description = "the blessed package to manage your versions by scm tags"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+packaging = ">=20.0"
+tomli = ">=1.0.0"
+
+[package.extras]
+toml = ["setuptools (>=42)", "tomli (>=1.0.0)"]
+
+[[package]]
+name = "shellingham"
+version = "1.4.0"
+description = "Tool to Detect Surrounding Shell"
+category = "dev"
+optional = false
+python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,>=2.6"
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "smmap"
+version = "5.0.0"
+description = "A pure Python implementation of a sliding window memory map manager"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "sniffio"
+version = "1.2.0"
+description = "Sniff out which async library your code is running under"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "tomli"
+version = "1.2.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "tomlkit"
+version = "0.7.0"
+description = "Style preserving TOML library"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "tqdm"
+version = "4.62.3"
+description = "Fast, Extensible Progress Meter"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["py-make (>=0.1.0)", "twine", "wheel"]
+notebook = ["ipywidgets (>=6)"]
+telegram = ["requests"]
+
+[[package]]
+name = "twine"
+version = "3.4.2"
+description = "Collection of utilities for publishing packages on PyPI"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+colorama = ">=0.4.3"
+importlib-metadata = ">=3.6"
+keyring = ">=15.1"
+pkginfo = ">=1.4.2"
+readme-renderer = ">=21.0"
+requests = ">=2.20"
+requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0"
+rfc3986 = ">=1.4.0"
+tqdm = ">=4.14"
+
+[[package]]
+name = "typing-extensions"
+version = "3.10.0.2"
+description = "Backported and Experimental Type Hints for Python 3.5+"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "uritemplate"
+version = "3.0.1"
+description = "URI templates"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "urllib3"
+version = "1.26.7"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.extras]
+brotli = ["brotlipy (>=0.6.0)"]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "virtualenv"
+version = "20.9.0"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[package.dependencies]
+"backports.entry-points-selectable" = ">=1.0.4"
+distlib = ">=0.3.1,<1"
+filelock = ">=3.2,<4"
+platformdirs = ">=2,<3"
+six = ">=1.9.0,<2"
+
+[package.extras]
+docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"]
+testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
+
+[[package]]
+name = "webencodings"
+version = "0.5.1"
+description = "Character encoding aliases for legacy web content"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "zipp"
+version = "3.6.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = ">=3.9,<4"
+content-hash = "3430e320792a94dc82788d41c44a8625599c8741ca1d9c3bab192c8803fed3c0"
+
+[metadata.files]
+anyio = [
+    {file = "anyio-3.3.4-py3-none-any.whl", hash = "sha256:4fd09a25ab7fa01d34512b7249e366cd10358cdafc95022c7ff8c8f8a5026d66"},
+    {file = "anyio-3.3.4.tar.gz", hash = "sha256:67da67b5b21f96b9d3d65daa6ea99f5d5282cb09f50eb4456f8fb51dffefc3ff"},
+]
+atomicwrites = [
+    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
+    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+]
+attrs = [
+    {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
+    {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
+]
+"backports.entry-points-selectable" = [
+    {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"},
+    {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"},
+]
+bleach = [
+    {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"},
+    {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"},
+]
+cachecontrol = [
+    {file = "CacheControl-0.12.6-py2.py3-none-any.whl", hash = "sha256:10d056fa27f8563a271b345207402a6dcce8efab7e5b377e270329c62471b10d"},
+    {file = "CacheControl-0.12.6.tar.gz", hash = "sha256:be9aa45477a134aee56c8fac518627e1154df063e85f67d4f83ce0ccc23688e8"},
+]
+cachetools = [
+    {file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"},
+    {file = "cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693"},
+]
+cachy = [
+    {file = "cachy-0.3.0-py2.py3-none-any.whl", hash = "sha256:338ca09c8860e76b275aff52374330efedc4d5a5e45dc1c5b539c1ead0786fe7"},
+    {file = "cachy-0.3.0.tar.gz", hash = "sha256:186581f4ceb42a0bbe040c407da73c14092379b1e4c0e327fdb72ae4a9b269b1"},
+]
+certifi = [
+    {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
+    {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
+]
+cffi = [
+    {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
+    {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
+    {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
+    {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
+    {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
+    {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
+    {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
+    {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
+    {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
+    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
+    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
+    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
+    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
+    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
+    {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
+    {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
+    {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
+    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
+    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
+    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
+    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
+    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
+    {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
+    {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
+    {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
+    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
+    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
+    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
+    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
+    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
+    {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
+    {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
+    {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
+    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
+    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
+    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
+    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
+    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
+    {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
+    {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
+    {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
+    {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
+    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
+    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
+    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
+    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
+    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
+    {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
+    {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
+    {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
+]
+cfgv = [
+    {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+    {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
+charset-normalizer = [
+    {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"},
+    {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"},
+]
+cleo = [
+    {file = "cleo-0.8.1-py2.py3-none-any.whl", hash = "sha256:141cda6dc94a92343be626bb87a0b6c86ae291dfc732a57bf04310d4b4201753"},
+    {file = "cleo-0.8.1.tar.gz", hash = "sha256:3d0e22d30117851b45970b6c14aca4ab0b18b1b53c8af57bed13208147e4069f"},
+]
+click = [
+    {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
+    {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
+]
+click-log = [
+    {file = "click-log-0.3.2.tar.gz", hash = "sha256:16fd1ca3fc6b16c98cea63acf1ab474ea8e676849dc669d86afafb0ed7003124"},
+    {file = "click_log-0.3.2-py2.py3-none-any.whl", hash = "sha256:eee14dc37cdf3072158570f00406572f9e03e414accdccfccd4c538df9ae322c"},
+]
+clikit = [
+    {file = "clikit-0.6.2-py2.py3-none-any.whl", hash = "sha256:71268e074e68082306e23d7369a7b99f824a0ef926e55ba2665e911f7208489e"},
+    {file = "clikit-0.6.2.tar.gz", hash = "sha256:442ee5db9a14120635c5990bcdbfe7c03ada5898291f0c802f77be71569ded59"},
+]
+colorama = [
+    {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
+    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
+]
+coverage = [
+    {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"},
+    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"},
+    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"},
+    {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"},
+    {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"},
+    {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"},
+    {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"},
+    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"},
+    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"},
+    {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"},
+    {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"},
+    {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"},
+    {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"},
+    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"},
+    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"},
+    {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"},
+    {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"},
+    {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"},
+    {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"},
+    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"},
+    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"},
+    {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"},
+    {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"},
+    {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"},
+    {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"},
+    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"},
+    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"},
+    {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"},
+    {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"},
+    {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"},
+    {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"},
+    {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"},
+    {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"},
+]
+crashtest = [
+    {file = "crashtest-0.3.1-py3-none-any.whl", hash = "sha256:300f4b0825f57688b47b6d70c6a31de33512eb2fa1ac614f780939aa0cf91680"},
+    {file = "crashtest-0.3.1.tar.gz", hash = "sha256:42ca7b6ce88b6c7433e2ce47ea884e91ec93104a4b754998be498a8e6c3d37dd"},
+]
+cryptography = [
+    {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"},
+    {file = "cryptography-35.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6"},
+    {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d"},
+    {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa"},
+    {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e"},
+    {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992"},
+    {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6"},
+    {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d"},
+    {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6"},
+    {file = "cryptography-35.0.0-cp36-abi3-win32.whl", hash = "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8"},
+    {file = "cryptography-35.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588"},
+    {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953"},
+    {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6"},
+    {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd"},
+    {file = "cryptography-35.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76"},
+    {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999"},
+    {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad"},
+    {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2"},
+    {file = "cryptography-35.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c"},
+    {file = "cryptography-35.0.0.tar.gz", hash = "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d"},
+]
+distlib = [
+    {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"},
+    {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"},
+]
+docutils = [
+    {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
+    {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
+]
+dotty-dict = [
+    {file = "dotty_dict-1.3.0.tar.gz", hash = "sha256:eb0035a3629ecd84397a68f1f42f1e94abd1c34577a19cd3eacad331ee7cbaf0"},
+]
+filelock = [
+    {file = "filelock-3.3.1-py3-none-any.whl", hash = "sha256:2b5eb3589e7fdda14599e7eb1a50e09b4cc14f34ed98b8ba56d33bfaafcbef2f"},
+    {file = "filelock-3.3.1.tar.gz", hash = "sha256:34a9f35f95c441e7b38209775d6e0337f9a3759f3565f6c5798f19618527c76f"},
+]
+flake8 = [
+    {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
+    {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
+]
+gitdb = [
+    {file = "gitdb-4.0.8-py3-none-any.whl", hash = "sha256:6875cbaed01f1b750394f372607803768fc7dad7c58c7ceb5f5917e980d779b2"},
+    {file = "gitdb-4.0.8.tar.gz", hash = "sha256:858966a9310649cb24a387c101429bb5a1110068a312517722b0281077e78bc6"},
+]
+gitpython = [
+    {file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"},
+    {file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"},
+]
+google-api-core = [
+    {file = "google-api-core-2.1.1.tar.gz", hash = "sha256:646d9399c3c478fe475cfe523e84572ab31a340814ea977fb2774eca5a6549a2"},
+    {file = "google_api_core-2.1.1-py2.py3-none-any.whl", hash = "sha256:39ae7ca2208090c2942ebe1b38ca2928441a14b795c725717d38d7e30adb5fbf"},
+]
+google-api-python-client = [
+    {file = "google-api-python-client-2.23.0.tar.gz", hash = "sha256:f117a595717fc384446f6235019e6a83fc9df821bd9d05dba7ff14aa96c70f52"},
+    {file = "google_api_python_client-2.23.0-py2.py3-none-any.whl", hash = "sha256:a7b364eff63ca75d827cfb241a0f8567157976e879046c1ff20ddf735bad618e"},
+]
+google-auth = [
+    {file = "google-auth-2.3.0.tar.gz", hash = "sha256:2800f6dfad29c6ced5faf9ca0c38ea8ba1ebe2559b10c029bd021e3de3301627"},
+    {file = "google_auth-2.3.0-py2.py3-none-any.whl", hash = "sha256:91892727c09cf5d090c391936a8e67ef5b9a9794c2f426b3d0ceedddbcc0ef50"},
+]
+google-auth-httplib2 = [
+    {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"},
+    {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"},
+]
+google-auth-oauthlib = [
+    {file = "google-auth-oauthlib-0.4.6.tar.gz", hash = "sha256:a90a072f6993f2c327067bf65270046384cda5a8ecb20b94ea9a687f1f233a7a"},
+    {file = "google_auth_oauthlib-0.4.6-py2.py3-none-any.whl", hash = "sha256:3f2a6e802eebbb6fb736a370fbf3b055edcb6b52878bf2f26330b5e041316c73"},
+]
+googleapis-common-protos = [
+    {file = "googleapis-common-protos-1.53.0.tar.gz", hash = "sha256:a88ee8903aa0a81f6c3cec2d5cf62d3c8aa67c06439b0496b49048fb1854ebf4"},
+    {file = "googleapis_common_protos-1.53.0-py2.py3-none-any.whl", hash = "sha256:f6d561ab8fb16b30020b940e2dd01cd80082f4762fa9f3ee670f4419b4b8dbd0"},
+]
+h11 = [
+    {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
+    {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
+]
+html5lib = [
+    {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"},
+    {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"},
+]
+httpcore = [
+    {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"},
+    {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"},
+]
+httplib2 = [
+    {file = "httplib2-0.20.1-py3-none-any.whl", hash = "sha256:8fa4dbf2fbf839b71f8c7837a831e00fcdc860feca99b8bda58ceae4bc53d185"},
+    {file = "httplib2-0.20.1.tar.gz", hash = "sha256:0efbcb8bfbfbc11578130d87d8afcc65c2274c6eb446e59fc674e4d7c972d327"},
+]
+httpx = [
+    {file = "httpx-0.20.0-py3-none-any.whl", hash = "sha256:33af5aad9bdc82ef1fc89219c1e36f5693bf9cd0ebe330884df563445682c0f8"},
+    {file = "httpx-0.20.0.tar.gz", hash = "sha256:09606d630f070d07f9ff28104fbcea429ea0014c1e89ac90b4d8de8286c40e7b"},
+]
+identify = [
+    {file = "identify-2.3.1-py2.py3-none-any.whl", hash = "sha256:5a5000bd3293950d992843c0ef3d82b90a582de2161557bda7f493c8c8864f26"},
+    {file = "identify-2.3.1.tar.gz", hash = "sha256:8a92c56893e9a4ce951f09a50489986615e3eba7b4c60610e0b25f93ca4487ba"},
+]
+idna = [
+    {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
+    {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
+]
+importlib-metadata = [
+    {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
+    {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
+]
+iniconfig = [
+    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+invoke = [
+    {file = "invoke-1.6.0-py2-none-any.whl", hash = "sha256:e6c9917a1e3e73e7ea91fdf82d5f151ccfe85bf30cc65cdb892444c02dbb5f74"},
+    {file = "invoke-1.6.0-py3-none-any.whl", hash = "sha256:769e90caeb1bd07d484821732f931f1ad8916a38e3f3e618644687fc09cb6317"},
+    {file = "invoke-1.6.0.tar.gz", hash = "sha256:374d1e2ecf78981da94bfaf95366216aaec27c2d6a7b7d5818d92da55aa258d3"},
+]
+jeepney = [
+    {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"},
+    {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"},
+]
+keyring = [
+    {file = "keyring-21.8.0-py3-none-any.whl", hash = "sha256:4be9cbaaaf83e61d6399f733d113ede7d1c73bc75cb6aeb64eee0f6ac39b30ea"},
+    {file = "keyring-21.8.0.tar.gz", hash = "sha256:1746d3ac913d449a090caf11e9e4af00e26c3f7f7e81027872192b2398b98675"},
+]
+lockfile = [
+    {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"},
+    {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"},
+]
+mccabe = [
+    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
+    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+]
+msgpack = [
+    {file = "msgpack-1.0.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:b6d9e2dae081aa35c44af9c4298de4ee72991305503442a5c74656d82b581fe9"},
+    {file = "msgpack-1.0.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a99b144475230982aee16b3d249170f1cccebf27fb0a08e9f603b69637a62192"},
+    {file = "msgpack-1.0.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1026dcc10537d27dd2d26c327e552f05ce148977e9d7b9f1718748281b38c841"},
+    {file = "msgpack-1.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:fe07bc6735d08e492a327f496b7850e98cb4d112c56df69b0c844dbebcbb47f6"},
+    {file = "msgpack-1.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:9ea52fff0473f9f3000987f313310208c879493491ef3ccf66268eff8d5a0326"},
+    {file = "msgpack-1.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:26a1759f1a88df5f1d0b393eb582ec022326994e311ba9c5818adc5374736439"},
+    {file = "msgpack-1.0.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:497d2c12426adcd27ab83144057a705efb6acc7e85957a51d43cdcf7f258900f"},
+    {file = "msgpack-1.0.2-cp36-cp36m-win32.whl", hash = "sha256:e89ec55871ed5473a041c0495b7b4e6099f6263438e0bd04ccd8418f92d5d7f2"},
+    {file = "msgpack-1.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a4355d2193106c7aa77c98fc955252a737d8550320ecdb2e9ac701e15e2943bc"},
+    {file = "msgpack-1.0.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:d6c64601af8f3893d17ec233237030e3110f11b8a962cb66720bf70c0141aa54"},
+    {file = "msgpack-1.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f484cd2dca68502de3704f056fa9b318c94b1539ed17a4c784266df5d6978c87"},
+    {file = "msgpack-1.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f3e6aaf217ac1c7ce1563cf52a2f4f5d5b1f64e8729d794165db71da57257f0c"},
+    {file = "msgpack-1.0.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:8521e5be9e3b93d4d5e07cb80b7e32353264d143c1f072309e1863174c6aadb1"},
+    {file = "msgpack-1.0.2-cp37-cp37m-win32.whl", hash = "sha256:31c17bbf2ae5e29e48d794c693b7ca7a0c73bd4280976d408c53df421e838d2a"},
+    {file = "msgpack-1.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8ffb24a3b7518e843cd83538cf859e026d24ec41ac5721c18ed0c55101f9775b"},
+    {file = "msgpack-1.0.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:b28c0876cce1466d7c2195d7658cf50e4730667196e2f1355c4209444717ee06"},
+    {file = "msgpack-1.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:87869ba567fe371c4555d2e11e4948778ab6b59d6cc9d8460d543e4cfbbddd1c"},
+    {file = "msgpack-1.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b55f7db883530b74c857e50e149126b91bb75d35c08b28db12dcb0346f15e46e"},
+    {file = "msgpack-1.0.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:ac25f3e0513f6673e8b405c3a80500eb7be1cf8f57584be524c4fa78fe8e0c83"},
+    {file = "msgpack-1.0.2-cp38-cp38-win32.whl", hash = "sha256:0cb94ee48675a45d3b86e61d13c1e6f1696f0183f0715544976356ff86f741d9"},
+    {file = "msgpack-1.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:e36a812ef4705a291cdb4a2fd352f013134f26c6ff63477f20235138d1d21009"},
+    {file = "msgpack-1.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:2a5866bdc88d77f6e1370f82f2371c9bc6fc92fe898fa2dec0c5d4f5435a2694"},
+    {file = "msgpack-1.0.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:92be4b12de4806d3c36810b0fe2aeedd8d493db39e2eb90742b9c09299eb5759"},
+    {file = "msgpack-1.0.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:de6bd7990a2c2dabe926b7e62a92886ccbf809425c347ae7de277067f97c2887"},
+    {file = "msgpack-1.0.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5a9ee2540c78659a1dd0b110f73773533ee3108d4e1219b5a15a8d635b7aca0e"},
+    {file = "msgpack-1.0.2-cp39-cp39-win32.whl", hash = "sha256:c747c0cc08bd6d72a586310bda6ea72eeb28e7505990f342552315b229a19b33"},
+    {file = "msgpack-1.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:d8167b84af26654c1124857d71650404336f4eb5cc06900667a493fc619ddd9f"},
+    {file = "msgpack-1.0.2.tar.gz", hash = "sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984"},
+]
+nodeenv = [
+    {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
+    {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
+]
+notion-client = [
+    {file = "notion-client-0.7.0.tar.gz", hash = "sha256:e45140bc878c6695afd09e635459148cbaaf882318b2aaa3477e09c05bb9350b"},
+    {file = "notion_client-0.7.0-py2.py3-none-any.whl", hash = "sha256:b3199cd5a97890ccb0607baf84f14448aa7aedf98559ea5150b886bafffed4f3"},
+]
+numpy = [
+    {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"},
+    {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"},
+    {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"},
+    {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"},
+    {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"},
+    {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"},
+    {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"},
+    {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"},
+    {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"},
+    {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"},
+    {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"},
+    {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"},
+    {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"},
+    {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"},
+    {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"},
+    {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"},
+    {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"},
+    {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"},
+    {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"},
+    {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"},
+]
+oauthlib = [
+    {file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"},
+    {file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"},
+]
+packaging = [
+    {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
+    {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
+]
+pandas = [
+    {file = "pandas-1.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68408a39a54ebadb9014ee5a4fae27b2fe524317bc80adf56c9ac59e8f8ea431"},
+    {file = "pandas-1.3.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86b16b1b920c4cb27fdd65a2c20258bcd9c794be491290660722bb0ea765054d"},
+    {file = "pandas-1.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37d63e78e87eb3791da7be4100a65da0383670c2b59e493d9e73098d7a879226"},
+    {file = "pandas-1.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e2fb11f86f6253bb1df26e3aeab3bf2e000aaa32a953ec394571bec5dc6fd6"},
+    {file = "pandas-1.3.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7326b37de08d42dd3fff5b7ef7691d0fd0bf2428f4ba5a2bdc3b3247e9a52e4c"},
+    {file = "pandas-1.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2f29b4da6f6ae7c68f4b3708d9d9e59fa89b2f9e87c2b64ce055cbd39f729e"},
+    {file = "pandas-1.3.3-cp37-cp37m-win32.whl", hash = "sha256:3f5020613c1d8e304840c34aeb171377dc755521bf5e69804991030c2a48aec3"},
+    {file = "pandas-1.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c399200631db9bd9335d013ec7fce4edb98651035c249d532945c78ad453f23a"},
+    {file = "pandas-1.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a800df4e101b721e94d04c355e611863cc31887f24c0b019572e26518cbbcab6"},
+    {file = "pandas-1.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3334a5a9eeaca953b9db1b2b165dcdc5180b5011f3bec3a57a3580c9c22eae68"},
+    {file = "pandas-1.3.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fd2889d8116d7acef0709e4c82b8560a8b22b0f77471391d12c27596e90267"},
+    {file = "pandas-1.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7557b39c8e86eb0543a17a002ac1ea0f38911c3c17095bc9350d0a65b32d801c"},
+    {file = "pandas-1.3.3-cp38-cp38-win32.whl", hash = "sha256:629138b7cf81a2e55aa29ce7b04c1cece20485271d1f6c469c6a0c03857db6a4"},
+    {file = "pandas-1.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:45649503e167d45360aa7c52f18d1591a6d5c70d2f3a26bc90a3297a30ce9a66"},
+    {file = "pandas-1.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebbed7312547a924df0cbe133ff1250eeb94cdff3c09a794dc991c5621c8c735"},
+    {file = "pandas-1.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9f1b54d7efc9df05320b14a48fb18686f781aa66cc7b47bb62fabfc67a0985c"},
+    {file = "pandas-1.3.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9bc59855598cb57f68fdabd4897d3ed2bc3a3b3bef7b868a0153c4cd03f3207"},
+    {file = "pandas-1.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4def2ef2fb7fcd62f2aa51bacb817ee9029e5c8efe42fe527ba21f6a3ddf1a9f"},
+    {file = "pandas-1.3.3-cp39-cp39-win32.whl", hash = "sha256:f7d84f321674c2f0f31887ee6d5755c54ca1ea5e144d6d54b3bbf566dd9ea0cc"},
+    {file = "pandas-1.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:e574c2637c9d27f322e911650b36e858c885702c5996eda8a5a60e35e6648cf2"},
+    {file = "pandas-1.3.3.tar.gz", hash = "sha256:272c8cb14aa9793eada6b1ebe81994616e647b5892a370c7135efb2924b701df"},
+]
+pastel = [
+    {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"},
+    {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"},
+]
+pexpect = [
+    {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
+    {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
+]
+pkginfo = [
+    {file = "pkginfo-1.7.1-py2.py3-none-any.whl", hash = "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779"},
+    {file = "pkginfo-1.7.1.tar.gz", hash = "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd"},
+]
+platformdirs = [
+    {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"},
+    {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"},
+]
+pluggy = [
+    {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
+    {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
+]
+poetry = [
+    {file = "poetry-1.1.11-py2.py3-none-any.whl", hash = "sha256:628e2a933c9f7c28fa0edbee09f9e2e6b25301e610929b5414f3d55ed40fc712"},
+    {file = "poetry-1.1.11.tar.gz", hash = "sha256:7d7d22f55fbebb830cc85b1c69cd7a91fd85f49e5396e7a14b953645a470f69e"},
+]
+poetry-core = [
+    {file = "poetry-core-1.0.7.tar.gz", hash = "sha256:98c11c755a16ef6c5673c22ca94a3802a7df4746a0853a70b6fae8b9f5cac206"},
+    {file = "poetry_core-1.0.7-py2.py3-none-any.whl", hash = "sha256:4f8a7f5390d772f42c4c4c3f188e6424b802cb4b57466c6633a1b9ac36f18a43"},
+]
+poetry2setup = [
+    {file = "poetry2setup-1.0.0-py2.py3-none-any.whl", hash = "sha256:b69a4efabfda24870fd8d08b37b15fbbee5eec0b62711feaef610f94835517be"},
+    {file = "poetry2setup-1.0.0.tar.gz", hash = "sha256:ef1177303996b661eeec3de5027a1af84e106a1d2cb92c73152fe6ce47700cc3"},
+]
+pre-commit = [
+    {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"},
+    {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"},
+]
+protobuf = [
+    {file = "protobuf-3.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:01a0645ef3acddfbc90237e1cdfae1086130fc7cb480b5874656193afd657083"},
+    {file = "protobuf-3.19.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d3861c9721a90ba83ee0936a9cfcc4fa1c4b4144ac9658fb6f6343b38558e9b4"},
+    {file = "protobuf-3.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b64be5d7270cf5e76375bac049846e8a9543a2d4368b69afe78ab725380a7487"},
+    {file = "protobuf-3.19.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2f6046b9e2feee0dce994493186e8715b4392ed5f50f356280ad9c2f9f93080a"},
+    {file = "protobuf-3.19.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac2f8ec942d414609aba0331952ae12bb823e8f424bbb6b8c422f1cef32dc842"},
+    {file = "protobuf-3.19.0-cp36-cp36m-win32.whl", hash = "sha256:3fea09aa04ef2f8b01fcc9bb87f19509934f8a35d177c865b8f9ee5c32b60c1b"},
+    {file = "protobuf-3.19.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d1f4277d321f60456845ca9b882c4845736f1f5c1c69eb778eba22a97977d8af"},
+    {file = "protobuf-3.19.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8488c2276f14f294e890cc1260ab342a13e90cd20dcc03319d2eea258f1fd321"},
+    {file = "protobuf-3.19.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:36bf292f44966c67080e535321501717f4f1eba30faef8f2cd4b0c745a027211"},
+    {file = "protobuf-3.19.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99af73ae34c93e0e2ace57ea2e70243f34fc015c8c23fd39ee93652e726f7e7"},
+    {file = "protobuf-3.19.0-cp37-cp37m-win32.whl", hash = "sha256:f7a031cf8e2fc14acc0ba694f6dff0a01e06b70d817eba6edc72ee6cc20517ac"},
+    {file = "protobuf-3.19.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d4ca5f0c7bc8d2e6966ca3bbd85e9ebe7191b6e21f067896d4af6b28ecff29fe"},
+    {file = "protobuf-3.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9a8a880593015ef2c83f7af797fa4fbf583b2c98b4bd94e46c5b61fee319d84b"},
+    {file = "protobuf-3.19.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:6f16925f5c977dd7787973a50c242e60c22b1d1182aba6bec7bd02862579c10f"},
+    {file = "protobuf-3.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9097327d277b0aa4a3224e61cd6850aef3269172397715299bcffc9f90293c9"},
+    {file = "protobuf-3.19.0-cp38-cp38-win32.whl", hash = "sha256:708d04394a63ee9bdc797938b6e15ed5bf24a1cb37743eb3886fd74a5a67a234"},
+    {file = "protobuf-3.19.0-cp38-cp38-win_amd64.whl", hash = "sha256:ee4d07d596357f51316b6ecf1cc1927660e9d5e418385bb1c51fd2496cd9bee7"},
+    {file = "protobuf-3.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34a77b8fafdeb8f89fee2b7108ae60d8958d72e33478680cc1e05517892ecc46"},
+    {file = "protobuf-3.19.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4f93e0f6af796ddd1502225ff8ea25340ced186ca05b601c44d5c88b45ba80a0"},
+    {file = "protobuf-3.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:942dd6bc8bd2a3c6a156d8ab0f80bd45313f22b78e1176283270054dcc8ca4c2"},
+    {file = "protobuf-3.19.0-cp39-cp39-win32.whl", hash = "sha256:7b3867795708ac88fde8d6f34f0d9a50af56087e41f624bdb2e9ff808ea5dda7"},
+    {file = "protobuf-3.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:a74432e9d28a6072a2359a0f49f81eb14dd718e7dbbfb6c0789b456c49e1f130"},
+    {file = "protobuf-3.19.0-py2.py3-none-any.whl", hash = "sha256:c96e94d3e523a82caa3e5f74b35dd1c4884199358d01c950d95c341255ff48bc"},
+    {file = "protobuf-3.19.0.tar.gz", hash = "sha256:6a1dc6584d24ef86f5b104bcad64fa0fe06ed36e5687f426e0445d363a041d18"},
+]
+ptyprocess = [
+    {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
+    {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
+]
+py = [
+    {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
+    {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+]
+pyasn1 = [
+    {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
+    {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
+    {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
+    {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
+    {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
+    {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
+    {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
+    {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
+    {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
+    {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
+    {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
+    {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
+    {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
+]
+pyasn1-modules = [
+    {file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
+    {file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"},
+    {file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"},
+    {file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"},
+    {file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"},
+    {file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
+    {file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"},
+    {file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"},
+    {file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"},
+    {file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"},
+    {file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"},
+    {file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"},
+    {file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"},
+]
+pycodestyle = [
+    {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
+    {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
+]
+pycparser = [
+    {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
+    {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
+]
+pyflakes = [
+    {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
+    {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
+]
+pygments = [
+    {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
+    {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"},
+]
+pylev = [
+    {file = "pylev-1.4.0-py2.py3-none-any.whl", hash = "sha256:7b2e2aa7b00e05bb3f7650eb506fc89f474f70493271a35c242d9a92188ad3dd"},
+    {file = "pylev-1.4.0.tar.gz", hash = "sha256:9e77e941042ad3a4cc305dcdf2b2dec1aec2fbe3dd9015d2698ad02b173006d1"},
+]
+pyparsing = [
+    {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
+    {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+]
+pytest = [
+    {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
+    {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
+]
+pytest-cov = [
+    {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
+    {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
+]
+python-dateutil = [
+    {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
+    {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+]
+python-gitlab = [
+    {file = "python-gitlab-2.10.1.tar.gz", hash = "sha256:7afa7d7c062fa62c173190452265a30feefb844428efc58ea5244f3b9fc0d40f"},
+    {file = "python_gitlab-2.10.1-py3-none-any.whl", hash = "sha256:581a219759515513ea9399e936ed7137437cfb681f52d2641626685c492c999d"},
+]
+python-semantic-release = [
+    {file = "python-semantic-release-7.19.2.tar.gz", hash = "sha256:8ca0e5f72d31e7b0603b95caad6fb2d5315483ac1fadd86648771966d9ec6f2c"},
+    {file = "python_semantic_release-7.19.2-py3-none-any.whl", hash = "sha256:b2c8bb16a643fee0831be4d06138bc1440ebd4f252c3397d41abde179ea56852"},
+]
+pytz = [
+    {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"},
+    {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"},
+]
+pywin32-ctypes = [
+    {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
+    {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
+]
+pyyaml = [
+    {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
+    {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
+    {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
+    {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
+    {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
+    {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
+    {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"},
+    {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"},
+    {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"},
+    {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"},
+    {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"},
+    {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"},
+    {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"},
+    {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"},
+    {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"},
+]
+readme-renderer = [
+    {file = "readme_renderer-30.0-py2.py3-none-any.whl", hash = "sha256:3286806450d9961d6e3b5f8a59f77e61503799aca5155c8d8d40359b4e1e1adc"},
+    {file = "readme_renderer-30.0.tar.gz", hash = "sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8"},
+]
+requests = [
+    {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
+    {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
+]
+requests-oauthlib = [
+    {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"},
+    {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"},
+    {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
+]
+requests-toolbelt = [
+    {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
+    {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
+]
+rfc3986 = [
+    {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
+    {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
+]
+rsa = [
+    {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"},
+    {file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"},
+]
+secretstorage = [
+    {file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"},
+    {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"},
+]
+semver = [
+    {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"},
+    {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"},
+]
+setuptools-scm = [
+    {file = "setuptools_scm-6.3.2-py3-none-any.whl", hash = "sha256:4c64444b1d49c4063ae60bfe1680f611c8b13833d556fd1d6050c0023162a119"},
+    {file = "setuptools_scm-6.3.2.tar.gz", hash = "sha256:a49aa8081eeb3514eb9728fa5040f2eaa962d6c6f4ec9c32f6c1fba88f88a0f2"},
+]
+shellingham = [
+    {file = "shellingham-1.4.0-py2.py3-none-any.whl", hash = "sha256:536b67a0697f2e4af32ab176c00a50ac2899c5a05e0d8e2dadac8e58888283f9"},
+    {file = "shellingham-1.4.0.tar.gz", hash = "sha256:4855c2458d6904829bd34c299f11fdeed7cfefbf8a2c522e4caea6cd76b3171e"},
+]
+six = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+smmap = [
+    {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
+    {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
+]
+sniffio = [
+    {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"},
+    {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"},
+]
+toml = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+tomli = [
+    {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"},
+    {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"},
+]
+tomlkit = [
+    {file = "tomlkit-0.7.0-py2.py3-none-any.whl", hash = "sha256:6babbd33b17d5c9691896b0e68159215a9387ebfa938aa3ac42f4a4beeb2b831"},
+    {file = "tomlkit-0.7.0.tar.gz", hash = "sha256:ac57f29693fab3e309ea789252fcce3061e19110085aa31af5446ca749325618"},
+]
+tqdm = [
+    {file = "tqdm-4.62.3-py2.py3-none-any.whl", hash = "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c"},
+    {file = "tqdm-4.62.3.tar.gz", hash = "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"},
+]
+twine = [
+    {file = "twine-3.4.2-py3-none-any.whl", hash = "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218"},
+    {file = "twine-3.4.2.tar.gz", hash = "sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936"},
+]
+typing-extensions = [
+    {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
+    {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
+    {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
+]
+uritemplate = [
+    {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"},
+    {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"},
+]
+urllib3 = [
+    {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
+    {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
+]
+virtualenv = [
+    {file = "virtualenv-20.9.0-py2.py3-none-any.whl", hash = "sha256:1d145deec2da86b29026be49c775cc5a9aab434f85f7efef98307fb3965165de"},
+    {file = "virtualenv-20.9.0.tar.gz", hash = "sha256:bb55ace18de14593947354e5e6cd1be75fb32c3329651da62e92bf5d0aab7213"},
+]
+webencodings = [
+    {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
+    {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
+]
+zipp = [
+    {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
+    {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2beafba
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,54 @@
+[tool.poetry]
+name = "notion-gcal-sync"
+version = "1.0.0"
+description = "Bidirectional synchronize calendar events within notion and google calendar"
+readme = "README.md"
+repository = "https://github.com/Ravio1i/notion-gcal-sync"
+
+authors = ["Luka Kroeger <luka.kroeger@gmail.com>"]
+license = 'GNU General Public License v3.0'
+
+exclude = ["*.json", "*.csv", "**/config.yml"]
+
+[tool.poetry.dependencies]
+python = ">=3.9,<4"
+notion-client= "0.7.0"
+google-api-python-client = "2.23.0"
+google-auth-oauthlib = "0.4.6"
+pyyaml = "5.4.1"
+pandas = "1.3.3"
+click = "^8.0.3"
+
+[tool.poetry.dev-dependencies]
+pytest = "6.2.4"
+pytest-cov = "3.0.0"
+flake8 = "4.0.1"
+poetry = "^1.1.11"
+poetry2setup = "^1.0.0"
+pre-commit = "^2.15.0"
+python-semantic-release = "^7.19.2"
+
+[tool.poetry.scripts]
+notion-gcal-sync = "notion_gcal_sync.__main__:main"
+
+[tool.black]
+line-length = 128
+include = '\.pyi?$'
+    exclude = '''
+    /(
+        \.git
+      | build
+      | dist
+      | blib2to3
+      | tests/assets
+    )/
+'''
+
+[tool.semantic_release]
+version_variable = [
+    "notion_gcal_sync/__init__.py:__version__",
+    "pyproject.toml:version"]
+branch = "main"
+upload_to_pypi = false
+upload_to_release = true
+build_command = "pip install poetry && poetry build"
diff --git a/requirements.txt b/requirements.txt
index 8bcd9af..279a765 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,5 @@ google-api-python-client==2.23.0
 google-auth-oauthlib==0.4.6
 pyyaml==5.4.1
 pandas==1.3.3
-flake8==4.0.1
-pytest~=6.2.4
-pytest-cov==3.0.0
-flake8==4.0.1
\ No newline at end of file
+setuptools~=58.0.4
+click~=8.0.3
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 2d1b55b..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-[aliases]
-test=pytest
-
-[flake8]
-max-line-length=127
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 6322c14..0000000
--- a/setup.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from setuptools import setup, find_packages
-
-with open("README.md", "r", encoding="utf-8") as fh:
-    long_description = fh.read()
-
-setup(
-    name='notion-gcal-sync',
-    version='1.0.0',
-    url='https://github.com/Ravio1i/notion-gcal-sync',
-    license='GNU General Public License v3.0',
-    author='Ravio1i',
-    author_email='luka.kroeger@gmail.com',
-    description='Bidirectional synchronize calendar events within notion and google calendar',
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    entry_points={
-        'console_scripts': ['notion-gcal-sync=notion_gcal_sync.__main__:main']
-    },
-    packages=find_packages(include=['notion_gcal_sync', 'notion_gcal_sync.*']),
-    package_data={'notion_gcal_sync': ['config.default.yml']},
-    python_requires=">=3.9",
-    install_requires=[
-        'notion-client==0.7.0',
-        'google-api-python-client==2.23.0',
-        'google-auth-oauthlib==0.4.6',
-        'pyyaml==5.4.1',
-        'pandas==1.3.3'
-    ],
-    setup_requires=['pytest-runner'],
-    test_requires=[
-        'pytest~=6.2.4',
-        'pytest-cov==3.0.0',
-        'flake8==4.0.1'
-    ]
-)


Test patch:
diff --git a/tests/assets/__init__.py b/tests/assets/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/assets/test_config.yml b/tests/assets/test_config.yml
new file mode 100644
index 0000000..76015a8
--- /dev/null
+++ b/tests/assets/test_config.yml
@@ -0,0 +1,31 @@
+---
+
+default_event_length: 60
+no_date_action: 'skip' # ['skip', 'today']
+
+timezone_name: 'Europe/Berlin'
+timezone_diff: '+02:00'
+
+gcal_calendars:
+  Default: '***@gmail.com'
+  Work: '***@group.calendar.google.com'
+gcal_default_calendar_name: "Default"
+
+notion_database_url: 'https://www.notion.so/*/***?v=***&p='
+notion_token: '***'
+notion_columns:
+  name: 'Name'
+  date: 'Date'
+  tags: 'Tags'
+  description: 'Description'
+  location: 'Location'
+  last_updated_time: 'Last Updated'
+  last_synced_time: 'Last Synced'
+  gcal_event_id: 'GCal event Id'
+  recurrent_event: 'GCal Recurrence'
+  gcal_event_url: 'GCal event url'
+  gcal_calendar_name: 'Calendar'
+  gcal_calendar_id: 'GCal calendar Id'
+  to_delete: 'Delete?'
+  deleted: 'Deleted'
+  read_only: 'Read Only'
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..f0f382f
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,82 @@
+from datetime import datetime
+
+import pytest
+
+from notion_gcal_sync.config import Config
+from notion_gcal_sync.events.Event import Event
+from notion_gcal_sync.utils import Time
+
+
+@pytest.fixture(scope='module')
+def notion_columns_fixture():
+    return {
+        'name': 'Name',
+        'date': 'Date',
+        'recurrent_event': 'Recurrence',
+        'tags': 'Tags',
+        'description': 'Description',
+        'location': 'Location',
+        'last_updated_time': 'Last Updated',
+        'last_synced_time': 'Last Synced',
+        'gcal_event_id': 'GCal event Id',
+        'gcal_event_url': 'GCal event url',
+        'gcal_calendar_name': 'Calendar',
+        'gcal_calendar_id': 'GCal calendar Id',
+        'read_only': 'Read Only',
+        'to_delete': 'To Delete',
+        'deleted': 'Deleted',
+    }
+
+
+@pytest.fixture(scope='module')
+def config_dict_fixture(notion_columns_fixture):
+    return {
+        'default_event_length': 60,
+        'no_date_action': 'skip',
+        'gcal_calendars': {'Default': 'dude@gmail.com', 'Calendar2': 'abc123@group.calendar.google.com'},
+        'gcal_default_calendar_name': 'Default',
+        'notion_columns': notion_columns_fixture,
+        'notion_database_url': 'https://www.notion.so/*/***?v=***&p=',
+        'notion_token': 'SECRET',
+        'timezone_diff': '+02:00',
+        'timezone_name': 'Europe/Berlin',
+    }
+
+
+@pytest.fixture(scope='module')
+def time_fixture():
+    return Time('Europe/Berlin', '+02:00')
+
+
+@pytest.fixture(scope='module')
+def config_fixture(notion_columns_fixture, time_fixture):
+    return Config(
+        default_event_length=60,
+        no_date_action='skip',
+        gcal_calendars={'Default': 'dude@gmail.com', 'Calendar2': 'abc123@group.calendar.google.com'},
+        gcal_default_calendar_name='Default',
+        notion_database_url='https://www.notion.so/*/***?v=***&p=',
+        notion_token='SECRET',
+        notion_columns=notion_columns_fixture,
+    )
+
+
+@pytest.fixture(scope='module')
+def event_fixture(config_fixture):
+    return Event(
+        name='name',
+        description='description',
+        location='Vatikan',
+        gcal_event_id='abc123',
+        gcal_calendar_name='Default',
+        gcal_calendar_id='dude@gmail.com',
+        time_start=datetime(2021, 8, 1, 12, 30),
+        time_end=datetime(2021, 8, 1, 14, 30),
+        recurrent_event='',
+        time_last_updated=datetime(2021, 10, 1, 2, 30),
+        time_last_synced='2021-10-12T07:47',
+        notion_page_url='https://www.notion.so',
+        gcal_page_url='calendar.google.com',
+        read_only=False,
+        cfg=config_fixture,
+    )
diff --git a/tests/fixtures.py b/tests/fixtures.py
deleted file mode 100644
index f3f7901..0000000
--- a/tests/fixtures.py
+++ /dev/null
@@ -1,64 +0,0 @@
-from datetime import datetime
-
-import pytest
-
-from notion_gcal_sync.config import Config
-from notion_gcal_sync.events.Event import Event
-from notion_gcal_sync.utils import Time
-
-
-@pytest.fixture()
-def columns():
-    """
-    :return:
-    """
-    return {
-        "name": "Name",
-        "date": 'Date',
-        "recurrent_event": 'Recurrence',
-        "tags": 'Tags',
-        "description": 'Description',
-        "location": 'Location',
-        "last_updated_time": 'Last Updated',
-        "last_synced_time": 'Last Synced',
-        "gcal_event_id": 'GCal event Id',
-        "gcal_event_url": 'GCal event url',
-        "gcal_calendar_name": 'Calendar',
-        "gcal_calendar_id": 'GCal calendar Id',
-        "read_only": 'Read Only',
-        "to_delete": 'Delete?',
-        "deleted": 'Deleted'
-    }
-
-
-@pytest.fixture()
-def time():
-    """
-    :return:
-    """
-    return Time("Europe/Berlin", "+02:00")
-
-
-@pytest.fixture()
-def config(columns, time):
-    """
-    :param columns:
-    :param time:
-    :return:
-    """
-    return Config(60, "skip", "dude@gmail.com", "Default",
-                  {"Default": "dude@gmail.com", "Calendar2": "abc123@group.calendar.google.com"},
-                  "https://www.notion.so/bla", "SECRET", columns, time)
-
-
-@pytest.fixture()
-def event(config):
-    """
-    :param config:
-    :return:
-    """
-    return Event(name="name", description="description", location="Vatikan", gcal_event_id="abc123",
-                 gcal_calendar_name="Default", gcal_calendar_id="dude@gmail.com", time_start=datetime(2021, 8, 1, 12, 30),
-                 time_end=datetime(2021, 8, 1, 14, 30), recurrent_event="", time_last_updated=datetime(2021, 10, 1, 2, 30),
-                 time_last_synced="2021-10-12T07:47", notion_page_url="https://www.notion.so",
-                 gcal_page_url="calendar.google.com", read_only=False, cfg=config)
diff --git a/tests/unit/events/test_Event.py b/tests/unit/events/test_Event.py
index cad7a93..fdd956c 100644
--- a/tests/unit/events/test_Event.py
+++ b/tests/unit/events/test_Event.py
@@ -1,42 +1,61 @@
-from tests.fixtures import *
+from datetime import datetime
+
+import pytest
+
+from notion_gcal_sync.events.Event import Event
+
+
+def test_empty_event():
+    assert Event()
 
 
 @pytest.mark.parametrize(
-    "test_input, expected",
-    [("False", False), (None, False), (True, True), ("True", True)])
-def test_read_only(event, test_input, expected):
-    event.read_only = test_input
+    'test_input, expected', [('False', False), (None, False), (True, True), ('True', True)],
+)
+def test_read_only(event_fixture, test_input, expected):
+    event = Event(read_only=test_input)
     assert event.read_only == expected
 
 
-def test_default_event_length(config):
-    event = Event(time_start=datetime(2021, 12, 10, 10, 30), time_end=datetime(2021, 12, 10, 10, 30), cfg=config)
+def test_default_event_length(config_fixture):
+    event = Event(time_start=datetime(2021, 12, 10, 10, 30), time_end=datetime(2021, 12, 10, 10, 30), cfg=config_fixture, )
     assert event.time_end == datetime(2021, 12, 10, 11, 30)
 
 
-def test_dict_from_class(event):
-    assert event.dict_from_class() == dict(name="name", description="description", location="Vatikan", gcal_event_id="abc123",
-                                           gcal_calendar_name="Default", gcal_calendar_id="dude@gmail.com",
-                                           time_start=datetime(2021, 8, 1, 12, 30),
-                                           time_end=datetime(2021, 8, 1, 14, 30), recurrent_event="",
-                                           time_last_updated=datetime(2021, 10, 1, 2, 30),
-                                           time_last_synced="2021-10-12T07:47", notion_page_url="https://www.notion.so",
-                                           gcal_page_url="calendar.google.com", read_only=False)
+def test_dict_from_class(event_fixture):
+    assert event_fixture.to_dict() == dict(
+        name='name',
+        description='description',
+        location='Vatikan',
+        gcal_event_id='abc123',
+        gcal_calendar_name='Default',
+        gcal_calendar_id='dude@gmail.com',
+        time_start=datetime(2021, 8, 1, 12, 30),
+        time_end=datetime(2021, 8, 1, 14, 30),
+        recurrent_event='',
+        time_last_updated=datetime(2021, 10, 1, 2, 30),
+        time_last_synced='2021-10-12T07:47',
+        notion_page_url='https://www.notion.so',
+        gcal_page_url='calendar.google.com',
+        read_only=False,
+    )
 
 
 @pytest.mark.parametrize(
-    "test_calendar_id, test_calendar_name, expected",
-    [("dude@gmail.com", "Default", ("dude@gmail.com", "Default")),
-     ("dude@gmail.com", "asdf", ("dude@gmail.com", "Default")),
-     ("asdf", "Default", ("dude@gmail.com", "Default")),
-     (None, None, ("dude@gmail.com", "Default")),
-     ('something@gmail.com', 'Google Calendar', ('skip', 'skip')),
-     ])
-def test_set_calendar(event, test_calendar_id, test_calendar_name, expected):
-    assert event.set_calendar(test_calendar_id, test_calendar_name) == expected
-
-
-def test_different_calendar(config):
-    event = Event(gcal_calendar_id="dude@gmail.com", gcal_calendar_name="Calendar2", cfg=config)
-    assert event.gcal_calendar_name == "Calendar2"
-    assert event.gcal_calendar_id == "abc123@group.calendar.google.com"
+    'test_calendar_id, test_calendar_name, expected',
+    [
+        ('dude@gmail.com', 'Default', ('dude@gmail.com', 'Default')),
+        ('dude@gmail.com', 'asdf', ('dude@gmail.com', 'Default')),
+        ('asdf', 'Default', ('dude@gmail.com', 'Default')),
+        (None, None, ('dude@gmail.com', 'Default')),
+        ('something@gmail.com', 'Google Calendar', ('skip', 'skip')),
+    ],
+)
+def test_set_calendar(event_fixture, test_calendar_id, test_calendar_name, expected):
+    assert event_fixture.set_calendar(test_calendar_id, test_calendar_name) == expected
+
+
+def test_different_calendar(config_fixture):
+    event = Event(gcal_calendar_id='dude@gmail.com', gcal_calendar_name='Calendar2', cfg=config_fixture, )
+    assert event.gcal_calendar_name == 'Calendar2'
+    assert event.gcal_calendar_id == 'abc123@group.calendar.google.com'
diff --git a/tests/unit/events/test_NotionEvent.py b/tests/unit/events/test_NotionEvent.py
index f62a128..fb7d570 100644
--- a/tests/unit/events/test_NotionEvent.py
+++ b/tests/unit/events/test_NotionEvent.py
@@ -7,214 +7,171 @@
 
 
 def test_get_name():
-    column = "Name"
-    properties = {
-        "Name": {
-            "title": [{
-                "text": {
-                    "content": "Title"
-                }
-            }]
-        }
-    }
-    assert NotionEvent.get_name(properties, column) == "Title"
+    column = 'Name'
+    properties = {'Name': {'title': [{'text': {'content': 'Title'}}]}}
+    assert NotionEvent.get_name(properties, column) == 'Title'
 
 
 def test_get_name_invalid():
-    column = "Name"
+    column = 'Name'
     properties = {}
     assert NotionEvent.get_name(properties, column) == ''
 
 
 def test_get_last_edited_time():
-    column = "Last Edited Time"
-    time = Time(timezone_name="Europe/Berlin", timezone_diff="+02:00")
-    properties = {
-        column: {
-            "last_edited_time": "2021-10-01T11:37:00.000Z"
-        }
-    }
+    column = 'Last Edited Time'
+    time = Time(timezone_name='Europe/Berlin', timezone_diff='+02:00')
+    properties = {column: {'last_edited_time': '2021-10-01T11:37:00.000Z'}}
     expected = datetime(2021, 10, 1, 13, 37, tzinfo=timezone(timedelta(seconds=7200)))
     assert NotionEvent.get_last_edited_time(properties, column, time) == expected
 
 
 @pytest.mark.parametrize(
-    "test_start, test_end, expected",
-    [("2021-10-01T11:37:00.000Z", None, ("2021-10-01T11:37:00.000Z", "2021-10-01T11:37:00.000Z")),
-     ("2021-10-01T11:37:00.000Z", "2021-10-01T13:37:00.000Z", ("2021-10-01T11:37:00.000Z", "2021-10-01T13:37:00.000Z")),
-     ("2021-10-01", None, ("2021-10-01", "2021-10-01"))])
+    'test_start, test_end, expected',
+    [
+        ('2021-10-01T11:37:00.000Z', None, ('2021-10-01T11:37:00.000Z', '2021-10-01T11:37:00.000Z'),),
+        ('2021-10-01T11:37:00.000Z', '2021-10-01T13:37:00.000Z', ('2021-10-01T11:37:00.000Z', '2021-10-01T13:37:00.000Z'),),
+        ('2021-10-01', None, ('2021-10-01', '2021-10-01')),
+    ],
+)
 def test_get_time(test_start, test_end, expected):
-    column = "Date"
-    properties = {
-        column: {
-            "date": {
-                "start": test_start,
-                "end": test_end
-            }
-        }
-    }
+    column = 'Date'
+    properties = {column: {'date': {'start': test_start, 'end': test_end}}}
     assert NotionEvent.get_time(properties, column) == expected
 
 
 def test_get_time_invalid():
-    column = "Date"
+    column = 'Date'
     properties = {}
     assert NotionEvent.get_time(properties, column) == (None, None)
 
 
 def test_get_text():
-    column = "Description"
-    properties = {
-        column: {
-            "rich_text": [{
-                "text": {
-                    "content": "This is a text"
-                }
-            }]
-        }
-    }
-    assert NotionEvent.get_text(properties, column) == "This is a text"
+    column = 'Description'
+    properties = {column: {'rich_text': [{'text': {'content': 'This is a text'}}]}}
+    assert NotionEvent.get_text(properties, column) == 'This is a text'
 
 
 def test_get_text_invalid():
-    column = "Description"
+    column = 'Description'
     properties = {}
-    assert NotionEvent.get_text(properties, column) == ""
+    assert NotionEvent.get_text(properties, column) == ''
 
 
 def test_get_url():
-    column = "Url"
-    properties = {
-        column: {
-            "url": "https://google.com"
-        }
-    }
-    assert NotionEvent.get_url(properties, column) == "https://google.com"
+    column = 'Url'
+    properties = {column: {'url': 'https://google.com'}}
+    assert NotionEvent.get_url(properties, column) == 'https://google.com'
 
 
 def test_get_select():
-    column = "Select"
-    properties = {
-        column: {
-            "select": {
-                "name": "Selection1"
-            }
-        }
-    }
-    assert NotionEvent.get_select(properties, column) == "Selection1"
+    column = 'Select'
+    properties = {column: {'select': {'name': 'Selection1'}}}
+    assert NotionEvent.get_select(properties, column) == 'Selection1'
 
 
 def test_get_select_invalid():
-    column = "Select"
+    column = 'Select'
     properties = {}
-    assert NotionEvent.get_select(properties, column) == ""
+    assert NotionEvent.get_select(properties, column) == ''
 
 
 def test_get_multiselect():
-    column = "MultiSelect"
-    properties = {
-        column: {
-            "multi_select": [{
-                "name": "Selection1"
-            }, {
-                "name": "Selection2"
-            }]
-        }
-    }
-    assert NotionEvent.get_multiselect(properties, column) == ["Selection1", "Selection2"]
+    column = 'MultiSelect'
+    properties = {column: {'multi_select': [{'name': 'Selection1'}, {'name': 'Selection2'}]}}
+    assert NotionEvent.get_multiselect(properties, column) == [
+        'Selection1',
+        'Selection2',
+    ]
 
 
 def test_get_multiselect_invalid():
-    column = "MultiSelect"
+    column = 'MultiSelect'
     properties = {}
     assert NotionEvent.get_multiselect(properties, column) == []
 
 
-@pytest.mark.parametrize("test_input, expected", [(False, False), (True, True)])
+@pytest.mark.parametrize('test_input, expected', [(False, False), (True, True)])
 def test_get_checkbox(test_input, expected):
-    column = "Checkbox"
-    properties = {
-        column: {
-            "checkbox": test_input
-        }
-    }
+    column = 'Checkbox'
+    properties = {column: {'checkbox': test_input}}
     assert NotionEvent.get_checkbox(properties, column) == expected
 
 
 def test_get_checkbox_invalid():
-    column = "Checkbox"
+    column = 'Checkbox'
     properties = {}
     assert not NotionEvent.get_checkbox(properties, column)
 
 # def test_body():
-    # NotionEvent("Name", "Test", "Heaven", "event_id", "calendar_name", "calendar_id", datetime(2021, 2, 1),
-    #     datetime(2021, 2, 2), "",
-    #             datetime(2021, 1, 1, 13, 37), "NSYNC", "https://notion.so", "https://google.com", "notion_id", False, )
-    # test_body = {
-    #     "properties": {
-    #         self.cfg.col_name: {
-    #             "title": [{
-    #                 "text": {
-    #                     "content": self.name
-    #                 }
-    #             }]
-    #         },
-    #         self.cfg.col_date: {
-    #             "date": {
-    #                 "start": time_start,
-    #                 "end": time_end
-    #             }
-    #         },
-    #         self.cfg.col_recurrent_event: {
-    #             "rich_text": [{
-    #                 "text": {
-    #                     "content": self.recurrent_event
-    #                 }
-    #             }]
-    #         },
-    #         self.cfg.col_description: {
-    #             "rich_text": [{
-    #                 "text": {
-    #                     "content": self.description
-    #                 }
-    #             }]
-    #         },
-    #         self.cfg.col_gcal_calendar_id: {
-    #             "select": {
-    #                 "name": self.gcal_calendar_id
-    #             }
-    #         },
-    #         self.cfg.col_gcal_calendar_name: {
-    #             "select": {
-    #                 "name": self.gcal_calendar_name
-    #             }
-    #         },
-    #         self.cfg.col_location: {
-    #             "rich_text": [{
-    #                 "text": {
-    #                     "content": self.location
-    #                 }
-    #             }]
-    #         },
-    #         self.cfg.col_gcal_event_id: {
-    #             "rich_text": [{
-    #                 "text": {
-    #                     "content": self.gcal_event_id
-    #                 }
-    #             }]
-    #         },
-    #         self.cfg.col_last_synced_time: {
-    #             "rich_text": [{
-    #                 "text": {
-    #                     "content": self.cfg.time.now(),
-    #                 }
-    #             }]
-    #         },
-    #         self.cfg.col_gcal_event_url: {
-    #             "url": self.gcal_page_url
-    #         },
-    #         self.cfg.col_read_only: {
-    #             "checkbox": bool(self.read_only)
-    #         }
-    #     },
-    # }
+# NotionEvent("Name", "Test", "Heaven", "event_id", "calendar_name", "calendar_id", datetime(2021, 2, 1),
+#     datetime(2021, 2, 2), "",
+#             datetime(2021, 1, 1, 13, 37), "NSYNC", "https://notion.so", "https://google.com", "notion_id", False, )
+# test_body = {
+#     "properties": {
+#         self.cfg.col_name: {
+#             "title": [{
+#                 "text": {
+#                     "content": self.name
+#                 }
+#             }]
+#         },
+#         self.cfg.col_date: {
+#             "date": {
+#                 "start": time_start,
+#                 "end": time_end
+#             }
+#         },
+#         self.cfg.col_recurrent_event: {
+#             "rich_text": [{
+#                 "text": {
+#                     "content": self.recurrent_event
+#                 }
+#             }]
+#         },
+#         self.cfg.col_description: {
+#             "rich_text": [{
+#                 "text": {
+#                     "content": self.description
+#                 }
+#             }]
+#         },
+#         self.cfg.col_gcal_calendar_id: {
+#             "select": {
+#                 "name": self.gcal_calendar_id
+#             }
+#         },
+#         self.cfg.col_gcal_calendar_name: {
+#             "select": {
+#                 "name": self.gcal_calendar_name
+#             }
+#         },
+#         self.cfg.col_location: {
+#             "rich_text": [{
+#                 "text": {
+#                     "content": self.location
+#                 }
+#             }]
+#         },
+#         self.cfg.col_gcal_event_id: {
+#             "rich_text": [{
+#                 "text": {
+#                     "content": self.gcal_event_id
+#                 }
+#             }]
+#         },
+#         self.cfg.col_last_synced_time: {
+#             "rich_text": [{
+#                 "text": {
+#                     "content": self.cfg.time.now(),
+#                 }
+#             }]
+#         },
+#         self.cfg.col_gcal_event_url: {
+#             "url": self.gcal_page_url
+#         },
+#         self.cfg.col_read_only: {
+#             "checkbox": bool(self.read_only)
+#         }
+#     },
+# }
diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py
index 9175855..481d0d4 100644
--- a/tests/unit/test_config.py
+++ b/tests/unit/test_config.py
@@ -1,59 +1,97 @@
-from tests.fixtures import *
+import pytest
 
+from notion_gcal_sync.config import Config
 
-@pytest.mark.parametrize(
-    "test_url, expected",
-    [("https://www.notion.so/subdomain/THE_ID?v=asdfasdf123&p=", "THE_ID"),
-     ("https://www.notion.so/THE_ID?v=asdfasdf123&p=", "THE_ID"),
-     ("https://www.notion.so/bla", None),
-     ("https://www.notion.so/bla?v=", None)])
-def test_database_id(time, columns, test_url, expected):
-    config = Config(60, "skip", "dude@gmail.com", "Default", {"Default": "dude@gmail.com"}, test_url,
-                    "SECRET", columns, time)
 
-    assert config.database_id == expected
+def test_empty_config():
+    assert Config()
+
+
+@pytest.mark.parametrize(
+    'test_url, expected',
+    [
+        ('https://www.notion.so/subdomain/THE_ID?v=asdfasdf123&p=', 'THE_ID'),
+        ('https://www.notion.so/THE_ID?v=asdfasdf123&p=', 'THE_ID'),
+        ('https://www.notion.so/THE_ID?v=asdfasdf123', 'THE_ID'),
+        ('https://www.notion.so/THE_ID?v=', None),
+        ('https://www.notion.so/THE_ID', None),
+        ('https://www.notion.so/THE_ID&p=', None),
+        ('http://www.notion.so/THE_ID?v=asdfasdf123&p=', None),
+        (None, None),
+    ],
+)
+def test_database_id(time_fixture, notion_columns_fixture, test_url, expected):
+    config = Config(
+        60, 'skip', {'Default': 'dude@gmail.com'}, 'Default', test_url, 'SECRET', notion_columns_fixture, time_fixture,
+    )
+    assert config.notion_database_id == expected
 
 
-def test_default_event_length(config):
-    config.default_event_length = "123"
-    assert config.default_event_length == 60
+def test_default_event_length(config_fixture):
+    config_fixture.default_event_length = '123'
+    assert config_fixture.default_event_length == 60
 
 
 @pytest.mark.parametrize(
-    "test_action, expected",
-    [("skip", "skip"), ("today", "today"), ("something_else", "skip")])
-def test_no_date_action(config, test_action, expected):
-    config.no_date_action = test_action
-    assert config.no_date_action == expected
+    'test_action, expected', [('skip', 'skip'), ('today', 'today'), ('something_else', 'skip')],
+)
+def test_no_date_action(config_fixture, test_action, expected):
+    config_fixture.no_date_action = test_action
+    assert config_fixture.no_date_action == expected
 
 
 @pytest.mark.parametrize(
-    "test_calendar_id, expected",
-    [("dude@gmail.com", "Default"), ("abc123@group.calendar.google.com", "Calendar2"),
-     ("error@group.calendar.google.com", None), (None, None)])
-def test_get_calendar_name(config, test_calendar_id, expected):
-    assert config.get_calendar_name(test_calendar_id) == expected
+    'test_calendar_id, expected',
+    [
+        ('dude@gmail.com', 'Default'),
+        ('abc123@group.calendar.google.com', 'Calendar2'),
+        ('error@group.calendar.google.com', None),
+        (None, None),
+    ],
+)
+def test_get_calendar_name(config_fixture, test_calendar_id, expected):
+    assert config_fixture.get_calendar_name(test_calendar_id) == expected
+
+
+def test_notion_invalid_columns(notion_columns_fixture):
+    del notion_columns_fixture['name']
+    with pytest.raises(ValueError):
+        Config(notion_columns=notion_columns_fixture)
 
 
 @pytest.mark.parametrize(
-    "test_calendar_name, expected",
-    [("Default", "dude@gmail.com"), ("Calendar2", "abc123@group.calendar.google.com"),
-     ("error@group.calendar.google.com", None), (None, None)])
-def test_get_calendar_idd(config, test_calendar_name, expected):
-    assert config.get_calendar_id(test_calendar_name) == expected
+    'test_calendar_name, expected',
+    [
+        ('Default', 'dude@gmail.com'),
+        ('Calendar2', 'abc123@group.calendar.google.com'),
+        ('error@group.calendar.google.com', None),
+        (None, None),
+    ],
+)
+def test_get_calendar_id(config_fixture, test_calendar_name, expected):
+    assert config_fixture.get_calendar_id(test_calendar_name) == expected
 
 
 @pytest.mark.parametrize(
-    "test_calendar_id, expected",
-    [("dude@gmail.com", True), ("abc123@group.calendar.google.com", True),
-     ("error@group.calendar.google.com", False), (None, False)])
-def test_is_valid_calendar_id(config, test_calendar_id, expected):
-    assert config.is_valid_calendar_id(test_calendar_id) == expected
+    'test_calendar_id, expected',
+    [
+        ('dude@gmail.com', True),
+        ('abc123@group.calendar.google.com', True),
+        ('error@group.calendar.google.com', False),
+        (None, False),
+    ],
+)
+def test_is_valid_calendar_id(config_fixture, test_calendar_id, expected):
+    assert config_fixture.is_valid_calendar_id(test_calendar_id) == expected
 
 
 @pytest.mark.parametrize(
-    "test_calendar_name, expected",
-    [("Default", True), ("Calendar2", True),
-     ("error@group.calendar.google.com", False), (None, False)])
-def test_is_valid_calendar_name(config, test_calendar_name, expected):
-    assert config.is_valid_calendar_name(test_calendar_name) == expected
+    'test_calendar_name, expected',
+    [('Default', True), ('Calendar2', True), ('error@group.calendar.google.com', False), (None, False)],
+)
+def test_is_valid_calendar_name(config_fixture, test_calendar_name, expected):
+    assert config_fixture.is_valid_calendar_name(test_calendar_name) == expected
+
+
+def test_to_dict(config_fixture, config_dict_fixture):
+    assert config_fixture.to_dict() == config_dict_fixture
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index ea4186f..7cf79e4 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -7,65 +7,81 @@
 
 
 @pytest.mark.parametrize(
-    "test_input, expected",
-    [("2017-10-25T19:00:00.000+02:00", False),
-     ("2017-10-25T19:00:00.000", False),
-     ("2017-10-25", True),
-     (datetime(2017, 10, 25, 00, 00, tzinfo=timezone(timedelta(seconds=7200))), False),
-     (None, None)])
+    'test_input, expected',
+    [
+        ('2017-10-25T19:00:00.000+02:00', False),
+        ('2017-10-25T19:00:00.000', False),
+        ('2017-10-25', True),
+        (datetime(2017, 10, 25, 00, 00, tzinfo=timezone(timedelta(seconds=7200))), False,),
+        (None, None),
+    ],
+)
 def test_is_date(test_input, expected):
     assert Time.is_date(test_input) == expected
 
 
 @pytest.mark.parametrize(
-    "test_input, expected",
-    [(datetime(2017, 10, 25, 13, 37, 10, 11, tzinfo=timezone(timedelta(seconds=7200))), "2017-10-25T13:37:10+02:00"),
-     (None, None)])
+    'test_input, expected',
+    [
+        (datetime(2017, 10, 25, 13, 37, 10, 11, tzinfo=timezone(timedelta(seconds=7200))), '2017-10-25T13:37:10+02:00',),
+        (None, None),
+    ],
+)
 def test_datetime_to_str(test_input, expected):
     assert Time.datetime_to_str(test_input) == expected
 
 
 @pytest.mark.parametrize(
-    "test_input, expected",
-    [(datetime(2017, 10, 25), "2017-10-25"),
-     (datetime(2017, 10, 25, 13, 37, 10, 11, tzinfo=timezone(timedelta(seconds=7200))), "2017-10-25"),
-     (None, None)])
+    'test_input, expected',
+    [
+        (datetime(2017, 10, 25), '2017-10-25'),
+        (datetime(2017, 10, 25, 13, 37, 10, 11, tzinfo=timezone(timedelta(seconds=7200))), '2017-10-25',),
+        (None, None),
+    ],
+)
 def test_datetime_to_str_date(test_input, expected):
     assert Time.datetime_to_str_date(test_input) == expected
 
 
 @pytest.mark.parametrize(
-    "test_input, expected",
-    [(Time("Europe/Berlin", "+02:00"), timedelta(hours=2, minutes=0)),
-     (Time("UTC", "+00:00"), timedelta(hours=0, minutes=0)),
-     (Time("America/Los_Angeles", "-07:00"), timedelta(hours=-7, minutes=0))])
+    'test_input, expected',
+    [
+        (Time('Europe/Berlin', '+02:00'), timedelta(hours=2, minutes=0)),
+        (Time('UTC', '+00:00'), timedelta(hours=0, minutes=0)),
+        (Time('America/Los_Angeles', '-07:00'), timedelta(hours=-7, minutes=0)),
+    ],
+)
 def test_time(test_input, expected):
     assert test_input.timezone_diff_delta == expected
 
 
 @pytest.fixture()
 def time():
-    return Time("Europe/Berlin", "+02:00")
+    return Time('Europe/Berlin', '+02:00')
 
 
 @pytest.mark.parametrize(
-    "test_input, expected",
-    [(None, None), ("2017-10-25", datetime(2017, 10, 25)),
-     ("2017-10-25T00:00", datetime(2017, 10, 25, 00, 00, tzinfo=timezone(timedelta(seconds=7200)))),
-     ("2017-10-25T10:10", datetime(2017, 10, 25, 10, 10, tzinfo=timezone(timedelta(seconds=7200)))),
-     ("2017-10-25T10:10+01:00", datetime(2017, 10, 25, 11, 10, tzinfo=timezone(timedelta(seconds=7200)))),
-     ("2017-10-25T10:10+03:00", datetime(2017, 10, 25, 9, 10, tzinfo=timezone(timedelta(seconds=7200))))])
-def test_str_to_datetime(time, test_input, expected):
-    assert time.str_to_datetime(test_input) == expected
+    'test_input, expected',
+    [
+        (None, None),
+        ('2017-10-25', datetime(2017, 10, 25)),
+        ('2017-10-25T00:00', datetime(2017, 10, 25, 00, 00, tzinfo=timezone(timedelta(seconds=7200))),),
+        ('2017-10-25T10:10', datetime(2017, 10, 25, 10, 10, tzinfo=timezone(timedelta(seconds=7200))),),
+        ('2017-10-25T10:10+01:00', datetime(2017, 10, 25, 11, 10, tzinfo=timezone(timedelta(seconds=7200))),),
+        ('2017-10-25T10:10+03:00', datetime(2017, 10, 25, 9, 10, tzinfo=timezone(timedelta(seconds=7200))),),
+    ],
+)
+def test_str_to_datetime(time_fixture, test_input, expected):
+    assert time_fixture.str_to_datetime(test_input) == expected
 
 
 def test_utc_str_to_datetime():
-    time = Time("UTC", "+00:00")
-    assert time.str_to_datetime("2017-10-25T10:10+03:00") == datetime(2017, 10, 25, 7, 10, tzinfo=ZoneInfo("UTC"))
+    time = Time('UTC', '+00:00')
+    assert time.str_to_datetime('2017-10-25T10:10+03:00') == datetime(2017, 10, 25, 7, 10, tzinfo=ZoneInfo('UTC'))
 
 
 @pytest.mark.parametrize(
-    "test_input, expected",
-    [(datetime(2017, 10, 25), datetime(2017, 10, 25)), (None, None)])
-def test_to_datetime(time, test_input, expected):
-    assert time.to_datetime(test_input) == expected
+    'test_input, expected', [(datetime(2017, 10, 25), datetime(2017, 10, 25)), (None, None)],
+)
+def test_to_datetime(time_fixture, test_input, expected):
+    assert time_fixture.to_datetime(test_input) == expected