github-heatmap 1.4.0__tar.gz → 2.2.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.4.0 → github_heatmap-2.2.0}/PKG-INFO +10 -2
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/README.md +8 -8
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/cli.py +53 -15
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/config.py +3 -2
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/drawer.py +30 -8
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/base_loader.py +29 -9
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/config.py +2 -2
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/github_loader.py +2 -1
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/gitlab_loader.py +1 -1
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/notion_loader.py +62 -22
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/weread_loader.py +23 -15
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/poster.py +20 -10
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/skyline/skyline.py +1 -1
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/utils.py +53 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/PKG-INFO +10 -2
- github_heatmap-2.2.0/pyproject.toml +6 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/setup.py +1 -1
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/tests/test_notion_loader.py +44 -5
- github_heatmap-2.2.0/tests/test_poster_utils.py +91 -0
- github_heatmap-1.4.0/pyproject.toml +0 -2
- github_heatmap-1.4.0/tests/test_poster_utils.py +0 -46
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/LICENSE +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/__init__.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/__main__.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/circluar_drawer.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/err.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/html_parser/__init__.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/html_parser/github_parser.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/html_parser/gitlab_parser.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/html_parser/jike_parse.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/html_parser/kindle_parser.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/__init__.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/apple_health_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/bbdc_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/bilibili_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/chatgpt_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/cichang_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/covid_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/dota2_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/duolingo_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/forest_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/from_github_issue_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/garmin_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/gpx_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/jike_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/json_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/kindle_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/leetcode_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/multiple_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/neodb_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/nrc_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/ns_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/openlanguage_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/shanbay_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/strava_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/summary_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/todoist_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/wakatime_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/youtube_loader.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/skyline/__init__.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/skyline/config.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/skyline/font/__init__.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/structures.py +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/SOURCES.txt +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/dependency_links.txt +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/entry_points.txt +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/requires.txt +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/top_level.txt +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/setup.cfg +0 -0
- {github_heatmap-1.4.0 → github_heatmap-2.2.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: github_heatmap
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Make everything a GitHub svg poster and Skyline!
|
|
5
5
|
Home-page: https://github.com/malinkang/GitHubPoster
|
|
6
6
|
Author: malinkang
|
|
@@ -31,3 +31,11 @@ Requires-Dist: stravalib; extra == "all"
|
|
|
31
31
|
Requires-Dist: PyGithub; extra == "all"
|
|
32
32
|
Requires-Dist: sdf_fork; extra == "all"
|
|
33
33
|
Requires-Dist: pandas; extra == "all"
|
|
34
|
+
Dynamic: author
|
|
35
|
+
Dynamic: author-email
|
|
36
|
+
Dynamic: home-page
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: provides-extra
|
|
40
|
+
Dynamic: requires-dist
|
|
41
|
+
Dynamic: summary
|
|
@@ -435,18 +435,18 @@ github_heatmap nike --nike_refresh_token="your nike_refresh_token" --year 2012-2
|
|
|
435
435
|
2. 点击「New integration」添加基础信息后,创建新的 Token
|
|
436
436
|
3. 提交后可以看到 `Secrets` 下的 `Internal Integration Token`
|
|
437
437
|
|
|
438
|
-
获取用于生成 Poster 的 Notion
|
|
438
|
+
获取用于生成 Poster 的 Notion 数据源 ID(`data_source_id`),查看[官方文档](https://developers.notion.com/reference/query-a-data-source)获取更多信息。
|
|
439
439
|
|
|
440
|
-
1.
|
|
441
|
-
2.
|
|
442
|
-
3. 其中 `{
|
|
440
|
+
1. 在 Notion 中打开对应的数据源页面
|
|
441
|
+
2. 复制页面链接,链接一般类似 `https://www.notion.so/{workspace_name}/{data_source_id}?v={view_id}`
|
|
442
|
+
3. 其中 `{data_source_id}` 部分即为数据源 ID
|
|
443
443
|
|
|
444
|
-
|
|
444
|
+
注:数据源需要至少包含一个类型为 `Date` 的属性作为日期字段,以及一个数值/公式/rollup 属性作为热力图统计值。生成时通过 `--date_prop_name` 和 `--value_prop_name` 指定字段名。
|
|
445
445
|
|
|
446
446
|
```
|
|
447
|
-
python3 -m github_heatmap notion --notion_token="your notion_token" --
|
|
447
|
+
python3 -m github_heatmap notion --notion_token="your notion_token" --data_source_id="your data_source_id" --date_prop_name="your date_prop_name" --value_prop_name="your value_prop_name"
|
|
448
448
|
or
|
|
449
|
-
github_heatmap notion --notion_token="your notion_token" --
|
|
449
|
+
github_heatmap notion --notion_token="your notion_token" --data_source_id="your data_source_id" --date_prop_name="your date_prop_name" --value_prop_name="your value_prop_name"
|
|
450
450
|
```
|
|
451
451
|
|
|
452
452
|
</details>
|
|
@@ -738,4 +738,4 @@ python3 -m github_heatmap neodb --neodb_token <token> --mark_type <complete, wis
|
|
|
738
738
|
|
|
739
739
|
谢谢就够了
|
|
740
740
|
|
|
741
|
-
Just enjoy it
|
|
741
|
+
Just enjoy it
|
|
@@ -7,21 +7,22 @@ import os
|
|
|
7
7
|
import sys
|
|
8
8
|
|
|
9
9
|
from github_heatmap.circluar_drawer import CircularDrawer
|
|
10
|
-
from github_heatmap.config import TYPE_INFO_DICT
|
|
11
|
-
from github_heatmap.drawer import Drawer
|
|
12
|
-
from github_heatmap.err import DepNotInstalledError
|
|
13
|
-
from github_heatmap.loader import LOADER_DICT
|
|
14
|
-
from github_heatmap.poster import Poster
|
|
15
|
-
from github_heatmap.utils import parse_years, reduce_year_list
|
|
16
10
|
from github_heatmap.config import (
|
|
17
|
-
HEAD_FONT_SIZE,
|
|
18
|
-
YEAR_FONT_SIZE,
|
|
19
|
-
MONTH_FONT_SIZE,
|
|
20
11
|
DOM_BOX_PADING,
|
|
21
12
|
DOM_BOX_TUPLE,
|
|
13
|
+
GITHUB_LEVEL_COLORS,
|
|
14
|
+
HEAD_FONT_SIZE,
|
|
15
|
+
MARGIN_LEFT,
|
|
22
16
|
MARGIN_TOP,
|
|
23
|
-
|
|
17
|
+
MONTH_FONT_SIZE,
|
|
18
|
+
TYPE_INFO_DICT,
|
|
19
|
+
YEAR_FONT_SIZE,
|
|
24
20
|
)
|
|
21
|
+
from github_heatmap.drawer import Drawer
|
|
22
|
+
from github_heatmap.err import DepNotInstalledError
|
|
23
|
+
from github_heatmap.loader import LOADER_DICT
|
|
24
|
+
from github_heatmap.poster import Poster
|
|
25
|
+
from github_heatmap.utils import build_level_colors, parse_years, reduce_year_list
|
|
25
26
|
|
|
26
27
|
OUT_FOLDER = os.path.join(os.getcwd(), "OUT_FOLDER")
|
|
27
28
|
|
|
@@ -50,16 +51,32 @@ def run():
|
|
|
50
51
|
args = args_parser.parse_args()
|
|
51
52
|
# without title
|
|
52
53
|
no_title_types = ("issue", "multiple", "json")
|
|
54
|
+
base_track_color = args.track_color or args.loader.track_color or "#4DD2FF"
|
|
55
|
+
use_exact_github_colors = (
|
|
56
|
+
args.type == "github"
|
|
57
|
+
and not args.track_color
|
|
58
|
+
and not args.special_color1
|
|
59
|
+
and not args.special_color2
|
|
60
|
+
)
|
|
61
|
+
level_colors = (
|
|
62
|
+
list(GITHUB_LEVEL_COLORS)
|
|
63
|
+
if use_exact_github_colors
|
|
64
|
+
else build_level_colors(
|
|
65
|
+
base_track_color, args.special_color1, args.special_color2
|
|
66
|
+
)
|
|
67
|
+
)
|
|
53
68
|
|
|
54
69
|
p.colors = {
|
|
55
70
|
"background": args.background_color,
|
|
56
|
-
"track":
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"special2": args.special_color2 or args.special_color,
|
|
71
|
+
"track": base_track_color,
|
|
72
|
+
"special": level_colors[2],
|
|
73
|
+
"special2": level_colors[3],
|
|
60
74
|
"text": args.text_color,
|
|
61
75
|
"dom": args.dom_color,
|
|
62
76
|
}
|
|
77
|
+
p.level_colors = level_colors
|
|
78
|
+
p.use_github_level_mapping = not (args.special_number1 or args.special_number2)
|
|
79
|
+
p.use_raw_level = args.use_raw_level
|
|
63
80
|
|
|
64
81
|
p.tooltip_template = args.tooltip_template or None
|
|
65
82
|
|
|
@@ -68,6 +85,7 @@ def run():
|
|
|
68
85
|
p.colors["track"] = "#025DB8"
|
|
69
86
|
p.colors["special"] = "#FFD100"
|
|
70
87
|
p.colors["special2"] = "#FFD100"
|
|
88
|
+
p.level_colors = build_level_colors("#025DB8", "#FFD100", "#FFD100")
|
|
71
89
|
|
|
72
90
|
# set animate
|
|
73
91
|
p.set_with_animation(args.with_animation)
|
|
@@ -111,6 +129,7 @@ def run():
|
|
|
111
129
|
else:
|
|
112
130
|
p.units = args.loader.unit
|
|
113
131
|
p.tooltip_by_date = getattr(loader, "tooltip_by_date_dict", {}) or {}
|
|
132
|
+
p.level_thresholds = getattr(loader, "level_thresholds", ())
|
|
114
133
|
p.set_tracks(tracks, years, type_list)
|
|
115
134
|
else:
|
|
116
135
|
if args.unit:
|
|
@@ -119,6 +138,20 @@ def run():
|
|
|
119
138
|
p.units = args.loader.unit
|
|
120
139
|
p.set_tracks({}, [to_year], type_list)
|
|
121
140
|
p.tooltip_by_date = {}
|
|
141
|
+
p.level_thresholds_by_type = {
|
|
142
|
+
child_loader._type: getattr(child_loader, "level_thresholds", ())
|
|
143
|
+
for child_loader in loader.loader_list
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# explicit level thresholds override quartile calculation
|
|
147
|
+
if args.level_thresholds:
|
|
148
|
+
p.level_thresholds = tuple(float(v) for v in args.level_thresholds.split(","))
|
|
149
|
+
p.use_github_level_mapping = True
|
|
150
|
+
|
|
151
|
+
# explicit level colors override computed colors
|
|
152
|
+
if args.level_colors:
|
|
153
|
+
p.level_colors = [c.strip() for c in args.level_colors.split(",")]
|
|
154
|
+
p.use_github_level_mapping = True
|
|
122
155
|
|
|
123
156
|
# set title
|
|
124
157
|
# we don't know issue content so use name
|
|
@@ -144,7 +177,12 @@ def run():
|
|
|
144
177
|
MARGIN_TOP
|
|
145
178
|
+ HEAD_FONT_SIZE
|
|
146
179
|
+ poster_length
|
|
147
|
-
* (
|
|
180
|
+
* (
|
|
181
|
+
YEAR_FONT_SIZE
|
|
182
|
+
+ MONTH_FONT_SIZE
|
|
183
|
+
+ DOM_BOX_PADING * 3
|
|
184
|
+
+ (DOM_BOX_PADING + DOM_BOX_TUPLE[0]) * 7
|
|
185
|
+
)
|
|
148
186
|
)
|
|
149
187
|
if not os.path.exists(OUT_FOLDER):
|
|
150
188
|
os.mkdir(OUT_FOLDER)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
DOM_BOX_TUPLE = (4,4)
|
|
1
|
+
DOM_BOX_TUPLE = (4, 4)
|
|
2
2
|
DOM_BOX_TUPLE_LIST_FOR_TWO = ((2.6, 1.3), (2.6, 1.3))
|
|
3
3
|
DOM_BOX_TUPLE_LIST_FOR_THREE = ((2.7, 0.9), (2.7, 0.9), (2.7, 0.9))
|
|
4
4
|
|
|
@@ -11,7 +11,7 @@ DOM_BOX_DICT = {
|
|
|
11
11
|
3: {"dom": DOM_BOX_TUPLE_LIST_FOR_THREE},
|
|
12
12
|
}
|
|
13
13
|
MARGIN_LEFT = 10
|
|
14
|
-
MARGIN_TOP
|
|
14
|
+
MARGIN_TOP = 4
|
|
15
15
|
HEAD_FONT_SIZE = 6
|
|
16
16
|
YEAR_FONT_SIZE = 3
|
|
17
17
|
MONTH_FONT_SIZE = 2.5
|
|
@@ -20,6 +20,7 @@ DOM_BOX_RADIUS = 1
|
|
|
20
20
|
DOM_BOX_PADING = 1.4
|
|
21
21
|
|
|
22
22
|
DEFAULT_DOM_COLOR = "#444444"
|
|
23
|
+
GITHUB_LEVEL_COLORS = ("#9BE9A8", "#40C463", "#30A14E", "#216E39")
|
|
23
24
|
MONTH_NAMES = [
|
|
24
25
|
"Jan",
|
|
25
26
|
"Feb",
|
|
@@ -7,15 +7,15 @@ from github_heatmap.config import (
|
|
|
7
7
|
COLOR_TUPLE,
|
|
8
8
|
DEFAULT_DOM_COLOR,
|
|
9
9
|
DOM_BOX_DICT,
|
|
10
|
+
DOM_BOX_PADING,
|
|
11
|
+
DOM_BOX_RADIUS,
|
|
10
12
|
DOM_BOX_TUPLE,
|
|
13
|
+
MONTH_FONT_SIZE,
|
|
11
14
|
MONTH_NAMES,
|
|
12
15
|
YEAR_FONT_SIZE,
|
|
13
|
-
MONTH_FONT_SIZE,
|
|
14
|
-
DOM_BOX_PADING,
|
|
15
|
-
DOM_BOX_RADIUS,
|
|
16
16
|
)
|
|
17
17
|
from github_heatmap.err import BaseDrawError
|
|
18
|
-
from github_heatmap.utils import interpolate_color, make_key_times
|
|
18
|
+
from github_heatmap.utils import interpolate_color, make_key_times, resolve_github_level
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class Drawer:
|
|
@@ -71,7 +71,21 @@ class Drawer:
|
|
|
71
71
|
return f"{date_title} {value} {self.poster.units}"
|
|
72
72
|
return date_title
|
|
73
73
|
|
|
74
|
-
def
|
|
74
|
+
def _resolve_level_thresholds(self, type_name=None):
|
|
75
|
+
if type_name and self.poster.level_thresholds_by_type:
|
|
76
|
+
return self.poster.level_thresholds_by_type.get(type_name, ())
|
|
77
|
+
return self.poster.level_thresholds
|
|
78
|
+
|
|
79
|
+
def _make_github_level_color(self, length, type_name=None):
|
|
80
|
+
level = resolve_github_level(length, self._resolve_level_thresholds(type_name), self.poster.use_raw_level)
|
|
81
|
+
if level == 0:
|
|
82
|
+
return self.poster.colors.get("dom")
|
|
83
|
+
return self.poster.level_colors[level - 1]
|
|
84
|
+
|
|
85
|
+
def make_color(self, length_range, length, type_name=None):
|
|
86
|
+
if self.poster.use_github_level_mapping and self.poster.level_colors:
|
|
87
|
+
return self._make_github_level_color(length, type_name)
|
|
88
|
+
|
|
75
89
|
sp2 = self.poster.special_number.get("special_number2")
|
|
76
90
|
sp1 = self.poster.special_number.get("special_number1")
|
|
77
91
|
has_special = False
|
|
@@ -122,7 +136,10 @@ class Drawer:
|
|
|
122
136
|
color = self.poster.colors.get("dom")
|
|
123
137
|
if day_tracks:
|
|
124
138
|
color = self.make_color(self.poster.length_range_by_date, day_tracks)
|
|
125
|
-
if
|
|
139
|
+
if (
|
|
140
|
+
not self.poster.use_github_level_mapping
|
|
141
|
+
and day_tracks >= self.poster.special_number["special_number1"]
|
|
142
|
+
):
|
|
126
143
|
color = self.poster.colors.get("special2") or self.poster.colors.get(
|
|
127
144
|
"special"
|
|
128
145
|
)
|
|
@@ -171,7 +188,7 @@ class Drawer:
|
|
|
171
188
|
if not num:
|
|
172
189
|
continue
|
|
173
190
|
dom = dom_tuple[index]
|
|
174
|
-
color = self.make_color(length_range, num)
|
|
191
|
+
color = self.make_color(length_range, num, _type)
|
|
175
192
|
rect = dr.rect(
|
|
176
193
|
(rect_x, rect_y),
|
|
177
194
|
dom,
|
|
@@ -248,7 +265,11 @@ class Drawer:
|
|
|
248
265
|
style=self.month_names_style,
|
|
249
266
|
)
|
|
250
267
|
)
|
|
251
|
-
if
|
|
268
|
+
if (
|
|
269
|
+
index > 0
|
|
270
|
+
and index < 53
|
|
271
|
+
and month != MONTH_NAMES[github_rect_day.month - 1]
|
|
272
|
+
):
|
|
252
273
|
month = MONTH_NAMES[github_rect_day.month - 1]
|
|
253
274
|
dr.add(
|
|
254
275
|
dr.text(
|
|
@@ -308,6 +329,7 @@ class Drawer:
|
|
|
308
329
|
"special_number1": loader.special_number1,
|
|
309
330
|
"special_number2": loader.special_number2,
|
|
310
331
|
}
|
|
332
|
+
self.poster.level_thresholds = getattr(loader, "level_thresholds", ())
|
|
311
333
|
self.poster.tooltip_by_date = (
|
|
312
334
|
getattr(loader, "tooltip_by_date_dict", {}) or {}
|
|
313
335
|
)
|
|
@@ -7,6 +7,7 @@ import pendulum
|
|
|
7
7
|
from requests.utils import cookiejar_from_dict
|
|
8
8
|
|
|
9
9
|
from github_heatmap.loader.config import TIME_ZONE
|
|
10
|
+
from github_heatmap.utils import make_github_level_thresholds
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class LoadError(Exception):
|
|
@@ -29,6 +30,7 @@ class BaseLoader(ABC):
|
|
|
29
30
|
self.number_by_date_dict = defaultdict(int)
|
|
30
31
|
self.special_number1 = None
|
|
31
32
|
self.special_number2 = None
|
|
33
|
+
self.level_thresholds = ()
|
|
32
34
|
self.number_list = []
|
|
33
35
|
self.year_list = self._make_years_list()
|
|
34
36
|
self.try_import_deps()
|
|
@@ -51,10 +53,10 @@ class BaseLoader(ABC):
|
|
|
51
53
|
|
|
52
54
|
def make_special_number(self):
|
|
53
55
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
special_number2 top 20 % - 50%
|
|
56
|
+
Keep the legacy special-number thresholds for compatibility, and
|
|
57
|
+
also compute GitHub-style quartile thresholds for five-level maps.
|
|
57
58
|
"""
|
|
59
|
+
self.level_thresholds = make_github_level_thresholds(self.number_list)
|
|
58
60
|
# before python below 3.5 maybe need to sort
|
|
59
61
|
number_list_set = sorted(list(set(self.number_list)))
|
|
60
62
|
number_list_set_len = len(number_list_set)
|
|
@@ -117,8 +119,8 @@ class BaseLoader(ABC):
|
|
|
117
119
|
dest="track_color",
|
|
118
120
|
metavar="COLOR",
|
|
119
121
|
type=str,
|
|
120
|
-
default=
|
|
121
|
-
help=
|
|
122
|
+
default=None,
|
|
123
|
+
help="Base positive-level color (default: loader-specific color).",
|
|
122
124
|
)
|
|
123
125
|
group.add_argument(
|
|
124
126
|
"--dom-color",
|
|
@@ -140,15 +142,15 @@ class BaseLoader(ABC):
|
|
|
140
142
|
"--special-color1",
|
|
141
143
|
dest="special_color1",
|
|
142
144
|
metavar="COLOR",
|
|
143
|
-
default=
|
|
144
|
-
help=
|
|
145
|
+
default=None,
|
|
146
|
+
help="Optional third-level color override for the five-level heatmap.",
|
|
145
147
|
)
|
|
146
148
|
group.add_argument(
|
|
147
149
|
"--special-color2",
|
|
148
150
|
dest="special_color2",
|
|
149
151
|
metavar="COLOR",
|
|
150
|
-
default=
|
|
151
|
-
help="
|
|
152
|
+
default=None,
|
|
153
|
+
help="Optional fourth-level color override for the five-level heatmap.",
|
|
152
154
|
)
|
|
153
155
|
group.add_argument(
|
|
154
156
|
"--special-number1",
|
|
@@ -231,6 +233,24 @@ class BaseLoader(ABC):
|
|
|
231
233
|
action="store_true",
|
|
232
234
|
help="if account is CN",
|
|
233
235
|
)
|
|
236
|
+
group.add_argument(
|
|
237
|
+
"--use-raw-level",
|
|
238
|
+
dest="use_raw_level",
|
|
239
|
+
action="store_true",
|
|
240
|
+
help="Use raw value (1-4) as level directly, skip quartile calculation",
|
|
241
|
+
)
|
|
242
|
+
group.add_argument(
|
|
243
|
+
"--level-thresholds",
|
|
244
|
+
dest="level_thresholds",
|
|
245
|
+
default="",
|
|
246
|
+
help="Explicit level thresholds (comma-separated, e.g. '360,450,99999'), overrides quartile calculation",
|
|
247
|
+
)
|
|
248
|
+
group.add_argument(
|
|
249
|
+
"--level-colors",
|
|
250
|
+
dest="level_colors",
|
|
251
|
+
default="",
|
|
252
|
+
help="Explicit level colors (comma-separated hex, e.g. '#E74C3C,#F1C40F,#2ECC71,#2ECC71'), overrides computed colors",
|
|
253
|
+
)
|
|
234
254
|
# special here
|
|
235
255
|
group.add_argument(
|
|
236
256
|
"--stand-with-ukraine",
|
|
@@ -115,8 +115,8 @@ JIKE_PERSON_URL = "https://web.okjike.com/u/{user_id}"
|
|
|
115
115
|
BBDC_API_URL = "https://learnywhere.cn/bb/dashboard/profile/search?userId={user_id}"
|
|
116
116
|
|
|
117
117
|
# Notion
|
|
118
|
-
NOTION_API_URL = "https://api.notion.com/v1/
|
|
119
|
-
NOTION_API_VERSION = "
|
|
118
|
+
NOTION_API_URL = "https://api.notion.com/v1/data_sources/{data_source_id}/query"
|
|
119
|
+
NOTION_API_VERSION = "2026-03-11"
|
|
120
120
|
|
|
121
121
|
# Weread
|
|
122
122
|
WEREAD_BASE_URL = "https://weread.qq.com/"
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
2
3
|
import time
|
|
4
|
+
import warnings
|
|
3
5
|
from collections import defaultdict
|
|
6
|
+
from datetime import datetime, timedelta
|
|
4
7
|
|
|
5
8
|
import pendulum
|
|
6
9
|
import requests
|
|
7
|
-
import json
|
|
8
10
|
|
|
9
11
|
from github_heatmap.loader.base_loader import BaseLoader
|
|
10
12
|
from github_heatmap.loader.config import NOTION_API_URL, NOTION_API_VERSION
|
|
@@ -17,12 +19,28 @@ class NotionLoader(BaseLoader):
|
|
|
17
19
|
def __init__(self, from_year, to_year, _type, **kwargs):
|
|
18
20
|
super().__init__(from_year, to_year, _type)
|
|
19
21
|
self.number_by_date_dict = self.generate_date_dict(from_year, to_year)
|
|
20
|
-
self.notion_token = kwargs.get("notion_token"
|
|
21
|
-
self.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
self.notion_token = (kwargs.get("notion_token") or "").strip()
|
|
23
|
+
self.data_source_id = (kwargs.get("data_source_id") or "").strip()
|
|
24
|
+
deprecated_database_id = (kwargs.get("database_id") or "").strip()
|
|
25
|
+
if deprecated_database_id and not self.data_source_id:
|
|
26
|
+
warnings.warn(
|
|
27
|
+
"--database_id is deprecated; use --data_source_id instead.",
|
|
28
|
+
DeprecationWarning,
|
|
29
|
+
stacklevel=2,
|
|
30
|
+
)
|
|
31
|
+
self.data_source_id = deprecated_database_id
|
|
32
|
+
self.date_prop_name = kwargs.get("date_prop_name") or ""
|
|
33
|
+
self.value_prop_name = kwargs.get("value_prop_name") or ""
|
|
34
|
+
self.data_source_filter = kwargs.get("data_source_filter") or ""
|
|
35
|
+
deprecated_database_filter = kwargs.get("database_filter") or ""
|
|
36
|
+
if deprecated_database_filter and not self.data_source_filter:
|
|
37
|
+
warnings.warn(
|
|
38
|
+
"--database_filter is deprecated; use --data_source_filter instead.",
|
|
39
|
+
DeprecationWarning,
|
|
40
|
+
stacklevel=2,
|
|
41
|
+
)
|
|
42
|
+
self.data_source_filter = deprecated_database_filter
|
|
43
|
+
self.tooltip_prop_name = (kwargs.get("tooltip_prop_name") or "").strip()
|
|
26
44
|
self.tooltip_by_date_dict = defaultdict(list)
|
|
27
45
|
|
|
28
46
|
@classmethod
|
|
@@ -33,11 +51,17 @@ class NotionLoader(BaseLoader):
|
|
|
33
51
|
type=str,
|
|
34
52
|
help="The Notion internal integration token.",
|
|
35
53
|
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--data_source_id",
|
|
56
|
+
dest="data_source_id",
|
|
57
|
+
type=str,
|
|
58
|
+
help="The Notion data source id.",
|
|
59
|
+
)
|
|
36
60
|
parser.add_argument(
|
|
37
61
|
"--database_id",
|
|
38
62
|
dest="database_id",
|
|
39
63
|
type=str,
|
|
40
|
-
help=
|
|
64
|
+
help=argparse.SUPPRESS,
|
|
41
65
|
)
|
|
42
66
|
parser.add_argument(
|
|
43
67
|
"--date_prop_name",
|
|
@@ -45,7 +69,7 @@ class NotionLoader(BaseLoader):
|
|
|
45
69
|
type=str,
|
|
46
70
|
default="Datetime",
|
|
47
71
|
required=optional,
|
|
48
|
-
help="The
|
|
72
|
+
help="The Notion data source property name that stores the date.",
|
|
49
73
|
)
|
|
50
74
|
parser.add_argument(
|
|
51
75
|
"--value_prop_name",
|
|
@@ -53,15 +77,23 @@ class NotionLoader(BaseLoader):
|
|
|
53
77
|
type=str,
|
|
54
78
|
default="Datetime",
|
|
55
79
|
required=optional,
|
|
56
|
-
help="The
|
|
57
|
-
)
|
|
80
|
+
help="The Notion data source property name used as the heatmap value.",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--data_source_filter",
|
|
84
|
+
dest="data_source_filter",
|
|
85
|
+
type=str,
|
|
86
|
+
default="",
|
|
87
|
+
required=False,
|
|
88
|
+
help="Optional Notion data source query filter in JSON format.",
|
|
89
|
+
)
|
|
58
90
|
parser.add_argument(
|
|
59
91
|
"--database_filter",
|
|
60
92
|
dest="database_filter",
|
|
61
93
|
type=str,
|
|
62
94
|
default="",
|
|
63
95
|
required=False,
|
|
64
|
-
help=
|
|
96
|
+
help=argparse.SUPPRESS,
|
|
65
97
|
)
|
|
66
98
|
parser.add_argument(
|
|
67
99
|
"--tooltip_prop_name",
|
|
@@ -90,27 +122,28 @@ class NotionLoader(BaseLoader):
|
|
|
90
122
|
]
|
|
91
123
|
},
|
|
92
124
|
}
|
|
93
|
-
if self.
|
|
94
|
-
payload["filter"]["and"].append(json.loads(self.
|
|
125
|
+
if self.data_source_filter:
|
|
126
|
+
payload["filter"]["and"].append(json.loads(self.data_source_filter))
|
|
95
127
|
if start_cursor:
|
|
96
128
|
payload.update({"start_cursor": start_cursor})
|
|
97
|
-
|
|
129
|
+
print(payload)
|
|
98
130
|
headers = {
|
|
99
131
|
"Accept": "application/json",
|
|
100
132
|
"Notion-Version": NOTION_API_VERSION,
|
|
101
133
|
"Content-Type": "application/json",
|
|
102
134
|
"Authorization": "Bearer " + self.notion_token,
|
|
103
135
|
}
|
|
104
|
-
|
|
136
|
+
print(headers)
|
|
105
137
|
try:
|
|
106
138
|
resp = requests.post(
|
|
107
|
-
NOTION_API_URL.format(
|
|
139
|
+
NOTION_API_URL.format(data_source_id=self.data_source_id),
|
|
108
140
|
json=payload,
|
|
109
141
|
headers=headers,
|
|
110
142
|
)
|
|
111
143
|
except requests.RequestException:
|
|
144
|
+
print("Failed to connect to Notion API.")
|
|
112
145
|
return data_list
|
|
113
|
-
|
|
146
|
+
print(getattr(resp, "text", ""))
|
|
114
147
|
if not resp.ok:
|
|
115
148
|
# Treat non-OK responses as an empty result set so we can still draw
|
|
116
149
|
# a heatmap even when the Notion API yields no rows for the period.
|
|
@@ -119,6 +152,7 @@ class NotionLoader(BaseLoader):
|
|
|
119
152
|
data = resp.json()
|
|
120
153
|
except ValueError:
|
|
121
154
|
return data_list
|
|
155
|
+
print(len(data.get("results", [])))
|
|
122
156
|
results = data["results"]
|
|
123
157
|
next_cursor = data["next_cursor"]
|
|
124
158
|
data_list.extend(results)
|
|
@@ -209,7 +243,9 @@ class NotionLoader(BaseLoader):
|
|
|
209
243
|
[p.get("name") or p.get("id", "") for p in item.get("people", [])]
|
|
210
244
|
).strip()
|
|
211
245
|
if item_type == "relation":
|
|
212
|
-
return ", ".join(
|
|
246
|
+
return ", ".join(
|
|
247
|
+
[r.get("id", "") for r in item.get("relation", [])]
|
|
248
|
+
).strip()
|
|
213
249
|
if item_type == "date":
|
|
214
250
|
return cls._format_date(item.get("date", {}))
|
|
215
251
|
if item_type in ("url", "email", "phone_number"):
|
|
@@ -272,7 +308,9 @@ class NotionLoader(BaseLoader):
|
|
|
272
308
|
if prop_type == "date":
|
|
273
309
|
return cls._format_date(prop.get("date", {}))
|
|
274
310
|
if prop_type == "relation":
|
|
275
|
-
return ", ".join(
|
|
311
|
+
return ", ".join(
|
|
312
|
+
[r.get("id", "") for r in prop.get("relation", [])]
|
|
313
|
+
).strip()
|
|
276
314
|
if prop_type == "files":
|
|
277
315
|
texts = []
|
|
278
316
|
for file_obj in prop.get("files", []):
|
|
@@ -308,7 +346,9 @@ class NotionLoader(BaseLoader):
|
|
|
308
346
|
if rollup_type == "date":
|
|
309
347
|
return cls._format_date(rollup.get("date", {}))
|
|
310
348
|
if rollup_type == "array":
|
|
311
|
-
texts = [
|
|
349
|
+
texts = [
|
|
350
|
+
cls._extract_rollup_item(item) for item in rollup.get("array", [])
|
|
351
|
+
]
|
|
312
352
|
texts = [text for text in texts if text]
|
|
313
353
|
return ", ".join(texts)
|
|
314
354
|
return ""
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
2
4
|
import pendulum
|
|
3
5
|
import requests
|
|
4
|
-
import os
|
|
5
6
|
|
|
6
7
|
from github_heatmap.loader.base_loader import BaseLoader
|
|
7
8
|
from github_heatmap.loader.config import WEREAD_BASE_URL, WEREAD_HISTORY_URL
|
|
8
9
|
|
|
9
10
|
headers = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
"User-Agent": "WeRead/8.2.5 WRBrand/xiaomi Dalvik/2.1.0 (Linux; U; Android 12; Redmi Note 7 Pro Build/SQ3A.220705.004)",
|
|
12
|
+
"Connection": "Keep-Alive",
|
|
13
|
+
"Accept-Encoding": "gzip",
|
|
14
|
+
"baseapi": "32",
|
|
15
|
+
"appver": "8.2.5.10163885",
|
|
16
|
+
"osver": "12",
|
|
17
|
+
"channelId": "11",
|
|
18
|
+
"basever": "8.2.5.10163885",
|
|
19
|
+
"Content-Type": "application/json; charset=UTF-8",
|
|
19
20
|
}
|
|
21
|
+
|
|
22
|
+
|
|
20
23
|
class WereadLoader(BaseLoader):
|
|
21
24
|
track_color = "#2EA8F7"
|
|
22
25
|
unit = "mins"
|
|
@@ -45,7 +48,11 @@ class WereadLoader(BaseLoader):
|
|
|
45
48
|
# )
|
|
46
49
|
|
|
47
50
|
def refresh_token(self):
|
|
48
|
-
body = {
|
|
51
|
+
body = {
|
|
52
|
+
"deviceId": self.device_id,
|
|
53
|
+
"refreshToken": self.token,
|
|
54
|
+
"activationCode": self.activation_code,
|
|
55
|
+
}
|
|
49
56
|
r = self.session.post(
|
|
50
57
|
"https://api.notionhub.app/refresh-weread-token", json=body
|
|
51
58
|
)
|
|
@@ -54,13 +61,14 @@ class WereadLoader(BaseLoader):
|
|
|
54
61
|
vid = response_data.get("vid")
|
|
55
62
|
accessToken = response_data.get("accessToken")
|
|
56
63
|
if vid and accessToken:
|
|
57
|
-
self.session.headers.update(
|
|
64
|
+
self.session.headers.update(
|
|
65
|
+
{"vid": str(vid), "accessToken": accessToken}
|
|
66
|
+
)
|
|
58
67
|
else:
|
|
59
68
|
print("Failed to refresh token")
|
|
60
69
|
else:
|
|
61
70
|
print("Failed to refresh token")
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
|
|
64
72
|
def get_api_data(self):
|
|
65
73
|
r = self.session.get(WEREAD_HISTORY_URL)
|
|
66
74
|
if not r.ok:
|
|
@@ -75,7 +83,7 @@ class WereadLoader(BaseLoader):
|
|
|
75
83
|
|
|
76
84
|
def make_track_dict(self):
|
|
77
85
|
api_data = self.get_api_data()
|
|
78
|
-
if
|
|
86
|
+
if "readTimes" in api_data:
|
|
79
87
|
readTimes = dict(sorted(api_data["readTimes"].items(), reverse=True))
|
|
80
88
|
for k, v in readTimes.items():
|
|
81
89
|
k = pendulum.from_timestamp(int(k), tz=self.time_zone)
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
"""Create a poster from track data."""
|
|
2
|
+
|
|
2
3
|
from collections import defaultdict
|
|
3
4
|
|
|
4
5
|
import svgwrite
|
|
5
6
|
|
|
7
|
+
from github_heatmap.config import HEAD_FONT_SIZE, MARGIN_LEFT, MARGIN_TOP
|
|
6
8
|
from github_heatmap.structures import XY, ValueRange
|
|
7
|
-
|
|
8
|
-
HEAD_FONT_SIZE,
|
|
9
|
-
MARGIN_LEFT,
|
|
10
|
-
MARGIN_TOP
|
|
11
|
-
)
|
|
9
|
+
|
|
12
10
|
|
|
13
11
|
class Poster:
|
|
14
12
|
def __init__(self):
|
|
@@ -28,7 +26,7 @@ class Poster:
|
|
|
28
26
|
self.width = 400
|
|
29
27
|
self.height = 300
|
|
30
28
|
self.years = None
|
|
31
|
-
self.offset = XY(MARGIN_LEFT,MARGIN_TOP)
|
|
29
|
+
self.offset = XY(MARGIN_LEFT, MARGIN_TOP)
|
|
32
30
|
# maybe support more type
|
|
33
31
|
self.tracks_drawer = None
|
|
34
32
|
self.trans = None
|
|
@@ -36,6 +34,11 @@ class Poster:
|
|
|
36
34
|
self.animation_time = 10
|
|
37
35
|
self.year_tracks_date_count_dict = defaultdict(int)
|
|
38
36
|
self.year_tracks_type_dict = defaultdict(dict)
|
|
37
|
+
self.level_colors = []
|
|
38
|
+
self.level_thresholds = ()
|
|
39
|
+
self.level_thresholds_by_type = {}
|
|
40
|
+
self.use_github_level_mapping = True
|
|
41
|
+
self.use_raw_level = False
|
|
39
42
|
|
|
40
43
|
# for year summary
|
|
41
44
|
self.is_summary = False
|
|
@@ -103,10 +106,17 @@ class Poster:
|
|
|
103
106
|
def __draw_header(self, d):
|
|
104
107
|
self.offset.y += HEAD_FONT_SIZE
|
|
105
108
|
text_color = self.colors["text"]
|
|
106
|
-
title_style =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
title_style = (
|
|
110
|
+
f"font-size:{HEAD_FONT_SIZE}px; font-family:Arial; font-weight:bold;"
|
|
111
|
+
)
|
|
112
|
+
d.add(
|
|
113
|
+
d.text(
|
|
114
|
+
self.title,
|
|
115
|
+
insert=(self.offset.x, self.offset.y),
|
|
116
|
+
fill=text_color,
|
|
117
|
+
style=title_style,
|
|
118
|
+
)
|
|
119
|
+
)
|
|
110
120
|
|
|
111
121
|
def __draw_footer(self, d):
|
|
112
122
|
self.tracks_drawer.draw_footer(d)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import math
|
|
1
2
|
import re
|
|
2
3
|
from itertools import count as itercount
|
|
3
4
|
from itertools import takewhile
|
|
@@ -20,6 +21,21 @@ def interpolate_color(color1, color2, ratio):
|
|
|
20
21
|
return c3.hex_l
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def build_level_colors(track_color, special_color1=None, special_color2=None):
|
|
25
|
+
"""
|
|
26
|
+
Build four positive contribution shades for a five-level heatmap.
|
|
27
|
+
"""
|
|
28
|
+
level1 = interpolate_color(track_color, "#ffffff", 0.35)
|
|
29
|
+
if special_color1 and special_color2:
|
|
30
|
+
level2 = interpolate_color(track_color, special_color1, 0.35)
|
|
31
|
+
return [level1, level2, special_color1, special_color2]
|
|
32
|
+
|
|
33
|
+
level2 = track_color
|
|
34
|
+
level3 = interpolate_color(track_color, "#000000", 0.18)
|
|
35
|
+
level4 = interpolate_color(track_color, "#000000", 0.35)
|
|
36
|
+
return [level1, level2, level3, level4]
|
|
37
|
+
|
|
38
|
+
|
|
23
39
|
def parse_years(s):
|
|
24
40
|
"""Parse a plaintext range of years into a pair of years
|
|
25
41
|
|
|
@@ -59,6 +75,43 @@ def make_key_times(year_count):
|
|
|
59
75
|
return [str(round(i, 2)) for i in s]
|
|
60
76
|
|
|
61
77
|
|
|
78
|
+
def make_github_level_thresholds(number_list):
|
|
79
|
+
"""
|
|
80
|
+
GitHub exposes contribution levels as NONE plus four quartiles.
|
|
81
|
+
This uses the non-zero daily values and a nearest-rank percentile.
|
|
82
|
+
"""
|
|
83
|
+
positive_values = sorted(v for v in number_list if v > 0)
|
|
84
|
+
if not positive_values:
|
|
85
|
+
return ()
|
|
86
|
+
|
|
87
|
+
total = len(positive_values)
|
|
88
|
+
|
|
89
|
+
def nearest_rank(percentile):
|
|
90
|
+
index = max(0, math.ceil(total * percentile) - 1)
|
|
91
|
+
return positive_values[index]
|
|
92
|
+
|
|
93
|
+
return tuple(nearest_rank(p) for p in (0.25, 0.50, 0.75))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def resolve_github_level(value, thresholds, use_raw_level=False):
|
|
97
|
+
if value <= 0:
|
|
98
|
+
return 0
|
|
99
|
+
if use_raw_level:
|
|
100
|
+
# 直接使用传入的值作为级别(1-4),跳过四分位数计算
|
|
101
|
+
return max(1, min(4, int(value)))
|
|
102
|
+
if not thresholds:
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
q1, q2, q3 = thresholds
|
|
106
|
+
if value <= q1:
|
|
107
|
+
return 1
|
|
108
|
+
if value <= q2:
|
|
109
|
+
return 2
|
|
110
|
+
if value <= q3:
|
|
111
|
+
return 3
|
|
112
|
+
return 4
|
|
113
|
+
|
|
114
|
+
|
|
62
115
|
def reduce_year_list(year_list, tracks_dict):
|
|
63
116
|
"""
|
|
64
117
|
format year list
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: github_heatmap
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Make everything a GitHub svg poster and Skyline!
|
|
5
5
|
Home-page: https://github.com/malinkang/GitHubPoster
|
|
6
6
|
Author: malinkang
|
|
@@ -31,3 +31,11 @@ Requires-Dist: stravalib; extra == "all"
|
|
|
31
31
|
Requires-Dist: PyGithub; extra == "all"
|
|
32
32
|
Requires-Dist: sdf_fork; extra == "all"
|
|
33
33
|
Requires-Dist: pandas; extra == "all"
|
|
34
|
+
Dynamic: author
|
|
35
|
+
Dynamic: author-email
|
|
36
|
+
Dynamic: home-page
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: provides-extra
|
|
40
|
+
Dynamic: requires-dist
|
|
41
|
+
Dynamic: summary
|
|
@@ -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="
|
|
9
|
+
version="2.2.0",
|
|
10
10
|
description="Make everything a GitHub svg poster and Skyline!",
|
|
11
11
|
packages=find_packages(),
|
|
12
12
|
include_package_data=True,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import pytest
|
|
1
2
|
import requests
|
|
2
3
|
|
|
3
4
|
from github_heatmap.loader import notion_loader
|
|
@@ -10,7 +11,7 @@ def build_loader():
|
|
|
10
11
|
2024,
|
|
11
12
|
"notion",
|
|
12
13
|
notion_token="token",
|
|
13
|
-
|
|
14
|
+
data_source_id="ds",
|
|
14
15
|
date_prop_name="Date",
|
|
15
16
|
value_prop_name="Value",
|
|
16
17
|
tooltip_prop_name="Tooltip",
|
|
@@ -46,10 +47,7 @@ def test_notion_loader_make_track_dict_with_tooltips():
|
|
|
46
47
|
loader.make_track_dict()
|
|
47
48
|
|
|
48
49
|
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
|
-
)
|
|
50
|
+
assert loader.tooltip_by_date_dict["2024-01-01"] == "Study\nRead, Write"
|
|
53
51
|
|
|
54
52
|
|
|
55
53
|
def test_notion_loader_extract_property_text_variants():
|
|
@@ -133,3 +131,44 @@ def test_notion_loader_handles_request_exception(monkeypatch):
|
|
|
133
131
|
assert tracks
|
|
134
132
|
assert all(value == 0 for value in tracks.values())
|
|
135
133
|
assert loader.number_list
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_notion_loader_queries_data_source_endpoint(monkeypatch):
|
|
137
|
+
loader = build_loader()
|
|
138
|
+
captured = {}
|
|
139
|
+
|
|
140
|
+
class DummyResp:
|
|
141
|
+
ok = True
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def json():
|
|
145
|
+
return {"results": [], "next_cursor": None, "has_more": False}
|
|
146
|
+
|
|
147
|
+
def fake_post(url, json, headers):
|
|
148
|
+
captured["url"] = url
|
|
149
|
+
captured["json"] = json
|
|
150
|
+
captured["headers"] = headers
|
|
151
|
+
return DummyResp()
|
|
152
|
+
|
|
153
|
+
monkeypatch.setattr(notion_loader.requests, "post", fake_post)
|
|
154
|
+
|
|
155
|
+
loader.get_api_data()
|
|
156
|
+
|
|
157
|
+
assert captured["url"].endswith("/v1/data_sources/ds/query")
|
|
158
|
+
assert captured["headers"]["Notion-Version"] == "2026-03-11"
|
|
159
|
+
assert captured["json"]["filter"]["and"][0]["property"] == "Date"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_notion_loader_accepts_deprecated_database_id():
|
|
163
|
+
with pytest.warns(DeprecationWarning, match="--database_id is deprecated"):
|
|
164
|
+
loader = NotionLoader(
|
|
165
|
+
2024,
|
|
166
|
+
2024,
|
|
167
|
+
"notion",
|
|
168
|
+
notion_token="token",
|
|
169
|
+
database_id="legacy-db",
|
|
170
|
+
date_prop_name="Date",
|
|
171
|
+
value_prop_name="Value",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert loader.data_source_id == "legacy-db"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from github_heatmap.config import GITHUB_LEVEL_COLORS
|
|
2
|
+
from github_heatmap.drawer import Drawer
|
|
3
|
+
from github_heatmap.poster import Poster
|
|
4
|
+
from github_heatmap.utils import (
|
|
5
|
+
build_level_colors,
|
|
6
|
+
interpolate_color,
|
|
7
|
+
make_github_level_thresholds,
|
|
8
|
+
make_key_times,
|
|
9
|
+
parse_years,
|
|
10
|
+
resolve_github_level,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_interpolate_color():
|
|
15
|
+
assert interpolate_color("#000000", "#ffffff", 0) == "#000000"
|
|
16
|
+
assert interpolate_color("#000000", "#ffffff", 1) == "#ffffff"
|
|
17
|
+
assert interpolate_color("#000000", "#ffffff", 0.5) == "#7f7f7f"
|
|
18
|
+
assert interpolate_color("#000000", "#ffffff", -100) == "#000000"
|
|
19
|
+
assert interpolate_color("#000000", "#ffffff", 12345) == "#ffffff"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_parse_years():
|
|
23
|
+
assert parse_years("2012") == (2012, 2012)
|
|
24
|
+
assert parse_years("2015-2021") == (2015, 2021)
|
|
25
|
+
assert parse_years("2021-2015") == (2015, 2021)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_make_key_times():
|
|
29
|
+
assert make_key_times(5) == ["0", "0.2", "0.4", "0.6", "0.8", "1"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_make_github_level_thresholds():
|
|
33
|
+
assert make_github_level_thresholds([]) == ()
|
|
34
|
+
assert make_github_level_thresholds([0, 1, 1, 2, 3, 5, 8, 13]) == (1, 3, 8)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_resolve_github_level():
|
|
38
|
+
thresholds = (2, 5, 9)
|
|
39
|
+
|
|
40
|
+
assert resolve_github_level(0, thresholds) == 0
|
|
41
|
+
assert resolve_github_level(2, thresholds) == 1
|
|
42
|
+
assert resolve_github_level(5, thresholds) == 2
|
|
43
|
+
assert resolve_github_level(9, thresholds) == 3
|
|
44
|
+
assert resolve_github_level(10, thresholds) == 4
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_build_level_colors_uses_exact_palette_when_provided():
|
|
48
|
+
assert list(GITHUB_LEVEL_COLORS) == [
|
|
49
|
+
"#9BE9A8",
|
|
50
|
+
"#40C463",
|
|
51
|
+
"#30A14E",
|
|
52
|
+
"#216E39",
|
|
53
|
+
]
|
|
54
|
+
assert len(build_level_colors("#40C463")) == 4
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_drawer_tooltip_formatting():
|
|
58
|
+
poster = Poster()
|
|
59
|
+
poster.units = "XP"
|
|
60
|
+
drawer = Drawer(poster)
|
|
61
|
+
|
|
62
|
+
assert drawer._format_tooltip("2024-01-01", 10) == "2024-01-01 10 XP"
|
|
63
|
+
assert drawer._format_tooltip("2024-01-01") == "2024-01-01"
|
|
64
|
+
|
|
65
|
+
poster.tooltip_template = "{date}: {value}{unit}"
|
|
66
|
+
assert drawer._format_tooltip("2024-01-01", 5) == "2024-01-01: 5XP"
|
|
67
|
+
assert drawer._format_tooltip("2024-01-01") == "2024-01-01"
|
|
68
|
+
|
|
69
|
+
poster.tooltip_template = "{date} {value} for {type}"
|
|
70
|
+
assert drawer._format_tooltip("2024-01-01", 7, "run") == "2024-01-01 7 for run"
|
|
71
|
+
|
|
72
|
+
poster.tooltip_by_date = {"2024-01-01": "Custom"}
|
|
73
|
+
assert drawer._format_tooltip("2024-01-01", 5) == "Custom"
|
|
74
|
+
|
|
75
|
+
poster.tooltip_by_date = {"2024-01-02": {"run": "Run note"}}
|
|
76
|
+
assert drawer._format_tooltip("2024-01-02", 3, "run") == "Run note"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_drawer_make_color_uses_github_levels():
|
|
80
|
+
poster = Poster()
|
|
81
|
+
poster.use_github_level_mapping = True
|
|
82
|
+
poster.level_colors = list(GITHUB_LEVEL_COLORS)
|
|
83
|
+
poster.level_thresholds = (2, 5, 9)
|
|
84
|
+
poster.colors["dom"] = "#ebedf0"
|
|
85
|
+
drawer = Drawer(poster)
|
|
86
|
+
|
|
87
|
+
assert drawer.make_color(poster.length_range_by_date, 0) == "#ebedf0"
|
|
88
|
+
assert drawer.make_color(poster.length_range_by_date, 2) == "#9BE9A8"
|
|
89
|
+
assert drawer.make_color(poster.length_range_by_date, 5) == "#40C463"
|
|
90
|
+
assert drawer.make_color(poster.length_range_by_date, 9) == "#30A14E"
|
|
91
|
+
assert drawer.make_color(poster.length_range_by_date, 12) == "#216E39"
|
|
@@ -1,46 +0,0 @@
|
|
|
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"
|
|
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.4.0 → github_heatmap-2.2.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
|