yasserrmd commited on
Commit
ee0a5bc
Β·
verified Β·
1 Parent(s): d752a14

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +158 -337
app.py CHANGED
@@ -5,39 +5,23 @@ from datetime import datetime, timedelta
5
  import time
6
  import os
7
 
8
- def get_contribution_count_graphql(username, github_token, days=30):
9
  """
10
- Get ACCURATE contribution count using GitHub GraphQL API with proper date range
11
  """
12
  if not github_token:
13
- return 0, "No token provided"
14
 
15
- # Calculate exact date range
16
  end_date = datetime.now()
17
  start_date = end_date - timedelta(days=days)
18
 
19
- # Format dates in ISO format with timezone
20
- from_date = start_date.strftime('%Y-%m-%dT%H:%M:%SZ')
21
- to_date = end_date.strftime('%Y-%m-%dT%H:%M:%SZ')
22
-
23
- # GraphQL query - get detailed contribution data
24
  query = """
25
  query($username: String!, $from: DateTime!, $to: DateTime!) {
26
  user(login: $username) {
27
  contributionsCollection(from: $from, to: $to) {
28
  contributionCalendar {
29
  totalContributions
30
- weeks {
31
- contributionDays {
32
- contributionCount
33
- date
34
- }
35
- }
36
  }
37
- totalCommitContributions
38
- totalIssueContributions
39
- totalPullRequestContributions
40
- totalPullRequestReviewContributions
41
  }
42
  }
43
  }
@@ -45,8 +29,8 @@ def get_contribution_count_graphql(username, github_token, days=30):
45
 
46
  variables = {
47
  "username": username,
48
- "from": from_date,
49
- "to": to_date
50
  }
51
 
52
  headers = {
@@ -58,283 +42,132 @@ def get_contribution_count_graphql(username, github_token, days=30):
58
  response = requests.post(
59
  'https://api.github.com/graphql',
60
  json={'query': query, 'variables': variables},
61
- headers=headers,
62
- timeout=10
63
  )
64
 
65
  if response.status_code == 200:
66
  data = response.json()
67
-
68
- # Check for errors in response
69
- if 'errors' in data:
70
- return 0, f"GraphQL Error: {data['errors']}"
71
-
72
  if 'data' in data and data['data'] and data['data']['user']:
73
- contrib_collection = data['data']['user']['contributionsCollection']
74
-
75
- # Get the total contributions from the calendar
76
- total = contrib_collection['contributionCalendar']['totalContributions']
77
-
78
- # Detailed breakdown (for debugging)
79
- commits = contrib_collection.get('totalCommitContributions', 0)
80
- issues = contrib_collection.get('totalIssueContributions', 0)
81
- prs = contrib_collection.get('totalPullRequestContributions', 0)
82
- reviews = contrib_collection.get('totalPullRequestReviewContributions', 0)
83
-
84
- debug_info = f"Total: {total} (Commits: {commits}, Issues: {issues}, PRs: {prs}, Reviews: {reviews})"
85
-
86
- return total, debug_info
87
- else:
88
- return 0, "User not found or no data"
89
- else:
90
- return 0, f"HTTP Error {response.status_code}: {response.text[:200]}"
91
-
92
  except Exception as e:
93
- return 0, f"Exception: {str(e)}"
94
-
95
- def is_uae_location(location):
96
- """
97
- Check if location string indicates UAE - case insensitive matching
98
- """
99
- if not location:
100
- return False
101
-
102
- # Convert to lowercase and strip whitespace for comparison
103
- location_lower = location.lower().strip()
104
-
105
- # Country-level keywords (highest priority)
106
- country_keywords = [
107
- 'united arab emirates',
108
- 'uae',
109
- 'u.a.e',
110
- 'u.a.e.',
111
- 'emirates',
112
- ]
113
-
114
- # City keywords (secondary check)
115
- city_keywords = [
116
- 'dubai',
117
- 'abu dhabi',
118
- 'abudhabi',
119
- 'sharjah',
120
- 'ajman',
121
- 'ras al khaimah',
122
- 'ras al-khaimah',
123
- 'rak',
124
- 'fujairah',
125
- 'umm al quwain',
126
- 'umm al-quwain',
127
- 'al ain',
128
- 'dubayy',
129
- 'دبي',
130
- 'Ψ§Ω„Ψ₯Ω…Ψ§Ψ±Ψ§Ψͺ'
131
- ]
132
-
133
- # Check country keywords first (case insensitive)
134
- for keyword in country_keywords:
135
- if keyword.lower() in location_lower:
136
- return True
137
-
138
- # Then check city keywords (case insensitive)
139
- for keyword in city_keywords:
140
- if keyword.lower() in location_lower:
141
- return True
142
 
143
- return False
144
 
145
- def fetch_github_users(github_token=None, max_users=200, min_followers=0, days=30):
146
  """
