Skip to content

Commit 173560a

Browse files
authored
1 parent 6fd73e6 commit 173560a

5 files changed

Lines changed: 439 additions & 0 deletions

File tree

app/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from flask import Flask
2+
3+
app = Flask(__name__)
4+
5+
from app.views import * # Importing the views to register routes
6+
7+
def create_app():
8+
return app

app/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Configuration file
2+
3+
GITHUB_TOKEN = "your_github_token"
4+
GRAPHQL_URL = "https://api.github.com/graphql"

app/services.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import requests
2+
from app.config import GITHUB_TOKEN, GRAPHQL_URL
3+
4+
def assign_rating(total_contributions):
5+
if total_contributions >= 1000:
6+
return 'A'
7+
elif total_contributions >= 750:
8+
return 'B'
9+
elif total_contributions >= 300:
10+
return 'C'
11+
elif total_contributions >= 250:
12+
return 'D'
13+
elif total_contributions >= 100:
14+
return 'E'
15+
else:
16+
return 'F'
17+
18+
def fetch_github_data(username):
19+
headers = {
20+
"Authorization": f"Bearer {GITHUB_TOKEN}",
21+
"Content-Type": "application/json"
22+
}
23+
24+
# Fetch user profile (followers, following, etc.)
25+
user_response = requests.get(f'https://api.github.com/users/{username}', headers=headers)
26+
if user_response.status_code != 200:
27+
return {'error': 'Unable to fetch user data'}
28+
29+
user_data = user_response.json()
30+
31+
# Fetch repos data
32+
repos_response = requests.get(f'https://api.github.com/users/{username}/repos', headers=headers)
33+
if repos_response.status_code != 200:
34+
return {'error': 'Unable to fetch repository data'}
35+
36+
repos_data = repos_response.json()
37+
total_followers = user_data.get('followers', 0)
38+
total_following = user_data.get('following', 0)
39+
total_stars = sum(repo.get('stargazers_count', 0) for repo in repos_data)
40+
41+
# Call without headers now
42+
total_commits = get_total_commits(username)
43+
total_prs = get_total_pull_requests(username, headers)
44+
merged_prs = get_merged_pull_requests(username, headers)
45+
pr_reviewed = get_total_pr_reviewed(username, headers)
46+
issues = get_total_issues(username, headers)
47+
discussions_started = get_total_discussions_started(username, headers)
48+
discussions_answered = get_total_discussions_answered(username, headers)
49+
50+
# Fetch total contributions over all years using GraphQL
51+
total_contributions = get_total_contributions(username)
52+
53+
merged_pr_percentage = (merged_prs / total_prs * 100) if total_prs > 0 else 0
54+
55+
# Calculate rating based on total contributions
56+
rating = assign_rating(total_contributions)
57+
58+
return {
59+
'username': username,
60+
'total_stars': total_stars,
61+
'total_commits': total_commits,
62+
'total_prs': total_prs,
63+
'merged_prs': merged_prs,
64+
'merged_pr_percentage': merged_pr_percentage,
65+
'pr_reviewed': pr_reviewed,
66+
'issues': issues,
67+
'discussions_started': discussions_started,
68+
'discussions_answered': discussions_answered,
69+
'total_followers': total_followers,
70+
'total_following': total_following,
71+
'total_contributions': total_contributions,
72+
'rating': rating
73+
}
74+
75+
# Additional helper functions for GitHub API requests
76+
77+
def get_total_commits(username):
78+
# GraphQL query to fetch contribution years
79+
query = """
80+
query ($username: String!) {
81+
user(login: $username) {
82+
contributionsCollection {
83+
contributionYears
84+
}
85+
}
86+
}
87+
"""
88+
89+
variables = {"username": username}
90+
headers = {"Authorization": f"Bearer {GITHUB_TOKEN}"}
91+
92+
response = requests.post(GRAPHQL_URL, json={'query': query, 'variables': variables}, headers=headers)
93+
94+
# Log the full response for debugging purposes
95+
try:
96+
response_data = response.json()
97+
except ValueError:
98+
print(f"Failed to parse response as JSON. Response text: {response.text}")
99+
return 0
100+
101+
# Check if 'data' key exists
102+
if 'data' not in response_data:
103+
print(f"Error: 'data' key missing in the response. Full response: {response_data}")
104+
return 0
105+
106+
try:
107+
contribution_years = response_data['data']['user']['contributionsCollection']['contributionYears']
108+
except KeyError:
109+
print("Error: contribution years not found in the response.")
110+
return 0
111+
112+
total_commits = 0
113+
114+
# Iterate through each year to calculate total commits
115+
for year in contribution_years:
116+
year_query = """
117+
query ($username: String!, $startDate: DateTime!, $endDate: DateTime!) {
118+
user(login: $username) {
119+
contributionsCollection(from: $startDate, to: $endDate) {
120+
totalCommitContributions
121+
contributionCalendar {
122+
totalContributions
123+
}
124+
commitContributionsByRepository {
125+
contributions {
126+
totalCount
127+
}
128+
}
129+
}
130+
}
131+
}
132+
"""
133+
year_variables = {
134+
"username": username,
135+
"startDate": f"{year}-01-01T00:00:00Z",
136+
"endDate": f"{year}-12-31T23:59:59Z"
137+
}
138+
139+
year_response = requests.post(GRAPHQL_URL, json={'query': year_query, 'variables': year_variables}, headers=headers)
140+
141+
try:
142+
year_data = year_response.json()
143+
except ValueError:
144+
print(f"Failed to parse year response as JSON. Response text: {year_response.text}")
145+
continue
146+
147+
if 'data' not in year_data:
148+
print(f"Error: 'data' key missing in the year response. Full response: {year_data}")
149+
continue
150+
151+
try:
152+
contributions_collection = year_data['data']['user']['contributionsCollection']
153+
total_commit_contributions = contributions_collection.get('totalCommitContributions', 0)
154+
total_contributions = contributions_collection['contributionCalendar']['totalContributions']
155+
156+
if total_commit_contributions == 0: # Using fallback estimation
157+
# Estimate commits using total contributions and percentage of commits
158+
commit_percentage = calculate_commit_percentage(username, headers, year_variables)
159+
estimated_commits = int(total_contributions * (commit_percentage / 100))
160+
total_commits += estimated_commits
161+
else:
162+
total_commits += total_commit_contributions
163+
164+
except KeyError:
165+
print(f"Error extracting commit data for year {year}.")
166+
167+
return total_commits
168+
169+
# Fallback function to estimate commit percentage if no exact commit data is available
170+
def calculate_commit_percentage(username, headers, year_variables):
171+
# This function estimates the commit percentage for a given user
172+
commit_percentage = 95 # Fallback to an estimated percentage, e.g., 95%
173+
# You can implement any additional logic to fetch or estimate the commit percentage
174+
return commit_percentage
175+
176+
177+
178+
def get_total_pull_requests(username, headers):
179+
prs = requests.get(f'https://api.github.com/search/issues?q=type:pr+author:{username}', headers=headers).json()
180+
return prs['total_count']
181+
182+
def get_merged_pull_requests(username, headers):
183+
prs = requests.get(f'https://api.github.com/search/issues?q=type:pr+is:merged+author:{username}', headers=headers).json()
184+
return prs['total_count']
185+
186+
def get_total_pr_reviewed(username, headers):
187+
prs_reviewed = requests.get(f'https://api.github.com/search/issues?q=type:pr+reviewed-by:{username}', headers=headers).json()
188+
return prs_reviewed['total_count']
189+
190+
def get_total_issues(username, headers):
191+
issues = requests.get(f'https://api.github.com/search/issues?q=type:issue+author:{username}', headers=headers).json()
192+
return issues['total_count']
193+
194+
def get_total_discussions_started(username, headers):
195+
discussions = requests.get(f'https://api.github.com/search/issues?q=type:discussion+author:{username}', headers=headers).json()
196+
return discussions['total_count']
197+
198+
def get_total_discussions_answered(username, headers):
199+
discussions = requests.get(f'https://api.github.com/search/issues?q=type:discussion+commenter:{username}', headers=headers).json()
200+
return discussions['total_count']
201+
202+
def get_total_contributions(username):
203+
# GraphQL query to fetch contribution years
204+
query = """
205+
query ($username: String!) {
206+
user(login: $username) {
207+
contributionsCollection {
208+
contributionYears
209+
}
210+
}
211+
}
212+
"""
213+
214+
variables = {"username": username}
215+
headers = {"Authorization": f"Bearer {GITHUB_TOKEN}"}
216+
217+
response = requests.post(GRAPHQL_URL, json={'query': query, 'variables': variables}, headers=headers)
218+
219+
if response.status_code != 200:
220+
print(f"Failed to fetch contribution years for {username}. Status Code: {response.status_code}")
221+
return 0
222+
223+
try:
224+
data = response.json()
225+
contribution_years = data['data']['user']['contributionsCollection']['contributionYears']
226+
except KeyError:
227+
print("Error fetching contribution years.")
228+
return 0
229+
230+
total_contributions = 0
231+
for year in contribution_years:
232+
year_query = """
233+
query ($username: String!, $startDate: DateTime!, $endDate: DateTime!) {
234+
user(login: $username) {
235+
contributionsCollection(from: $startDate, to: $endDate) {
236+
contributionCalendar {
237+
totalContributions
238+
}
239+
}
240+
}
241+
}
242+
"""
243+
year_variables = {
244+
"username": username,
245+
"startDate": f"{year}-01-01T00:00:00Z",
246+
"endDate": f"{year}-12-31T23:59:59Z"
247+
}
248+
249+
year_response = requests.post(GRAPHQL_URL, json={'query': year_query, 'variables': year_variables}, headers=headers)
250+
251+
if year_response.status_code == 200:
252+
year_data = year_response.json()
253+
year_contributions = year_data['data']['user']['contributionsCollection']['contributionCalendar']['totalContributions']
254+
total_contributions += year_contributions
255+
256+
return total_contributions

