-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathticktick-to-todoist.py
executable file
·154 lines (121 loc) · 5.06 KB
/
ticktick-to-todoist.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/usr/bin/env python3
from ticktick import api
import pandas as pd
import re
import pytz
from getpass import getpass
from datetime import datetime, date
def ordinalize(number):
if number == 1:
return("1st")
if number == 2:
return("2nd")
if number == 3:
return("3rd")
else:
return("{}th".format(number))
def full_day_of_week(abbrev):
dow_map = {
"MO": "Monday",
"TU": "Tuesday",
"WE": "Wednesday",
"TH": "Thursday",
"FR": "Friday",
"SA": "Saturday",
"SU": "Sunday"
}
return(dow_map[abbrev])
def naturalize(rrule):
if 'RRULE:' not in rrule:
return ''
# Parse TickTick RRULE into dict
rrule = re.sub(r"^RRULE:", "", rrule)
rrules = rrule.split(";")
rule_dict = {}
for r in rrules:
name, value = r.split("=")
rule_dict[name] = value
# Reconstruct as an natural language statement
# Figure out repeat period
if rule_dict["FREQ"] == "DAILY":
natural_period = "day"
if rule_dict["FREQ"] == "WEEKLY":
natural_period = "week"
if rule_dict["FREQ"] == "MONTHLY":
natural_period = "month"
if rule_dict["FREQ"] == "YEARLY":
natural_period = "year"
# Convert INTERVAL + FREQ into natural language i.e. "every X <period>(s)"
if int(rule_dict["INTERVAL"]) > 1:
natural_period = "every {} {}s".format(int(rule_dict["INTERVAL"]), natural_period)
else:
natural_period = "every {}".format(natural_period)
if rule_dict.get("BYDAY"):
unparsed_byday = rule_dict.get("BYDAY")
weekday_of_month, repeat_on_weekday = re.match(r"([0-9])?([A-Z]{2})", unparsed_byday).groups()
# When there is an integer in front, e.g. "1SA", this means the nth Saturday of every month
if weekday_of_month:
return("on the {} {} of {}".format(ordinalize(int(weekday_of_month)), full_day_of_week(repeat_on_weekday), natural_period))
# When there isn't an integer in front, e.g. "SA", this means Saturday every X weeks
else:
return(natural_period)
else:
return(natural_period)
def get_due_date(dd, tz):
if re.match(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\+\d{4}$", dd):
dt = datetime.strptime(dd, '%Y-%m-%dT%H:%M:%S.%f%z')
local_dt = dt.replace(tzinfo=pytz.utc).astimezone(pytz.timezone(tz))
return(local_dt.strftime("%Y-%m-%d"))
else:
return ''
def ticktick_list_to_todoist_df(task_list):
# Todoist and TickTick do subtasks a little differently
# Pull nested task lists into their own tasks with an indent
unnested = []
for item in task_list:
if len(item["items"]) == 0:
unnested.append(item | {"level": 1})
else:
unnested.append(item | {"items": [], "level": 1})
for sub_item in item["items"]:
unnested.append(sub_item | {"level": 2})
# Fill in minimal set of columns for Todoist CSV format
if len(unnested) > 0:
todoist_df = pd.DataFrame(unnested).\
assign(# Fill in fields that may not be present in the TickTick list
content=lambda x: x.content.fillna('') if 'content' in x.columns else '',
dueDate=lambda x: x.dueDate.fillna('') if 'dueDate' in x.columns else '',
repeatFlag=lambda x: x.repeatFlag.fillna('') if 'repeatFlag' in x.columns else '').\
assign(TYPE="task",
# Todoist CSV format won't allow for task notes, so pull any notes into task title
CONTENT=lambda x: [' - '.join([t, c]) if c else t for (t,c) in zip(x.title, x.content)],
PRIORITY=4,
INDENT=lambda x: x.level,
AUTHOR="",
RESPONSIBLE="",
DATE=lambda x: [" ".join([get_due_date(dd, tz), naturalize(rep)])
for dd, tz, rep in zip(x.dueDate, x.timeZone, x.repeatFlag)],
DATE_LANG="en",
TIMEZONE="")\
[["TYPE", "CONTENT", "PRIORITY", "INDENT", "AUTHOR", "RESPONSIBLE", "DATE", "DATE_LANG", "TIMEZONE"]]
return(todoist_df)
else:
return None
def main():
username = input("TickTick username: ")
password = getpass(prompt="TickTick password: ")
client = api.TickTickClient(username, password) # Enter correct username and password
# Build list of all tasks by project
tasks_by_project = {}
# Inbox isn't a "project"
tasks_by_project["Inbox"] = client.get_by_fields(projectId=client.inbox_id)
for project in client.get_by_fields(isOwner=True, search="projects"):
tasks_by_project[project["name"]] = client.task.get_from_project(project["id"])
# Convert task lists and write out to CSV
for project, tasks in tasks_by_project.items():
print('Exporting project "{}"'.format(project))
df = ticktick_list_to_todoist_df(tasks)
if df is not None:
df.to_csv("{}.csv".format(project), index=False)
if __name__ == "__main__":
main()