147
- Fetch GitHub users from UAE with ACCURATE contribution counts
 
148
  """
149
- # Read token from environment variable if not provided
150
  if not github_token:
151
  github_token = os.getenv('GITHUB_TOKEN')
152
-
153
- if not github_token:
154
- return pd.DataFrame(), "❌ GitHub token is REQUIRED. Please provide a token with 'read:user' scope."
155
 
156
  headers = {
157
  'Accept': 'application/vnd.github.v3+json',
158
- 'Authorization': f'token {github_token}'
159
  }
160
 
161
- all_users = {} # Use dict to avoid duplicates by login
 
 
 
 
 
 
 
 
 
 
 
162
  status_updates = []
163
- total_api_calls = 0
164
- contribution_errors = []
165
 
166
  try:
167
- # Country-level searches
168
- country_searches = [
169
- 'United+Arab+Emirates',
170
- 'UAE',
171
- '"United Arab Emirates"',
172
- ]
173
-
174
- status_updates.append(f"πŸ” Phase 1: Country-level search (UAE)...")
175
- status_updates.append(f"πŸ“… Counting contributions from last {days} days")
176
-
177
- for search_term in country_searches:
178
- if total_api_calls >= 150:
179
- break
180
-
181
- # Multiple sort methods to get diverse users
182
- sort_methods = [
183
- ('followers', 'desc'),
184
- ('repositories', 'desc'),
185
- ('joined', 'desc'),
186
- ]
187
-
188
- for sort_by, order in sort_methods:
189
- if total_api_calls >= 150:
190
- break
191
-
192
- search_url = f'https://api.github.com/search/users?q=location:{search_term}+followers:>={min_followers}&sort={sort_by}&order={order}&per_page=100'
193
-
194
- response = requests.get(search_url, headers=headers)
195
- total_api_calls += 1
196
-
197
- if response.status_code == 200:
198
- data = response.json()
199
- users = data.get('items', [])
200
-
201
- status_updates.append(f" πŸ“ Search '{search_term}' by {sort_by}: {len(users)} users")
202
-
203
- for user in users:
204
- if user['login'] not in all_users:
205
- if total_api_calls >= 150:
206
- break
207
-
208
- # Fetch detailed user info
209
- user_response = requests.get(user['url'], headers=headers)
210
- total_api_calls += 1
211
 
212
- if user_response.status_code == 200:
213
- user_data = user_response.json()
214
- user_location = user_data.get('location', '')
215
-
216
- if is_uae_location(user_location):
217
- # Get ACCURATE contribution count via GraphQL
218
- contributions, debug_info = get_contribution_count_graphql(
219
- user['login'],
220
- github_token,
221
- days
222
- )
223
-
224
- if contributions == 0 and "Error" in debug_info:
225
- contribution_errors.append(f"{user['login']}: {debug_info}")
226
-
227
- all_users[user['login']] = {
228
- 'login': user_data.get('login', ''),
229
- 'name': user_data.get('name') or user_data.get('login', ''),
230
- 'avatar': user_data.get('avatar_url', ''),
231
- 'followers': user_data.get('followers', 0),
232
- 'public_repos': user_data.get('public_repos', 0),
233
- 'contributions': contributions,
234
- 'location': user_data.get('location', ''),
235
- 'bio': user_data.get('bio', ''),
236
- 'company': user_data.get('company', ''),
237
- 'debug_info': debug_info
238
- }
239
-
240
- if len(all_users) % 20 == 0:
241
- status_updates.append(f" βœ… Processed {len(all_users)} unique UAE users...")
242
-
243
- time.sleep(0.8) # Rate limiting - be more conservative
244
-
245
- time.sleep(1.5)
246
-
247
- elif response.status_code == 403:
248
- status_updates.append(f"⚠️ Rate limit hit after {total_api_calls} calls")
249
- break
250
- elif response.status_code == 422:
251
- continue
252
- else:
253
- status_updates.append(f" ❌ Search error: {response.status_code}")
254
-
255
- # Phase 2: Major cities
256
- if len(all_users) < max_users * 1.5 and total_api_calls < 130:
257
- status_updates.append(f"πŸ” Phase 2: Major city searches...")
258
-
259
- cities = ['Dubai', 'Abu+Dhabi', 'Sharjah']
260
-
261
- for city in cities:
262
- if total_api_calls >= 150:
263
- break
264
-
265
- search_url = f'https://api.github.com/search/users?q=location:{city}+followers:>={min_followers}&sort=followers&order=desc&per_page=50'
266
- response = requests.get(search_url, headers=headers)
267
- total_api_calls += 1
268
-
269
- if response.status_code == 200:
270
- users = response.json().get('items', [])
271
-
272
- for user in users[:25]:
273
- if user['login'] not in all_users and total_api_calls < 150:
274
- user_response = requests.get(user['url'], headers=headers)
275
- total_api_calls += 1
276
 
277
- if user_response.status_code == 200:
278
- user_data = user_response.json()
279
- if is_uae_location(user_data.get('location', '')):
280
- contributions, debug_info = get_contribution_count_graphql(
281
- user['login'], github_token, days
282
- )
283
-
284
- all_users[user['login']] = {
285
- 'login': user_data.get('login', ''),
286
- 'name': user_data.get('name') or user_data.get('login', ''),
287
- 'avatar': user_data.get('avatar_url', ''),
288
- 'followers': user_data.get('followers', 0),
289
- 'public_repos': user_data.get('public_repos', 0),
290
- 'contributions': contributions,
291
- 'location': user_data.get('location', ''),
292
- 'bio': user_data.get('bio', ''),
293
- 'company': user_data.get('company', ''),
294
- 'debug_info': debug_info
295
- }
296
-
297
- time.sleep(0.8)
298
- time.sleep(1.5)
299
-
300
- # Convert and sort
301
- users_list = list(all_users.values())
302
- status_updates.append(f"πŸ“Š Found {len(users_list)} unique UAE developers")
303
-
304
- # Sort by contributions (primary), then followers (secondary)
305
- users_list.sort(key=lambda x: (x['contributions'], x['followers']), reverse=True)
306
-
307
- # Take top N
308
- top_users = users_list[:max_users]
309
-
310
- # Add rank
 
 
 
 
 
 
 
 
 
311
  for i, user in enumerate(top_users, 1):
312
  user['rank'] = i
313
-
314
- # Create DataFrame
315
  df = pd.DataFrame(top_users)
316
-
317
  if not df.empty:
318
  display_df = df[['rank', 'name', 'login', 'contributions', 'followers', 'public_repos', 'location']].copy()
319
  display_df.columns = ['Rank', 'Name', 'Username', 'Contributions', 'Followers', 'Public Repos', 'Location']
320
  display_df['GitHub Profile'] = df['login'].apply(lambda x: f"https://github.com/{x}")
321
-
322
- status_message = f"""βœ… Successfully fetched top {len(df)} contributors (last {days} days)
323
- πŸ“Š Total unique users found: {len(all_users)}
324
- πŸ”§ API calls made: {total_api_calls}
325
- πŸ“… Date range: {datetime.now() - timedelta(days=days)} to {datetime.now()}
326
-
327
- Recent status:
328
- """ + "\n".join(status_updates[-6:])
329
-
330
- if contribution_errors:
331
- status_message += f"\n\n⚠️ Contribution fetch errors: {len(contribution_errors)}"
332
- status_message += f"\n(First 3: {', '.join(contribution_errors[:3])})"
333
-
334
  return display_df, status_message
335
  else:
336
  return pd.DataFrame(), "⚠️ No users found\n" + "\n".join(status_updates)
337
-
338
  except Exception as e:
339
  return pd.DataFrame(), f"❌ Error: {str(e)}\n" + "\n".join(status_updates)
340
 
@@ -342,53 +175,49 @@ def search_users(df, search_term):
342
  """Filter users based on search term"""
343
  if df is None or df.empty:
344
  return df
 
345
  if not search_term:
346
  return df
347
-
348
  search_term = search_term.lower()
349
  mask = (
350
  df['Name'].str.lower().str.contains(search_term, na=False) |
351
- df['Username'].str.lower().str.contains(search_term, na=False) |
352
- df['Location'].str.lower().str.contains(search_term, na=False)
353
  )
354
  return df[mask]
355
 
356
  # Create Gradio interface
357
  with gr.Blocks(theme=gr.themes.Soft(), title="GitHub UAE Top Contributors") as app:
358
-
359
  gr.Markdown("""
