1- from collections import namedtuple
21import datetime
32import math
43import os
54import sys
6- from typing import Any , Dict , List
7- from github .InputFileContent import InputFileContent
5+ from collections import namedtuple
6+ from itertools import takewhile
7+ from typing import Any , Callable , Dict , List
88
99import requests
1010from github import Github
11+ from github .InputFileContent import InputFileContent
12+
13+ TitleAndValue = namedtuple ("TitleAndValue" , "title value" )
14+
15+ STATS_TYPE_LEVEL = "level-xp"
16+ STATS_TYPE_RECENT_XP = "recent-xp"
17+ STATS_TYPE_XP = "xp"
18+ ALLOWED_STATS_TYPES = [
19+ STATS_TYPE_RECENT_XP ,
20+ STATS_TYPE_XP ,
21+ STATS_TYPE_LEVEL ,
22+ ]
23+ DEFAULT_STATS_TYPE = STATS_TYPE_LEVEL
1124
1225TOP_LANGUAGES_COUNT = 10
26+ MAX_LINE_LENGTH = 54
1327WIDTH_JUSTIFICATION_SEPARATOR = ":"
1428RECENT_STATS_SEPARATOR = " + "
1529TOTAL_XP_TITLE = "Total XP"
16- PAST_WEEK_SUFFIX_STRING = " (past week)"
17- NEW_XP_SUFFIX_STRING = " (new xp)"
18- LEVEL_STRING_FORMAT = "lvl {level:>3} [{xp:>9,} XP]"
19- GIST_TITLE = "💻 My Code::Stats XP"
20- MAX_LINE_LENGTH = 54
30+ VALUE_FORMAT = {
31+ STATS_TYPE_LEVEL : "lvl {level:>3} ({xp:>9,} XP)" ,
32+ STATS_TYPE_RECENT_XP : "lvl {level:>3} ({xp:>9,} XP) (+{recent_xp:>5,})" ,
33+ STATS_TYPE_XP : "{xp:>9,} XP" ,
34+ }
35+ GIST_TITLE = {
36+ STATS_TYPE_LEVEL : "💻 My Code::Stats XP (Top Languages)" ,
37+ STATS_TYPE_RECENT_XP : "💻 My Code::Stats XP (Recent Languages)" ,
38+ STATS_TYPE_XP : "💻 My Code::Stats XP (Top Languages)" ,
39+ }
40+ 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!" , "🤓" ),
45+ ]
2146
2247ENV_VAR_GIST_ID = "GIST_ID"
2348ENV_VAR_GITHUB_TOKEN = "GH_TOKEN"
2954 ENV_VAR_CODE_STATS_USERNAME ,
3055]
3156
32- STATS_TYPE_RECENT_XP = "recent-xp"
33- STATS_TYPE_XP = "xp"
34- STATS_TYPE_LEVEL = "level-xp"
35- ALLOWED_STATS_TYPES = [
36- STATS_TYPE_RECENT_XP ,
37- STATS_TYPE_XP ,
38- STATS_TYPE_LEVEL ,
39- ]
40- DEFAULT_STATS_TYPE = STATS_TYPE_LEVEL
41-
4257CODE_STATS_URL_FORMAT = "https://codestats.net/api/users/{user}"
4358CODE_STATS_DATE_KEY = "dates"
4459CODE_STATS_TOTAL_XP_KEY = "total_xp"
60+ CODE_STATS_TOTAL_NEW_XP_KEY = "new_xp"
4561CODE_STATS_LANGUAGES_KEY = "languages"
4662CODE_STATS_LANGUAGES_XP_KEY = "xps"
4763CODE_STATS_LANGUAGES_NEW_XP_KEY = "new_xps"
4864
4965XP_TO_LEVEL = lambda xp : math .floor (0.025 * math .sqrt (xp ))
5066
51- TitleAndValue = namedtuple ("TitleAndValue" , "title value" )
52-
5367
5468def validate_and_init () -> bool :
5569 env_vars_absent = [
@@ -72,86 +86,89 @@ def validate_and_init() -> bool:
7286 return True
7387
7488
75- def get_adjusted_line (title_and_value : TitleAndValue ) -> str :
76- separation = MAX_LINE_LENGTH - (
77- len (title_and_value .title ) + len (title_and_value .value ) + 2
78- )
79- separator = f" { WIDTH_JUSTIFICATION_SEPARATOR * separation } "
80- return title_and_value .title + separator + title_and_value .value
81-
82-
8389def get_code_stats_response (user : str ) -> Dict [str , Any ]:
8490 return requests .get (CODE_STATS_URL_FORMAT .format (user = user )).json ()
8591
8692
93+ def __get_formatted_value (
94+ xp : int , recent_xp_supplier : Callable [[], int ], stats_type : str
95+ ) -> str :
96+ value_format = VALUE_FORMAT [stats_type ]
97+ if stats_type == STATS_TYPE_LEVEL :
98+ return value_format .format (level = XP_TO_LEVEL (xp ), xp = xp )
99+ elif stats_type == STATS_TYPE_RECENT_XP :
100+ recent_xp = recent_xp_supplier ()
101+ formatted_value = value_format .format (
102+ level = XP_TO_LEVEL (xp ), xp = xp , recent_xp = recent_xp
103+ )
104+ return formatted_value if recent_xp > 0 else formatted_value [:- 9 ]
105+ elif stats_type == STATS_TYPE_XP :
106+ return value_format .format (xp = xp )
107+ raise RuntimeError (f"Unknown stats type { stats_type } " )
108+
109+
87110def get_total_xp_line (
88111 code_stats_response : Dict [str , Any ], stats_type : str
89112) -> TitleAndValue :
90- last_seven_days = [
91- str (datetime .date .today () - datetime .timedelta (days = i )) for i in range (7 )
92- ]
93- last_seven_days_xp = sum (
94- [
95- code_stats_response [CODE_STATS_DATE_KEY ][day ]
96- for day in last_seven_days
97- if day in code_stats_response [CODE_STATS_DATE_KEY ]
98- ]
99- )
100113 total_xp = code_stats_response [CODE_STATS_TOTAL_XP_KEY ]
101- total_xp_value = ""
102- if stats_type == STATS_TYPE_RECENT_XP :
103- total_xp_value = f"{ total_xp - last_seven_days_xp :,} " + (
104- f"{ RECENT_STATS_SEPARATOR } { last_seven_days_xp :,} { PAST_WEEK_SUFFIX_STRING } "
105- if last_seven_days_xp > 0
106- else ""
107- )
108- elif stats_type == STATS_TYPE_XP :
109- total_xp_value = f"{ total_xp :,} "
110- elif stats_type == STATS_TYPE_LEVEL :
111- total_xp_value = LEVEL_STRING_FORMAT .format (
112- level = XP_TO_LEVEL (total_xp ), xp = total_xp
113- )
114- return TitleAndValue (TOTAL_XP_TITLE , total_xp_value )
114+ recent_total_xp_supplier = lambda : code_stats_response [CODE_STATS_TOTAL_NEW_XP_KEY ]
115+ formatted_value = __get_formatted_value (
116+ total_xp , recent_total_xp_supplier , stats_type
117+ )
118+ return TitleAndValue (TOTAL_XP_TITLE , formatted_value )
115119
116120
117121def __get_language_xp_line (
118122 language : str , language_stats : Dict [str , int ], stats_type : str
119123) -> TitleAndValue :
120124 xp = language_stats [CODE_STATS_LANGUAGES_XP_KEY ]
121- recent_xp = language_stats [CODE_STATS_LANGUAGES_NEW_XP_KEY ]
122- language_xp_value = ""
123- if stats_type == STATS_TYPE_RECENT_XP :
124- language_xp_value = f"{ xp - recent_xp :,} " + (
125- f"{ RECENT_STATS_SEPARATOR } { recent_xp :,} { NEW_XP_SUFFIX_STRING } "
126- if recent_xp > 0
127- else ""
128- )
129- elif stats_type == STATS_TYPE_XP :
130- language_xp_value = f"{ xp :,} "
131- elif stats_type == STATS_TYPE_LEVEL :
132- language_xp_value = LEVEL_STRING_FORMAT .format (level = XP_TO_LEVEL (xp ), xp = xp )
133- return TitleAndValue (language , language_xp_value )
125+ recent_xp_supplier = lambda : language_stats [CODE_STATS_LANGUAGES_NEW_XP_KEY ]
126+ formatted_value = __get_formatted_value (xp , recent_xp_supplier , stats_type )
127+ return TitleAndValue (language , formatted_value )
134128
135129
136130def get_language_xp_lines (
137131 code_stats_response : Dict [str , Any ], stats_type : str
138132) -> List [TitleAndValue ]:
139- top_languages = sorted (
140- code_stats_response [CODE_STATS_LANGUAGES_KEY ].items (),
141- key = lambda t : t [1 ][CODE_STATS_LANGUAGES_XP_KEY ],
142- reverse = True ,
143- )[:TOP_LANGUAGES_COUNT ]
133+ if stats_type == STATS_TYPE_RECENT_XP :
134+ # Only considering languages with recent xp
135+ top_languages = list (
136+ takewhile (
137+ lambda t : t [1 ][CODE_STATS_LANGUAGES_NEW_XP_KEY ] > 0 ,
138+ sorted (
139+ code_stats_response [CODE_STATS_LANGUAGES_KEY ].items (),
140+ key = lambda t : t [1 ][CODE_STATS_LANGUAGES_NEW_XP_KEY ],
141+ reverse = True ,
142+ )[:TOP_LANGUAGES_COUNT ],
143+ ),
144+ )
145+ if not top_languages :
146+ return NO_RECENT_XP_LINES
147+ else :
148+ top_languages = sorted (
149+ code_stats_response [CODE_STATS_LANGUAGES_KEY ].items (),
150+ key = lambda t : t [1 ][CODE_STATS_LANGUAGES_XP_KEY ],
151+ reverse = True ,
152+ )[:TOP_LANGUAGES_COUNT ]
144153 return [
145154 __get_language_xp_line (language , stats , stats_type )
146155 for language , stats in top_languages
147156 ]
148157
149158
159+ def get_adjusted_line (title_and_value : TitleAndValue ) -> str :
160+ separation = MAX_LINE_LENGTH - (
161+ len (title_and_value .title ) + len (title_and_value .value ) + 2
162+ )
163+ separator = f" { WIDTH_JUSTIFICATION_SEPARATOR * separation } "
164+ return title_and_value .title + separator + title_and_value .value
165+
166+
150167def update_gist (title : str , content : str ) -> bool :
151168 access_token = os .environ [ENV_VAR_GITHUB_TOKEN ]
152169 gist_id = os .environ [ENV_VAR_GIST_ID ]
153170 gist = Github (access_token ).get_gist (gist_id )
154- # Shouldn't necessarily work, keeping for case of single file made in hurry to get gist id.
171+ # Works only for single file. Should we clear all files and create new file?
155172 old_title = list (gist .files .keys ())[0 ]
156173 gist .edit (title , {old_title : InputFileContent (content , title )})
157174 print (f"{ title } \n { content } " )
@@ -179,8 +196,10 @@ def main():
179196 "Validations failed! See the messages above for more information"
180197 )
181198
199+ stats_type = os .environ [ENV_VAR_STATS_TYPE ]
200+ title = GIST_TITLE [stats_type ]
182201 content = get_content ()
183- update_gist (GIST_TITLE , content )
202+ update_gist (title , content )
184203
185204
186205if __name__ == "__main__" :
0 commit comments