github-heatmap 1.3.8__tar.gz → 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/PKG-INFO +1 -1
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/cli.py +4 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/drawer.py +46 -4
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/base_loader.py +6 -0
- github_heatmap-1.4.0/github_heatmap/loader/notion_loader.py +314 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/poster.py +2 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap.egg-info/PKG-INFO +1 -1
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap.egg-info/SOURCES.txt +1 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/setup.py +1 -1
- github_heatmap-1.4.0/tests/test_notion_loader.py +135 -0
- github_heatmap-1.4.0/tests/test_poster_utils.py +46 -0
- github_heatmap-1.3.8/github_heatmap/loader/notion_loader.py +0 -149
- github_heatmap-1.3.8/tests/test_poster_utils.py +0 -19
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/LICENSE +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/README.md +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/__init__.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/__main__.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/circluar_drawer.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/config.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/err.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/html_parser/__init__.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/html_parser/github_parser.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/html_parser/gitlab_parser.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/html_parser/jike_parse.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/html_parser/kindle_parser.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/__init__.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/apple_health_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/bbdc_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/bilibili_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/chatgpt_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/cichang_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/config.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/covid_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/dota2_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/duolingo_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/forest_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/from_github_issue_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/garmin_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/github_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/gitlab_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/gpx_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/jike_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/json_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/kindle_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/leetcode_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/multiple_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/neodb_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/nrc_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/ns_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/openlanguage_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/shanbay_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/strava_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/summary_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/todoist_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/wakatime_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/weread_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/youtube_loader.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/skyline/__init__.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/skyline/config.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/skyline/font/__init__.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/skyline/skyline.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/structures.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/utils.py +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap.egg-info/dependency_links.txt +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap.egg-info/entry_points.txt +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap.egg-info/requires.txt +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap.egg-info/top_level.txt +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/pyproject.toml +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/setup.cfg +0 -0
- {github_heatmap-1.3.8 → github_heatmap-1.4.0}/tests/__init__.py +0 -0
|
@@ -61,6 +61,8 @@ def run():
|
|
|
61
61
|
"dom": args.dom_color,
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
p.tooltip_template = args.tooltip_template or None
|
|
65
|
+
|
|
64
66
|
# if special color (Stand with Ukraine) change the color
|
|
65
67
|
if args.stand_with_ukraine:
|
|
66
68
|
p.colors["track"] = "#025DB8"
|
|
@@ -108,6 +110,7 @@ def run():
|
|
|
108
110
|
p.units = args.unit
|
|
109
111
|
else:
|
|
110
112
|
p.units = args.loader.unit
|
|
113
|
+
p.tooltip_by_date = getattr(loader, "tooltip_by_date_dict", {}) or {}
|
|
111
114
|
p.set_tracks(tracks, years, type_list)
|
|
112
115
|
else:
|
|
113
116
|
if args.unit:
|
|
@@ -115,6 +118,7 @@ def run():
|
|
|
115
118
|
else:
|
|
116
119
|
p.units = args.loader.unit
|
|
117
120
|
p.set_tracks({}, [to_year], type_list)
|
|
121
|
+
p.tooltip_by_date = {}
|
|
118
122
|
|
|
119
123
|
# set title
|
|
120
124
|
# we don't know issue content so use name
|
|
@@ -35,6 +35,42 @@ class Drawer:
|
|
|
35
35
|
zip(self.poster.type_list, COLOR_TUPLE[: len(self.poster.type_list)])
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
+
def _format_tooltip(self, date_title, value=None, type_name=None):
|
|
39
|
+
custom_map = getattr(self.poster, "tooltip_by_date", {}) or {}
|
|
40
|
+
custom = None
|
|
41
|
+
if custom_map:
|
|
42
|
+
entry = custom_map.get(date_title)
|
|
43
|
+
if isinstance(entry, dict):
|
|
44
|
+
if type_name and type_name in entry:
|
|
45
|
+
custom = entry.get(type_name)
|
|
46
|
+
elif "__default__" in entry:
|
|
47
|
+
custom = entry.get("__default__")
|
|
48
|
+
else:
|
|
49
|
+
custom = entry
|
|
50
|
+
if isinstance(custom, list):
|
|
51
|
+
custom = "\n".join([str(item) for item in custom if item is not None])
|
|
52
|
+
if custom is not None and custom != "":
|
|
53
|
+
return str(custom)
|
|
54
|
+
template = self.poster.tooltip_template
|
|
55
|
+
if template and value is not None:
|
|
56
|
+
context = {
|
|
57
|
+
"date": date_title,
|
|
58
|
+
"value": value if value is not None else "",
|
|
59
|
+
"unit": self.poster.units if value is not None else "",
|
|
60
|
+
"type": type_name or "",
|
|
61
|
+
}
|
|
62
|
+
try:
|
|
63
|
+
tooltip = template.format(**context)
|
|
64
|
+
except KeyError:
|
|
65
|
+
tooltip = template
|
|
66
|
+
if tooltip:
|
|
67
|
+
return tooltip
|
|
68
|
+
if value is not None:
|
|
69
|
+
if type_name:
|
|
70
|
+
return f"{date_title} {value} for {type_name}"
|
|
71
|
+
return f"{date_title} {value} {self.poster.units}"
|
|
72
|
+
return date_title
|
|
73
|
+
|
|
38
74
|
def make_color(self, length_range, length):
|
|
39
75
|
sp2 = self.poster.special_number.get("special_number2")
|
|
40
76
|
sp1 = self.poster.special_number.get("special_number1")
|
|
@@ -90,7 +126,9 @@ class Drawer:
|
|
|
90
126
|
color = self.poster.colors.get("special2") or self.poster.colors.get(
|
|
91
127
|
"special"
|
|
92
128
|
)
|
|
93
|
-
|
|
129
|
+
tooltip = self._format_tooltip(date_title, day_tracks)
|
|
130
|
+
else:
|
|
131
|
+
tooltip = self._format_tooltip(date_title)
|
|
94
132
|
rect = dr.rect(
|
|
95
133
|
(rect_x, rect_y),
|
|
96
134
|
DOM_BOX_TUPLE,
|
|
@@ -100,7 +138,7 @@ class Drawer:
|
|
|
100
138
|
)
|
|
101
139
|
if with_animation:
|
|
102
140
|
rect = self.__add_animation(rect, key_times, animate_index)
|
|
103
|
-
rect.set_desc(title=
|
|
141
|
+
rect.set_desc(title=tooltip)
|
|
104
142
|
yield rect
|
|
105
143
|
|
|
106
144
|
def _gen_day_boxes(
|
|
@@ -126,6 +164,7 @@ class Drawer:
|
|
|
126
164
|
types_len = len(day_tracks)
|
|
127
165
|
dom_tuple = DOM_BOX_DICT.get(types_len).get("dom")
|
|
128
166
|
index = 0
|
|
167
|
+
base_title = date_title
|
|
129
168
|
for _type in self.poster.type_list:
|
|
130
169
|
num = day_tracks.get(_type, 0)
|
|
131
170
|
length_range = self.poster.length_range_by_date_dict.get(_type, 1)
|
|
@@ -140,10 +179,10 @@ class Drawer:
|
|
|
140
179
|
rx=DOM_BOX_RADIUS,
|
|
141
180
|
ry=DOM_BOX_RADIUS,
|
|
142
181
|
)
|
|
143
|
-
|
|
182
|
+
tooltip = self._format_tooltip(base_title, num, _type)
|
|
144
183
|
if with_animation:
|
|
145
184
|
rect = self.__add_animation(rect, key_times, animate_index)
|
|
146
|
-
rect.set_desc(title=
|
|
185
|
+
rect.set_desc(title=tooltip)
|
|
147
186
|
yield rect
|
|
148
187
|
rect_y += dom_tuple[index][1]
|
|
149
188
|
index += 1
|
|
@@ -269,6 +308,9 @@ class Drawer:
|
|
|
269
308
|
"special_number1": loader.special_number1,
|
|
270
309
|
"special_number2": loader.special_number2,
|
|
271
310
|
}
|
|
311
|
+
self.poster.tooltip_by_date = (
|
|
312
|
+
getattr(loader, "tooltip_by_date_dict", {}) or {}
|
|
313
|
+
)
|
|
272
314
|
self.poster.colors["track"] = loader.track_color or "#4DD2FF"
|
|
273
315
|
self.poster.units = loader.unit
|
|
274
316
|
self.poster.compute_track_statistics([loader._type])
|
|
@@ -218,6 +218,12 @@ class BaseLoader(ABC):
|
|
|
218
218
|
default="",
|
|
219
219
|
help="unit",
|
|
220
220
|
)
|
|
221
|
+
group.add_argument(
|
|
222
|
+
"--tooltip-template",
|
|
223
|
+
dest="tooltip_template",
|
|
224
|
+
default="",
|
|
225
|
+
help="format string for tooltip, supports {date}, {value}, {unit}, {type}",
|
|
226
|
+
)
|
|
221
227
|
# is_cn here
|
|
222
228
|
group.add_argument(
|
|
223
229
|
"--cn",
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
import time
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
|
|
5
|
+
import pendulum
|
|
6
|
+
import requests
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from github_heatmap.loader.base_loader import BaseLoader
|
|
10
|
+
from github_heatmap.loader.config import NOTION_API_URL, NOTION_API_VERSION
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NotionLoader(BaseLoader):
|
|
14
|
+
track_color = "#40C463"
|
|
15
|
+
unit = "times"
|
|
16
|
+
|
|
17
|
+
def __init__(self, from_year, to_year, _type, **kwargs):
|
|
18
|
+
super().__init__(from_year, to_year, _type)
|
|
19
|
+
self.number_by_date_dict = self.generate_date_dict(from_year, to_year)
|
|
20
|
+
self.notion_token = kwargs.get("notion_token", "").strip()
|
|
21
|
+
self.database_id = kwargs.get("database_id", "").strip()
|
|
22
|
+
self.date_prop_name = kwargs.get("date_prop_name", "")
|
|
23
|
+
self.value_prop_name = kwargs.get("value_prop_name", "")
|
|
24
|
+
self.database_filter = kwargs.get("database_filter", "")
|
|
25
|
+
self.tooltip_prop_name = kwargs.get("tooltip_prop_name", "").strip()
|
|
26
|
+
self.tooltip_by_date_dict = defaultdict(list)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def add_loader_arguments(cls, parser, optional):
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--notion_token",
|
|
32
|
+
dest="notion_token",
|
|
33
|
+
type=str,
|
|
34
|
+
help="The Notion internal integration token.",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--database_id",
|
|
38
|
+
dest="database_id",
|
|
39
|
+
type=str,
|
|
40
|
+
help="The Notion database id.",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--date_prop_name",
|
|
44
|
+
dest="date_prop_name",
|
|
45
|
+
type=str,
|
|
46
|
+
default="Datetime",
|
|
47
|
+
required=optional,
|
|
48
|
+
help="The database property name which stored the datetime.",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--value_prop_name",
|
|
52
|
+
dest="value_prop_name",
|
|
53
|
+
type=str,
|
|
54
|
+
default="Datetime",
|
|
55
|
+
required=optional,
|
|
56
|
+
help="The database property name which stored the datetime.",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--database_filter",
|
|
60
|
+
dest="database_filter",
|
|
61
|
+
type=str,
|
|
62
|
+
default="",
|
|
63
|
+
required=False,
|
|
64
|
+
help="The database property name which stored the datetime.",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--tooltip_prop_name",
|
|
68
|
+
dest="tooltip_prop_name",
|
|
69
|
+
type=str,
|
|
70
|
+
default="",
|
|
71
|
+
required=False,
|
|
72
|
+
help="The Notion property name used for tooltip text.",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def get_api_data(self, start_cursor="", page_size=100, data_list=None):
|
|
76
|
+
if data_list is None:
|
|
77
|
+
data_list = []
|
|
78
|
+
payload = {
|
|
79
|
+
"page_size": page_size,
|
|
80
|
+
"filter": {
|
|
81
|
+
"and": [
|
|
82
|
+
{
|
|
83
|
+
"property": self.date_prop_name,
|
|
84
|
+
"date": {"on_or_after": f"{self.from_year}-01-01"},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"property": self.date_prop_name,
|
|
88
|
+
"date": {"on_or_before": f"{self.to_year}-12-31"},
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
if self.database_filter:
|
|
94
|
+
payload["filter"]["and"].append(json.loads(self.database_filter))
|
|
95
|
+
if start_cursor:
|
|
96
|
+
payload.update({"start_cursor": start_cursor})
|
|
97
|
+
|
|
98
|
+
headers = {
|
|
99
|
+
"Accept": "application/json",
|
|
100
|
+
"Notion-Version": NOTION_API_VERSION,
|
|
101
|
+
"Content-Type": "application/json",
|
|
102
|
+
"Authorization": "Bearer " + self.notion_token,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
resp = requests.post(
|
|
107
|
+
NOTION_API_URL.format(database_id=self.database_id),
|
|
108
|
+
json=payload,
|
|
109
|
+
headers=headers,
|
|
110
|
+
)
|
|
111
|
+
except requests.RequestException:
|
|
112
|
+
return data_list
|
|
113
|
+
|
|
114
|
+
if not resp.ok:
|
|
115
|
+
# Treat non-OK responses as an empty result set so we can still draw
|
|
116
|
+
# a heatmap even when the Notion API yields no rows for the period.
|
|
117
|
+
return data_list
|
|
118
|
+
try:
|
|
119
|
+
data = resp.json()
|
|
120
|
+
except ValueError:
|
|
121
|
+
return data_list
|
|
122
|
+
results = data["results"]
|
|
123
|
+
next_cursor = data["next_cursor"]
|
|
124
|
+
data_list.extend(results)
|
|
125
|
+
if not data["has_more"]:
|
|
126
|
+
return data_list
|
|
127
|
+
# Avoid request limits
|
|
128
|
+
# The rate limit for incoming requests is an average
|
|
129
|
+
# of 3 requests per second.
|
|
130
|
+
# See https://developers.notion.com/reference/request-limits
|
|
131
|
+
time.sleep(0.3)
|
|
132
|
+
return self.get_api_data(
|
|
133
|
+
start_cursor=next_cursor, page_size=page_size, data_list=data_list
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def generate_date_dict(self, start_year, end_year):
|
|
137
|
+
start_date = datetime(start_year, 1, 1)
|
|
138
|
+
end_date = datetime(end_year, 12, 31)
|
|
139
|
+
|
|
140
|
+
# 使用字典推导式生成日期字典
|
|
141
|
+
return {
|
|
142
|
+
(start_date + timedelta(days=i)).strftime("%Y-%m-%d"): 0
|
|
143
|
+
for i in range((end_date - start_date).days + 1)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def make_track_dict(self):
|
|
147
|
+
data_list = self.get_api_data()
|
|
148
|
+
for result in data_list:
|
|
149
|
+
date = result["properties"][self.date_prop_name]["date"]
|
|
150
|
+
value = result["properties"][self.value_prop_name]
|
|
151
|
+
tooltip_text = ""
|
|
152
|
+
if self.tooltip_prop_name:
|
|
153
|
+
tooltip_prop = result["properties"].get(self.tooltip_prop_name)
|
|
154
|
+
tooltip_text = self._extract_property_text(tooltip_prop)
|
|
155
|
+
if date and value:
|
|
156
|
+
dt = date.get("start")
|
|
157
|
+
type = value.get("type")
|
|
158
|
+
if type == "formula" and value.get(type).get("type") == "number":
|
|
159
|
+
value = float(value.get(type).get("number"))
|
|
160
|
+
elif type == "rollup" and value.get(type).get("type") == "number":
|
|
161
|
+
value = float(value.get(type).get("number"))
|
|
162
|
+
else:
|
|
163
|
+
value = value.get(type)
|
|
164
|
+
date_str = pendulum.parse(dt).to_date_string()
|
|
165
|
+
self.number_by_date_dict[date_str] = (
|
|
166
|
+
self.number_by_date_dict.get(date_str, 0) + value
|
|
167
|
+
)
|
|
168
|
+
if tooltip_text:
|
|
169
|
+
self.tooltip_by_date_dict[date_str].append(tooltip_text.strip())
|
|
170
|
+
for _, v in self.number_by_date_dict.items():
|
|
171
|
+
self.number_list.append(v)
|
|
172
|
+
for key, texts in list(self.tooltip_by_date_dict.items()):
|
|
173
|
+
joined = "\n".join(filter(None, [t.strip() for t in texts if t]))
|
|
174
|
+
if joined:
|
|
175
|
+
self.tooltip_by_date_dict[key] = joined
|
|
176
|
+
else:
|
|
177
|
+
self.tooltip_by_date_dict.pop(key, None)
|
|
178
|
+
|
|
179
|
+
def get_all_track_data(self):
|
|
180
|
+
self.make_track_dict()
|
|
181
|
+
self.make_special_number()
|
|
182
|
+
return self.number_by_date_dict, self.year_list
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _format_date(date_dict):
|
|
186
|
+
if not date_dict:
|
|
187
|
+
return ""
|
|
188
|
+
start = date_dict.get("start")
|
|
189
|
+
end = date_dict.get("end")
|
|
190
|
+
if start and end and start != end:
|
|
191
|
+
return f"{start} - {end}"
|
|
192
|
+
return start or end or ""
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _join_plain_text(items):
|
|
196
|
+
if not items:
|
|
197
|
+
return ""
|
|
198
|
+
return "".join([item.get("plain_text", "") for item in items]).strip()
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def _extract_rollup_item(cls, item):
|
|
202
|
+
if not item:
|
|
203
|
+
return ""
|
|
204
|
+
item_type = item.get("type")
|
|
205
|
+
if item_type in ("title", "rich_text"):
|
|
206
|
+
return cls._join_plain_text(item.get(item_type))
|
|
207
|
+
if item_type == "people":
|
|
208
|
+
return ", ".join(
|
|
209
|
+
[p.get("name") or p.get("id", "") for p in item.get("people", [])]
|
|
210
|
+
).strip()
|
|
211
|
+
if item_type == "relation":
|
|
212
|
+
return ", ".join([r.get("id", "") for r in item.get("relation", [])]).strip()
|
|
213
|
+
if item_type == "date":
|
|
214
|
+
return cls._format_date(item.get("date", {}))
|
|
215
|
+
if item_type in ("url", "email", "phone_number"):
|
|
216
|
+
return item.get(item_type) or ""
|
|
217
|
+
if item_type == "number":
|
|
218
|
+
number_value = item.get("number")
|
|
219
|
+
return "" if number_value is None else str(number_value)
|
|
220
|
+
if item_type == "checkbox":
|
|
221
|
+
value = item.get("checkbox")
|
|
222
|
+
if value is None:
|
|
223
|
+
return ""
|
|
224
|
+
return "Yes" if value else "No"
|
|
225
|
+
if item_type == "formula":
|
|
226
|
+
return cls._extract_property_text(
|
|
227
|
+
{"type": "formula", "formula": item.get("formula", {})}
|
|
228
|
+
)
|
|
229
|
+
if item_type == "rollup":
|
|
230
|
+
return cls._extract_property_text(
|
|
231
|
+
{"type": "rollup", "rollup": item.get("rollup", {})}
|
|
232
|
+
)
|
|
233
|
+
return ""
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def _extract_property_text(cls, prop):
|
|
237
|
+
if not prop:
|
|
238
|
+
return ""
|
|
239
|
+
prop_type = prop.get("type")
|
|
240
|
+
if not prop_type:
|
|
241
|
+
return ""
|
|
242
|
+
if prop_type in ("title", "rich_text"):
|
|
243
|
+
return cls._join_plain_text(prop.get(prop_type))
|
|
244
|
+
if prop_type == "multi_select":
|
|
245
|
+
return ", ".join(
|
|
246
|
+
[item.get("name", "") for item in prop.get("multi_select", []) if item]
|
|
247
|
+
).strip()
|
|
248
|
+
if prop_type in ("select", "status"):
|
|
249
|
+
option = prop.get(prop_type) or {}
|
|
250
|
+
return option.get("name", "") if option else ""
|
|
251
|
+
if prop_type in ("url", "email", "phone_number"):
|
|
252
|
+
return prop.get(prop_type) or ""
|
|
253
|
+
if prop_type == "checkbox":
|
|
254
|
+
value = prop.get("checkbox")
|
|
255
|
+
if value is None:
|
|
256
|
+
return ""
|
|
257
|
+
return "Yes" if value else "No"
|
|
258
|
+
if prop_type == "number":
|
|
259
|
+
number_value = prop.get("number")
|
|
260
|
+
return "" if number_value is None else str(number_value)
|
|
261
|
+
if prop_type == "people":
|
|
262
|
+
return ", ".join(
|
|
263
|
+
[p.get("name") or p.get("id", "") for p in prop.get("people", [])]
|
|
264
|
+
).strip()
|
|
265
|
+
if prop_type in ("created_time", "last_edited_time"):
|
|
266
|
+
return prop.get(prop_type) or ""
|
|
267
|
+
if prop_type in ("created_by", "last_edited_by"):
|
|
268
|
+
person = prop.get(prop_type)
|
|
269
|
+
if isinstance(person, dict):
|
|
270
|
+
return person.get("name") or person.get("id", "") or ""
|
|
271
|
+
return ""
|
|
272
|
+
if prop_type == "date":
|
|
273
|
+
return cls._format_date(prop.get("date", {}))
|
|
274
|
+
if prop_type == "relation":
|
|
275
|
+
return ", ".join([r.get("id", "") for r in prop.get("relation", [])]).strip()
|
|
276
|
+
if prop_type == "files":
|
|
277
|
+
texts = []
|
|
278
|
+
for file_obj in prop.get("files", []):
|
|
279
|
+
name = file_obj.get("name")
|
|
280
|
+
if name:
|
|
281
|
+
texts.append(name)
|
|
282
|
+
continue
|
|
283
|
+
file_url = file_obj.get("file", {}).get("url")
|
|
284
|
+
external_url = file_obj.get("external", {}).get("url")
|
|
285
|
+
texts.append(file_url or external_url or "")
|
|
286
|
+
return ", ".join([t for t in texts if t]).strip()
|
|
287
|
+
if prop_type == "formula":
|
|
288
|
+
formula = prop.get("formula", {})
|
|
289
|
+
formula_type = formula.get("type")
|
|
290
|
+
if formula_type in ("string", "url", "email", "phone_number"):
|
|
291
|
+
return formula.get(formula_type) or ""
|
|
292
|
+
if formula_type == "boolean":
|
|
293
|
+
value = formula.get("boolean")
|
|
294
|
+
if value is None:
|
|
295
|
+
return ""
|
|
296
|
+
return "Yes" if value else "No"
|
|
297
|
+
if formula_type == "number":
|
|
298
|
+
number_value = formula.get("number")
|
|
299
|
+
return "" if number_value is None else str(number_value)
|
|
300
|
+
if formula_type == "date":
|
|
301
|
+
return cls._format_date(formula.get("date", {}))
|
|
302
|
+
if prop_type == "rollup":
|
|
303
|
+
rollup = prop.get("rollup", {})
|
|
304
|
+
rollup_type = rollup.get("type")
|
|
305
|
+
if rollup_type == "number":
|
|
306
|
+
number_value = rollup.get("number")
|
|
307
|
+
return "" if number_value is None else str(number_value)
|
|
308
|
+
if rollup_type == "date":
|
|
309
|
+
return cls._format_date(rollup.get("date", {}))
|
|
310
|
+
if rollup_type == "array":
|
|
311
|
+
texts = [cls._extract_rollup_item(item) for item in rollup.get("array", [])]
|
|
312
|
+
texts = [text for text in texts if text]
|
|
313
|
+
return ", ".join(texts)
|
|
314
|
+
return ""
|
|
@@ -6,7 +6,7 @@ setup(
|
|
|
6
6
|
author_email="linkang.ma@gmail.com",
|
|
7
7
|
url="https://github.com/malinkang/GitHubPoster",
|
|
8
8
|
license="MIT",
|
|
9
|
-
version="1.
|
|
9
|
+
version="1.4.0",
|
|
10
10
|
description="Make everything a GitHub svg poster and Skyline!",
|
|
11
11
|
packages=find_packages(),
|
|
12
12
|
include_package_data=True,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from github_heatmap.loader import notion_loader
|
|
4
|
+
from github_heatmap.loader.notion_loader import NotionLoader
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def build_loader():
|
|
8
|
+
return NotionLoader(
|
|
9
|
+
2024,
|
|
10
|
+
2024,
|
|
11
|
+
"notion",
|
|
12
|
+
notion_token="token",
|
|
13
|
+
database_id="db",
|
|
14
|
+
date_prop_name="Date",
|
|
15
|
+
value_prop_name="Value",
|
|
16
|
+
tooltip_prop_name="Tooltip",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_notion_loader_make_track_dict_with_tooltips():
|
|
21
|
+
loader = build_loader()
|
|
22
|
+
|
|
23
|
+
loader.get_api_data = lambda: [
|
|
24
|
+
{
|
|
25
|
+
"properties": {
|
|
26
|
+
"Date": {"date": {"start": "2024-01-01"}},
|
|
27
|
+
"Value": {"type": "number", "number": 5},
|
|
28
|
+
"Tooltip": {
|
|
29
|
+
"type": "rich_text",
|
|
30
|
+
"rich_text": [{"plain_text": "Study"}],
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"properties": {
|
|
36
|
+
"Date": {"date": {"start": "2024-01-01"}},
|
|
37
|
+
"Value": {"type": "number", "number": 2},
|
|
38
|
+
"Tooltip": {
|
|
39
|
+
"type": "multi_select",
|
|
40
|
+
"multi_select": [{"name": "Read"}, {"name": "Write"}],
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
loader.make_track_dict()
|
|
47
|
+
|
|
48
|
+
assert loader.number_by_date_dict["2024-01-01"] == 7
|
|
49
|
+
assert (
|
|
50
|
+
loader.tooltip_by_date_dict["2024-01-01"]
|
|
51
|
+
== "Study\nRead, Write"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_notion_loader_extract_property_text_variants():
|
|
56
|
+
rich_text = {
|
|
57
|
+
"type": "rich_text",
|
|
58
|
+
"rich_text": [{"plain_text": "Hello"}, {"plain_text": " World"}],
|
|
59
|
+
}
|
|
60
|
+
assert NotionLoader._extract_property_text(rich_text) == "Hello World"
|
|
61
|
+
|
|
62
|
+
select = {
|
|
63
|
+
"type": "select",
|
|
64
|
+
"select": {"name": "Done"},
|
|
65
|
+
}
|
|
66
|
+
assert NotionLoader._extract_property_text(select) == "Done"
|
|
67
|
+
|
|
68
|
+
multi_select = {
|
|
69
|
+
"type": "multi_select",
|
|
70
|
+
"multi_select": [{"name": "Tag1"}, {"name": "Tag2"}],
|
|
71
|
+
}
|
|
72
|
+
assert NotionLoader._extract_property_text(multi_select) == "Tag1, Tag2"
|
|
73
|
+
|
|
74
|
+
formula_number = {
|
|
75
|
+
"type": "formula",
|
|
76
|
+
"formula": {"type": "number", "number": 42},
|
|
77
|
+
}
|
|
78
|
+
assert NotionLoader._extract_property_text(formula_number) == "42"
|
|
79
|
+
|
|
80
|
+
rollup_array = {
|
|
81
|
+
"type": "rollup",
|
|
82
|
+
"rollup": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"array": [
|
|
85
|
+
{
|
|
86
|
+
"type": "rich_text",
|
|
87
|
+
"rich_text": [{"plain_text": "Note"}],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"type": "people",
|
|
91
|
+
"people": [{"name": "Alice"}, {"name": "Bob"}],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
assert NotionLoader._extract_property_text(rollup_array) == "Note, Alice, Bob"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_notion_loader_handles_http_error(monkeypatch):
|
|
100
|
+
loader = build_loader()
|
|
101
|
+
|
|
102
|
+
class DummyResp:
|
|
103
|
+
ok = False
|
|
104
|
+
status_code = 404
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def json():
|
|
108
|
+
return {"object": "error"}
|
|
109
|
+
|
|
110
|
+
monkeypatch.setattr(
|
|
111
|
+
notion_loader.requests, "post", lambda *args, **kwargs: DummyResp()
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
tracks, years = loader.get_all_track_data()
|
|
115
|
+
|
|
116
|
+
assert years == [2024]
|
|
117
|
+
assert tracks
|
|
118
|
+
assert all(value == 0 for value in tracks.values())
|
|
119
|
+
assert loader.number_list
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_notion_loader_handles_request_exception(monkeypatch):
|
|
123
|
+
loader = build_loader()
|
|
124
|
+
|
|
125
|
+
def fake_post(*args, **kwargs):
|
|
126
|
+
raise requests.RequestException("boom")
|
|
127
|
+
|
|
128
|
+
monkeypatch.setattr(notion_loader.requests, "post", fake_post)
|
|
129
|
+
|
|
130
|
+
tracks, years = loader.get_all_track_data()
|
|
131
|
+
|
|
132
|
+
assert years == [2024]
|
|
133
|
+
assert tracks
|
|
134
|
+
assert all(value == 0 for value in tracks.values())
|
|
135
|
+
assert loader.number_list
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from github_heatmap.drawer import Drawer
|
|
2
|
+
from github_heatmap.poster import Poster
|
|
3
|
+
from github_heatmap.utils import interpolate_color, make_key_times, parse_years
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_interpolate_color():
|
|
7
|
+
assert interpolate_color("#000000", "#ffffff", 0) == "#000000"
|
|
8
|
+
assert interpolate_color("#000000", "#ffffff", 1) == "#ffffff"
|
|
9
|
+
assert interpolate_color("#000000", "#ffffff", 0.5) == "#7f7f7f"
|
|
10
|
+
assert interpolate_color("#000000", "#ffffff", -100) == "#000000"
|
|
11
|
+
assert interpolate_color("#000000", "#ffffff", 12345) == "#ffffff"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_parse_years():
|
|
15
|
+
assert parse_years("2012") == (2012, 2012)
|
|
16
|
+
assert parse_years("2015-2021") == (2015, 2021)
|
|
17
|
+
assert parse_years("2021-2015") == (2015, 2021)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_make_key_times():
|
|
21
|
+
assert make_key_times(5) == ["0", "0.2", "0.4", "0.6", "0.8", "1"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_drawer_tooltip_formatting():
|
|
25
|
+
poster = Poster()
|
|
26
|
+
poster.units = "XP"
|
|
27
|
+
drawer = Drawer(poster)
|
|
28
|
+
|
|
29
|
+
assert drawer._format_tooltip("2024-01-01", 10) == "2024-01-01 10 XP"
|
|
30
|
+
assert drawer._format_tooltip("2024-01-01") == "2024-01-01"
|
|
31
|
+
|
|
32
|
+
poster.tooltip_template = "{date}: {value}{unit}"
|
|
33
|
+
assert drawer._format_tooltip("2024-01-01", 5) == "2024-01-01: 5XP"
|
|
34
|
+
assert drawer._format_tooltip("2024-01-01") == "2024-01-01"
|
|
35
|
+
|
|
36
|
+
poster.tooltip_template = "{date} {value} for {type}"
|
|
37
|
+
assert (
|
|
38
|
+
drawer._format_tooltip("2024-01-01", 7, "run")
|
|
39
|
+
== "2024-01-01 7 for run"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
poster.tooltip_by_date = {"2024-01-01": "Custom"}
|
|
43
|
+
assert drawer._format_tooltip("2024-01-01", 5) == "Custom"
|
|
44
|
+
|
|
45
|
+
poster.tooltip_by_date = {"2024-01-02": {"run": "Run note"}}
|
|
46
|
+
assert drawer._format_tooltip("2024-01-02", 3, "run") == "Run note"
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
2
|
-
import time
|
|
3
|
-
from collections import defaultdict
|
|
4
|
-
|
|
5
|
-
import pendulum
|
|
6
|
-
import requests
|
|
7
|
-
import json
|
|
8
|
-
|
|
9
|
-
from github_heatmap.loader.base_loader import BaseLoader, LoadError
|
|
10
|
-
from github_heatmap.loader.config import NOTION_API_URL, NOTION_API_VERSION
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class NotionLoader(BaseLoader):
|
|
14
|
-
track_color = "#40C463"
|
|
15
|
-
unit = "times"
|
|
16
|
-
|
|
17
|
-
def __init__(self, from_year, to_year, _type, **kwargs):
|
|
18
|
-
super().__init__(from_year, to_year, _type)
|
|
19
|
-
self.number_by_date_dict = self.generate_date_dict(from_year, to_year)
|
|
20
|
-
self.notion_token = kwargs.get("notion_token", "").strip()
|
|
21
|
-
self.database_id = kwargs.get("database_id", "").strip()
|
|
22
|
-
self.date_prop_name = kwargs.get("date_prop_name", "")
|
|
23
|
-
self.value_prop_name = kwargs.get("value_prop_name", "")
|
|
24
|
-
self.database_filter = kwargs.get("database_filter", "")
|
|
25
|
-
|
|
26
|
-
@classmethod
|
|
27
|
-
def add_loader_arguments(cls, parser, optional):
|
|
28
|
-
parser.add_argument(
|
|
29
|
-
"--notion_token",
|
|
30
|
-
dest="notion_token",
|
|
31
|
-
type=str,
|
|
32
|
-
help="The Notion internal integration token.",
|
|
33
|
-
)
|
|
34
|
-
parser.add_argument(
|
|
35
|
-
"--database_id",
|
|
36
|
-
dest="database_id",
|
|
37
|
-
type=str,
|
|
38
|
-
help="The Notion database id.",
|
|
39
|
-
)
|
|
40
|
-
parser.add_argument(
|
|
41
|
-
"--date_prop_name",
|
|
42
|
-
dest="date_prop_name",
|
|
43
|
-
type=str,
|
|
44
|
-
default="Datetime",
|
|
45
|
-
required=optional,
|
|
46
|
-
help="The database property name which stored the datetime.",
|
|
47
|
-
)
|
|
48
|
-
parser.add_argument(
|
|
49
|
-
"--value_prop_name",
|
|
50
|
-
dest="value_prop_name",
|
|
51
|
-
type=str,
|
|
52
|
-
default="Datetime",
|
|
53
|
-
required=optional,
|
|
54
|
-
help="The database property name which stored the datetime.",
|
|
55
|
-
)
|
|
56
|
-
parser.add_argument(
|
|
57
|
-
"--database_filter",
|
|
58
|
-
dest="database_filter",
|
|
59
|
-
type=str,
|
|
60
|
-
default="",
|
|
61
|
-
required=False,
|
|
62
|
-
help="The database property name which stored the datetime.",
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
def get_api_data(self, start_cursor="", page_size=100, data_list=[]):
|
|
66
|
-
payload = {
|
|
67
|
-
"page_size": page_size,
|
|
68
|
-
"filter": {
|
|
69
|
-
"and": [
|
|
70
|
-
{
|
|
71
|
-
"property": self.date_prop_name,
|
|
72
|
-
"date": {"on_or_after": f"{self.from_year}-01-01"},
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"property": self.date_prop_name,
|
|
76
|
-
"date": {"on_or_before": f"{self.to_year}-12-31"},
|
|
77
|
-
},
|
|
78
|
-
]
|
|
79
|
-
},
|
|
80
|
-
}
|
|
81
|
-
if self.database_filter:
|
|
82
|
-
payload["filter"]["and"].append(json.loads(self.database_filter))
|
|
83
|
-
if start_cursor:
|
|
84
|
-
payload.update({"start_cursor": start_cursor})
|
|
85
|
-
|
|
86
|
-
headers = {
|
|
87
|
-
"Accept": "application/json",
|
|
88
|
-
"Notion-Version": NOTION_API_VERSION,
|
|
89
|
-
"Content-Type": "application/json",
|
|
90
|
-
"Authorization": "Bearer " + self.notion_token,
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
resp = requests.post(
|
|
94
|
-
NOTION_API_URL.format(database_id=self.database_id),
|
|
95
|
-
json=payload,
|
|
96
|
-
headers=headers,
|
|
97
|
-
)
|
|
98
|
-
if not resp.ok:
|
|
99
|
-
raise LoadError("Can not get Notion data, please check your config")
|
|
100
|
-
data = resp.json()
|
|
101
|
-
results = data["results"]
|
|
102
|
-
next_cursor = data["next_cursor"]
|
|
103
|
-
data_list.extend(results)
|
|
104
|
-
if not data["has_more"]:
|
|
105
|
-
return data_list
|
|
106
|
-
# Avoid request limits
|
|
107
|
-
# The rate limit for incoming requests is an average
|
|
108
|
-
# of 3 requests per second.
|
|
109
|
-
# See https://developers.notion.com/reference/request-limits
|
|
110
|
-
time.sleep(0.3)
|
|
111
|
-
return self.get_api_data(
|
|
112
|
-
start_cursor=next_cursor, page_size=page_size, data_list=data_list
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
def generate_date_dict(self, start_year, end_year):
|
|
116
|
-
start_date = datetime(start_year, 1, 1)
|
|
117
|
-
end_date = datetime(end_year, 12, 31)
|
|
118
|
-
|
|
119
|
-
# 使用字典推导式生成日期字典
|
|
120
|
-
return {
|
|
121
|
-
(start_date + timedelta(days=i)).strftime("%Y-%m-%d"): 0
|
|
122
|
-
for i in range((end_date - start_date).days + 1)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
def make_track_dict(self):
|
|
126
|
-
data_list = self.get_api_data()
|
|
127
|
-
for result in data_list:
|
|
128
|
-
date = result["properties"][self.date_prop_name]["date"]
|
|
129
|
-
value = result["properties"][self.value_prop_name]
|
|
130
|
-
if date and value:
|
|
131
|
-
dt = date.get("start")
|
|
132
|
-
type = value.get("type")
|
|
133
|
-
if type == "formula" and value.get(type).get("type") == "number":
|
|
134
|
-
value = float(value.get(type).get("number"))
|
|
135
|
-
elif type == "rollup" and value.get(type).get("type") == "number":
|
|
136
|
-
value = float(value.get(type).get("number"))
|
|
137
|
-
else:
|
|
138
|
-
value = value.get(type)
|
|
139
|
-
date_str = pendulum.parse(dt).to_date_string()
|
|
140
|
-
self.number_by_date_dict[date_str] = (
|
|
141
|
-
self.number_by_date_dict.get(date_str, 0) + value
|
|
142
|
-
)
|
|
143
|
-
for _, v in self.number_by_date_dict.items():
|
|
144
|
-
self.number_list.append(v)
|
|
145
|
-
|
|
146
|
-
def get_all_track_data(self):
|
|
147
|
-
self.make_track_dict()
|
|
148
|
-
self.make_special_number()
|
|
149
|
-
return self.number_by_date_dict, self.year_list
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
from github_heatmap.utils import interpolate_color, make_key_times, parse_years
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def test_interpolate_color():
|
|
5
|
-
assert interpolate_color("#000000", "#ffffff", 0) == "#000000"
|
|
6
|
-
assert interpolate_color("#000000", "#ffffff", 1) == "#ffffff"
|
|
7
|
-
assert interpolate_color("#000000", "#ffffff", 0.5) == "#7f7f7f"
|
|
8
|
-
assert interpolate_color("#000000", "#ffffff", -100) == "#000000"
|
|
9
|
-
assert interpolate_color("#000000", "#ffffff", 12345) == "#ffffff"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_parse_years():
|
|
13
|
-
assert parse_years("2012") == (2012, 2012)
|
|
14
|
-
assert parse_years("2015-2021") == (2015, 2021)
|
|
15
|
-
assert parse_years("2021-2015") == (2015, 2021)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def test_make_key_times():
|
|
19
|
-
assert make_key_times(5) == ["0", "0.2", "0.4", "0.6", "0.8", "1"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github_heatmap-1.3.8 → github_heatmap-1.4.0}/github_heatmap/loader/from_github_issue_loader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|