360
- # πŸ‡¦πŸ‡ͺ GitHub UAE Top Contributors Leaderboard
361
- ### Accurate Contribution Counting via GitHub GraphQL API
362
-
363
- **This tool ranks UAE-based developers by their actual contribution count from GitHub's official API.**
364
-
365
- ⚠️ **IMPORTANT:** You MUST provide a GitHub Personal Access Token with `read:user` scope for accurate contribution counts.
366
-
367
- **Confirmed to find:** YASSERRMD (Sharjah), zning1994 (Dubai), nkapila6 (Dubai), tschm (Abu Dhabi) and many more!
368
  """)
369
-
370
  with gr.Row():
371
  token_input = gr.Textbox(
372
- label="GitHub Personal Access Token (REQUIRED - needs 'read:user' scope)",
373
- placeholder="ghp_xxxxxxxxxxxx or set GITHUB_TOKEN environment variable",
374
  type="password",
375
  scale=3
376
  )
377
  max_users_input = gr.Slider(
378
- label="Max Contributors to Display",
379
  minimum=10,
380
  maximum=500,
381
  value=200,
382
  step=10,
383
  scale=1
384
  )
385
-
386
  with gr.Row():
387
  min_followers_input = gr.Slider(
388
- label="Minimum Followers (0 = everyone)",
389
  minimum=0,
390
- maximum=50,
391
- value=0,
392
  step=1
393
  )
394
  days_input = gr.Slider(
@@ -398,97 +227,89 @@ with gr.Blocks(theme=gr.themes.Soft(), title="GitHub UAE Top Contributors") as a
398
  value=30,
399
  step=1
400
  )
401
-
402
  with gr.Row():
403
- fetch_btn = gr.Button("πŸš€ Fetch Top Contributors", variant="primary", size="lg")
404
-
405
- status_msg = gr.Textbox(label="Status & Debug Info", interactive=False, lines=8)
406
-
407
  with gr.Row():
408
  search_box = gr.Textbox(
409
- label="πŸ” Search Results",
410
- placeholder="Search by name, username, or location...",
411
  scale=4
412
  )
413
  clear_btn = gr.Button("Clear", scale=1)
414
-
 
415
  full_data = gr.State(value=pd.DataFrame())
416
-
 
417
  data_display = gr.Dataframe(
418
  headers=["Rank", "Name", "Username", "Contributions", "Followers", "Public Repos", "Location", "GitHub Profile"],
419
  datatype=["number", "str", "str", "number", "number", "number", "str", "str"],
420
  wrap=True,
421
  interactive=False
422
  )
423
-
424
  gr.Markdown("""
