Spaces:
Sleeping
Sleeping
Initial release: GitHub-style contribution graph for HF users
Browse files- README.md +34 -6
- app.py +490 -0
- hf_contributions.py +204 -0
- requirements.txt +3 -0
README.md
CHANGED
|
@@ -1,12 +1,40 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: HF Contributions Graph
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
short_description: GitHub-style contribution calendar for Hugging Face users
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# 🤗 Hugging Face Contributions Graph
|
| 15 |
+
|
| 16 |
+
A GitHub-style contribution calendar that visualizes your activity on Hugging Face Hub.
|
| 17 |
+
|
| 18 |
+
## Features
|
| 19 |
+
|
| 20 |
+
- **GitHub-style calendar**: Exact replica of GitHub's contribution graph UI
|
| 21 |
+
- **All activity types**: Tracks commits across models, datasets, and spaces
|
| 22 |
+
- **Interactive**: Click any day to see contribution details
|
| 23 |
+
- **Statistics**: Total contributions, streaks, and repo breakdown
|
| 24 |
+
- **Dark/Light themes**: Switch between color schemes
|
| 25 |
+
|
| 26 |
+
## How it works
|
| 27 |
+
|
| 28 |
+
This app uses the Hugging Face Hub API to:
|
| 29 |
+
1. Fetch all your public repositories (models, datasets, spaces)
|
| 30 |
+
2. Get commit history for each repository
|
| 31 |
+
3. Aggregate commits by date
|
| 32 |
+
4. Display as a contribution calendar
|
| 33 |
+
|
| 34 |
+
## Usage
|
| 35 |
+
|
| 36 |
+
Enter any Hugging Face username and click "Fetch Contributions" to see their activity graph.
|
| 37 |
+
|
| 38 |
+
## Privacy
|
| 39 |
+
|
| 40 |
+
Only public repositories are counted. Private repos require authentication.
|
app.py
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hugging Face Contributions Graph
|
| 3 |
+
|
| 4 |
+
A GitHub-style contribution calendar for Hugging Face users.
|
| 5 |
+
Shows daily commit activity across models, datasets, and spaces.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
from hf_contributions import fetch_user_contributions, ContributionStats
|
| 11 |
+
from typing import Optional
|
| 12 |
+
import json
|
| 13 |
+
|
| 14 |
+
# GitHub-style color palette
|
| 15 |
+
COLORS = {
|
| 16 |
+
"light": {
|
| 17 |
+
0: "#ebedf0", # No contributions
|
| 18 |
+
1: "#9be9a8", # Level 1 (1-3)
|
| 19 |
+
2: "#40c463", # Level 2 (4-6)
|
| 20 |
+
3: "#30a14e", # Level 3 (7-9)
|
| 21 |
+
4: "#216e39", # Level 4 (10+)
|
| 22 |
+
},
|
| 23 |
+
"dark": {
|
| 24 |
+
0: "#161b22",
|
| 25 |
+
1: "#0e4429",
|
| 26 |
+
2: "#006d32",
|
| 27 |
+
3: "#26a641",
|
| 28 |
+
4: "#39d353",
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def get_contribution_level(count: int) -> int:
|
| 34 |
+
"""Determine the contribution level (0-4) based on count."""
|
| 35 |
+
if count == 0:
|
| 36 |
+
return 0
|
| 37 |
+
elif count <= 3:
|
| 38 |
+
return 1
|
| 39 |
+
elif count <= 6:
|
| 40 |
+
return 2
|
| 41 |
+
elif count <= 9:
|
| 42 |
+
return 3
|
| 43 |
+
else:
|
| 44 |
+
return 4
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def generate_calendar_data(contributions: dict[str, int], days: int = 365) -> list[dict]:
|
| 48 |
+
"""Generate calendar data for the past N days."""
|
| 49 |
+
today = datetime.now().date()
|
| 50 |
+
start_date = today - timedelta(days=days - 1)
|
| 51 |
+
|
| 52 |
+
# Adjust to start from Sunday
|
| 53 |
+
days_since_sunday = start_date.weekday() + 1
|
| 54 |
+
if days_since_sunday == 7:
|
| 55 |
+
days_since_sunday = 0
|
| 56 |
+
start_date = start_date - timedelta(days=days_since_sunday)
|
| 57 |
+
|
| 58 |
+
calendar_data = []
|
| 59 |
+
current_date = start_date
|
| 60 |
+
|
| 61 |
+
while current_date <= today:
|
| 62 |
+
date_str = current_date.strftime("%Y-%m-%d")
|
| 63 |
+
count = contributions.get(date_str, 0)
|
| 64 |
+
level = get_contribution_level(count)
|
| 65 |
+
|
| 66 |
+
calendar_data.append({
|
| 67 |
+
"date": date_str,
|
| 68 |
+
"count": count,
|
| 69 |
+
"level": level,
|
| 70 |
+
"weekday": current_date.weekday(),
|
| 71 |
+
"month": current_date.month,
|
| 72 |
+
"day": current_date.day
|
| 73 |
+
})
|
| 74 |
+
|
| 75 |
+
current_date += timedelta(days=1)
|
| 76 |
+
|
| 77 |
+
return calendar_data
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def generate_contribution_graph_html(
|
| 81 |
+
stats: Optional[ContributionStats],
|
| 82 |
+
theme: str = "light",
|
| 83 |
+
username: str = ""
|
| 84 |
+
) -> str:
|
| 85 |
+
"""Generate the HTML/CSS for the GitHub-style contribution graph."""
|
| 86 |
+
|
| 87 |
+
if stats is None:
|
| 88 |
+
return """
|
| 89 |
+
<div style="text-align: center; padding: 40px; color: #666;">
|
| 90 |
+
<p>Enter a Hugging Face username to see their contribution graph</p>
|
| 91 |
+
</div>
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
colors = COLORS[theme]
|
| 95 |
+
calendar_data = generate_calendar_data(stats.contributions_by_date)
|
| 96 |
+
|
| 97 |
+
# Group by weeks
|
| 98 |
+
weeks = []
|
| 99 |
+
current_week = []
|
| 100 |
+
|
| 101 |
+
for day in calendar_data:
|
| 102 |
+
if len(current_week) == 7:
|
| 103 |
+
weeks.append(current_week)
|
| 104 |
+
current_week = []
|
| 105 |
+
current_week.append(day)
|
| 106 |
+
|
| 107 |
+
if current_week:
|
| 108 |
+
weeks.append(current_week)
|
| 109 |
+
|
| 110 |
+
# Generate month labels
|
| 111 |
+
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
| 112 |
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
| 113 |
+
month_positions = {}
|
| 114 |
+
|
| 115 |
+
for week_idx, week in enumerate(weeks):
|
| 116 |
+
for day in week:
|
| 117 |
+
if day["day"] <= 7: # First week of month
|
| 118 |
+
month_positions[week_idx] = months[day["month"] - 1]
|
| 119 |
+
break
|
| 120 |
+
|
| 121 |
+
# Build HTML
|
| 122 |
+
squares_html = ""
|
| 123 |
+
for week_idx, week in enumerate(weeks):
|
| 124 |
+
for day in week:
|
| 125 |
+
color = colors[day["level"]]
|
| 126 |
+
tooltip = f'{day["count"]} contributions on {day["date"]}'
|
| 127 |
+
squares_html += f'''
|
| 128 |
+
<div class="day"
|
| 129 |
+
data-date="{day["date"]}"
|
| 130 |
+
data-count="{day["count"]}"
|
| 131 |
+
data-level="{day["level"]}"
|
| 132 |
+
style="background-color: {color};"
|
| 133 |
+
title="{tooltip}"
|
| 134 |
+
onclick="window.selectDay('{day["date"]}', {day["count"]})">
|
| 135 |
+
</div>
|
| 136 |
+
'''
|
| 137 |
+
|
| 138 |
+
# Month labels HTML
|
| 139 |
+
months_html = ""
|
| 140 |
+
last_month_week = -4
|
| 141 |
+
for week_idx, month_name in sorted(month_positions.items()):
|
| 142 |
+
if week_idx - last_month_week >= 4: # At least 4 weeks apart
|
| 143 |
+
left_pos = week_idx * 15 # 11px square + 4px gap
|
| 144 |
+
months_html += f'<span style="position: absolute; left: {left_pos}px;">{month_name}</span>'
|
| 145 |
+
last_month_week = week_idx
|
| 146 |
+
|
| 147 |
+
bg_color = "#ffffff" if theme == "light" else "#0d1117"
|
| 148 |
+
text_color = "#24292f" if theme == "light" else "#c9d1d9"
|
| 149 |
+
border_color = "#d0d7de" if theme == "light" else "#30363d"
|
| 150 |
+
|
| 151 |
+
html = f'''
|
| 152 |
+
<style>
|
| 153 |
+
.contribution-graph {{
|
| 154 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
| 155 |
+
background: {bg_color};
|
| 156 |
+
padding: 16px;
|
| 157 |
+
border-radius: 6px;
|
| 158 |
+
border: 1px solid {border_color};
|
| 159 |
+
overflow-x: auto;
|
| 160 |
+
}}
|
| 161 |
+
|
| 162 |
+
.graph-header {{
|
| 163 |
+
display: flex;
|
| 164 |
+
justify-content: space-between;
|
| 165 |
+
align-items: center;
|
| 166 |
+
margin-bottom: 8px;
|
| 167 |
+
color: {text_color};
|
| 168 |
+
}}
|
| 169 |
+
|
| 170 |
+
.graph-title {{
|
| 171 |
+
font-size: 16px;
|
| 172 |
+
font-weight: 600;
|
| 173 |
+
}}
|
| 174 |
+
|
| 175 |
+
.graph-container {{
|
| 176 |
+
display: flex;
|
| 177 |
+
gap: 4px;
|
| 178 |
+
}}
|
| 179 |
+
|
| 180 |
+
.days-labels {{
|
| 181 |
+
display: flex;
|
| 182 |
+
flex-direction: column;
|
| 183 |
+
justify-content: space-around;
|
| 184 |
+
font-size: 10px;
|
| 185 |
+
color: {text_color};
|
| 186 |
+
padding-right: 8px;
|
| 187 |
+
height: 91px;
|
| 188 |
+
}}
|
| 189 |
+
|
| 190 |
+
.days-labels span {{
|
| 191 |
+
height: 11px;
|
| 192 |
+
line-height: 11px;
|
| 193 |
+
}}
|
| 194 |
+
|
| 195 |
+
.calendar-wrapper {{
|
| 196 |
+
position: relative;
|
| 197 |
+
}}
|
| 198 |
+
|
| 199 |
+
.months-row {{
|
| 200 |
+
position: relative;
|
| 201 |
+
height: 15px;
|
| 202 |
+
font-size: 10px;
|
| 203 |
+
color: {text_color};
|
| 204 |
+
margin-bottom: 4px;
|
| 205 |
+
}}
|
| 206 |
+
|
| 207 |
+
.calendar {{
|
| 208 |
+
display: grid;
|
| 209 |
+
grid-template-rows: repeat(7, 11px);
|
| 210 |
+
grid-auto-flow: column;
|
| 211 |
+
grid-auto-columns: 11px;
|
| 212 |
+
gap: 4px;
|
| 213 |
+
}}
|
| 214 |
+
|
| 215 |
+
.day {{
|
| 216 |
+
width: 11px;
|
| 217 |
+
height: 11px;
|
| 218 |
+
border-radius: 2px;
|
| 219 |
+
cursor: pointer;
|
| 220 |
+
transition: transform 0.1s ease;
|
| 221 |
+
}}
|
| 222 |
+
|
| 223 |
+
.day:hover {{
|
| 224 |
+
transform: scale(1.3);
|
| 225 |
+
outline: 1px solid {text_color};
|
| 226 |
+
outline-offset: 1px;
|
| 227 |
+
}}
|
| 228 |
+
|
| 229 |
+
.legend {{
|
| 230 |
+
display: flex;
|
| 231 |
+
align-items: center;
|
| 232 |
+
justify-content: flex-end;
|
| 233 |
+
gap: 4px;
|
| 234 |
+
margin-top: 8px;
|
| 235 |
+
font-size: 11px;
|
| 236 |
+
color: {text_color};
|
| 237 |
+
}}
|
| 238 |
+
|
| 239 |
+
.legend-item {{
|
| 240 |
+
width: 11px;
|
| 241 |
+
height: 11px;
|
| 242 |
+
border-radius: 2px;
|
| 243 |
+
}}
|
| 244 |
+
|
| 245 |
+
.stats-grid {{
|
| 246 |
+
display: grid;
|
| 247 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 248 |
+
gap: 16px;
|
| 249 |
+
margin-top: 16px;
|
| 250 |
+
padding-top: 16px;
|
| 251 |
+
border-top: 1px solid {border_color};
|
| 252 |
+
}}
|
| 253 |
+
|
| 254 |
+
.stat-card {{
|
| 255 |
+
text-align: center;
|
| 256 |
+
padding: 12px;
|
| 257 |
+
background: {colors[0]};
|
| 258 |
+
border-radius: 6px;
|
| 259 |
+
}}
|
| 260 |
+
|
| 261 |
+
.stat-value {{
|
| 262 |
+
font-size: 24px;
|
| 263 |
+
font-weight: 600;
|
| 264 |
+
color: {text_color};
|
| 265 |
+
}}
|
| 266 |
+
|
| 267 |
+
.stat-label {{
|
| 268 |
+
font-size: 12px;
|
| 269 |
+
color: {text_color};
|
| 270 |
+
opacity: 0.8;
|
| 271 |
+
}}
|
| 272 |
+
|
| 273 |
+
.selected-day-info {{
|
| 274 |
+
margin-top: 16px;
|
| 275 |
+
padding: 12px;
|
| 276 |
+
background: {colors[0]};
|
| 277 |
+
border-radius: 6px;
|
| 278 |
+
display: none;
|
| 279 |
+
color: {text_color};
|
| 280 |
+
}}
|
| 281 |
+
|
| 282 |
+
.selected-day-info.active {{
|
| 283 |
+
display: block;
|
| 284 |
+
}}
|
| 285 |
+
</style>
|
| 286 |
+
|
| 287 |
+
<div class="contribution-graph">
|
| 288 |
+
<div class="graph-header">
|
| 289 |
+
<span class="graph-title">{stats.total_commits} contributions in the last year</span>
|
| 290 |
+
</div>
|
| 291 |
+
|
| 292 |
+
<div class="graph-container">
|
| 293 |
+
<div class="days-labels">
|
| 294 |
+
<span></span>
|
| 295 |
+
<span>Mon</span>
|
| 296 |
+
<span></span>
|
| 297 |
+
<span>Wed</span>
|
| 298 |
+
<span></span>
|
| 299 |
+
<span>Fri</span>
|
| 300 |
+
<span></span>
|
| 301 |
+
</div>
|
| 302 |
+
|
| 303 |
+
<div class="calendar-wrapper">
|
| 304 |
+
<div class="months-row">
|
| 305 |
+
{months_html}
|
| 306 |
+
</div>
|
| 307 |
+
<div class="calendar">
|
| 308 |
+
{squares_html}
|
| 309 |
+
</div>
|
| 310 |
+
</div>
|
| 311 |
+
</div>
|
| 312 |
+
|
| 313 |
+
<div class="legend">
|
| 314 |
+
<span>Less</span>
|
| 315 |
+
<div class="legend-item" style="background-color: {colors[0]};"></div>
|
| 316 |
+
<div class="legend-item" style="background-color: {colors[1]};"></div>
|
| 317 |
+
<div class="legend-item" style="background-color: {colors[2]};"></div>
|
| 318 |
+
<div class="legend-item" style="background-color: {colors[3]};"></div>
|
| 319 |
+
<div class="legend-item" style="background-color: {colors[4]};"></div>
|
| 320 |
+
<span>More</span>
|
| 321 |
+
</div>
|
| 322 |
+
|
| 323 |
+
<div class="stats-grid">
|
| 324 |
+
<div class="stat-card">
|
| 325 |
+
<div class="stat-value">{stats.total_commits}</div>
|
| 326 |
+
<div class="stat-label">Total Contributions</div>
|
| 327 |
+
</div>
|
| 328 |
+
<div class="stat-card">
|
| 329 |
+
<div class="stat-value">{stats.models_count}</div>
|
| 330 |
+
<div class="stat-label">Models</div>
|
| 331 |
+
</div>
|
| 332 |
+
<div class="stat-card">
|
| 333 |
+
<div class="stat-value">{stats.datasets_count}</div>
|
| 334 |
+
<div class="stat-label">Datasets</div>
|
| 335 |
+
</div>
|
| 336 |
+
<div class="stat-card">
|
| 337 |
+
<div class="stat-value">{stats.spaces_count}</div>
|
| 338 |
+
<div class="stat-label">Spaces</div>
|
| 339 |
+
</div>
|
| 340 |
+
<div class="stat-card">
|
| 341 |
+
<div class="stat-value">{stats.longest_streak}</div>
|
| 342 |
+
<div class="stat-label">Longest Streak</div>
|
| 343 |
+
</div>
|
| 344 |
+
<div class="stat-card">
|
| 345 |
+
<div class="stat-value">{stats.current_streak}</div>
|
| 346 |
+
<div class="stat-label">Current Streak</div>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
|
| 350 |
+
<div id="selected-day" class="selected-day-info">
|
| 351 |
+
<strong id="day-date"></strong>: <span id="day-count"></span> contributions
|
| 352 |
+
</div>
|
| 353 |
+
</div>
|
| 354 |
+
|
| 355 |
+
<script>
|
| 356 |
+
window.selectDay = function(date, count) {{
|
| 357 |
+
const info = document.getElementById('selected-day');
|
| 358 |
+
const dateEl = document.getElementById('day-date');
|
| 359 |
+
const countEl = document.getElementById('day-count');
|
| 360 |
+
|
| 361 |
+
dateEl.textContent = date;
|
| 362 |
+
countEl.textContent = count;
|
| 363 |
+
info.classList.add('active');
|
| 364 |
+
|
| 365 |
+
// Highlight selected day
|
| 366 |
+
document.querySelectorAll('.day').forEach(d => d.style.outline = '');
|
| 367 |
+
event.target.style.outline = '2px solid #0969da';
|
| 368 |
+
event.target.style.outlineOffset = '1px';
|
| 369 |
+
}}
|
| 370 |
+
</script>
|
| 371 |
+
'''
|
| 372 |
+
|
| 373 |
+
return html
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
# Store the current stats globally for the detail view
|
| 377 |
+
current_stats: Optional[ContributionStats] = None
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def fetch_and_display(username: str, theme: str) -> tuple[str, str]:
|
| 381 |
+
"""Fetch contributions and generate the display."""
|
| 382 |
+
global current_stats
|
| 383 |
+
|
| 384 |
+
if not username or not username.strip():
|
| 385 |
+
return (
|
| 386 |
+
generate_contribution_graph_html(None, theme),
|
| 387 |
+
"Please enter a username"
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
username = username.strip()
|
| 391 |
+
|
| 392 |
+
try:
|
| 393 |
+
# Fetch the data
|
| 394 |
+
current_stats = fetch_user_contributions(username)
|
| 395 |
+
|
| 396 |
+
# Generate the graph HTML
|
| 397 |
+
graph_html = generate_contribution_graph_html(current_stats, theme, username)
|
| 398 |
+
|
| 399 |
+
# Generate summary
|
| 400 |
+
summary = f"""
|
| 401 |
+
### {username}'s Hugging Face Activity
|
| 402 |
+
|
| 403 |
+
- **Total Contributions:** {current_stats.total_commits}
|
| 404 |
+
- **Repositories:** {current_stats.total_repos} ({current_stats.models_count} models, {current_stats.datasets_count} datasets, {current_stats.spaces_count} spaces)
|
| 405 |
+
- **Longest Streak:** {current_stats.longest_streak} days
|
| 406 |
+
- **Current Streak:** {current_stats.current_streak} days
|
| 407 |
+
"""
|
| 408 |
+
if current_stats.most_active_day:
|
| 409 |
+
summary += f"- **Most Active Day:** {current_stats.most_active_day} ({current_stats.most_active_count} contributions)"
|
| 410 |
+
|
| 411 |
+
return graph_html, summary
|
| 412 |
+
|
| 413 |
+
except Exception as e:
|
| 414 |
+
return (
|
| 415 |
+
f'<div style="color: red; padding: 20px;">Error: {str(e)}</div>',
|
| 416 |
+
f"Error fetching data for {username}: {str(e)}"
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
# Create the Gradio interface
|
| 421 |
+
with gr.Blocks(
|
| 422 |
+
title="HF Contributions Graph",
|
| 423 |
+
theme=gr.themes.Soft(),
|
| 424 |
+
css="""
|
| 425 |
+
.gradio-container { max-width: 1200px !important; }
|
| 426 |
+
footer { display: none !important; }
|
| 427 |
+
"""
|
| 428 |
+
) as demo:
|
| 429 |
+
gr.Markdown("""
|
| 430 |
+
# 🤗 Hugging Face Contributions Graph
|
| 431 |
+
|
| 432 |
+
GitHub-style contribution calendar for Hugging Face users.
|
| 433 |
+
See your daily activity across models, datasets, and spaces!
|
| 434 |
+
""")
|
| 435 |
+
|
| 436 |
+
with gr.Row():
|
| 437 |
+
with gr.Column(scale=4):
|
| 438 |
+
username_input = gr.Textbox(
|
| 439 |
+
label="Hugging Face Username",
|
| 440 |
+
placeholder="e.g., huggingface, Qwen, meta-llama",
|
| 441 |
+
lines=1
|
| 442 |
+
)
|
| 443 |
+
with gr.Column(scale=1):
|
| 444 |
+
theme_dropdown = gr.Dropdown(
|
| 445 |
+
choices=["light", "dark"],
|
| 446 |
+
value="light",
|
| 447 |
+
label="Theme"
|
| 448 |
+
)
|
| 449 |
+
with gr.Column(scale=1):
|
| 450 |
+
fetch_btn = gr.Button("Fetch Contributions", variant="primary")
|
| 451 |
+
|
| 452 |
+
graph_output = gr.HTML(
|
| 453 |
+
value=generate_contribution_graph_html(None, "light"),
|
| 454 |
+
label="Contribution Graph"
|
| 455 |
+
)
|
| 456 |
+
|
| 457 |
+
summary_output = gr.Markdown(
|
| 458 |
+
value="Enter a username and click 'Fetch Contributions' to see their activity."
|
| 459 |
+
)
|
| 460 |
+
|
| 461 |
+
# Event handlers
|
| 462 |
+
fetch_btn.click(
|
| 463 |
+
fn=fetch_and_display,
|
| 464 |
+
inputs=[username_input, theme_dropdown],
|
| 465 |
+
outputs=[graph_output, summary_output]
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
username_input.submit(
|
| 469 |
+
fn=fetch_and_display,
|
| 470 |
+
inputs=[username_input, theme_dropdown],
|
| 471 |
+
outputs=[graph_output, summary_output]
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
theme_dropdown.change(
|
| 475 |
+
fn=lambda u, t: fetch_and_display(u, t) if u else (generate_contribution_graph_html(None, t), ""),
|
| 476 |
+
inputs=[username_input, theme_dropdown],
|
| 477 |
+
outputs=[graph_output, summary_output]
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
gr.Markdown("""
|
| 481 |
+
---
|
| 482 |
+
**How it works:** This app fetches your public repositories (models, datasets, spaces)
|
| 483 |
+
from the Hugging Face Hub API and counts commits to generate a contribution calendar.
|
| 484 |
+
|
| 485 |
+
**Note:** Only public repositories are counted. Private repos require authentication.
|
| 486 |
+
""")
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
if __name__ == "__main__":
|
| 490 |
+
demo.launch()
|
hf_contributions.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hugging Face Hub Contributions Fetcher
|
| 3 |
+
|
| 4 |
+
Fetches contribution data (commits) from a user's models, datasets, and spaces
|
| 5 |
+
using the Hugging Face Hub API.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from huggingface_hub import HfApi
|
| 9 |
+
from collections import defaultdict
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
from typing import Optional
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
import logging
|
| 14 |
+
|
| 15 |
+
logging.basicConfig(level=logging.INFO)
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class ContributionStats:
|
| 21 |
+
"""Statistics about a user's contributions."""
|
| 22 |
+
total_commits: int
|
| 23 |
+
total_repos: int
|
| 24 |
+
models_count: int
|
| 25 |
+
datasets_count: int
|
| 26 |
+
spaces_count: int
|
| 27 |
+
longest_streak: int
|
| 28 |
+
current_streak: int
|
| 29 |
+
most_active_day: Optional[str]
|
| 30 |
+
most_active_count: int
|
| 31 |
+
contributions_by_date: dict[str, int]
|
| 32 |
+
contributions_by_repo: dict[str, list[dict]]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def get_date_range(days: int = 365) -> list[str]:
|
| 36 |
+
"""Generate a list of date strings for the past N days."""
|
| 37 |
+
today = datetime.now().date()
|
| 38 |
+
return [
|
| 39 |
+
(today - timedelta(days=i)).strftime("%Y-%m-%d")
|
| 40 |
+
for i in range(days - 1, -1, -1)
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def calculate_streaks(contributions: dict[str, int], days: int = 365) -> tuple[int, int]:
|
| 45 |
+
"""Calculate longest and current contribution streaks."""
|
| 46 |
+
dates = get_date_range(days)
|
| 47 |
+
|
| 48 |
+
longest_streak = 0
|
| 49 |
+
current_streak = 0
|
| 50 |
+
temp_streak = 0
|
| 51 |
+
|
| 52 |
+
for date in dates:
|
| 53 |
+
if contributions.get(date, 0) > 0:
|
| 54 |
+
temp_streak += 1
|
| 55 |
+
longest_streak = max(longest_streak, temp_streak)
|
| 56 |
+
else:
|
| 57 |
+
temp_streak = 0
|
| 58 |
+
|
| 59 |
+
# Calculate current streak (from today backwards)
|
| 60 |
+
for date in reversed(dates):
|
| 61 |
+
if contributions.get(date, 0) > 0:
|
| 62 |
+
current_streak += 1
|
| 63 |
+
else:
|
| 64 |
+
break
|
| 65 |
+
|
| 66 |
+
return longest_streak, current_streak
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def fetch_user_contributions(
|
| 70 |
+
username: str,
|
| 71 |
+
token: Optional[str] = None,
|
| 72 |
+
days: int = 365,
|
| 73 |
+
progress_callback=None
|
| 74 |
+
) -> ContributionStats:
|
| 75 |
+
"""
|
| 76 |
+
Fetch all contributions for a Hugging Face user.
|
| 77 |
+
|
| 78 |
+
Args:
|
| 79 |
+
username: The HF username to fetch contributions for
|
| 80 |
+
token: Optional HF token for accessing private repos
|
| 81 |
+
days: Number of days to look back (default 365)
|
| 82 |
+
progress_callback: Optional callback for progress updates
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
ContributionStats object with all contribution data
|
| 86 |
+
"""
|
| 87 |
+
api = HfApi(token=token)
|
| 88 |
+
contributions_by_date = defaultdict(int)
|
| 89 |
+
contributions_by_repo = defaultdict(list)
|
| 90 |
+
|
| 91 |
+
cutoff_date = datetime.now() - timedelta(days=days)
|
| 92 |
+
|
| 93 |
+
# Collect all repos
|
| 94 |
+
repos = []
|
| 95 |
+
|
| 96 |
+
def update_progress(message: str):
|
| 97 |
+
if progress_callback:
|
| 98 |
+
progress_callback(message)
|
| 99 |
+
logger.info(message)
|
| 100 |
+
|
| 101 |
+
# Fetch models
|
| 102 |
+
update_progress(f"Fetching models for {username}...")
|
| 103 |
+
try:
|
| 104 |
+
models = list(api.list_models(author=username))
|
| 105 |
+
for model in models:
|
| 106 |
+
repos.append(("model", model.id))
|
| 107 |
+
except Exception as e:
|
| 108 |
+
logger.warning(f"Error fetching models: {e}")
|
| 109 |
+
models = []
|
| 110 |
+
|
| 111 |
+
# Fetch datasets
|
| 112 |
+
update_progress(f"Fetching datasets for {username}...")
|
| 113 |
+
try:
|
| 114 |
+
datasets = list(api.list_datasets(author=username))
|
| 115 |
+
for dataset in datasets:
|
| 116 |
+
repos.append(("dataset", dataset.id))
|
| 117 |
+
except Exception as e:
|
| 118 |
+
logger.warning(f"Error fetching datasets: {e}")
|
| 119 |
+
datasets = []
|
| 120 |
+
|
| 121 |
+
# Fetch spaces
|
| 122 |
+
update_progress(f"Fetching spaces for {username}...")
|
| 123 |
+
try:
|
| 124 |
+
spaces = list(api.list_spaces(author=username))
|
| 125 |
+
for space in spaces:
|
| 126 |
+
repos.append(("space", space.id))
|
| 127 |
+
except Exception as e:
|
| 128 |
+
logger.warning(f"Error fetching spaces: {e}")
|
| 129 |
+
spaces = []
|
| 130 |
+
|
| 131 |
+
total_repos = len(repos)
|
| 132 |
+
update_progress(f"Found {total_repos} repositories. Fetching commits...")
|
| 133 |
+
|
| 134 |
+
# Fetch commits for each repo
|
| 135 |
+
for idx, (repo_type, repo_id) in enumerate(repos):
|
| 136 |
+
update_progress(f"Processing {idx + 1}/{total_repos}: {repo_id}")
|
| 137 |
+
|
| 138 |
+
try:
|
| 139 |
+
commits = list(api.list_repo_commits(repo_id, repo_type=repo_type))
|
| 140 |
+
|
| 141 |
+
for commit in commits:
|
| 142 |
+
# Handle different date formats
|
| 143 |
+
if hasattr(commit, 'created_at'):
|
| 144 |
+
commit_date = commit.created_at
|
| 145 |
+
elif hasattr(commit, 'date'):
|
| 146 |
+
commit_date = commit.date
|
| 147 |
+
else:
|
| 148 |
+
continue
|
| 149 |
+
|
| 150 |
+
# Convert to datetime if string
|
| 151 |
+
if isinstance(commit_date, str):
|
| 152 |
+
try:
|
| 153 |
+
commit_date = datetime.fromisoformat(commit_date.replace('Z', '+00:00'))
|
| 154 |
+
except:
|
| 155 |
+
continue
|
| 156 |
+
|
| 157 |
+
# Skip commits older than cutoff
|
| 158 |
+
if commit_date.replace(tzinfo=None) < cutoff_date:
|
| 159 |
+
continue
|
| 160 |
+
|
| 161 |
+
date_str = commit_date.strftime("%Y-%m-%d")
|
| 162 |
+
contributions_by_date[date_str] += 1
|
| 163 |
+
|
| 164 |
+
contributions_by_repo[repo_id].append({
|
| 165 |
+
"date": date_str,
|
| 166 |
+
"message": getattr(commit, 'title', getattr(commit, 'message', 'No message')),
|
| 167 |
+
"repo_type": repo_type
|
| 168 |
+
})
|
| 169 |
+
|
| 170 |
+
except Exception as e:
|
| 171 |
+
logger.warning(f"Error fetching commits for {repo_id}: {e}")
|
| 172 |
+
continue
|
| 173 |
+
|
| 174 |
+
# Calculate statistics
|
| 175 |
+
total_commits = sum(contributions_by_date.values())
|
| 176 |
+
longest_streak, current_streak = calculate_streaks(contributions_by_date, days)
|
| 177 |
+
|
| 178 |
+
most_active_day = None
|
| 179 |
+
most_active_count = 0
|
| 180 |
+
if contributions_by_date:
|
| 181 |
+
most_active_day = max(contributions_by_date, key=contributions_by_date.get)
|
| 182 |
+
most_active_count = contributions_by_date[most_active_day]
|
| 183 |
+
|
| 184 |
+
return ContributionStats(
|
| 185 |
+
total_commits=total_commits,
|
| 186 |
+
total_repos=total_repos,
|
| 187 |
+
models_count=len(models),
|
| 188 |
+
datasets_count=len(datasets),
|
| 189 |
+
spaces_count=len(spaces),
|
| 190 |
+
longest_streak=longest_streak,
|
| 191 |
+
current_streak=current_streak,
|
| 192 |
+
most_active_day=most_active_day,
|
| 193 |
+
most_active_count=most_active_count,
|
| 194 |
+
contributions_by_date=dict(contributions_by_date),
|
| 195 |
+
contributions_by_repo=dict(contributions_by_repo)
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
if __name__ == "__main__":
|
| 200 |
+
# Test with a known active user
|
| 201 |
+
stats = fetch_user_contributions("huggingface")
|
| 202 |
+
print(f"Total commits: {stats.total_commits}")
|
| 203 |
+
print(f"Total repos: {stats.total_repos}")
|
| 204 |
+
print(f"Longest streak: {stats.longest_streak} days")
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0
|
| 2 |
+
huggingface_hub>=0.20.0
|
| 3 |
+
pandas>=2.0.0
|