33import asyncio
44import os
55import re
6+ import sys
7+ import time
68
79import aiohttp
810
@@ -22,6 +24,37 @@ def generate_output_folder() -> None:
2224 os .mkdir ("generated" )
2325
2426
27+ async def safe_get_stat (coroutine , description : str , max_retries : int = 3 , delay : int = 5 ):
28+ """
29+ Safely get a statistic with retry logic for 202 responses
30+
31+ :param coroutine: The async function/coroutine to execute
32+ :param description: Description for logging
33+ :param max_retries: Maximum number of retries
34+ :param delay: Delay between retries in seconds
35+ """
36+ for attempt in range (max_retries + 1 ):
37+ try :
38+ result = await coroutine
39+ print (f"✓ Successfully got { description } " )
40+ return result
41+ except Exception as e :
42+ if "202" in str (e ) or "Accepted" in str (e ):
43+ if attempt < max_retries :
44+ wait_time = delay * (2 ** attempt ) # Exponential backoff
45+ print (f"⏳ { description } returned 202 (processing). Waiting { wait_time } s... (attempt { attempt + 1 } /{ max_retries + 1 } )" )
46+ await asyncio .sleep (wait_time )
47+ continue
48+ else :
49+ print (f"❌ { description } failed after { max_retries + 1 } attempts: { e } " )
50+ raise
51+ else :
52+ print (f"❌ { description } failed with error: { e } " )
53+ raise
54+
55+ return None
56+
57+
2558################################################################################
2659# Individual Image Generation Functions
2760################################################################################
@@ -32,46 +65,73 @@ async def generate_overview(s: Stats) -> None:
3265 Generate an SVG badge with summary statistics
3366 :param s: Represents user's GitHub statistics
3467 """
35- with open ("templates/overview.svg" , "r" ) as f :
36- output = f .read ()
37-
38- output = re .sub ("{{ name }}" , await s .name , output )
39- output = re .sub ("{{ stars }}" , f"{ await s .stargazers :,} " , output )
40- output = re .sub ("{{ forks }}" , f"{ await s .forks :,} " , output )
41- output = re .sub ("{{ contributions }}" , f"{ await s .total_contributions :,} " , output )
42- changed = (await s .lines_changed )[0 ] + (await s .lines_changed )[1 ]
43- output = re .sub ("{{ lines_changed }}" , f"{ changed :,} " , output )
44- output = re .sub ("{{ views }}" , f"{ await s .views :,} " , output )
45- output = re .sub ("{{ repos }}" , f"{ len (await s .repos ):,} " , output )
46-
47- generate_output_folder ()
48- with open ("generated/overview.svg" , "w" ) as f :
49- f .write (output )
68+ print ("🔄 Generating overview badge..." )
69+
70+ try :
71+ with open ("templates/overview.svg" , "r" ) as f :
72+ output = f .read ()
73+
74+ # Get all stats with retry logic
75+ name = await safe_get_stat (s .name , "user name" )
76+ stars = await safe_get_stat (s .stargazers , "stargazers count" )
77+ forks = await safe_get_stat (s .forks , "forks count" )
78+ contributions = await safe_get_stat (s .total_contributions , "total contributions" )
79+ lines_changed_data = await safe_get_stat (s .lines_changed , "lines changed" )
80+ views = await safe_get_stat (s .views , "views count" )
81+ repos = await safe_get_stat (s .repos , "repositories list" )
82+
83+ # Process the data
84+ changed = lines_changed_data [0 ] + lines_changed_data [1 ]
85+
86+ output = re .sub ("{{ name }}" , name , output )
87+ output = re .sub ("{{ stars }}" , f"{ stars :,} " , output )
88+ output = re .sub ("{{ forks }}" , f"{ forks :,} " , output )
89+ output = re .sub ("{{ contributions }}" , f"{ contributions :,} " , output )
90+ output = re .sub ("{{ lines_changed }}" , f"{ changed :,} " , output )
91+ output = re .sub ("{{ views }}" , f"{ views :,} " , output )
92+ output = re .sub ("{{ repos }}" , f"{ len (repos ):,} " , output )
93+
94+ generate_output_folder ()
95+ with open ("generated/overview.svg" , "w" ) as f :
96+ f .write (output )
97+
98+ print ("✅ Overview badge generated successfully!" )
99+
100+ except Exception as e :
101+ print (f"❌ Failed to generate overview badge: { e } " )
102+ raise
50103
51104
52105async def generate_languages (s : Stats ) -> None :
53106 """
54107 Generate an SVG badge with summary languages used
55108 :param s: Represents user's GitHub statistics
56109 """
57- with open ("templates/languages.svg" , "r" ) as f :
58- output = f .read ()
59-
60- progress = ""
61- lang_list = ""
62- sorted_languages = sorted (
63- (await s .languages ).items (), reverse = True , key = lambda t : t [1 ].get ("size" )
64- )
65- delay_between = 150
66- for i , (lang , data ) in enumerate (sorted_languages ):
67- color = data .get ("color" )
68- color = color if color is not None else "#000000"
69- progress += (
70- f'<span style="background-color: { color } ;'
71- f'width: { data .get ("prop" , 0 ):0.3f} %;" '
72- f'class="progress-item"></span>'
110+ print ("🔄 Generating languages badge..." )
111+
112+ try :
113+ with open ("templates/languages.svg" , "r" ) as f :
114+ output = f .read ()
115+
116+ # Get languages data with retry logic
117+ languages_data = await safe_get_stat (s .languages , "languages data" )
118+
119+ progress = ""
120+ lang_list = ""
121+ sorted_languages = sorted (
122+ languages_data .items (), reverse = True , key = lambda t : t [1 ].get ("size" )
73123 )
74- lang_list += f"""
124+ delay_between = 150
125+
126+ for i , (lang , data ) in enumerate (sorted_languages ):
127+ color = data .get ("color" )
128+ color = color if color is not None else "#000000"
129+ progress += (
130+ f'<span style="background-color: { color } ;'
131+ f'width: { data .get ("prop" , 0 ):0.3f} %;" '
132+ f'class="progress-item"></span>'
133+ )
134+ lang_list += f"""
75135<li style="animation-delay: { i * delay_between } ms;">
76136<svg xmlns="http://www.w3.org/2000/svg" class="octicon" style="fill:{ color } ;"
77137viewBox="0 0 16 16" version="1.1" width="16" height="16"><path
@@ -82,12 +142,18 @@ async def generate_languages(s: Stats) -> None:
82142
83143"""
84144
85- output = re .sub (r"{{ progress }}" , progress , output )
86- output = re .sub (r"{{ lang_list }}" , lang_list , output )
145+ output = re .sub (r"{{ progress }}" , progress , output )
146+ output = re .sub (r"{{ lang_list }}" , lang_list , output )
87147
88- generate_output_folder ()
89- with open ("generated/languages.svg" , "w" ) as f :
90- f .write (output )
148+ generate_output_folder ()
149+ with open ("generated/languages.svg" , "w" ) as f :
150+ f .write (output )
151+
152+ print ("✅ Languages badge generated successfully!" )
153+
154+ except Exception as e :
155+ print (f"❌ Failed to generate languages badge: { e } " )
156+ raise
91157
92158
93159################################################################################
@@ -99,37 +165,74 @@ async def main() -> None:
99165 """
100166 Generate all badges
101167 """
168+ print ("🚀 Starting GitHub stats generation..." )
169+
170+ # Check environment variables
102171 access_token = os .getenv ("ACCESS_TOKEN" )
103172 if not access_token :
104- # access_token = os.getenv("GITHUB_TOKEN")
105- raise Exception ("A personal access token is required to proceed!" )
173+ access_token = os .getenv ("GITHUB_TOKEN" )
174+ if not access_token :
175+ raise Exception ("A personal access token is required to proceed!" )
176+
106177 user = os .getenv ("GITHUB_ACTOR" )
107178 if user is None :
108179 raise RuntimeError ("Environment variable GITHUB_ACTOR must be set." )
180+
181+ print (f"📊 Generating stats for user: { user } " )
182+
109183 exclude_repos = os .getenv ("EXCLUDED" )
110184 excluded_repos = (
111185 {x .strip () for x in exclude_repos .split ("," )} if exclude_repos else None
112186 )
187+
113188 exclude_langs = os .getenv ("EXCLUDED_LANGS" )
114189 excluded_langs = (
115190 {x .strip () for x in exclude_langs .split ("," )} if exclude_langs else None
116191 )
192+
117193 # Convert a truthy value to a Boolean
118194 raw_ignore_forked_repos = os .getenv ("EXCLUDE_FORKED_REPOS" )
119195 ignore_forked_repos = (
120196 not not raw_ignore_forked_repos
121197 and raw_ignore_forked_repos .strip ().lower () != "false"
122198 )
123- async with aiohttp .ClientSession () as session :
124- s = Stats (
125- user ,
126- access_token ,
127- session ,
128- exclude_repos = excluded_repos ,
129- exclude_langs = excluded_langs ,
130- ignore_forked_repos = ignore_forked_repos ,
131- )
132- await asyncio .gather (generate_languages (s ), generate_overview (s ))
199+
200+ if excluded_repos :
201+ print (f"📝 Excluding repositories: { excluded_repos } " )
202+ if excluded_langs :
203+ print (f"🚫 Excluding languages: { excluded_langs } " )
204+ if ignore_forked_repos :
205+ print ("🍴 Ignoring forked repositories" )
206+
207+ # Set longer timeout for aiohttp to handle slow GitHub API responses
208+ timeout = aiohttp .ClientTimeout (total = 300 ) # 5 minute timeout
209+
210+ try :
211+ async with aiohttp .ClientSession (timeout = timeout ) as session :
212+ s = Stats (
213+ user ,
214+ access_token ,
215+ session ,
216+ exclude_repos = excluded_repos ,
217+ exclude_langs = excluded_langs ,
218+ ignore_forked_repos = ignore_forked_repos ,
219+ )
220+
221+ # Generate both badges with proper error handling
222+ # Run them sequentially to avoid overwhelming the API
223+ print ("\n 📈 Starting badge generation..." )
224+ await generate_overview (s )
225+
226+ # Small delay between generations to be nice to the API
227+ await asyncio .sleep (2 )
228+
229+ await generate_languages (s )
230+
231+ print ("\n 🎉 All badges generated successfully!" )
232+
233+ except Exception as e :
234+ print (f"\n 💥 Fatal error during generation: { e } " )
235+ sys .exit (1 )
133236
134237
135238if __name__ == "__main__" :
0 commit comments