425
  ---
426
- ## How It Works:
427
-
428
- **Data Source:** GitHub GraphQL API (v4) - same data shown on user profiles
429
-
430
- **Contribution Types Counted:**
431
- - Commits to any branch
432
- - Pull requests opened
433
- - Issues opened
434
- - Pull request reviews
435
- - Repository creation and more
436
-
437
- **Search Strategy:**
438
- 1. **Country search:** "United Arab Emirates", "UAE" (primary)
439
- 2. **City search:** Dubai, Abu Dhabi, Sharjah (supplementary)
440
- 3. **Location verification:** Confirms UAE location in profile
441
- 4. **Multiple sort orders:** Followers, repos, recently joined
442
-
443
- **Ranking:** Primary by contributions (last N days), secondary by followers
444
-
445
- ---
446
-
447
- ## Setup GitHub Token:
448
-
449
- 1. Go to: https://github.com/settings/tokens
450
- 2. Click "Generate new token (classic)"
451
- 3. Name it: "UAE Contributors Tracker"
452
- 4. **Check the `read:user` scope** (REQUIRED for GraphQL API)
453
- 5. Generate and copy the token
454
- 6. Paste above OR set environment variable: `export GITHUB_TOKEN="ghp_..."`
455
-
456
- ---
457
-
458
- **Processing Time:** 2-5 minutes depending on settings
459
-
460
- **Note:** If you see 0 contributions for highly active users, there may be a GraphQL API error (check status output).
461
  """)
462
-
 
463
  def fetch_and_display(token, max_users, min_followers, days):
464
  df, msg = fetch_github_users(token if token else None, int(max_users), int(min_followers), int(days))
465
  return df, df, msg
466
-
467
  def filter_data(df, search):
468
  if df is None or df.empty:
469
  return df
470
  return search_users(df, search)
471
-
472
  def clear_search(df):
473
  return "", df
474
-
475
  fetch_btn.click(
476
  fn=fetch_and_display,
477
  inputs=[token_input, max_users_input, min_followers_input, days_input],
478
  outputs=[full_data, data_display, status_msg]
479
  )
480
-
481
  search_box.change(
482
  fn=filter_data,
483
  inputs=[full_data, search_box],
484
  outputs=data_display
485
  )
486
-
487
  clear_btn.click(
488
  fn=clear_search,
489
  inputs=[full_data],
490
  outputs=[search_box, data_display]
491
  )
492
 
 
493
  if __name__ == "__main__":
494
  app.launch()
 
5
  import time
6
  import os
7
 
8
+ def get_contribution_count(username, github_token, days=30):
9
  """
