1
+ """
2
+ Script to update a GitHub Gist with stats for a Code::Stats user.
3
+
4
+ Uses code stats api to fetch stats.
5
+ Supports various stats types, all resulting in different content for the gist.
6
+ Meant for Gists to be pinned on GitHub profile.
7
+
8
+ Test with
9
+ python codestats_box.py test <codestats-user> <stats-type>
10
+ to only print content. To also test gist update, use:
11
+ python codestats_box.py test <codestats-user> <stats-type> <gist-id> <github-token>
12
+ """
13
+
1
14
import datetime
2
15
import math
3
16
import os
10
23
from github import Github
11
24
from github .InputFileContent import InputFileContent
12
25
13
- TitleAndValue = namedtuple ("TitleAndValue " , "title value" )
26
+ LabelAndValue = namedtuple ("LabelAndValue " , "title value" )
14
27
28
+ # Type of stats
15
29
STATS_TYPE_LEVEL = "level-xp"
16
30
STATS_TYPE_RECENT_XP = "recent-xp"
17
31
STATS_TYPE_XP = "xp"
21
35
STATS_TYPE_LEVEL ,
22
36
]
23
37
DEFAULT_STATS_TYPE = STATS_TYPE_LEVEL
24
-
25
- TOP_LANGUAGES_COUNT = 10
26
- MAX_LINE_LENGTH = 54
27
- WIDTH_JUSTIFICATION_SEPARATOR = ":"
28
- RECENT_STATS_SEPARATOR = " + "
29
- TOTAL_XP_TITLE = "Total XP"
38
+ # Dicts for stats type dependent values
30
39
VALUE_FORMAT = {
31
40
STATS_TYPE_LEVEL : "lvl {level:>3} ({xp:>9,} XP)" ,
32
- STATS_TYPE_RECENT_XP : "lvl {level:>3} ({xp:>9,} XP) (+{recent_xp:>5 ,})" ,
41
+ STATS_TYPE_RECENT_XP : "lvl {level:>3} ({xp:>9,} XP) (+{recent_xp:>6 ,})" ,
33
42
STATS_TYPE_XP : "{xp:>9,} XP" ,
34
43
}
35
44
GIST_TITLE = {
36
45
STATS_TYPE_LEVEL : "💻 My Code::Stats XP (Top Languages)" ,
37
46
STATS_TYPE_RECENT_XP : "💻 My Code::Stats XP (Recent Languages)" ,
38
47
STATS_TYPE_XP : "💻 My Code::Stats XP (Top Languages)" ,
39
48
}
49
+ # Other configurable values
50
+ TOP_LANGUAGES_COUNT = 10
51
+ WIDTH_JUSTIFICATION_SEPARATOR = ":"
52
+ RECENT_STATS_SEPARATOR = " + "
53
+ TOTAL_XP_TITLE = "Total XP"
40
54
NO_RECENT_XP_LINES = [
41
- TitleAndValue ("Not been coding recently" , "🙈" ),
42
- TitleAndValue ("Probably busy with something else" , "🗓" ),
43
- TitleAndValue ("Or just taking a break" , "🌴" ),
44
- TitleAndValue ("But would be back to it soon!" , "🤓" ),
55
+ LabelAndValue ("Not been coding recently" , "🙈" ),
56
+ LabelAndValue ("Probably busy with something else" , "🗓" ),
57
+ LabelAndValue ("Or just taking a break" , "🌴" ),
58
+ LabelAndValue ("But would be back to it soon!" , "🤓" ),
45
59
]
46
-
60
+ # Internal constants
61
+ MAX_LINE_LENGTH = 54
47
62
ENV_VAR_GIST_ID = "GIST_ID"
48
63
ENV_VAR_GITHUB_TOKEN = "GH_TOKEN"
49
64
ENV_VAR_CODE_STATS_USERNAME = "CODE_STATS_USERNAME"
53
68
ENV_VAR_GITHUB_TOKEN ,
54
69
ENV_VAR_CODE_STATS_USERNAME ,
55
70
]
56
-
71
+ # Code stats API constants
57
72
CODE_STATS_URL_FORMAT = "https://codestats.net/api/users/{user}"
58
73
CODE_STATS_DATE_KEY = "dates"
59
74
CODE_STATS_TOTAL_XP_KEY = "total_xp"
60
75
CODE_STATS_TOTAL_NEW_XP_KEY = "new_xp"
61
76
CODE_STATS_LANGUAGES_KEY = "languages"
62
77
CODE_STATS_LANGUAGES_XP_KEY = "xps"
63
78
CODE_STATS_LANGUAGES_NEW_XP_KEY = "new_xps"
64
-
65
79
XP_TO_LEVEL = lambda xp : math .floor (0.025 * math .sqrt (xp ))
66
80
67
81
68
82
def validate_and_init () -> bool :
83
+ """Check environment variables present and valid."""
69
84
env_vars_absent = [
70
85
env
71
86
for env in REQUIRED_ENVS
@@ -87,12 +102,14 @@ def validate_and_init() -> bool:
87
102
88
103
89
104
def get_code_stats_response (user : str ) -> Dict [str , Any ]:
105
+ """Get statistics from codestats for user."""
90
106
return requests .get (CODE_STATS_URL_FORMAT .format (user = user )).json ()
91
107
92
108
93
109
def __get_formatted_value (
94
110
xp : int , recent_xp_supplier : Callable [[], int ], stats_type : str
95
111
) -> str :
112
+ """Get formatted value with xp and/or recent xp depending on stats type."""
96
113
value_format = VALUE_FORMAT [stats_type ]
97
114
if stats_type == STATS_TYPE_LEVEL :
98
115
return value_format .format (level = XP_TO_LEVEL (xp ), xp = xp )
@@ -109,27 +126,43 @@ def __get_formatted_value(
109
126
110
127
def get_total_xp_line (
111
128
code_stats_response : Dict [str , Any ], stats_type : str
112
- ) -> TitleAndValue :
129
+ ) -> LabelAndValue :
130
+ """Get label and formatted value for total xp.
131
+
132
+ Something along the lines of ("Total XP", "lvl 26 (1,104,152 XP)")
133
+ """
113
134
total_xp = code_stats_response [CODE_STATS_TOTAL_XP_KEY ]
114
135
recent_total_xp_supplier = lambda : code_stats_response [CODE_STATS_TOTAL_NEW_XP_KEY ]
115
136
formatted_value = __get_formatted_value (
116
137
total_xp , recent_total_xp_supplier , stats_type
117
138
)
118
- return TitleAndValue (TOTAL_XP_TITLE , formatted_value )
139
+ return LabelAndValue (TOTAL_XP_TITLE , formatted_value )
119
140
120
141
121
142
def __get_language_xp_line (
122
143
language : str , language_stats : Dict [str , int ], stats_type : str
123
- ) -> TitleAndValue :
144
+ ) -> LabelAndValue :
145
+ """Get label and formatted value for language xp.
146
+
147
+ Something along the lines of ("Java", "lvl 19 ( 580,523 XP)")
148
+ """
124
149
xp = language_stats [CODE_STATS_LANGUAGES_XP_KEY ]
125
150
recent_xp_supplier = lambda : language_stats [CODE_STATS_LANGUAGES_NEW_XP_KEY ]
126
151
formatted_value = __get_formatted_value (xp , recent_xp_supplier , stats_type )
127
- return TitleAndValue (language , formatted_value )
152
+ return LabelAndValue (language , formatted_value )
128
153
129
154
130
155
def get_language_xp_lines (
131
156
code_stats_response : Dict [str , Any ], stats_type : str
132
- ) -> List [TitleAndValue ]:
157
+ ) -> List [LabelAndValue ]:
158
+ """Get list of labels and formatted values for languages.
159
+
160
+ Decide which languages to include and return something like:
161
+ [
162
+ ("Java", "lvl 19 ( 580,523 XP)"),
163
+ ("Python", "lvl 7 ( 82,719 XP)"),
164
+ ]
165
+ """
133
166
if stats_type == STATS_TYPE_RECENT_XP :
134
167
# Only considering languages with recent xp
135
168
top_languages = list (
@@ -156,7 +189,11 @@ def get_language_xp_lines(
156
189
]
157
190
158
191
159
- def get_adjusted_line (title_and_value : TitleAndValue ) -> str :
192
+ def get_adjusted_line (title_and_value : LabelAndValue ) -> str :
193
+ """Format given label and value to single string separated by configured separator.
194
+
195
+ Something like (label, value) -> "label ::::::::::::: value"
196
+ """
160
197
separation = MAX_LINE_LENGTH - (
161
198
len (title_and_value .title ) + len (title_and_value .value ) + 2
162
199
)
@@ -165,6 +202,11 @@ def get_adjusted_line(title_and_value: TitleAndValue) -> str:
165
202
166
203
167
204
def update_gist (title : str , content : str ) -> bool :
205
+ """Update gist with provided title and content.
206
+
207
+ Use gist id and github token present in environment variables.
208
+ Replace first file in the gist.
209
+ """
168
210
access_token = os .environ [ENV_VAR_GITHUB_TOKEN ]
169
211
gist_id = os .environ [ENV_VAR_GIST_ID ]
170
212
gist = Github (access_token ).get_gist (gist_id )
@@ -174,7 +216,11 @@ def update_gist(title: str, content: str) -> bool:
174
216
print (f"{ title } \n { content } " )
175
217
176
218
177
- def get_content () -> str :
219
+ def get_stats () -> str :
220
+ """Get stats for codestats user according to stats type...
221
+
222
+ ...both extracted from environment variables.
223
+ """
178
224
code_stats_user_name = os .environ [ENV_VAR_CODE_STATS_USERNAME ]
179
225
code_stats_response = get_code_stats_response (code_stats_user_name )
180
226
@@ -190,36 +236,36 @@ def get_content() -> str:
190
236
191
237
192
238
def main ():
193
-
239
+ """Validate prerequisites, get content and update gist."""
194
240
if not validate_and_init ():
195
241
raise RuntimeError (
196
242
"Validations failed! See the messages above for more information"
197
243
)
198
244
199
245
stats_type = os .environ [ENV_VAR_STATS_TYPE ]
200
246
title = GIST_TITLE [stats_type ]
201
- content = get_content ()
247
+ content = get_stats ()
202
248
update_gist (title , content )
203
249
204
250
205
251
if __name__ == "__main__" :
206
252
import time
207
253
208
254
s = time .perf_counter ()
209
- # test with
210
- # python codestats_box.py test <codestats-user> <stats-type>
211
- # to only print content. To also test gist update, use:
212
- # python codestats_box.py test <codestats-user> <stats-type> <gist-id> <github-token>
213
255
if len (sys .argv ) > 1 :
256
+ # Test run
214
257
os .environ [ENV_VAR_CODE_STATS_USERNAME ] = sys .argv [2 ]
215
258
os .environ [ENV_VAR_STATS_TYPE ] = sys .argv [3 ]
216
259
if len (sys .argv ) > 4 :
260
+ # Testing gist update too
217
261
os .environ [ENV_VAR_GIST_ID ] = sys .argv [4 ]
218
262
os .environ [ENV_VAR_GITHUB_TOKEN ] = sys .argv [5 ]
219
263
main ()
220
264
else :
221
- print (get_content ())
265
+ # Testing stats content only
266
+ print (get_stats ())
222
267
else :
268
+ # Normal run
223
269
main ()
224
270
elapsed = time .perf_counter () - s
225
271
print (f"{ __file__ } executed in { elapsed :0.2f} seconds." )
0 commit comments