Skip to content

Commit df91046

Browse files
committed
Initial commit
0 parents  commit df91046

File tree

3 files changed

+374
-0
lines changed

3 files changed

+374
-0
lines changed

.gitignore

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
2+
# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode
3+
4+
### Python ###
5+
# Byte-compiled / optimized / DLL files
6+
__pycache__/
7+
*.py[cod]
8+
*$py.class
9+
10+
# C extensions
11+
*.so
12+
13+
# Distribution / packaging
14+
.Python
15+
build/
16+
develop-eggs/
17+
dist/
18+
downloads/
19+
eggs/
20+
.eggs/
21+
lib/
22+
lib64/
23+
parts/
24+
sdist/
25+
var/
26+
wheels/
27+
share/python-wheels/
28+
*.egg-info/
29+
.installed.cfg
30+
*.egg
31+
MANIFEST
32+
33+
# PyInstaller
34+
# Usually these files are written by a python script from a template
35+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
36+
*.manifest
37+
*.spec
38+
39+
# Installer logs
40+
pip-log.txt
41+
pip-delete-this-directory.txt
42+
43+
# Unit test / coverage reports
44+
htmlcov/
45+
.tox/
46+
.nox/
47+
.coverage
48+
.coverage.*
49+
.cache
50+
nosetests.xml
51+
coverage.xml
52+
*.cover
53+
*.py,cover
54+
.hypothesis/
55+
.pytest_cache/
56+
cover/
57+
58+
# Translations
59+
*.mo
60+
*.pot
61+
62+
# Django stuff:
63+
*.log
64+
local_settings.py
65+
db.sqlite3
66+
db.sqlite3-journal
67+
68+
# Flask stuff:
69+
instance/
70+
.webassets-cache
71+
72+
# Scrapy stuff:
73+
.scrapy
74+
75+
# Sphinx documentation
76+
docs/_build/
77+
78+
# PyBuilder
79+
.pybuilder/
80+
target/
81+
82+
# Jupyter Notebook
83+
.ipynb_checkpoints
84+
85+
# IPython
86+
profile_default/
87+
ipython_config.py
88+
89+
# pyenv
90+
# For a library or package, you might want to ignore these files since the code is
91+
# intended to run in multiple environments; otherwise, check them in:
92+
# .python-version
93+
94+
# pipenv
95+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
97+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
98+
# install all needed dependencies.
99+
#Pipfile.lock
100+
101+
# poetry
102+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103+
# This is especially recommended for binary packages to ensure reproducibility, and is more
104+
# commonly ignored for libraries.
105+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106+
#poetry.lock
107+
108+
# pdm
109+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110+
#pdm.lock
111+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112+
# in version control.
113+
# https://pdm.fming.dev/#use-with-ide
114+
.pdm.toml
115+
116+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117+
__pypackages__/
118+
119+
# Celery stuff
120+
celerybeat-schedule
121+
celerybeat.pid
122+
123+
# SageMath parsed files
124+
*.sage.py
125+
126+
# Environments
127+
.env
128+
.venv
129+
env/
130+
venv/
131+
ENV/
132+
env.bak/
133+
venv.bak/
134+
135+
# Spyder project settings
136+
.spyderproject
137+
.spyproject
138+
139+
# Rope project settings
140+
.ropeproject
141+
142+
# mkdocs documentation
143+
/site
144+
145+
# mypy
146+
.mypy_cache/
147+
.dmypy.json
148+
dmypy.json
149+
150+
# Pyre type checker
151+
.pyre/
152+
153+
# pytype static type analyzer
154+
.pytype/
155+
156+
# Cython debug symbols
157+
cython_debug/
158+
159+
# PyCharm
160+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162+
# and can be added to the global gitignore or merged into this file. For a more nuclear
163+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
164+
#.idea/
165+
166+
### Python Patch ###
167+
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
168+
poetry.toml
169+
170+
# ruff
171+
.ruff_cache/
172+
173+
# LSP config files
174+
pyrightconfig.json
175+
176+
### VisualStudioCode ###
177+
.vscode/*
178+
!.vscode/settings.json
179+
!.vscode/tasks.json
180+
!.vscode/launch.json
181+
!.vscode/extensions.json
182+
!.vscode/*.code-snippets
183+
184+
# Local History for Visual Studio Code
185+
.history/
186+
187+
# Built Visual Studio Code Extensions
188+
*.vsix
189+
190+
### VisualStudioCode Patch ###
191+
# Ignore all local history of files
192+
.history
193+
.ionide
194+
195+
# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode
196+
197+
*.json

README.md

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Google Tasks Transfer Tool <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5b/Google_Tasks_2021.svg/1079px-Google_Tasks_2021.svg.png" alt="Google Tasks icon" width="20" height="20">
2+
3+
A simple Python tool to export and import [Google Tasks](https://mail.google.com/tasks/canvas) items from one Google account to another.
4+
5+
## Features
6+
- [x] Browser login to Google accounts
7+
- [x] Transfer of multiple task lists
8+
- [x] Preserving completion/todo status
9+
- [x] Automatic retry upon hitting API quota
10+
- [ ] Does not support recurrence at the moment as info not available in Google's API
11+
12+
Contribution to this tool is welcomed!
13+
14+
## Usage
15+
### Authorize Credentials
16+
Follow steps in the official [quickstart guide](https://developers.google.com/tasks/quickstart/python#authorize_credentials_for_a_desktop_application) in section _Authorize credentials for a desktop application_. Place the downloaded `credentials.json` in the same directory root of `migrate.py` in this project.
17+
18+
### Install Dependencies
19+
```sh
20+
$ pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
21+
```
22+
23+
### Run the Program
24+
Run `python migrate.py` in your shell. A browser page will be opened and you will be prompted to log into your Google account to provide Tasks access. You will need to log in to your source (from which tasks will be exported) and destination (into which tasks will be imported) accounts **in order**.
25+
26+
Once you're done with logging in to both accounts, the transfer process will commence and you will see terminal outputs similar to the following:
27+
```
28+
(googleapi) ➜ python migrate.py
29+
Please visit this URL to authorize this application: [REDACTED]
30+
Please visit this URL to authorize this application: [REDACTED]
31+
Found 3 task lists in the source account.
32+
33+
Working on task list: My Tasks
34+
Found 300 tasks in the source account.
35+
Importing tasks to VkdYTmo2bUpkSVZKS2dzVw: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 300/300 [04:10<00:00, 1.20it/s]
36+
37+
Working on task list: Work
38+
Found 400 tasks in the source account.
39+
Importing tasks to ZmxWdV9hTEtNSDBPWm51Mw: 6%|███████ | 26/400 [00:12<02:43, 2.28it/s]
40+
Quota Exceeded. Retrying in 60 seconds (Attempt 1/3)
41+
Importing tasks to ZmxWdV9hTEtNSDBPWm51Mw: 6%|███████ | 26/400 [00:23<05:37, 1.11it/s]
42+
...
43+
```
44+
45+
Wait until transfer of each task lists is complete. Log into your Google Tasks on the destination account and verify.
46+
47+
## Acknowledgement
48+
49+
This tool was programmed with the help of OpenAI ChatGPT 3.5.

migrate.py

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
from google.auth.transport.requests import Request
2+
from google.oauth2.credentials import Credentials
3+
from google_auth_oauthlib.flow import InstalledAppFlow
4+
from googleapiclient.discovery import build
5+
from googleapiclient.errors import HttpError
6+
from time import sleep
7+
from tqdm import tqdm
8+
9+
# Function to authorize and get credentials using OAuth2 flow
10+
def authorize_google_tasks(api_name, api_version, scopes, token_file):
11+
flow = InstalledAppFlow.from_client_secrets_file('credentials.json', scopes=scopes)
12+
creds = flow.run_local_server(port=0)
13+
14+
# Save the credentials for the next run
15+
with open(token_file, 'w') as token:
16+
token.write(creds.to_json())
17+
18+
return creds
19+
20+
# Function to get or create a task list
21+
def get_or_create_tasklist(service, tasklist_title):
22+
try:
23+
# Try to get the task list
24+
tasklist = service.tasklists().list().execute()
25+
tasklist_id = next((tl['id'] for tl in tasklist.get('items', []) if tl['title'] == tasklist_title), None)
26+
27+
if tasklist_id:
28+
return tasklist_id
29+
except HttpError as e:
30+
if e.resp.status != 404:
31+
raise
32+
33+
# If the task list doesn't exist, create it
34+
new_tasklist = service.tasklists().insert(body={'title': tasklist_title}).execute()
35+
return new_tasklist['id']
36+
37+
# Function to get tasks from a specific task list (including completed tasks) with pagination
38+
def get_tasks(service, tasklist_id, max_results=100):
39+
all_tasks = []
40+
next_page_token = None
41+
42+
while True:
43+
# Retrieve tasks for the current page
44+
page_tasks = service.tasks().list(
45+
tasklist=tasklist_id,
46+
showCompleted=True,
47+
showDeleted=True,
48+
showHidden=True,
49+
maxResults=max_results,
50+
pageToken=next_page_token
51+
).execute()
52+
53+
# Add tasks from the current page to the overall list
54+
all_tasks.extend(page_tasks.get('items', []))
55+
56+
# Check if there are more pages
57+
next_page_token = page_tasks.get('nextPageToken')
58+
if not next_page_token:
59+
break
60+
61+
return all_tasks
62+
63+
# Function to create tasks in a specific task list with retry
64+
def create_tasks_with_retry(service, tasklist_id, tasks, max_retries=3):
65+
for task in tqdm(tasks, desc=f"Importing tasks to {tasklist_id}"):
66+
for retry_count in range(max_retries):
67+
try:
68+
# Check for recurring tasks and handle them correctly
69+
if 'recurrence' in task:
70+
# Clear the 'completed' field for recurring tasks
71+
task.pop('completed', None)
72+
# Insert the recurring task without specifying a due date/time
73+
service.tasks().insert(tasklist=tasklist_id, body=task).execute()
74+
else:
75+
# Insert non-recurring tasks
76+
service.tasks().insert(tasklist=tasklist_id, body=task).execute()
77+
78+
# Break out of the retry loop if successful
79+
break
80+
except HttpError as e:
81+
if e.resp.status == 403 and 'quotaExceeded' in str(e):
82+
# Quota exceeded, wait for some time and then retry
83+
print(f"Quota Exceeded. Retrying in 60 seconds (Attempt {retry_count + 1}/{max_retries})")
84+
sleep(60)
85+
else:
86+
# For other errors, raise the exception
87+
raise
88+
89+
# Function to create tasks in a specific task list
90+
def create_tasks(service, tasklist_id, tasks):
91+
create_tasks_with_retry(service, tasklist_id, tasks)
92+
93+
def main():
94+
# Set the API information
95+
api_name = 'tasks'
96+
api_version = 'v1'
97+
scopes = ['https://www.googleapis.com/auth/tasks']
98+
token_file_account1 = 'token_account1.json' # Replace with your token file for account 1
99+
token_file_account2 = 'token_account2.json' # Replace with your token file for account 2
100+
101+
# Authorize and get credentials for the source Google Tasks account
102+
creds_account1 = authorize_google_tasks(api_name, api_version, scopes, token_file_account1)
103+
service_account1 = build(api_name, api_version, credentials=creds_account1)
104+
105+
# Authorize and get credentials for the destination Google Tasks account
106+
creds_account2 = authorize_google_tasks(api_name, api_version, scopes, token_file_account2)
107+
service_account2 = build(api_name, api_version, credentials=creds_account2)
108+
109+
# Get task lists from the source account
110+
tasklists_account1 = service_account1.tasklists().list().execute().get('items', [])
111+
print(f"Found {len(tasklists_account1)} task lists in the source account.")
112+
113+
# Iterate through each task list and get tasks
114+
for tasklist_account1 in tasklists_account1:
115+
tasklist_title = tasklist_account1['title']
116+
tasklist_id_account2 = get_or_create_tasklist(service_account2, tasklist_title)
117+
118+
tasks_account1 = get_tasks(service_account1, tasklist_account1['id'])
119+
print(f"\nWorking on task list: {tasklist_title}")
120+
print(f"Found {len(tasks_account1)} tasks in the source account.")
121+
122+
# Create tasks in the destination account
123+
create_tasks(service_account2, tasklist_id_account2, tasks_account1)
124+
125+
print("\nTasks exported and imported successfully.")
126+
127+
if __name__ == '__main__':
128+
main()

0 commit comments

Comments
 (0)