10
+ Get accurate contribution count using GitHub GraphQL API
11
  """
12
  if not github_token:
13
+ return 0
14
 
 
15
  end_date = datetime.now()
16
  start_date = end_date - timedelta(days=days)
17
 
 
 
 
 
 
18
  query = """
19
  query($username: String!, $from: DateTime!, $to: DateTime!) {
20
  user(login: $username) {
21
  contributionsCollection(from: $from, to: $to) {
22
  contributionCalendar {
23
  totalContributions
 
 
 
 
 
 
24
  }
 
 
 
 
25
  }
26
  }
27
  }
 
29
 
30
  variables = {
31
  "username": username,
32
+ "from": start_date.isoformat(),
33
+ "to": end_date.isoformat()
34
  }
35
 
36
  headers = {
 
42
  response = requests.post(
43
  'https://api.github.com/graphql',
44
  json={'query': query, 'variables': variables},
45
+ headers=headers
 
46
  )
47
 
48
  if response.status_code == 200:
49
  data = response.json()
 
 
 
 
 
50
  if 'data' in data and data['data'] and data['data']['user']:
51
+ return data['data']['user']['contributionsCollection']['contributionCalendar']['totalContributions']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  except Exception as e:
53
+ print(f"Error fetching contributions for {username}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
+ return 0
56
 
57
+ def fetch_github_users(github_token=None, max_users=200, min_followers=10, days=30):
58
  """
59
+ Fetch GitHub users from UAE directly using GitHub API
60
+ Sorted by contributions (primary) and followers (secondary)
61
  """
 
62
  if not github_token:
63
  github_token = os.getenv('GITHUB_TOKEN')
 
 
 
64
 
65
  headers = {
66
  'Accept': 'application/vnd.github.v3+json',
 
67
  }
68
 
69
+ if github_token:
70
+ headers['Authorization'] = f'token {github_token}'
71
+
72
+ all_users = []
73
+
74
+ # Search locations in UAE - KEEP THE ORIGINAL WORKING APPROACH
75
+ locations = [
76
+ 'Dubai', 'Abu Dhabi', 'Sharjah', 'Ajman',
77
+ 'United Arab Emirates', 'UAE',
78
+ 'Ras Al Khaimah', 'Fujairah', 'Umm Al Quwain'
79
+ ]
80
+
81
  status_updates = []
 
 
82
 
83
  try:
84
+ for location in locations:
85
+ status_updates.append(f"πŸ” Searching users in {location}...")
86
+
87
+ # Search users by location and followers
88
+ search_url = f'https://api.github.com/search/users?q=location:{location}+followers:>={min_followers}&sort=followers&order=desc&per_page=100'
89
+
90
+ response = requests.get(search_url, headers=headers)
91
+
92
+ if response.status_code == 200:
93
+ data = response.json()
94
+ users = data.get('items', [])
95
+
96
+ for user in users:
97
+ if not any(u['login'] == user['login'] for u in all_users):
98
+ # Fetch detailed user info
99
+ user_url = user['url']
100
+ user_response = requests.get(user_url, headers=headers)
101
+
102
+ if user_response.status_code == 200:
103
+ user_data = user_response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
+ # Verify location contains UAE-related keywords (case insensitive)
106
+ user_location = (user_data.get('location') or '').lower()
107
+ # Keywords are already lowercase for comparison
108
+ uae_keywords = ['dubai', 'abu dhabi', 'sharjah', 'ajman', 'uae', 'u.a.e',
109
+ 'united arab emirates', 'ras al khaimah', 'fujairah',
110
+ 'umm al quwain', 'emirates', 'abudhabi', 'rak']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
+ # Check if any UAE keyword exists in the lowercased location
113
+ if not any(keyword in user_location for keyword in uae_keywords):
114
+ continue
115
+
116
+ # Get accurate contribution count using GraphQL (FIX: Use GraphQL instead of Events API)
117
+ contributions = get_contribution_count(user['login'], github_token, days)
118
+
119
+ all_users.append({
120
+ 'login': user_data.get('login', ''),
121
+ 'name': user_data.get('name', user_data.get('login', '')),
122
+ 'avatar': user_data.get('avatar_url', ''),
123
+ 'followers': user_data.get('followers', 0),
124
+ 'public_repos': user_data.get('public_repos', 0),
125
+ 'contributions': contributions,
126
+ 'location': user_data.get('location', ''),
127
+ 'bio': user_data.get('bio', ''),
128
+ 'company': user_data.get('company', ''),
129
+ })
130
+
131
+ time.sleep(0.5) # Rate limiting
132
+
133
+ if len(all_users) >= max_users * 2: # Fetch more to ensure we get top contributors
134
+ break
135
+
136
+ status_updates.append(f"βœ… Found {len(users)} users in {location}")
137
+ time.sleep(1) # Rate limiting between searches
138
+
139
+ elif response.status_code == 403:
140
+ status_updates.append(f"⚠️ Rate limit reached. Please add a GitHub token.")
141
+ break
142
+ else:
143
+ status_updates.append(f"❌ Error searching {location}: {response.status_code}")
144
+
145
+ if len(all_users) >= max_users * 2:
146
+ break
147
+
148
+ # Sort by CONTRIBUTIONS FIRST (descending), then by followers (descending)
149
+ all_users.sort(key=lambda x: (x['contributions'], x['followers']), reverse=True)
150
+
151
+ # Take top contributors based on max_users
152
+ top_users = all_users[:max_users]
153
+
154
+ # Add rank based on contribution order
155
  for i, user in enumerate(top_users, 1):
156
  user['rank'] = i
157
+
158
+ # Convert to DataFrame
159
  df = pd.DataFrame(top_users)
160
+
161
  if not df.empty:
162
  display_df = df[['rank', 'name', 'login', 'contributions', 'followers', 'public_repos', 'location']].copy()
163
  display_df.columns = ['Rank', 'Name', 'Username', 'Contributions', 'Followers', 'Public Repos', 'Location']
164
  display_df['GitHub Profile'] = df['login'].apply(lambda x: f"https://github.com/{x}")
165
+
166
+ status_message = f"βœ… Successfully fetched top {len(df)} contributors (last {days} days)\n" + "\n".join(status_updates[-5:])
 
 
 
 
 
 
 
 
 
 
 
167
  return display_df, status_message
168
  else:
169
  return pd.DataFrame(), "⚠️ No users found\n" + "\n".join(status_updates)
170
+
171
  except Exception as e:
172
  return pd.DataFrame(), f"❌ Error: {str(e)}\n" + "\n".join(status_updates)
173
 
 
175
  """Filter users based on search term"""
176
  if df is None or df.empty:
177
  return df
178
+
179
  if not search_term:
180
  return df
181
+
182
  search_term = search_term.lower()
183
  mask = (
184
  df['Name'].str.lower().str.contains(search_term, na=False) |
185
+ df['Username'].str.lower().str.contains(search_term, na=False)
 
186
  )
187
  return df[mask]
188
 
189
  # Create Gradio interface
190
  with gr.Blocks(theme=gr.themes.Soft(), title="GitHub UAE Top Contributors") as app:
191
+
192
  gr.Markdown("""
193
+ # πŸ† Top 200 GitHub Contributors in UAE (Last 30 Days)
194
+ ### Ranked by Contributions Using GitHub GraphQL API
195
+
196
+ **This version uses the WORKING search from v1 + ACCURATE GraphQL contribution counting**
 
 
 
 
197
  """)
198
+
199
  with gr.Row():
200
  token_input = gr.Textbox(
201
+ label="GitHub Personal Access Token (REQUIRED for GraphQL)",
202
+ placeholder="ghp_xxxxxxxxxxxx or leave empty to use GITHUB_TOKEN env var",
203
  type="password",
204
  scale=3
205
  )
206
  max_users_input = gr.Slider(
207
+ label="Max Contributors",
208
  minimum=10,
209
  maximum=500,
210
  value=200,
211
  step=10,
212
  scale=1
213
  )
214
+
215
  with gr.Row():
216
  min_followers_input = gr.Slider(
217
+ label="Minimum Followers (for initial search)",
218
  minimum=0,
219
+ maximum=100,
220
+ value=10,
221
  step=1
222
  )
223
  days_input = gr.Slider(
 
227
  value=30,
228
  step=1
229
  )
230
+
231
  with gr.Row():
232
+ fetch_btn = gr.Button("πŸš€ Fetch Top Contributors from GitHub", variant="primary", size="lg")
233
+
234
+ status_msg = gr.Textbox(label="Status", interactive=False, lines=3)
235
+
236
  with gr.Row():
237
  search_box = gr.Textbox(
238
+ label="πŸ” Search by Name or Username",
239
+ placeholder="Type to search...",
240
  scale=4
241
  )
242
  clear_btn = gr.Button("Clear", scale=1)
243
+
244
+ # Store the full dataframe
245
  full_data = gr.State(value=pd.DataFrame())
246
+
247
+ # Display dataframe
248
  data_display = gr.Dataframe(
249
  headers=["Rank", "Name", "Username", "Contributions", "Followers", "Public Repos", "Location", "GitHub Profile"],
250
  datatype=["number", "str", "str", "number", "number", "number", "str", "str"],
251
  wrap=True,
252
  interactive=False
253
  )
254
+
255
  gr.Markdown("""
256
  ---
257
+ **What Changed from Original:**
258
+ - βœ… Kept the WORKING search approach (searches Dubai, Abu Dhabi, Sharjah, UAE, etc.)
259
+ - βœ… Fixed contribution counting: Now uses GitHub GraphQL API for ACCURATE counts
260
+ - βœ… GraphQL returns the EXACT number from user's contribution graph (not estimated from events)
261
+
262
+ **Original Problem:**
263
+ - Events API only returns ~100 events and misses many contributions
264
+ - GraphQL API returns the actual total contributions for the date range
265
+
266
+ **Ranking:**
267
+ - Primary: Contributions in last N days (accurate from GraphQL)
268
+ - Secondary: Followers count
269
+
270
+ **Token Setup (REQUIRED):**
271
+ 1. Go to [GitHub Settings > Tokens](https://github.com/settings/tokens)
272
+ 2. Generate new token (classic)
273
+ 3. Select scope: `read:user` (REQUIRED for GraphQL)
274
+ 4. Copy and paste above OR set `GITHUB_TOKEN` env var
275
+
276
+ **Why Token is Required:**
277
+ - REST API works without token but with rate limits
278
+ - GraphQL API REQUIRES authentication
279
+ - Without token: can search users but contributions will be 0
 
 
 
 
 
 
 
 
 
 
 
 
280
  """)
281
+
282
+ # Event handlers
283
  def fetch_and_display(token, max_users, min_followers, days):
284
  df, msg = fetch_github_users(token if token else None, int(max_users), int(min_followers), int(days))
285
  return df, df, msg
286
+
287
  def filter_data(df, search):
288
  if df is None or df.empty:
289
  return df
290
  return search_users(df, search)
291
+
292
  def clear_search(df):
293
  return "", df
294
+
295
  fetch_btn.click(
296
  fn=fetch_and_display,
297
  inputs=[token_input, max_users_input, min_followers_input, days_input],
298
  outputs=[full_data, data_display, status_msg]
299
  )
300
+
301
  search_box.change(
302
  fn=filter_data,
303
  inputs=[full_data, search_box],
304
  outputs=data_display
305
  )
306
+
307
  clear_btn.click(
308
  fn=clear_search,
309
  inputs=[full_data],
310
  outputs=[search_box, data_display]
311
  )
312
 
313
+ # Launch the app
314
  if __name__ == "__main__":
315
  app.launch()