app/utils.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from PIL import Image, ImageDraw, ImageFont, ImageOps
2+
import time
3+
4+
def adjust_icon_color(icon, background_color):
5+
"""Adjust the icon color based on the background color."""
6+
if background_color.lower() in ['#1e1e1e', '#333', '#000000']: # Dark backgrounds
7+
# Ensure the icon is in the correct mode before inverting
8+
if icon.mode == 'RGBA':
9+
r, g, b, a = icon.split() # Preserve the alpha channel
10+
rgb_image = Image.merge('RGB', (r, g, b))
11+
inverted_image = ImageOps.invert(rgb_image)
12+
icon = Image.merge('RGBA', (inverted_image.split()[0], inverted_image.split()[1], inverted_image.split()[2], a))
13+
else:
14+
icon = ImageOps.invert(icon.convert('RGB')).convert('RGBA')
15+
return icon
16+
17+
def generate_stats_image(stats, bgColor, textColor, cardColor):
18+
# Create an image with dimensions 800x450
19+
img = Image.new('RGB', (800, 450), color=bgColor)
20+
draw = ImageDraw.Draw(img)
21+
22+
# Load the fonts (Using default fonts here, replace with truetype as needed)
23+
header_font = ImageFont.truetype("app/static/fonts/Font.ttf", 32)
24+
text_font = ImageFont.truetype("app/static/fonts/Font.ttf", 16)
25+
26+
if 'error' in stats:
27+
draw.text((10, 10), f"Error: {stats['error']}", font=text_font, fill='red')
28+
return img
29+
30+
# Load icons for each stat (right now all use star.png, customize paths as you have more icons)
31+
star_icon = adjust_icon_color(Image.open('app/static/icons/star.png').resize((20, 20)), bgColor)
32+
commit_icon = adjust_icon_color(Image.open('app/static/icons/commit.png').resize((20, 20)), bgColor)
33+
pr_icon = adjust_icon_color(Image.open('app/static/icons/pr.png').resize((20, 20)), bgColor)
34+
merged_pr_icon = adjust_icon_color(Image.open('app/static/icons/pr.png').resize((20, 20)), bgColor)
35+
issue_icon = adjust_icon_color(Image.open('app/static/icons/issue.png').resize((20, 20)), bgColor)
36+
discussions_icon = adjust_icon_color(Image.open('app/static/icons/discussion.png').resize((20, 20)), bgColor)
37+
follower_icon = adjust_icon_color(Image.open('app/static/icons/follower.png').resize((20, 20)), bgColor)
38+
following_icon = adjust_icon_color(Image.open('app/static/icons/following.png').resize((20, 20)), bgColor)
39+
contributions_icon = adjust_icon_color(Image.open('app/static/icons/contributions.png').resize((20, 20)), bgColor)
40+
41+
# Draw the card background
42+
card_width, card_height = 780, 440
43+
card_x, card_y = 10, 10
44+
draw.rectangle((card_x, card_y, card_x + card_width, card_y + card_height), fill=cardColor)
45+
46+
# Draw the username and the stats on the image
47+
draw.text((card_x + 20, card_y + 20), f"{stats['username']}'s GitHub Stats", font=header_font, fill=textColor)
48+
49+
# Place icons before each stat
50+
icon_y_start = card_y + 70
51+
spacing = 30 # Spacing between each stat
52+
53+
# Column settings for alignment
54+
icon_x = card_x + 20
55+
text_x = card_x + 50
56+
value_x = card_x + 400 # Fixed column for all the stat values
57+
58+
# Paste the icons and draw the corresponding text
59+
img.paste(star_icon, (icon_x, icon_y_start), star_icon)
60+
draw.text((text_x, icon_y_start), "Total Stars:", font=text_font, fill=textColor)
61+
draw.text((value_x, icon_y_start), f"{stats['total_stars']}", font=text_font, fill=textColor)
62+
63+
img.paste(commit_icon, (icon_x, icon_y_start + spacing), commit_icon)
64+
draw.text((text_x, icon_y_start + spacing), "Total Commits:", font=text_font, fill=textColor)
65+
draw.text((value_x, icon_y_start + spacing), f"{stats['total_commits']}", font=text_font, fill=textColor)
66+
67+
img.paste(pr_icon, (icon_x, icon_y_start + spacing * 2), pr_icon)
68+
draw.text((text_x, icon_y_start + spacing * 2), "Total PRs:", font=text_font, fill=textColor)
69+
draw.text((value_x, icon_y_start + spacing * 2), f"{stats['total_prs']}", font=text_font, fill=textColor)
70+
71+
img.paste(merged_pr_icon, (icon_x, icon_y_start + spacing * 3), merged_pr_icon)
72+
draw.text((text_x, icon_y_start + spacing * 3), "Total Merged PRs:", font=text_font, fill=textColor)
73+
draw.text((value_x, icon_y_start + spacing * 3), f"{stats['merged_prs']}", font=text_font, fill=textColor)
74+
75+
img.paste(issue_icon, (icon_x, icon_y_start + spacing * 4), issue_icon)
76+
draw.text((text_x, icon_y_start + spacing * 4), "Total Issues:", font=text_font, fill=textColor)
77+
draw.text((value_x, icon_y_start + spacing * 4), f"{stats['issues']}", font=text_font, fill=textColor)
78+
79+
img.paste(discussions_icon, (icon_x, icon_y_start + spacing * 5), discussions_icon)
80+
draw.text((text_x, icon_y_start + spacing * 5), "Discussions Started:", font=text_font, fill=textColor)
81+
draw.text((value_x, icon_y_start + spacing * 5), f"{stats['discussions_started']}", font=text_font, fill=textColor)
82+
83+
img.paste(follower_icon, (icon_x, icon_y_start + spacing * 6), follower_icon)
84+
draw.text((text_x, icon_y_start + spacing * 6), "Total Followers:", font=text_font, fill=textColor)
85+
draw.text((value_x, icon_y_start + spacing * 6), f"{stats['total_followers']}", font=text_font, fill=textColor)
86+
87+
img.paste(following_icon, (icon_x, icon_y_start + spacing * 7), following_icon)
88+
draw.text((text_x, icon_y_start + spacing * 7), "Total Following:", font=text_font, fill=textColor)
89+
draw.text((value_x, icon_y_start + spacing * 7), f"{stats['total_following']}", font=text_font, fill=textColor)
90+
91+
img.paste(contributions_icon, (icon_x, icon_y_start + spacing * 8), contributions_icon)
92+
draw.text((text_x, icon_y_start + spacing * 8), "Total Contributions:", font=text_font, fill=textColor)
93+
draw.text((value_x, icon_y_start + spacing * 8), f"{stats['total_contributions']}", font=text_font, fill=textColor)
94+
95+
# Draw the rating inside a circle at the bottom right
96+
rating_x, rating_y = card_x + 600, card_y + 200
97+
rating_radius = 66
98+
draw.ellipse((rating_x - rating_radius, rating_y - rating_radius, rating_x + rating_radius, rating_y + rating_radius), fill=textColor)
99+
100+
# Draw the rating text inside the circle
101+
rating_font = ImageFont.truetype("app/static/fonts/Font.ttf", 32)
102+
rating_text = stats['rating']
103+
104+
# Use font.getbbox() to get the text size
105+
bbox = rating_font.getbbox(rating_text)
106+
text_width, text_height = bbox[2] - bbox[0], bbox[3] - bbox[1]
107+
draw.text((rating_x - text_width / 2, rating_y - text_height / 2), rating_text, font=rating_font, fill=cardColor)
108+
109+
# Add last updated time at the bottom of the card
110+
last_updated_text = f"Last updated: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stats['last_updated']))}"
111+
draw.text((card_x + 266, card_y + card_height - 90), last_updated_text, font=text_font, fill=textColor)
112+
113+
# Add a small note to inform the user about the 24-hour update cycle (split into two lines)
114+
update_info_text_1 = "* Stats are updated every 24 hours automatically to prevent excessive API requests"
115+
update_info_text_2 = " and to ensure that the service remains efficient and avoids hitting rate limits."
116+
117+
# Place the two lines of the update info text
118+
draw.text((card_x + 20, card_y + card_height - 46), update_info_text_1, font=text_font, fill=textColor)
119+
draw.text((card_x + 20, card_y + card_height - 26), update_info_text_2, font=text_font, fill=textColor)
120+
121+
122+
return img

0 commit comments

Comments
 (0)