Created
April 16, 2023 15:43
-
-
Save kozak127/f6ca5c7df719bf2a56c4eb54f721a283 to your computer and use it in GitHub Desktop.
Microsoft ToDo -> Google Calendar Tasks migration script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## MS TODO -> Google Calendar/Tasks migration script | |
Script will add pending and completed tasks, with the completion date to Google Tasks. It also recognizes notes in the tasks. It ignores nested tasks and repeat rules. | |
## HOW TO USE | |
(abridged version of the abridged version. Sorry. You will figure this out) | |
1. Download all tasks from MS TODO list using https://www.todovodo.com/home in JSON format. Move it to the script folder | |
2. Add string ```{ "list" : ``` at the start of the JSON file | |
3. Add string ```}``` at the end of the JSON file | |
4. In Google Cloud Console, enable Task API | |
5. In Google Cloud Console, configure OAuth consent screen. Add yourself as a test user. Add Task API as scope | |
6. In Google Cloud Console, configure and download OAuth2 credential | |
7. Rename downloaded credential to ```credentials.json```. Move it to the script folder | |
8. Create desired task list in Google Tasks | |
9. Run code for the first time. It will blow up with errors. Ignore that, it printed task list IDs. Copy them, and put in TASKLIST_ID dictionary | |
10. In LIST_JSON variable, put the path to the JSON downloaded from todovodo | |
11. In LIST_NAME, specify the list name (the same as in TASKLIST_ID) | |
12. Run the code | |
Note: sometimes, Google will return error "Quota Exceeded" in the logs. Ignore that, the script will try to resume operations in 30 seconds | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import print_function | |
import os.path | |
import json | |
import time | |
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 googleapiclient.errors import HttpError | |
# If modifying these scopes, delete the file token.json. | |
SCOPES = ['https://www.googleapis.com/auth/tasks'] | |
TASKLIST_ID = { | |
"Zakupy": "YndMd1g2QWJFUHNCaTR3TQ", | |
"Żyjątka": "QUZnbklIelYwY1p0azl5aw", | |
"TODO": "MEhyUkVuQ1dxUi1wd1VmTw", | |
"Static": "b1RZN3JTdkh2dXRsc1dvRg", | |
"R-Ka": "clhlMTdWYzVCQkhkVElRZg", | |
"Rachunki": "LVRPRmlIY3ZHTWdHeWNBSA", | |
"Praca": "Q2ZWTjR6Yy10RnhaaldiWQ", | |
"Porządki": "c0gzMFZXTHl5T1J0XzhfTQ", | |
"Mieszkanie": "TnQxREk2Q2ZEcFVua2t1cw", | |
"Lekarze": "R2oxTlNSZlJaajAtVURZQQ", | |
"CBF1000": "eV9OSVBPa3lCd29zVWZYNA" | |
} | |
LIST_JSON = "zakupy.json" | |
LIST_NAME = "Zakupy" | |
def main(): | |
creds = get_credentials() | |
try: | |
service = build('tasks', 'v1', credentials=creds) | |
results = service.tasklists().list(maxResults=20).execute() | |
items = results.get('items', []) | |
if not items: | |
print('No task lists found.') | |
return | |
print('Task lists:') | |
for item in items: | |
print(u'{0} ({1})'.format(item['title'], item['id'])) | |
print('######') | |
tasks_ms = read_tasks_from_json(LIST_JSON) | |
for task_ms in tasks_ms: | |
task_gcal = map_task(task_ms) | |
insert_task(service, task_gcal, LIST_NAME) | |
except HttpError as err: | |
print(err) | |
def get_credentials(): | |
creds = None | |
# The file token.json stores the user's access and refresh tokens, and is | |
# created automatically when the authorization flow completes for the first | |
# time. | |
if os.path.exists('token.json'): | |
creds = Credentials.from_authorized_user_file('token.json', SCOPES) | |
# If there are no (valid) credentials available, let the user log in. | |
if not creds or not creds.valid: | |
if creds and creds.expired and creds.refresh_token: | |
creds.refresh(Request()) | |
else: | |
flow = InstalledAppFlow.from_client_secrets_file( | |
'credentials.json', SCOPES) | |
creds = flow.run_local_server(port=0) | |
# Save the credentials for the next run | |
with open('token.json', 'w') as token: | |
token.write(creds.to_json()) | |
return creds | |
def read_tasks_from_json(filename): | |
json_file = open(file=filename, encoding="utf-8-sig") | |
open_json_file = json.load(json_file) | |
return open_json_file["list"] | |
def map_task(task_ms): | |
task_gcal = { | |
"kind": "tasks#task", | |
"title": task_ms["title"], | |
"status": "needsAction" | |
} | |
last_modified_date_time = task_ms["lastModifiedDateTime"] | |
last_modified_date_time = fix_datetime_formatting(last_modified_date_time) | |
task_gcal["updated"] = last_modified_date_time | |
if task_ms["status"] == "completed": | |
task_gcal["status"] = "completed" | |
completed_date_time = task_ms["completedDateTime"]["dateTime"] | |
completed_date_time = fix_datetime_formatting(completed_date_time) | |
task_gcal["completed"] = completed_date_time | |
try: | |
due_date = task_ms["dueDateTime"]["dateTime"] | |
if due_date is not None: | |
due_date = fix_datetime_formatting(due_date) | |
task_gcal["due"] = due_date | |
except Exception: | |
pass | |
notes = task_ms["body"]["content"] | |
if (notes is not None) and (notes != ""): | |
task_gcal["notes"] = notes | |
print(task_gcal["title"] + " ### " + task_gcal["updated"]) | |
return task_gcal | |
def fix_datetime_formatting(datetime): | |
if (datetime is not None) and (not datetime.endswith("Z")): | |
datetime = datetime + "Z" | |
return datetime | |
def insert_task(service, task, tasklist_name): | |
while(True): | |
try: | |
tasklist_id = TASKLIST_ID[tasklist_name] | |
service.tasks().insert(tasklist=tasklist_id, body=task).execute() | |
return | |
except HttpError as err: | |
print(err) | |
time.sleep(30) | |
if __name__ == '__main__': | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
google-api-python-client | |
google-auth-httplib2 | |
google-auth-oauthlib |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"list": [ | |
{ | |
"@odata.etag": "W/\"I0svlcvxtUC4XtS1gMFc9gAD4iK+Bg==\"", | |
"importance": "normal", | |
"isReminderOn": false, | |
"status": "completed", | |
"title": "Test title 1", | |
"createdDateTime": "2022-08-01T08:37:58.6549082Z", | |
"lastModifiedDateTime": "2022-08-01T10:56:24.7930901Z", | |
"hasAttachments": false, | |
"categories": [], | |
"id": "AQMkADAwATM3ZmYAZS0xMjIAMC0wN2MxLTAwAi0wMAoARgAAA1eYUkldCT1Kgvrj5HJ0bzcHACNLL5XL8bVAuF7UtYDBXPYAAZwvaY0AAAAjSy_Vy-G1QLhe1LWAwVz2AAPiAXOtAAAA", | |
"body": { | |
"content": "test_note", | |
"contentType": "text" | |
}, | |
"completedDateTime": { | |
"dateTime": "2022-07-31T22:00:00.0000000", | |
"timeZone": "UTC" | |
}, | |
"dueDateTime": { | |
"dateTime": "2022-07-31T22:00:00.0000000", | |
"timeZone": "UTC" | |
} | |
}, | |
{ | |
"@odata.etag": "W/\"I0svlcvxtUC4XtS1gMFc9gADGGyaHA==\"", | |
"importance": "normal", | |
"isReminderOn": false, | |
"status": "notStarted", | |
"title": "Test title 2", | |
"createdDateTime": "2020-02-23T20:13:38.2540287Z", | |
"lastModifiedDateTime": "2021-09-29T22:31:23.7018542Z", | |
"hasAttachments": false, | |
"categories": [], | |
"id": "AQMkADAwATM3ZmYAZS0xMjIAMC0wN2MxLTAwAi0wMAoARgAAA1eYUkldCT1Kgvrj5HJ0bzcHACNLL5XL8bVAuF7UtYDBXPYAAZwvaY0AAAAjSy_Vy-G1QLhe1LWAwVz2AAGcMB5YAAAA", | |
"body": { | |
"content": "", | |
"contentType": "text" | |
} | |
} | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment