├── NotionGCal.py ├── README.md ├── configSync.py ├── credentials.json ├── experiment.py ├── gcal-to-notion.py ├── notion-to-gcal.py └── src └── img1.png /NotionGCal.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | from notion_client import Client 3 | import datetime 4 | import pickle 5 | import os.path 6 | from googleapiclient.discovery import build 7 | from google_auth_oauthlib.flow import InstalledAppFlow 8 | from google.auth.transport.requests import Request 9 | 10 | SCOPES = ['https://www.googleapis.com/auth/calendar'] 11 | 12 | ####################################### 13 | 14 | CREDENTIALS_FILE = 'credentials.json' 15 | 16 | NOTION_TOKEN = "secret_Bg53dmHuIFfzqbYyNZp9VrVFL4EenamUInis5UsQXH4"#The secret code from Notion Integration (it should look like this: NOTION_TOKEN = "secret_XXXXXXXXXXXXX") 17 | 18 | database_id = "" #Get the string of numbers before the "?" on your Notion dashboard URL (it should look like this: database_id = "xxxxxxxxxxxxxxxxxxxxxxxxxx") 19 | 20 | dateProperty = "Date:" #The name of the 'Date' property for your items in your Notion Database (it should look like this: dateProperty = 'Date') 21 | 22 | nameProperty = "Name" #The name of the 'Title' property for your items in your Notion Database (it should look like this: nameProperty = 'Name') 23 | 24 | ######################################## 25 | 26 | def get_calendar_service(): 27 | creds = None 28 | # The file token.pickle stores the user's access and refresh tokens, and is 29 | # created automatically when the authorization flow completes for the first 30 | # time. 31 | if os.path.exists('token.pickle'): 32 | with open('token.pickle', 'rb') as token: 33 | creds = pickle.load(token) 34 | # If there are no (valid) credentials available, let the user log in. 35 | if not creds or not creds.valid: 36 | if creds and creds.expired and creds.refresh_token: 37 | creds.refresh(Request()) 38 | else: 39 | flow = InstalledAppFlow.from_client_secrets_file( 40 | CREDENTIALS_FILE, SCOPES) 41 | creds = flow.run_local_server(port=0) 42 | 43 | # Save the credentials for the next run 44 | with open('token.pickle', 'wb') as token: 45 | pickle.dump(creds, token) 46 | 47 | service = build('calendar', 'v3', credentials=creds) 48 | return service 49 | 50 | def list_calendars(): 51 | service = get_calendar_service() 52 | # Call the Calendar API 53 | calendars_result = service.calendarList().list().execute() 54 | 55 | calendars = calendars_result.get('items', []) 56 | 57 | for calendar in calendars: 58 | summary = calendar['summary'] 59 | id = calendar['id'] 60 | return calendars 61 | 62 | os.environ['NOTION_TOKEN'] = NOTION_TOKEN 63 | notion = Client(auth = os.environ["NOTION_TOKEN"]) 64 | 65 | today = date.today() 66 | print("Today's date:", today) 67 | 68 | today = today.strftime('%Y-%m-%d') 69 | print() 70 | 71 | if True: 72 | 73 | service = get_calendar_service() 74 | calendars = list_calendars() 75 | for calendar in calendars: 76 | print(calendar['summary'],"\n") 77 | 78 | while True: 79 | event_items = [] 80 | for calendar in calendars: 81 | events = service.events().list(calendarId = calendar['id']).execute() 82 | 83 | for single_item in events['items']: 84 | event_items.append(single_item) 85 | 86 | GCal_today = [] 87 | for event in event_items: 88 | try: 89 | if event['start']['dateTime'][:10] == today : 90 | print(event['summary']) 91 | print() 92 | GCal_today.append(event) 93 | except: 94 | try: 95 | if event['start']['date'][:10] == today : 96 | print(event['summary']) 97 | print() 98 | GCal_today.append(event) 99 | except: 100 | try: 101 | if event['originalStartTime']['dateTime'][:10] == today : 102 | print(event['summary']) 103 | print() 104 | GCal_today.append(event) 105 | except: 106 | if event['originalStartTime']['date'][:10] == today : 107 | print(event['summary']) 108 | print() 109 | GCal_today.append(event) 110 | 111 | page_token = events.get('nextPageToken') 112 | if not page_token: 113 | break 114 | 115 | 116 | print() 117 | #Takes GCal Event and adds it to Notion DB 118 | 119 | #Takes all tasks for today from Notion and stores it in Notion_today 120 | Notion_tasks_today = [] 121 | Notion_tasks_ID = [] 122 | Notion_today = notion.databases.query( 123 | **{ 124 | "database_id": database_id, 125 | "filter": { 126 | "and": [ 127 | { 128 | "property": dateProperty, 129 | "date": { 130 | "equals": today 131 | } 132 | } 133 | ] 134 | } 135 | 136 | } 137 | ) 138 | id_id = {} 139 | #Takes all Notion tasks and checks if they have anything in the GCal_ID property 140 | for Action_index, Action_Item in enumerate(Notion_today['results']): 141 | Action_Sub = Action_Item['properties']['GCal_ID']['rich_text'] 142 | Notion_tasks_ID.append(Action_Sub) 143 | 144 | #Takes only the 'plain_text' values 145 | for list_index, list_item in enumerate(Notion_tasks_ID): 146 | 147 | if list_item != []: 148 | Notion_tasks_ID[list_index] = list_item[0]['plain_text'] 149 | 150 | for ID_Guess in Notion_today['results']: 151 | 152 | if ID_Guess['properties']['GCal_ID']['rich_text'] != []: 153 | 154 | if ID_Guess['properties']['GCal_ID']['rich_text'][0]['plain_text'] == list_item[0]['plain_text']: 155 | id_id[Notion_tasks_ID[list_index]] = ID_Guess['id'] 156 | else: 157 | 158 | Notion_tasks_ID[list_index] = None 159 | 160 | 161 | while None in Notion_tasks_ID: 162 | 163 | Notion_tasks_ID.remove(None) 164 | 165 | 166 | #Prints the different daily tasks 167 | for Action_index, Action_Item in enumerate(Notion_today['results']): 168 | 169 | Action_Sub = Action_Item['properties'][nameProperty]['title'][0]['text']['content'] 170 | 171 | Notion_tasks_today.append(Action_Sub) 172 | 173 | #Attach GCal_ID to Notion_Page_ID 174 | 175 | 176 | 177 | #Add GCal event to Notion 178 | for GCal_Event in GCal_today: 179 | 180 | #Check if the event name is already on Notion_today 181 | if GCal_Event['id'] not in Notion_tasks_ID : 182 | 183 | #Add the page 184 | try: 185 | new_page = notion.pages.create( 186 | **{ 187 | "parent": { 188 | "database_id": database_id, 189 | }, 190 | 191 | "properties": { 192 | 'GCal_ID': { 193 | 'type':'rich_text', 194 | 'rich_text': [ { 195 | 'id':'_}uo', 196 | 'type':'text', 197 | 'text': { 198 | 'content': str(GCal_Event['id']) 199 | } 200 | } ] 201 | }, 202 | 203 | nameProperty: { 204 | "type": 'title', 205 | "title": [ 206 | { 207 | "type": 'text', 208 | "text": { 209 | "content": GCal_Event['summary'], 210 | }, 211 | }, 212 | ], 213 | }, 214 | dateProperty: { 215 | "type": 'date', 216 | 'date': { 217 | 'start': GCal_Event['start']['dateTime'][:19], 218 | 'end': GCal_Event['end']['dateTime'][:19], 219 | } 220 | } 221 | }, 222 | }, 223 | ) 224 | except: 225 | new_page = notion.pages.create( 226 | **{ 227 | "parent": { 228 | "database_id": database_id, 229 | }, 230 | "properties": { 231 | 'GCal_ID': { 232 | 'type':'rich_text', 233 | 'rich_text': [ { 234 | 'id':'_}uo', 235 | 'type':'text', 236 | 'text': { 237 | 'content': str(GCal_Event['id']) 238 | } 239 | } ] 240 | }, 241 | nameProperty: { 242 | "type": 'title', 243 | "title": [ 244 | { 245 | "type": 'text', 246 | "text": { 247 | "content": GCal_Event['summary'], 248 | }, 249 | }, 250 | ], 251 | }, 252 | dateProperty: { 253 | "type": 'date', 254 | 'date': { 255 | 'start': GCal_Event['start']['date'], 256 | 'end': None, 257 | } 258 | } 259 | }, 260 | }, 261 | ) 262 | 263 | else: 264 | try: 265 | updated_page = notion.pages.update( 266 | **{ 267 | "page_id": id_id[GCal_Event['id']], 268 | "properties": { 269 | 'GCal_ID': { 270 | 'type':'rich_text', 271 | 'rich_text': [ { 272 | 'id':'_}uo', 273 | 'type':'text', 274 | 'text': { 275 | 'content': str(GCal_Event['id']) 276 | } 277 | } ] 278 | }, 279 | nameProperty: { 280 | "type": 'title', 281 | "title": [ 282 | { 283 | "type": 'text', 284 | "text": { 285 | "content": GCal_Event['summary'], 286 | }, 287 | }, 288 | ], 289 | }, 290 | dateProperty: { 291 | "type": 'date', 292 | 'date': { 293 | 'start': GCal_Event['start']['date'], 294 | 'end': None, 295 | } 296 | } 297 | } 298 | } 299 | ) 300 | except: 301 | updated_page = notion.pages.update( 302 | **{ 303 | "page_id": id_id[GCal_Event['id']], 304 | "properties": { 305 | 'GCal_ID': { 306 | 'type':'rich_text', 307 | 'rich_text': [ { 308 | 'id':'_}uo', 309 | 'type':'text', 310 | 'text': { 311 | 'content': str(GCal_Event['id']) 312 | } 313 | } ] 314 | }, 315 | nameProperty: { 316 | "type": 'title', 317 | "title": [ 318 | { 319 | "type": 'text', 320 | "text": { 321 | "content": GCal_Event['summary'], 322 | }, 323 | }, 324 | ], 325 | }, 326 | dateProperty: { 327 | "type": 'date', 328 | 'date': { 329 | 'start': GCal_Event['start']['dateTime'][:19], 330 | 'end': GCal_Event['end']['dateTime'][:19], 331 | } 332 | } 333 | } 334 | } 335 | ) 336 | 337 | 338 | print('Sync done.') 339 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notion-GCal Sync 2 | 3 | ## Installation and configuration 4 | 5 | 1. [Visit this link](https://yassen.notion.site/285f273cd96d4048802db3a57114b69a?v=063a958492aa4d07be8a426bba4e78b8) and duplicate your very own Notion To-Do List database. 6 | 7 | ![How to Duplicate Notion Template](https://github.com/yassenshopov/notion-gcal-sync/blob/main/src/img1.png) 8 | 9 | 2. Clone the repository 10 | 11 | ```shell 12 | git clone https://github.com/yassenshopov/notion-gcal-sync.git 13 | ``` 14 | 15 | 3. 16 | 17 | [![Stargazers repo roster for @yassenshopov/Notion-GCal-Sync](https://reporoster.com/stars/yassenshopov/Notion-GCal-Sync)](https://github.com/yassenshopov/Notion-GCal-Sync/stargazers) 18 | -------------------------------------------------------------------------------- /configSync.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from notion_client import Client 3 | import os 4 | 5 | NOTION_TOKEN = input("What is your secret token (from integration)?") #the secret_something from Notion Integration 6 | 7 | database_id = input("What is the URL of your database?") #get the mess of numbers before the "?" on your dashboard URL 8 | 9 | os.environ['NOTION_TOKEN'] = NOTION_TOKEN 10 | 11 | notion = Client(auth=os.environ["NOTION_TOKEN"]) 12 | 13 | Notion_today = notion.databases.query( 14 | **{ 15 | "database_id": database_id, 16 | "filter": { 17 | "and": [ 18 | ] 19 | } 20 | 21 | } 22 | ) 23 | 24 | with open('NotionDBdata.csv', mode="w") as file: 25 | 26 | fields = ["Title","Date","Tag","GCal_ID"] 27 | 28 | csvwriter = csv.writer(file) 29 | 30 | csvwriter.writerow(fields) 31 | 32 | rows = [[]] 33 | 34 | properties = Notion_today['results'][0]['properties'] 35 | for prop in properties: 36 | if properties[prop]['type'] == 'title': 37 | print("The name of your main property in your Notion Database:",prop) 38 | titleAns = input("Is this correct? [Y/N]\n") 39 | if titleAns == "Y" or titleAns == "Yes" or titleAns == "1" or titleAns == 1: 40 | rows[0].append(prop) 41 | print(rows) 42 | else: 43 | print("Your Database seems to be unsuitable. Use the template to create a working one and try again.") 44 | break 45 | if properties[prop]['type'] == "date": 46 | print(prop) 47 | -------------------------------------------------------------------------------- /credentials.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yassenshopov/notion-gcal-sync/061f5d944b94793524f22bf97096f54c1dc41ad5/credentials.json -------------------------------------------------------------------------------- /experiment.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | r = requests.get("https://api.notion.com/v1/oauth/authorize?owner=user&client_id=463558a3-725e-4f37-b6d3-0889894f68de&redirect_uri=https%3A%2F%2Fexample.com%2Fauth%2Fnotion%2Fcallback&response_type=code", params = "{client_id:'c0881eda-977e-4f58-88ee-1558366971b7', response_type: 'code', owner: 'user'}") 4 | 5 | print(r.text) -------------------------------------------------------------------------------- /gcal-to-notion.py: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | ##### Import section. 3 | ##### Necesary PIPs: time, datetime, notion_client, 4 | ##### google-api-python-client, os. 5 | ############################################################################ 6 | 7 | from datetime import date 8 | import time 9 | from cal_setup import get_calendar_service 10 | import list_calendars 11 | import os 12 | from notion_client import Client 13 | 14 | ########################################################################### 15 | ##### Setting Up. Here you need to provide your Notion credentials. 16 | ########################################################################### 17 | 18 | NOTION_TOKEN = #the secret_something from Notion Integration 19 | 20 | database_id = #get the mess of numbers before the "?" on your dashboard URL 21 | 22 | ##### This is where we set up the connection with the Notion API 23 | 24 | os.environ['NOTION_TOKEN'] = NOTION_TOKEN 25 | 26 | notion = Client(auth=os.environ["NOTION_TOKEN"]) 27 | 28 | ##### This is where you enter the names of your Notion Columns: 29 | 30 | title = ## Put the 'Title' Property name here 31 | 32 | timing = ## Put the 'Date' Property name here 33 | 34 | tag = ## Put the 'Tag' (multi-select) Property name here 35 | 36 | ############################################################################ 37 | ##### Acquiring today's date to sync only today's tasks. 38 | ##### Timezone is determined here. 39 | ############################################################################ 40 | 41 | today = date.today() 42 | print("Today's date:", today) 43 | today = today.strftime('%Y-%m-%d') 44 | tz = time.tzname[time.daylight] 45 | print(tz) 46 | if tz == "BST": 47 | tz = "Europe/London" 48 | elif tz == "EEST": 49 | tz = "Europe/Sofia" 50 | 51 | print(tz,"\n") 52 | 53 | ############################################################################ 54 | ##### Run cal_setup to provide GCal access. 55 | ############################################################################ 56 | 57 | service = get_calendar_service() 58 | calendarList = list_calendars.list_calendars() 59 | 60 | ############################################################################ 61 | ##### Use the list "exceptions" to specify calendars you do not want to sync. 62 | ##### Enter their titles in quotation marks, separated by commas. 63 | ############################################################################ 64 | 65 | exceptions = [] 66 | 67 | ############################################################################ 68 | ##### Initialise empty lists for future use. 69 | ############################################################################ 70 | 71 | eventsGCal = [] 72 | eventsGcalToday = [] 73 | 74 | for calendar in calendarList: 75 | if calendar['summary'] in exceptions: 76 | calendarList.remove(calendar) 77 | 78 | ############################################################################ 79 | ##### Take all the events from GCal's Calendars and filter them for today. 80 | ############################################################################ 81 | 82 | else: 83 | events = service.events().list(calendarId = calendar['id']).execute() 84 | for event in events['items']: 85 | event['calendarId'] = calendar['id'] 86 | eventsGCal.append(event) 87 | 88 | for event in eventsGCal: 89 | 90 | ############################################################################ 91 | ##### Check for cancelled recurring events. 92 | ############################################################################ 93 | 94 | if event['status'] != 'cancelled': 95 | 96 | ############################################################################ 97 | ##### Case 1: Full-day events. ('start'['date']). 98 | ############################################################################ 99 | 100 | try: 101 | if event['start']['date'][:10] == today: 102 | eventsGcalToday.append(event) 103 | print('Case 1:',event) 104 | print(event['start']['date'],end="\n\n") 105 | except: 106 | 107 | ############################################################################ 108 | ##### Case 2: Scheduled during-the-day events. ('start'['dateTime']). 109 | ############################################################################ 110 | 111 | try: 112 | if event['start']['dateTime'][:10] == today: 113 | eventsGcalToday.append(event) 114 | print('Case 2:',event) 115 | print(event['start']['dateTime'],end="\n\n") 116 | except: 117 | print('"start" Exception Occured') 118 | 119 | ############################################################################ 120 | ##### Copy the events data from eventsGCalToday to Notion DB. 121 | ##### Sanity Check #1: If eventsGCalToday is empty. 122 | ##### Sanity Check #2: If event description has Sync Check (Synced: ✅). 123 | ############################################################################ 124 | 125 | if eventsGcalToday != []: 126 | for GCalEvent in eventsGcalToday: 127 | 128 | try: 129 | if '[Synced ✅]' not in GCalEvent['description']: 130 | 131 | ############################################################################ 132 | ##### Case 1: Full-day events. ('start'['date']). 133 | ############################################################################ 134 | 135 | try: 136 | new_page = notion.pages.create( 137 | 138 | **{ 139 | "parent": {"database_id": database_id}, 140 | "properties":{ 141 | 'GCal_ID': {'rich_text': [{'text': {'content': GCalEvent['id']}}]}, 142 | title: {"title": [{"text": {"content": GCalEvent['summary']}}]}, 143 | tag: {'multi_select':[{'name': GCalEvent['organizer']['displayName']}]}, 144 | timing: {'date': {'start': GCalEvent['start']['date'][:10],'end': None}} 145 | 146 | } 147 | } 148 | 149 | ) 150 | 151 | ############################################################################ 152 | ##### Update GCal event with [Synced ✅] mark in description. 153 | ############################################################################ 154 | 155 | updateGCal = service.events().patch(calendarId = GCalEvent['calendarId'], eventId = GCalEvent['id'], body={"description": '[Synced ✅]',}).execute() 156 | 157 | ############################################################################ 158 | ##### Case 2: Scheduled during-the-day events. ('start'['dateTime']). 159 | ############################################################################ 160 | 161 | except: 162 | new_page = notion.pages.create( 163 | 164 | **{ 165 | "parent": {"database_id": database_id}, 166 | "properties":{ 167 | 'GCal_ID': {'rich_text': [{'text': {'content': GCalEvent['id']}}]}, 168 | title: {"title": [{"text": {"content": GCalEvent['summary']}}]}, 169 | tag: {'multi_select':[{'name': GCalEvent['organizer']['displayName']}]}, 170 | timing: {'date': {'start': GCalEvent['start']['dateTime'],'end': GCalEvent['end']['dateTime']}} 171 | 172 | } 173 | } 174 | 175 | ) 176 | 177 | ############################################################################ 178 | ##### Update GCal event with [Synced ✅] mark in description. 179 | ############################################################################ 180 | 181 | updateGCal = service.events().patch(calendarId = GCalEvent['calendarId'], eventId = GCalEvent['id'], body={"description": '[Synced ✅]',}).execute() 182 | 183 | ''' 184 | 185 | else: 186 | 187 | ############################################################################ 188 | ##### This part of the code updates the data of already-synced events. 189 | ############################################################################ 190 | 191 | ############################################################################ 192 | ##### Case 1: Full-day events. ('start'['date']). 193 | ############################################################################ 194 | 195 | try: 196 | new_page = notion.pages.update( 197 | 198 | **{ 199 | "parent": {"database_id": database_id}, 200 | "properties":{ 201 | 'GCal_ID': {'rich_text': [{'text': {'content': GCalEvent['id']}}]}, 202 | title: {"title": [{"text": {"content": GCalEvent['summary']}}]}, 203 | tag: {'multi_select':[{'name': GCalEvent['organizer']['displayName']}]}, 204 | timing: {'date': {'start': GCalEvent['start']['date'][:10],'end': None}} 205 | 206 | } 207 | } 208 | 209 | ) 210 | 211 | ''' 212 | 213 | except: 214 | 215 | ############################################################################ 216 | ##### Case 1: Full-day events. ('start'['date']). 217 | ############################################################################ 218 | 219 | try: 220 | new_page = notion.pages.create( 221 | 222 | **{ 223 | "parent": {"database_id": database_id}, 224 | "properties":{ 225 | 'GCal_ID': {'rich_text': [{'text': {'content': GCalEvent['id']}}]}, 226 | title: {"title": [{"text": {"content": GCalEvent['summary']}}]}, 227 | tag: {'multi_select':[{'name': GCalEvent['organizer']['displayName']}]}, 228 | timing: {'date': {'start': GCalEvent['start']['date'][:10],'end': None}} 229 | 230 | } 231 | } 232 | 233 | ) 234 | 235 | ############################################################################ 236 | ##### Update GCal event with [Synced ✅] mark in description. 237 | ############################################################################ 238 | 239 | updateGCal = service.events().patch(calendarId = GCalEvent['calendarId'], eventId = GCalEvent['id'], body={"description": '[Synced ✅]',}).execute() 240 | 241 | ############################################################################ 242 | ##### Case 2: Scheduled during-the-day events. ('start'['dateTime']). 243 | ############################################################################ 244 | 245 | except: 246 | new_page = notion.pages.create( 247 | 248 | **{ 249 | "parent": {"database_id": database_id}, 250 | "properties":{ 251 | 'GCal_ID': {'rich_text': [{'text': {'content': GCalEvent['id']}}]}, 252 | title: {"title": [{"text": {"content": GCalEvent['summary']}}]}, 253 | tag: {'multi_select':[{'name': GCalEvent['organizer']['displayName']}]}, 254 | timing: {'date': {'start': GCalEvent['start']['dateTime'],'end': GCalEvent['end']['dateTime']}} 255 | } 256 | } 257 | ) 258 | 259 | ############################################################################ 260 | ##### Update GCal event with [Synced ✅] mark in description. 261 | ############################################################################ 262 | 263 | updateGCal = service.events().patch(calendarId = GCalEvent['calendarId'], eventId = GCalEvent['id'], body={"description": '[Synced ✅]',}).execute() 264 | 265 | 266 | -------------------------------------------------------------------------------- /notion-to-gcal.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yassenshopov/notion-gcal-sync/061f5d944b94793524f22bf97096f54c1dc41ad5/src/img1.png --------------------------------------------------------------------------------