Skip to content

Commit

Permalink
Merge pull request #1 from iamshaynez/dev-todoist
Browse files Browse the repository at this point in the history
add todoist loader
  • Loading branch information
iamshaynez authored Nov 23, 2022
2 parents ac53fe6 + 847749f commit 0448d47
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 1 deletion.
21 changes: 20 additions & 1 deletion README-EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -510,6 +510,25 @@ Option argument `count_type`, you can specify statistics type:

</details>

### Todoist

<details>
<summary>Make <code> Todoist Task Completion </code> GitHub poster</summary>

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)

<br>

```
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"
```
</details>


# Contribution

- Any Issues PR welcome.
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Make everything a GitHub svg poster and [skyline](https://skyline.github.com/)!
- **[微信读书](#微信读书)**
- **[总结](#Summary)**
- **[Covid](#Covid)**
- **[Todoist](#Todoist)**

## 下载

Expand Down Expand Up @@ -599,6 +600,23 @@ github_poster covid --covid_area US --year 2020-2022 --me US
```
</details>

### Todoist

<details>
<summary>Make <code> Todoist 完成任务 </code> GitHub poster</summary>

Todoist因为接口限制,只有Pro Plan的付费用户可以获取所有的历史数据,并统计对应的热图。

Token获取请参考:[Todoist Developer Docs](https://developer.todoist.com/guides/#developing-with-todoist)

<br>

```
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"
```
</details>

# 参与项目

Expand Down
1 change: 1 addition & 0 deletions github_poster/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@
"bbdc": "BBDC",
"weread": "WeRead",
"covid": "COVID-19",
"todoist": "Todoist",
}
3 changes: 3 additions & 0 deletions github_poster/loader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -54,6 +55,7 @@
"summary": SummaryLoader,
"weread": WereadLoader,
"covid": CovidLoader,
"todoist": TodoistLoader,
}

__all__ = (
Expand Down Expand Up @@ -85,4 +87,5 @@
"BBDCLoader",
"WereadLoader",
"CovidLoader",
"TodoistLoader",
)
142 changes: 142 additions & 0 deletions github_poster/loader/todoist_loader.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 0448d47

Please sign in to comment.