efecelik commited on
Commit
20a2ac1
·
1 Parent(s): 461ab98

Initial release: GitHub-style contribution graph for HF users

Browse files
Files changed (4) hide show
  1. README.md +34 -6
  2. app.py +490 -0
  3. hf_contributions.py +204 -0
  4. requirements.txt +3 -0
README.md CHANGED
@@ -1,12 +1,40 @@
1
  ---
2
- title: Hf Contributions Graph
3
- emoji: 🦀
4
- colorFrom: gray
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.3.0
8
  app_file: app.py
9
  pinned: false
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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