Skip to content

Commit 70bfd71

Browse files
authored
Update generate_images.py
1 parent e0580e0 commit 70bfd71

1 file changed

Lines changed: 152 additions & 49 deletions

File tree

generate_images.py

Lines changed: 152 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import asyncio
44
import os
55
import re
6+
import sys
7+
import time
68

79
import 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

52105
async 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};"
77137
viewBox="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

135238
if __name__ == "__main__":

0 commit comments

Comments
 (0)