From 847749ff71beb4cd76cd2f25815a71f9c93fdb13 Mon Sep 17 00:00:00 2001 From: Xiaowen Zhang Date: Wed, 23 Nov 2022 14:31:35 +0800 Subject: [PATCH] add todoist loader --- README-EN.md | 21 +++- README.md | 18 ++++ github_poster/config.py | 1 + github_poster/loader/__init__.py | 3 + github_poster/loader/todoist_loader.py | 142 +++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 github_poster/loader/todoist_loader.py diff --git a/README-EN.md b/README-EN.md index b118390ee..90c92bf9b 100644 --- a/README-EN.md +++ b/README-EN.md @@ -37,7 +37,7 @@ Make everything a GitHub svg poster and [skyline](https://skyline.github.com/)! - **[Multiple](#Multiple)** - **[Jike](#Jike)** - **[Summary](#Summary)** - +- **[Todoist](#Todoist)** ## Download ``` @@ -510,6 +510,25 @@ Option argument `count_type`, you can specify statistics type: +### Todoist + +
+Make Todoist Task Completion GitHub poster + +Because of Todoist policies, only users with Pro Plan(or above) can retrieve full historical activity from APIs. + +Get your token please find on [Todoist Developer Docs](https://developer.todoist.com/guides/#developing-with-todoist) + +
+ +``` +python3 -m github_poster todoist --year 2021-2022 --todoist_token "your todoist dev token" --me "your name" +or +github_poster todoist --year 2021-2022 --todoist_token "your todoist dev token" --me "your name" +``` +
+ + # Contribution - Any Issues PR welcome. diff --git a/README.md b/README.md index 85973fa76..f116822e5 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Make everything a GitHub svg poster and [skyline](https://skyline.github.com/)! - **[微信读书](#微信读书)** - **[总结](#Summary)** - **[Covid](#Covid)** +- **[Todoist](#Todoist)** ## 下载 @@ -599,6 +600,23 @@ github_poster covid --covid_area US --year 2020-2022 --me US ``` +### Todoist + +
+Make Todoist 完成任务 GitHub poster + +Todoist因为接口限制,只有Pro Plan的付费用户可以获取所有的历史数据,并统计对应的热图。 + +Token获取请参考:[Todoist Developer Docs](https://developer.todoist.com/guides/#developing-with-todoist) + +
+ +``` +python3 -m github_poster todoist --year 2021-2022 --todoist_token "your todoist dev token" --me "your name" +or +github_poster todoist --year 2021-2022 --todoist_token "your todoist dev token" --me "your name" +``` +
# 参与项目 diff --git a/github_poster/config.py b/github_poster/config.py index 44e9d5bd5..5b26cabfa 100644 --- a/github_poster/config.py +++ b/github_poster/config.py @@ -46,4 +46,5 @@ "bbdc": "BBDC", "weread": "WeRead", "covid": "COVID-19", + "todoist": "Todoist", } diff --git a/github_poster/loader/__init__.py b/github_poster/loader/__init__.py index b5cd57b7c..57d93d589 100644 --- a/github_poster/loader/__init__.py +++ b/github_poster/loader/__init__.py @@ -25,6 +25,7 @@ from github_poster.loader.wakatime_loader import WakaTimeLoader from github_poster.loader.weread_loader import WereadLoader from github_poster.loader.youtube_loader import YouTubeLoader +from github_poster.loader.todoist_loader import TodoistLoader LOADER_DICT = { "bbdc": BBDCLoader, @@ -54,6 +55,7 @@ "summary": SummaryLoader, "weread": WereadLoader, "covid": CovidLoader, + "todoist": TodoistLoader, } __all__ = ( @@ -85,4 +87,5 @@ "BBDCLoader", "WereadLoader", "CovidLoader", + "TodoistLoader", ) diff --git a/github_poster/loader/todoist_loader.py b/github_poster/loader/todoist_loader.py new file mode 100644 index 000000000..45155bcf1 --- /dev/null +++ b/github_poster/loader/todoist_loader.py @@ -0,0 +1,142 @@ +import datetime +import json + +import pandas as pd +import requests + +from github_poster.loader.base_loader import BaseLoader + + +class TodoistLoader(BaseLoader): + track_color = "#FFE411" + unit = "tasks" + + def __init__(self, from_year, to_year, _type, **kwargs): + super().__init__(from_year, to_year, _type) + self.from_year = from_year + self.to_year = to_year + self.todoist_token = kwargs.get("todoist_token", "") + # another magic number, try 3 times for calling api + self.MAXIMAL_RETRY = 3 + + @classmethod + def add_loader_arguments(cls, parser, optional): + # add argument for loader + parser.add_argument( + "--todoist_token", + dest="todoist_token", + type=str, + required=optional, + help="dev token", + ) + + # call with token + def response(self, url, postdata): + # headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/2.0 Safari/536.11'} + headers = {"Authorization": "Bearer {0}".format(self.todoist_token)} + res = requests.post(url=url, data=postdata, headers=headers) + resposn = res.json() + return resposn + + # call with re-try since todoist api some time get 502 + def response_with_retry(self, url, postdata, times): + # time.sleep(1) + try: + return self.response(url, postdata) + except Exception as e: + if times >= self.MAXIMAL_RETRY: + print( + f">> Exceed maximal retry {self.MAXIMAL_RETRY}, Raise exception..." + ) + raise (e) # will stop the program without further handling + else: + times += 1 + print(f">> Exception, Retry {times} begins...") + return self.response_with_retry(url, postdata, times) + + # call todoist dev api to get activity of completed tasks + # refer https://developer.todoist.com/sync/v9/#activity + def todoist_completed_activity(self, page, limit, offset): + data = { + "event_type": "completed", + "page": page, + "limit": limit, + "offset": offset, + } + url = "https://api.todoist.com/sync/v9/activity/get" + re = self.response_with_retry(url, data, 1) + return re + + # json expect to be list of events format + # with event_date, event_type, id + def normalize_df(self, jsondata): + if jsondata["count"] == 0: + return pd.DataFrame(columns=["event_date", "event_type", "id"]) + df = pd.json_normalize(jsondata["events"]) + df = df[["event_date", "event_type", "id"]] + df["event_date"] = df["event_date"].str.slice(0, 10) + return df + + def count_to_dict(self, df): + return df.groupby(["event_date"])["event_date"].count().to_dict() + + # refer https://developer.todoist.com/sync/v9/#activity + # todoist api only allows you to get activity data by page from current day + # we will have to calculate the pages based on from and to year then manipulate the dict data + def get_api_data(self): + # init critical dates + today = datetime.datetime.today().strftime("%Y-%m-%d") + current_year = datetime.datetime.now().year + # 52.14 weeks a year, add 53 pages per full year + number_of_days = datetime.date.today().timetuple().tm_yday + 365 * ( + current_year - self.from_year + ) + # current year + page_from = ( + 0 + if current_year == self.to_year + else datetime.date.today().timetuple().tm_yday // 7 + ) + page_to = number_of_days // 7 + 1 + print("Todoist API Page range ({0},{1})".format(page_from, page_to)) + last_day_of_to_year = datetime.datetime(self.to_year, 12, 31).strftime( + "%Y-%m-%d" + ) + first_day_of_from_year = datetime.datetime(self.from_year, 1, 1).strftime( + "%Y-%m-%d" + ) + + df = pd.DataFrame(columns=["event_date", "event_type", "id"]) + # magic number 3 is to cover the 0.14 extra week of every year when counting number of pages + for page in range(page_from, page_to + 3): + offset = 0 + limit = 100 + while True: + res = self.todoist_completed_activity(page, limit, offset) + if res["count"] >= offset: + offset = offset + limit + df_res = self.normalize_df(res) + df = pd.concat([df, df_res]) + else: + break + + df = df[df["event_date"] >= first_day_of_from_year] + df = df[df["event_date"] <= last_day_of_to_year] + + return df + + def make_track_dict(self): + # generate statistics data + df = self.get_api_data() + df_dict = self.count_to_dict(df) + self.number_by_date_dict = df_dict + # print(df_dict) + for _, v in self.number_by_date_dict.items(): + self.number_list.append(v) + + def get_all_track_data(self): + self.make_track_dict() + self.make_special_number() + # print(self.year_list) + print("不积跬步,无以至千里。Todoist欢迎你。") + return self.number_by_date_dict, self.year_list