fffiloni commited on
Commit
bc33023
·
verified ·
1 Parent(s): aaa0c7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +526 -761
app.py CHANGED
@@ -402,56 +402,130 @@ def build_failed_bucket(title: str):
402
 
403
 
404
  # ============================================================
405
- # Community activity fetch
406
  # ============================================================
407
- def empty_community_state(selected_year: int):
408
- return {
409
- "username": "",
410
- "selected_year": int(selected_year),
411
- "fetched": False,
412
- "pr_opened": empty_activity_bucket("Pull Requests"),
413
- "pr_activity": empty_activity_bucket("PR Activity"),
414
- "thread_opened": empty_activity_bucket("Community Threads"),
415
- "discussion_activity": empty_activity_bucket("Community Activity"),
416
- "logs": "Community activity not fetched yet.",
417
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
 
419
 
420
- def fetch_user_discussion_activity(
 
 
 
 
 
 
 
 
 
 
 
421
  username: str,
422
  selected_year: int,
423
- mode: str,
 
 
 
 
 
424
  ):
425
  username = (username or "").strip()
426
  selected_year = int(selected_year)
427
 
428
- title_map = {
429
- "pr_opened": "Pull Requests",
430
- "pr_activity": "PR Activity",
431
- "thread_opened": "Community Threads",
432
- "discussion_activity": "Community Activity",
433
- }
434
- title = title_map.get(mode, "Community")
435
 
436
  if not username:
437
- return {
438
- "title": title,
439
- "fetched": True,
440
- "dates": [],
441
- "count": 0,
442
- "logs": "No username provided.",
443
- }
444
 
445
- dates = []
446
- seen_ids = set()
 
 
447
  skip = 0
448
  limit = 20
449
- cursor = None
450
  pages_fetched = 0
451
 
452
  MAX_PAGES = 40
453
- MAX_DURATION = 25
454
- OPEN_EVENT_EPSILON_SECONDS = 90
455
  start = time.time()
456
 
457
  logs = []
@@ -465,94 +539,6 @@ def fetch_user_discussion_activity(
465
  }
466
  )
467
 
468
- def normalize_payload(payload):
469
- if isinstance(payload, list):
470
- return payload, None
471
-
472
- if isinstance(payload, dict):
473
- events = payload.get("recentActivity")
474
- if isinstance(events, list):
475
- return events, payload.get("cursor")
476
-
477
- for key in ["items", "events", "activity"]:
478
- value = payload.get(key)
479
- if isinstance(value, list):
480
- return value, payload.get("cursor")
481
-
482
- return [], None
483
-
484
- def extract_author_name(discussion: dict) -> str | None:
485
- author = discussion.get("author")
486
- if isinstance(author, str):
487
- return author
488
- if isinstance(author, dict):
489
- for key in ["name", "username", "login"]:
490
- value = author.get(key)
491
- if isinstance(value, str):
492
- return value
493
- return None
494
-
495
- def parse_ts(raw_value):
496
- if not raw_value:
497
- return None
498
- try:
499
- return pd.to_datetime(raw_value, utc=True)
500
- except Exception:
501
- return None
502
-
503
- def event_matches_mode(event: dict, discussion: dict) -> tuple[bool, str | None]:
504
- is_pr = bool(discussion.get("isPullRequest", False))
505
- author_name = extract_author_name(discussion)
506
- repo_id = event.get("repoId", "unknown-repo")
507
- discussion_num = discussion.get("num", "unknown-num")
508
- event_id = event.get("eventId", "unknown-event")
509
-
510
- if mode == "pr_opened":
511
- if not is_pr:
512
- return False, None
513
- if not author_name or author_name.lower() != username.lower():
514
- return False, None
515
- return True, f"{repo_id}::pr::{discussion_num}"
516
-
517
- if mode == "pr_activity":
518
- if not is_pr:
519
- return False, None
520
- return True, f"event::{event_id}"
521
-
522
- if mode == "thread_opened":
523
- if is_pr:
524
- return False, None
525
- if not author_name or author_name.lower() != username.lower():
526
- return False, None
527
- return True, f"{repo_id}::thread::{discussion_num}"
528
-
529
- if mode == "discussion_activity":
530
- if is_pr:
531
- return False, None
532
- return True, f"event::{event_id}"
533
-
534
- return False, None
535
-
536
- def extract_date_for_mode(event: dict, discussion: dict):
537
- event_ts = parse_ts(event.get("time"))
538
- created_ts = parse_ts(discussion.get("createdAt"))
539
-
540
- if mode in {"pr_opened", "thread_opened"}:
541
- if created_ts is None:
542
- return None
543
- return created_ts.tz_localize(None).date()
544
-
545
- if mode in {"pr_activity", "discussion_activity"}:
546
- if event_ts is None:
547
- return None
548
- if created_ts is not None:
549
- delta_seconds = abs((event_ts - created_ts).total_seconds())
550
- if delta_seconds <= OPEN_EVENT_EPSILON_SECONDS:
551
- return None
552
- return event_ts.tz_localize(None).date()
553
-
554
- return None
555
-
556
  try:
557
  while True:
558
  if time.time() - start > MAX_DURATION:
@@ -567,7 +553,7 @@ def fetch_user_discussion_activity(
567
 
568
  params = {
569
  "limit": limit,
570
- "activityType": "discussion",
571
  "feedType": "user",
572
  "entity": username,
573
  }
@@ -586,7 +572,8 @@ def fetch_user_discussion_activity(
586
  payload = response.json()
587
  pages_fetched += 1
588
 
589
- events, next_cursor = normalize_payload(payload)
 
590
 
591
  if not events:
592
  logs.append("No more events.")
@@ -598,28 +585,35 @@ def fetch_user_discussion_activity(
598
  if not isinstance(event, dict):
599
  continue
600
 
601
- event_ts = parse_ts(event.get("time"))
602
- if event_ts is not None and event_ts.year < selected_year:
 
 
 
 
 
 
 
 
603
  stop_early = True
604
  break
605
 
606
- discussion = event.get("discussionData") or {}
607
- if not isinstance(discussion, dict):
608
  continue
609
 
610
- keep, unique_id = event_matches_mode(event, discussion)
611
- if not keep or not unique_id:
 
612
  continue
613
 
614
- dt = extract_date_for_mode(event, discussion)
615
- if dt is None or dt.year != selected_year:
616
  continue
617
 
618
- if unique_id in seen_ids:
619
  continue
620
 
621
- seen_ids.add(unique_id)
622
- dates.append(dt)
623
 
624
  if stop_early:
625
  logs.append("Stopped early after reaching feed events older than selected year.")
@@ -630,156 +624,46 @@ def fetch_user_discussion_activity(
630
  else:
631
  skip += limit
632
 
633
- logs.append(f"Fetched {pages_fetched} page(s) from recent-activity.")
634
- logs.append(f"Found {len(dates)} unique {mode} event(s) in {selected_year}.")
635
-
636
- return {
637
- "title": title,
638
  "fetched": True,
639
- "dates": dates,
640
- "count": len(dates),
641
- "logs": "\n".join(logs),
642
  }
643
 
644
- except Exception as e:
645
- return {
646
- "title": title,
647
- "fetched": True,
648
- "dates": [],
649
- "count": 0,
650
- "logs": f"{title} fetch failed: {str(e)}",
651
- }
652
 
 
 
 
 
 
 
 
653
 
654
- def fetch_all_community_activity(username: str, selected_year: int):
655
- username = (username or "").strip()
656
- selected_year = int(selected_year)
657
-
658
- cache_key = (username.lower(), selected_year, "community")
659
- cached = get_cache(cache_key)
660
- if cached:
661
- return cached
662
-
663
- if not username:
664
- state = empty_community_state(selected_year)
665
- state["fetched"] = True
666
- state["logs"] = "No username provided."
667
- set_cache(cache_key, state)
668
- return state
669
-
670
- pr_opened = fetch_user_discussion_activity(username, selected_year, "pr_opened")
671
- pr_activity = fetch_user_discussion_activity(username, selected_year, "pr_activity")
672
- thread_opened = fetch_user_discussion_activity(username, selected_year, "thread_opened")
673
- discussion_activity = fetch_user_discussion_activity(username, selected_year, "discussion_activity")
674
-
675
- combined_logs = "\n\n".join(
676
- [
677
- "[Pull Requests]\n" + pr_opened["logs"],
678
- "[PR Activity]\n" + pr_activity["logs"],
679
- "[Community Threads]\n" + thread_opened["logs"],
680
- "[Community Activity]\n" + discussion_activity["logs"],
681
- ]
682
- )
683
-
684
- result = {
685
- "username": username,
686
- "selected_year": selected_year,
687
- "fetched": True,
688
- "pr_opened": pr_opened,
689
- "pr_activity": pr_activity,
690
- "thread_opened": thread_opened,
691
- "discussion_activity": discussion_activity,
692
- "logs": combined_logs,
693
- }
694
-
695
- set_cache(cache_key, result)
696
- return result
697
 
 
 
698
 
699
- # ============================================================
700
- # Generic bucketed sections config
701
- # ============================================================
702
- BUCKETED_SECTION_CONFIGS = {
703
- "upvotes": {
704
- "section_title": "Upvotes activity",
705
- "chart_title": "Upvotes",
706
- "palette_name": "purple",
707
- "context_subject_label": "upvotes activity",
708
- "radio_label": "Upvotes view",
709
- "default_mode": "all",
710
- "mode_labels": {
711
- "all": "All",
712
- "papers": "Papers",
713
- "blog": "Blog",
714
- "changelog": "Changelog",
715
- },
716
- "bucket_titles": {
717
- "papers": "Paper Upvotes",
718
- "blog": "Blog Upvotes",
719
- "changelog": "Changelog Upvotes",
720
- },
721
- "bucket_log_labels": {
722
- "papers": "paper upvote(s)",
723
- "blog": "blog upvote(s)",
724
- "changelog": "changelog upvote(s)",
725
- },
726
- "empty_logs_text": "Upvotes activity not fetched yet.",
727
- "activity_type": "upvote",
728
- },
729
- "likes": {
730
- "section_title": "Likes activity",
731
- "chart_title": "Likes",
732
- "palette_name": "blue",
733
- "context_subject_label": "likes activity",
734
- "radio_label": "Likes view",
735
- "default_mode": "all",
736
- "mode_labels": {
737
- "all": "All",
738
- "models": "Models",
739
- "datasets": "Datasets",
740
- "spaces": "Spaces",
741
- },
742
- "bucket_titles": {
743
- "models": "Model Likes",
744
- "datasets": "Dataset Likes",
745
- "spaces": "Space Likes",
746
- },
747
- "bucket_log_labels": {
748
- "models": "model like(s)",
749
- "datasets": "dataset like(s)",
750
- "spaces": "space like(s)",
751
- },
752
- "empty_logs_text": "Likes activity not fetched yet.",
753
- "activity_type": "like",
754
- },
755
- "papers": {
756
- "section_title": "Papers activity",
757
- "chart_title": "Papers",
758
- "palette_name": "teal",
759
- "context_subject_label": "papers activity",
760
- "radio_label": "Papers view",
761
- "default_mode": "all",
762
- "mode_labels": {
763
- "all": "All",
764
- "daily": "Daily Papers",
765
- "authored": "Authored",
766
- },
767
- "bucket_titles": {
768
- "daily": "Daily Papers",
769
- "authored": "Authored Papers",
770
- },
771
- "bucket_log_labels": {
772
- "daily": "daily paper submission(s)",
773
- "authored": "authored paper(s)",
774
- },
775
- "empty_logs_text": "Papers activity not fetched yet.",
776
- "activity_type": "update-paper",
777
- },
778
- }
779
 
 
 
780
 
781
- def get_bucketed_section_config(section_key: str):
782
- return BUCKETED_SECTION_CONFIGS[section_key]
783
 
784
 
785
  # ============================================================
@@ -847,24 +731,35 @@ def classify_paper_event(event: dict):
847
  return bucket_key, unique_id
848
 
849
 
850
- BUCKETED_CLASSIFIERS = {
851
- "upvotes": classify_upvote_event,
852
- "likes": classify_like_event,
853
- "papers": classify_paper_event,
 
 
 
 
 
 
 
 
 
854
  }
855
 
856
 
857
  def fetch_generic_bucketed_section(section_key: str, username: str, selected_year: int):
858
  cfg = get_bucketed_section_config(section_key)
 
 
859
  return fetch_bucketed_recent_activity(
860
  username=username,
861
  selected_year=selected_year,
862
  cache_scope=section_key,
863
- activity_type=cfg["activity_type"],
864
  bucket_titles=cfg["bucket_titles"],
865
  bucket_log_labels=cfg["bucket_log_labels"],
866
  empty_logs_text=cfg["empty_logs_text"],
867
- classifier=BUCKETED_CLASSIFIERS[section_key],
868
  )
869
 
870
 
@@ -878,432 +773,303 @@ def empty_generic_section_state(section_key: str, selected_year: int):
878
 
879
 
880
  # ============================================================
881
- # Contribution grid helpers
882
- # ============================================================
883
- PALETTES = {
884
- "green": ["#0e4429", "#006d32", "#26a641", "#39d353"],
885
- "blue": ["#0a3069", "#0969da", "#54aeff", "#79c0ff"],
886
- "pink": ["#6e1f4d", "#bf3989", "#db61a2", "#f778ba"],
887
- "orange": ["#5f370e", "#a14d00", "#d97706", "#f59e0b"],
888
- "purple": ["#4c1d95", "#6d28d9", "#8b5cf6", "#c4b5fd"],
889
- "teal": ["#0f766e", "#0d9488", "#14b8a6", "#5eead4"],
890
- }
891
-
892
- TOOLTIP_SAFE_COLUMNS = 2
893
-
894
-
895
- def level_from_count(count: int) -> int:
896
- if count <= 0:
897
- return 0
898
- if count == 1:
899
- return 1
900
- if count <= 3:
901
- return 2
902
- if count <= 7:
903
- return 3
904
- return 4
905
-
906
-
907
- def level_color(palette_name: str, level: int) -> str:
908
- if level <= 0:
909
- return "var(--gh-empty-cell)"
910
- palette = PALETTES[palette_name]
911
- return palette[min(level - 1, len(palette) - 1)]
912
-
913
-
914
- def build_daily_count_map(df: pd.DataFrame):
915
- if df.empty:
916
- return {}
917
-
918
- counts = (
919
- df.copy()
920
- .assign(date=pd.to_datetime(df["date"]).dt.date, count=1)
921
- .groupby("date", as_index=False)["count"]
922
- .sum()
923
- )
924
- return {row["date"]: int(row["count"]) for _, row in counts.iterrows()}
925
-
926
-
927
- def get_calendar_bounds(year: int):
928
- start = pd.Timestamp(f"{year}-01-01")
929
- end = pd.Timestamp(f"{year}-12-31")
930
-
931
- grid_start = start - pd.Timedelta(days=start.dayofweek)
932
- grid_end = end + pd.Timedelta(days=(6 - end.dayofweek))
933
-
934
- all_days = pd.date_range(grid_start, grid_end, freq="D")
935
- num_weeks = len(all_days) // 7
936
- return start, end, grid_start, grid_end, all_days, num_weeks
937
-
938
-
939
- def month_positions_for_year(year: int, grid_start: pd.Timestamp):
940
- month_starts = pd.date_range(f"{year}-01-01", f"{year}-12-31", freq="MS")
941
- return [(d.strftime("%b"), int((d - grid_start).days // 7)) for d in month_starts]
942
-
943
-
944
- # ============================================================
945
- # Name helpers
946
- # ============================================================
947
- def resolve_display_name(
948
- username: str,
949
- profile: gr.OAuthProfile | None,
950
- identity: dict | None = None,
951
- ):
952
- username = (username or "").strip()
953
- if not username:
954
- return ""
955
-
956
- profile_username = getattr(profile, "username", None) if profile is not None else None
957
- profile_name = getattr(profile, "name", None) if profile is not None else None
958
-
959
- if profile_username and profile_username.lower() == username.lower():
960
- if isinstance(profile_name, str) and profile_name.strip():
961
- return f"{profile_name.strip()} (@{profile_username})"
962
- return f"@{profile_username}"
963
-
964
- if identity:
965
- display_name = identity.get("display_name")
966
- if isinstance(display_name, str) and display_name.strip():
967
- return display_name
968
-
969
- fullname = identity.get("fullname")
970
- identity_username = identity.get("username") or username
971
- if isinstance(fullname, str) and fullname.strip():
972
- return f"{fullname.strip()} (@{identity_username})"
973
-
974
- if isinstance(identity_username, str) and identity_username.strip():
975
- return f"@{identity_username.strip()}"
976
-
977
- return f"@{username}"
978
-
979
-
980
- # ============================================================
981
- # Banner helpers
982
  # ============================================================
983
- def render_status_badge(text: str):
984
- return f'<div class="gh-status">{text}</div>'
 
 
 
 
 
 
 
 
 
985
 
986
 
987
- def render_idle_context_banner(
988
  username: str,
989
- profile: gr.OAuthProfile | None,
 
990
  ):
991
  username = (username or "").strip()
992
- viewer = getattr(profile, "username", None) if profile else None
993
-
994
- if viewer:
995
- if not username or username.lower() == viewer.lower():
996
- return render_status_badge(
997
- f"Logged in as <b>{html.escape(viewer)}</b> · Fetch your activity or enter another username"
998
- )
999
- return render_status_badge(
1000
- f"Logged in as <b>{html.escape(viewer)}</b> · Ready to fetch <b>{html.escape(username)}</b> · Public repos only"
1001
- )
1002
-
1003
- if username:
1004
- return render_status_badge(
1005
- f"Not logged in · Ready to fetch <b>{html.escape(username)}</b> · Public repos only"
1006
- )
1007
-
1008
- return render_status_badge("Not logged in · Enter a username to get started")
1009
-
1010
-
1011
- def render_invalid_username_banner(username: str, error_text: str | None = None):
1012
- username = (username or "").strip()
1013
- message = error_text or f"Username or org '{username}' was not found."
1014
- return render_status_badge(html.escape(message))
1015
-
1016
 
1017
- def render_context_banner(app_state: dict | None, profile: gr.OAuthProfile | None):
1018
- if not app_state or not app_state.get("username"):
1019
- return render_idle_context_banner("", profile)
 
 
 
 
1020
 
1021
- username = app_state["username"]
1022
- selected_year = int(app_state["selected_year"])
1023
- is_own = bool(app_state.get("is_own_profile", False))
1024
- can_toggle = bool(app_state.get("can_toggle_visibility", False))
1025
- viewer = getattr(profile, "username", None) if profile else None
 
 
 
1026
 
1027
- parts = []
 
 
 
 
 
1028
 
1029
- if viewer:
1030
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1031
- else:
1032
- parts.append("Not logged in")
1033
 
1034
- if viewer and username.lower() == viewer.lower():
1035
- parts.append("Viewing <b>your activity</b>")
1036
- else:
1037
- parts.append(f"Viewing <b>{html.escape(username)}</b>")
1038
 
1039
- parts.append(str(selected_year))
 
 
 
 
 
 
 
1040
 
1041
- if is_own and can_toggle:
1042
- parts.append("Private repos available")
1043
- else:
1044
- parts.append("Public repos only")
1045
 
1046
- return render_status_badge(" · ".join(parts))
 
 
 
1047
 
 
 
 
 
1048
 
1049
- def render_fetching_context_banner(
1050
- username,
1051
- selected_year,
1052
- profile: gr.OAuthProfile | None,
1053
- ):
1054
- username = (username or "").strip()
1055
- selected_year = int(selected_year)
1056
- viewer = getattr(profile, "username", None) if profile else None
1057
 
1058
- if not username:
1059
- return render_idle_context_banner("", profile)
 
 
 
 
 
 
 
 
1060
 
1061
- parts = []
 
 
 
 
 
 
1062
 
1063
- if viewer:
1064
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1065
- else:
1066
- parts.append("Not logged in")
 
 
1067
 
1068
- if viewer and username.lower() == viewer.lower():
1069
- parts.append("Fetching <b>your activity</b>")
1070
- parts.append(str(selected_year))
1071
- parts.append("Checking public and private repos")
1072
- else:
1073
- parts.append(f"Fetching <b>{html.escape(username)}</b>")
1074
- parts.append(str(selected_year))
1075
- parts.append("Checking public repos")
1076
 
1077
- return render_status_badge(" · ".join(parts))
 
 
 
1078
 
 
 
 
 
 
 
1079
 
1080
- def render_community_idle_banner(
1081
- username,
1082
- selected_year,
1083
- profile: gr.OAuthProfile | None,
1084
- ):
1085
- username = (username or "").strip()
1086
- selected_year = int(selected_year)
1087
- viewer = getattr(profile, "username", None) if profile else None
1088
 
1089
- parts = []
1090
- if viewer:
1091
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1092
- else:
1093
- parts.append("Not logged in")
1094
 
1095
- if not username:
1096
- parts.append("Enter a username to fetch community activity")
1097
- return render_status_badge(" · ".join(parts))
1098
 
1099
- if viewer and username.lower() == viewer.lower():
1100
- parts.append("Fetch your community activity or enter another username")
1101
- parts.append(str(selected_year))
1102
- else:
1103
- parts.append(f"Ready to fetch community activity for <b>{html.escape(username)}</b>")
1104
- parts.append(str(selected_year))
1105
 
1106
- return render_status_badge(" · ".join(parts))
 
 
 
 
 
 
 
1107
 
 
1108
 
1109
- def render_community_invalid_banner(username: str, error_text: str | None = None):
1110
- username = (username or "").strip()
1111
- message = error_text or f"Username or org '{username}' was not found."
1112
- return render_status_badge(html.escape(message))
 
1113
 
 
 
 
1114
 
1115
- def render_community_fetching_banner(
1116
- username,
1117
- selected_year,
1118
- profile: gr.OAuthProfile | None,
1119
- ):
1120
- username = (username or "").strip()
1121
- selected_year = int(selected_year)
1122
- viewer = getattr(profile, "username", None) if profile else None
1123
 
1124
- if not username:
1125
- return render_community_idle_banner("", selected_year, profile)
 
 
 
 
 
 
 
 
1126
 
1127
- parts = []
1128
- if viewer:
1129
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1130
- else:
1131
- parts.append("Not logged in")
 
1132
 
1133
- if viewer and username.lower() == viewer.lower():
1134
- parts.append("Fetching <b>your community activity</b>")
1135
- else:
1136
- parts.append(f"Fetching community activity for <b>{html.escape(username)}</b>")
1137
 
1138
- parts.append(str(selected_year))
1139
- return render_status_badge(" · ".join(parts))
1140
 
 
 
 
1141
 
1142
- def render_community_loaded_banner(
1143
- community_state: dict | None,
1144
- profile: gr.OAuthProfile | None,
1145
- ):
1146
- if not community_state or not community_state.get("username"):
1147
- year = community_state.get("selected_year", datetime.now().year) if community_state else datetime.now().year
1148
- return render_community_idle_banner("", year, profile)
1149
 
1150
- username = community_state["username"]
1151
- selected_year = int(community_state["selected_year"])
1152
- viewer = getattr(profile, "username", None) if profile else None
1153
 
1154
- parts = []
1155
- if viewer:
1156
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1157
- else:
1158
- parts.append("Not logged in")
1159
 
1160
- if viewer and username.lower() == viewer.lower():
1161
- parts.append("Viewing <b>your community activity</b>")
1162
- else:
1163
- parts.append(f"Viewing community activity for <b>{html.escape(username)}</b>")
1164
 
1165
- parts.append(str(selected_year))
1166
- return render_status_badge(" · ".join(parts))
 
1167
 
 
 
 
1168
 
1169
- def render_bucketed_idle_banner(
1170
- section_key: str,
1171
- username,
1172
- selected_year,
1173
- profile: gr.OAuthProfile | None,
1174
- ):
1175
- cfg = get_bucketed_section_config(section_key)
1176
- username = (username or "").strip()
1177
- selected_year = int(selected_year)
1178
- viewer = getattr(profile, "username", None) if profile else None
1179
- subject = cfg["context_subject_label"]
1180
 
1181
- parts = []
1182
- if viewer:
1183
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1184
- else:
1185
- parts.append("Not logged in")
1186
 
1187
- if not username:
1188
- parts.append(f"Enter a username to fetch {subject}")
1189
- return render_status_badge(" · ".join(parts))
1190
 
1191
- if viewer and username.lower() == viewer.lower():
1192
- parts.append(f"Fetch your {subject} or enter another username")
1193
- parts.append(str(selected_year))
1194
- else:
1195
- parts.append(f"Ready to fetch {subject} for <b>{html.escape(username)}</b>")
1196
- parts.append(str(selected_year))
1197
 
1198
- return render_status_badge(" · ".join(parts))
 
1199
 
 
 
 
 
 
 
 
1200
 
1201
- def render_bucketed_invalid_banner(section_key: str, username: str, error_text: str | None = None):
1202
- _ = section_key
1203
- username = (username or "").strip()
1204
- message = error_text or f"Username or org '{username}' was not found."
1205
- return render_status_badge(html.escape(message))
 
 
 
1206
 
1207
 
1208
- def render_bucketed_fetching_banner(
1209
- section_key: str,
1210
- username,
1211
- selected_year,
1212
- profile: gr.OAuthProfile | None,
1213
- ):
1214
- cfg = get_bucketed_section_config(section_key)
1215
  username = (username or "").strip()
1216
  selected_year = int(selected_year)
1217
- viewer = getattr(profile, "username", None) if profile else None
1218
- subject = cfg["context_subject_label"]
1219
-
1220
- if not username:
1221
- return render_bucketed_idle_banner(section_key, "", selected_year, profile)
1222
-
1223
- parts = []
1224
- if viewer:
1225
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1226
- else:
1227
- parts.append("Not logged in")
1228
-
1229
- if viewer and username.lower() == viewer.lower():
1230
- parts.append(f"Fetching <b>your {subject}</b>")
1231
- else:
1232
- parts.append(f"Fetching {subject} for <b>{html.escape(username)}</b>")
1233
-
1234
- parts.append(str(selected_year))
1235
- return render_status_badge(" · ".join(parts))
1236
 
 
 
 
 
1237
 
1238
- def render_bucketed_loaded_banner(
1239
- section_key: str,
1240
- section_state: dict | None,
1241
- profile: gr.OAuthProfile | None,
1242
- ):
1243
- cfg = get_bucketed_section_config(section_key)
1244
-
1245
- if not section_state or not section_state.get("username"):
1246
- year = section_state.get("selected_year", datetime.now().year) if section_state else datetime.now().year
1247
- return render_bucketed_idle_banner(section_key, "", year, profile)
1248
 
1249
- username = section_state["username"]
1250
- selected_year = int(section_state["selected_year"])
1251
- viewer = getattr(profile, "username", None) if profile else None
1252
- subject = cfg["context_subject_label"]
1253
 
1254
- parts = []
1255
- if viewer:
1256
- parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
1257
- else:
1258
- parts.append("Not logged in")
 
 
 
1259
 
1260
- if viewer and username.lower() == viewer.lower():
1261
- parts.append(f"Viewing <b>your {subject}</b>")
1262
- else:
1263
- parts.append(f"Viewing {subject} for <b>{html.escape(username)}</b>")
 
 
 
 
 
 
1264
 
1265
- parts.append(str(selected_year))
1266
- return render_status_badge(" · ".join(parts))
1267
 
1268
 
1269
  # ============================================================
1270
  # Render helpers
1271
  # ============================================================
1272
- def render_metric_cards(
1273
- total_activity: int,
1274
- created_by_type: dict,
1275
- repo_commit_counts_by_type: dict,
1276
- selected_year: int,
1277
- ):
1278
- return f"""
1279
- <div class="stats-grid">
1280
- <div class="gh-card">
1281
- <div class="gh-card-label">Total activity</div>
1282
- <div class="gh-card-value">{total_activity}</div>
1283
- <div class="gh-card-sub">Creations + repo contributions in {selected_year}</div>
1284
- </div>
1285
-
1286
- <div class="gh-card">
1287
- <div class="gh-card-label">🧠 Models created</div>
1288
- <div class="gh-card-value">{created_by_type["model"]}</div>
1289
- <div class="gh-card-sub">{repo_commit_counts_by_type["model"]} contributions on model repos</div>
1290
- </div>
1291
-
1292
- <div class="gh-card">
1293
- <div class="gh-card-label">📦 Datasets created</div>
1294
- <div class="gh-card-value">{created_by_type["dataset"]}</div>
1295
- <div class="gh-card-sub">{repo_commit_counts_by_type["dataset"]} contributions on dataset repos</div>
1296
- </div>
1297
-
1298
- <div class="gh-card">
1299
- <div class="gh-card-label">🚀 Spaces created</div>
1300
- <div class="gh-card-value">{created_by_type["space"]}</div>
1301
- <div class="gh-card-sub">{repo_commit_counts_by_type["space"]} contributions on space repos</div>
1302
- </div>
1303
- </div>
1304
- """
1305
-
1306
-
1307
  def render_empty_chart(title: str):
1308
  safe_title = html.escape(title)
1309
  return f"""
@@ -1493,22 +1259,23 @@ def render_discussion_combined_chart(community_state: dict | None, mode: str, us
1493
 
1494
  def render_bucketed_chart(section_key: str, section_state: dict | None, mode: str, username: str, year: int):
1495
  cfg = get_bucketed_section_config(section_key)
 
 
1496
 
1497
  if not section_state or not section_state.get("fetched"):
1498
- return render_empty_chart(cfg["chart_title"])
1499
 
1500
- bucket_titles = cfg["bucket_titles"]
1501
- default_mode = cfg["default_mode"]
1502
  mode = mode or default_mode
 
1503
 
1504
  if mode == "all":
1505
  merged = []
1506
- for bucket_key in bucket_titles:
1507
- merged.extend(section_state[bucket_key]["dates"])
1508
- subtitle = f"{len(merged)} total {cfg['chart_title'].lower()} event(s) by {username} in {year}"
1509
  else:
1510
  merged = list(section_state.get(mode, {}).get("dates", []))
1511
- bucket_title = bucket_titles.get(mode, mode)
1512
  subtitle = f"{len(merged)} {bucket_title.lower()} by {username} in {year}"
1513
 
1514
  df = pd.DataFrame(merged, columns=["date"])
@@ -1516,7 +1283,7 @@ def render_bucketed_chart(section_key: str, section_state: dict | None, mode: st
1516
  return render_contrib_chart(
1517
  df,
1518
  year,
1519
- cfg["chart_title"],
1520
  palette_name=cfg["palette_name"],
1521
  subtitle=subtitle,
1522
  )
@@ -1769,7 +1536,7 @@ def render_app_state(
1769
 
1770
 
1771
  # ============================================================
1772
- # Repo handlers
1773
  # ============================================================
1774
  def prepare_repo_fetch_ui(
1775
  username,
@@ -1869,9 +1636,6 @@ def rerender_from_state(app_state, visibility_mode, profile: gr.OAuthProfile | N
1869
  return render_app_state(app_state, visibility_mode, profile)
1870
 
1871
 
1872
- # ============================================================
1873
- # Community handlers
1874
- # ============================================================
1875
  def maybe_reset_community_section(
1876
  current_community_state,
1877
  username,
@@ -1954,96 +1718,6 @@ def maybe_reset_community_section(
1954
  )
1955
 
1956
 
1957
- def prepare_community_fetch_ui(username, selected_year, profile: gr.OAuthProfile | None):
1958
- username = (username or "").strip()
1959
- if username:
1960
- identity = validate_username(username, profile)
1961
- if not identity.get("exists"):
1962
- return render_community_invalid_banner(username, identity.get("error"))
1963
- return render_community_fetching_banner(username, selected_year, profile)
1964
-
1965
-
1966
- def fetch_and_render_community(
1967
- current_state,
1968
- username,
1969
- selected_year,
1970
- pr_mode,
1971
- discussion_mode,
1972
- profile: gr.OAuthProfile | None,
1973
- ):
1974
- username = (username or "").strip()
1975
- selected_year = int(selected_year)
1976
- pr_mode = pr_mode or "both"
1977
- discussion_mode = discussion_mode or "both"
1978
-
1979
- identity = validate_username(username, profile)
1980
- if username and not identity.get("exists"):
1981
- empty_state = empty_community_state(selected_year)
1982
- empty_state["username"] = username
1983
- empty_state["selected_year"] = selected_year
1984
- empty_state["logs"] = identity.get("error") or f"Username or org '{username}' was not found."
1985
- return (
1986
- empty_state,
1987
- render_community_invalid_banner(username, identity.get("error")),
1988
- render_empty_chart("Pull Requests"),
1989
- render_empty_chart("Community Discussions"),
1990
- empty_state["logs"],
1991
- gr.update(value="both", visible="hidden"),
1992
- gr.update(value="both", visible="hidden"),
1993
- )
1994
-
1995
- state = fetch_all_community_activity(username, selected_year)
1996
-
1997
- pr_chart = render_pr_combined_chart(
1998
- state,
1999
- pr_mode,
2000
- username=username,
2001
- year=selected_year,
2002
- )
2003
-
2004
- discussion_chart = render_discussion_combined_chart(
2005
- state,
2006
- discussion_mode,
2007
- username=username,
2008
- year=selected_year,
2009
- )
2010
-
2011
- return (
2012
- state,
2013
- render_community_loaded_banner(state, profile),
2014
- pr_chart,
2015
- discussion_chart,
2016
- state["logs"],
2017
- gr.update(value=pr_mode, visible=True),
2018
- gr.update(value=discussion_mode, visible=True),
2019
- )
2020
-
2021
-
2022
- def rerender_pr_from_state(community_state, pr_mode):
2023
- if not community_state:
2024
- return render_empty_chart("Pull Requests")
2025
- return render_pr_combined_chart(
2026
- community_state,
2027
- pr_mode or "both",
2028
- username=(community_state.get("username") or ""),
2029
- year=int(community_state.get("selected_year") or datetime.now().year),
2030
- )
2031
-
2032
-
2033
- def rerender_discussions_from_state(community_state, discussion_mode):
2034
- if not community_state:
2035
- return render_empty_chart("Community Discussions")
2036
- return render_discussion_combined_chart(
2037
- community_state,
2038
- discussion_mode or "both",
2039
- username=(community_state.get("username") or ""),
2040
- year=int(community_state.get("selected_year") or datetime.now().year),
2041
- )
2042
-
2043
-
2044
- # ============================================================
2045
- # Generic bucketed handlers
2046
- # ============================================================
2047
  def maybe_reset_bucketed_section(
2048
  section_key: str,
2049
  current_state,
@@ -2113,6 +1787,15 @@ def maybe_reset_bucketed_section(
2113
  )
2114
 
2115
 
 
 
 
 
 
 
 
 
 
2116
  def prepare_bucketed_fetch_ui(section_key: str, username, selected_year, profile: gr.OAuthProfile | None):
2117
  username = (username or "").strip()
2118
  if username:
@@ -2122,6 +1805,62 @@ def prepare_bucketed_fetch_ui(section_key: str, username, selected_year, profile
2122
  return render_bucketed_fetching_banner(section_key, username, selected_year, profile)
2123
 
2124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2125
  def fetch_and_render_bucketed_section(
2126
  section_key: str,
2127
  current_state,
@@ -2169,6 +1908,28 @@ def fetch_and_render_bucketed_section(
2169
  )
2170
 
2171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2172
  def rerender_bucketed_from_state(section_key: str, section_state, mode):
2173
  cfg = get_bucketed_section_config(section_key)
2174
  if not section_state:
@@ -2183,7 +1944,7 @@ def rerender_bucketed_from_state(section_key: str, section_state, mode):
2183
 
2184
 
2185
  # ============================================================
2186
- # Factory helpers
2187
  # ============================================================
2188
  def make_prepare_bucketed_fetch_ui(section_key: str):
2189
  def _fn(username, selected_year, profile: gr.OAuthProfile | None):
@@ -2824,6 +2585,10 @@ button.gh-cell {
2824
 
2825
  year_options = list(range(datetime.now().year, 2017, -1))
2826
 
 
 
 
 
2827
  with gr.Blocks(title="HF Contributions") as demo:
2828
  app_state = gr.State(value=None)
2829
  community_state = gr.State(value=None)
@@ -2936,21 +2701,21 @@ with gr.Blocks(title="HF Contributions") as demo:
2936
 
2937
  with gr.Column(elem_classes="toggles-container"):
2938
  upvotes_view_mode = gr.Radio(
2939
- choices=[(label, key) for key, label in get_bucketed_section_config("upvotes")["mode_labels"].items()],
2940
- value=get_bucketed_section_config("upvotes")["default_mode"],
2941
- label=get_bucketed_section_config("upvotes")["radio_label"],
2942
  visible="hidden",
2943
  elem_classes=["gh-toggle-radio"],
2944
  elem_id="upvotes-view-toggle",
2945
  )
2946
 
2947
- upvotes_chart = gr.HTML(render_empty_chart(get_bucketed_section_config("upvotes")["chart_title"]))
2948
 
2949
  upvotes_logs_box = gr.Textbox(
2950
  label="Upvotes Fetch Logs",
2951
  interactive=False,
2952
  lines=10,
2953
- value=get_bucketed_section_config("upvotes")["empty_logs_text"],
2954
  )
2955
 
2956
  gr.Markdown("## Likes activity")
@@ -2962,21 +2727,21 @@ with gr.Blocks(title="HF Contributions") as demo:
2962
 
2963
  with gr.Column(elem_classes="toggles-container"):
2964
  likes_view_mode = gr.Radio(
2965
- choices=[(label, key) for key, label in get_bucketed_section_config("likes")["mode_labels"].items()],
2966
- value=get_bucketed_section_config("likes")["default_mode"],
2967
- label=get_bucketed_section_config("likes")["radio_label"],
2968
  visible="hidden",
2969
  elem_classes=["gh-toggle-radio"],
2970
  elem_id="likes-view-toggle",
2971
  )
2972
 
2973
- likes_chart = gr.HTML(render_empty_chart(get_bucketed_section_config("likes")["chart_title"]))
2974
 
2975
  likes_logs_box = gr.Textbox(
2976
  label="Likes Fetch Logs",
2977
  interactive=False,
2978
  lines=10,
2979
- value=get_bucketed_section_config("likes")["empty_logs_text"],
2980
  )
2981
 
2982
  gr.Markdown("## Papers activity")
@@ -2988,21 +2753,21 @@ with gr.Blocks(title="HF Contributions") as demo:
2988
 
2989
  with gr.Column(elem_classes="toggles-container"):
2990
  papers_view_mode = gr.Radio(
2991
- choices=[(label, key) for key, label in get_bucketed_section_config("papers")["mode_labels"].items()],
2992
- value=get_bucketed_section_config("papers")["default_mode"],
2993
- label=get_bucketed_section_config("papers")["radio_label"],
2994
  visible="hidden",
2995
  elem_classes=["gh-toggle-radio"],
2996
  elem_id="papers-view-toggle",
2997
  )
2998
 
2999
- papers_chart = gr.HTML(render_empty_chart(get_bucketed_section_config("papers")["chart_title"]))
3000
 
3001
  papers_logs_box = gr.Textbox(
3002
  label="Papers Fetch Logs",
3003
  interactive=False,
3004
  lines=10,
3005
- value=get_bucketed_section_config("papers")["empty_logs_text"],
3006
  )
3007
 
3008
  repo_fetch_outputs = [
 
402
 
403
 
404
  # ============================================================
405
+ # Generic bucketed section configs
406
  # ============================================================
407
+ BUCKETED_SECTION_CONFIGS = {
408
+ "upvotes": {
409
+ "section_title": "Upvotes activity",
410
+ "chart_title": "Upvotes",
411
+ "palette_name": "purple",
412
+ "context_subject_label": "upvotes activity",
413
+ "radio_label": "Upvotes view",
414
+ "default_mode": "all",
415
+ "empty_logs_text": "Upvotes activity not fetched yet.",
416
+ "mode_labels": {
417
+ "all": "All",
418
+ "papers": "Papers",
419
+ "blog": "Blog",
420
+ "changelog": "Changelog",
421
+ },
422
+ "bucket_titles": {
423
+ "papers": "Paper Upvotes",
424
+ "blog": "Blog Upvotes",
425
+ "changelog": "Changelog Upvotes",
426
+ },
427
+ "bucket_log_labels": {
428
+ "papers": "paper upvote(s)",
429
+ "blog": "blog upvote(s)",
430
+ "changelog": "changelog upvote(s)",
431
+ },
432
+ },
433
+ "likes": {
434
+ "section_title": "Likes activity",
435
+ "chart_title": "Likes",
436
+ "palette_name": "blue",
437
+ "context_subject_label": "likes activity",
438
+ "radio_label": "Likes view",
439
+ "default_mode": "all",
440
+ "empty_logs_text": "Likes activity not fetched yet.",
441
+ "mode_labels": {
442
+ "all": "All",
443
+ "models": "Models",
444
+ "datasets": "Datasets",
445
+ "spaces": "Spaces",
446
+ },
447
+ "bucket_titles": {
448
+ "models": "Model Likes",
449
+ "datasets": "Dataset Likes",
450
+ "spaces": "Space Likes",
451
+ },
452
+ "bucket_log_labels": {
453
+ "models": "model like(s)",
454
+ "datasets": "dataset like(s)",
455
+ "spaces": "space like(s)",
456
+ },
457
+ },
458
+ "papers": {
459
+ "section_title": "Papers activity",
460
+ "chart_title": "Papers",
461
+ "palette_name": "teal",
462
+ "context_subject_label": "papers activity",
463
+ "radio_label": "Papers view",
464
+ "default_mode": "all",
465
+ "empty_logs_text": "Papers activity not fetched yet.",
466
+ "mode_labels": {
467
+ "all": "All",
468
+ "daily": "Daily Papers",
469
+ "authored": "Authored",
470
+ },
471
+ "bucket_titles": {
472
+ "daily": "Daily Papers",
473
+ "authored": "Authored Papers",
474
+ },
475
+ "bucket_log_labels": {
476
+ "daily": "daily paper submission(s)",
477
+ "authored": "authored paper(s)",
478
+ },
479
+ },
480
+ }
481
 
482
 
483
+ def get_bucketed_section_config(section_key: str):
484
+ return BUCKETED_SECTION_CONFIGS[section_key]
485
+
486
+
487
+ def build_radio_choices(mode_labels: dict[str, str]):
488
+ return [(label, key) for key, label in mode_labels.items()]
489
+
490
+
491
+ # ============================================================
492
+ # Generic bucketed recent-activity fetch
493
+ # ============================================================
494
+ def fetch_bucketed_recent_activity(
495
  username: str,
496
  selected_year: int,
497
+ cache_scope: str,
498
+ activity_type: str,
499
+ bucket_titles: dict[str, str],
500
+ bucket_log_labels: dict[str, str],
501
+ empty_logs_text: str,
502
+ classifier,
503
  ):
504
  username = (username or "").strip()
505
  selected_year = int(selected_year)
506
 
507
+ cache_key = (username.lower(), selected_year, cache_scope)
508
+ cached = get_cache(cache_key)
509
+ if cached:
510
+ return cached
 
 
 
511
 
512
  if not username:
513
+ state = empty_bucketed_activity_state(selected_year, empty_logs_text, bucket_titles)
514
+ state["fetched"] = True
515
+ state["logs"] = "No username provided."
516
+ set_cache(cache_key, state)
517
+ return state
 
 
518
 
519
+ bucket_dates = {key: [] for key in bucket_titles}
520
+ bucket_seen_ids = {key: set() for key in bucket_titles}
521
+
522
+ cursor = None
523
  skip = 0
524
  limit = 20
 
525
  pages_fetched = 0
526
 
527
  MAX_PAGES = 40
528
+ MAX_DURATION = 20
 
529
  start = time.time()
530
 
531
  logs = []
 
539
  }
540
  )
541
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
  try:
543
  while True:
544
  if time.time() - start > MAX_DURATION:
 
553
 
554
  params = {
555
  "limit": limit,
556
+ "activityType": activity_type,
557
  "feedType": "user",
558
  "entity": username,
559
  }
 
572
  payload = response.json()
573
  pages_fetched += 1
574
 
575
+ events = payload.get("recentActivity", [])
576
+ next_cursor = payload.get("cursor")
577
 
578
  if not events:
579
  logs.append("No more events.")
 
585
  if not isinstance(event, dict):
586
  continue
587
 
588
+ raw_time = event.get("time")
589
+ if not raw_time:
590
+ continue
591
+
592
+ try:
593
+ event_ts = pd.to_datetime(raw_time, utc=True)
594
+ except Exception:
595
+ continue
596
+
597
+ if event_ts.year < selected_year:
598
  stop_early = True
599
  break
600
 
601
+ if event_ts.year != selected_year:
 
602
  continue
603
 
604
+ bucket_key, unique_id = classifier(event)
605
+
606
+ if not bucket_key or not unique_id:
607
  continue
608
 
609
+ if bucket_key not in bucket_dates:
 
610
  continue
611
 
612
+ if unique_id in bucket_seen_ids[bucket_key]:
613
  continue
614
 
615
+ bucket_seen_ids[bucket_key].add(unique_id)
616
+ bucket_dates[bucket_key].append(event_ts.tz_localize(None).date())
617
 
618
  if stop_early:
619
  logs.append("Stopped early after reaching feed events older than selected year.")
 
624
  else:
625
  skip += limit
626
 
627
+ result = {
628
+ "username": username,
629
+ "selected_year": selected_year,
 
 
630
  "fetched": True,
631
+ "logs": "",
 
 
632
  }
633
 
634
+ for key, title in bucket_titles.items():
635
+ result[key] = build_fetched_bucket(
636
+ title=title,
637
+ dates=bucket_dates[key],
638
+ selected_year=selected_year,
639
+ label=bucket_log_labels[key],
640
+ )
 
641
 
642
+ logs.append(f"Fetched {pages_fetched} page(s) from recent-activity.")
643
+ logs.append(
644
+ " | ".join(
645
+ f"{bucket_titles[key]}: {len(bucket_dates[key])}"
646
+ for key in bucket_titles
647
+ )
648
+ )
649
 
650
+ result["logs"] = "\n".join(logs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
 
652
+ set_cache(cache_key, result)
653
+ return result
654
 
655
+ except Exception as e:
656
+ failed = {
657
+ "username": username,
658
+ "selected_year": selected_year,
659
+ "fetched": True,
660
+ "logs": f"{cache_scope.capitalize()} fetch failed: {str(e)}",
661
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662
 
663
+ for key, title in bucket_titles.items():
664
+ failed[key] = build_failed_bucket(title)
665
 
666
+ return failed
 
667
 
668
 
669
  # ============================================================
 
731
  return bucket_key, unique_id
732
 
733
 
734
+ BUCKETED_SECTION_RUNTIME = {
735
+ "upvotes": {
736
+ "activity_type": "upvote",
737
+ "classifier": classify_upvote_event,
738
+ },
739
+ "likes": {
740
+ "activity_type": "like",
741
+ "classifier": classify_like_event,
742
+ },
743
+ "papers": {
744
+ "activity_type": "update-paper",
745
+ "classifier": classify_paper_event,
746
+ },
747
  }
748
 
749
 
750
  def fetch_generic_bucketed_section(section_key: str, username: str, selected_year: int):
751
  cfg = get_bucketed_section_config(section_key)
752
+ runtime = BUCKETED_SECTION_RUNTIME[section_key]
753
+
754
  return fetch_bucketed_recent_activity(
755
  username=username,
756
  selected_year=selected_year,
757
  cache_scope=section_key,
758
+ activity_type=runtime["activity_type"],
759
  bucket_titles=cfg["bucket_titles"],
760
  bucket_log_labels=cfg["bucket_log_labels"],
761
  empty_logs_text=cfg["empty_logs_text"],
762
+ classifier=runtime["classifier"],
763
  )
764
 
765
 
 
773
 
774
 
775
  # ============================================================
776
+ # Community activity fetch
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777
  # ============================================================
778
+ def empty_community_state(selected_year: int):
779
+ return {
780
+ "username": "",
781
+ "selected_year": int(selected_year),
782
+ "fetched": False,
783
+ "pr_opened": empty_activity_bucket("Pull Requests"),
784
+ "pr_activity": empty_activity_bucket("PR Activity"),
785
+ "thread_opened": empty_activity_bucket("Community Threads"),
786
+ "discussion_activity": empty_activity_bucket("Community Activity"),
787
+ "logs": "Community activity not fetched yet.",
788
+ }
789
 
790
 
791
+ def fetch_user_discussion_activity(
792
  username: str,
793
+ selected_year: int,
794
+ mode: str,
795
  ):
796
  username = (username or "").strip()
797
+ selected_year = int(selected_year)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
 
799
+ title_map = {
800
+ "pr_opened": "Pull Requests",
801
+ "pr_activity": "PR Activity",
802
+ "thread_opened": "Community Threads",
803
+ "discussion_activity": "Community Activity",
804
+ }
805
+ title = title_map.get(mode, "Community")
806
 
807
+ if not username:
808
+ return {
809
+ "title": title,
810
+ "fetched": True,
811
+ "dates": [],
812
+ "count": 0,
813
+ "logs": "No username provided.",
814
+ }
815
 
816
+ dates = []
817
+ seen_ids = set()
818
+ skip = 0
819
+ limit = 20
820
+ cursor = None
821
+ pages_fetched = 0
822
 
823
+ MAX_PAGES = 40
824
+ MAX_DURATION = 25
825
+ OPEN_EVENT_EPSILON_SECONDS = 90
826
+ start = time.time()
827
 
828
+ logs = []
 
 
 
829
 
830
+ session = requests.Session()
831
+ session.headers.update(
832
+ {
833
+ "User-Agent": "hf-contributions-app/1.0",
834
+ "Accept": "application/json",
835
+ "Cache-Control": "no-cache",
836
+ }
837
+ )
838
 
839
+ def normalize_payload(payload):
840
+ if isinstance(payload, list):
841
+ return payload, None
 
842
 
843
+ if isinstance(payload, dict):
844
+ events = payload.get("recentActivity")
845
+ if isinstance(events, list):
846
+ return events, payload.get("cursor")
847
 
848
+ for key in ["items", "events", "activity"]:
849
+ value = payload.get(key)
850
+ if isinstance(value, list):
851
+ return value, payload.get("cursor")
852
 
853
+ return [], None
 
 
 
 
 
 
 
854
 
855
+ def extract_author_name(discussion: dict) -> str | None:
856
+ author = discussion.get("author")
857
+ if isinstance(author, str):
858
+ return author
859
+ if isinstance(author, dict):
860
+ for key in ["name", "username", "login"]:
861
+ value = author.get(key)
862
+ if isinstance(value, str):
863
+ return value
864
+ return None
865
 
866
+ def parse_ts(raw_value):
867
+ if not raw_value:
868
+ return None
869
+ try:
870
+ return pd.to_datetime(raw_value, utc=True)
871
+ except Exception:
872
+ return None
873
 
874
+ def event_matches_mode(event: dict, discussion: dict) -> tuple[bool, str | None]:
875
+ is_pr = bool(discussion.get("isPullRequest", False))
876
+ author_name = extract_author_name(discussion)
877
+ repo_id = event.get("repoId", "unknown-repo")
878
+ discussion_num = discussion.get("num", "unknown-num")
879
+ event_id = event.get("eventId", "unknown-event")
880
 
881
+ if mode == "pr_opened":
882
+ if not is_pr:
883
+ return False, None
884
+ if not author_name or author_name.lower() != username.lower():
885
+ return False, None
886
+ return True, f"{repo_id}::pr::{discussion_num}"
 
 
887
 
888
+ if mode == "pr_activity":
889
+ if not is_pr:
890
+ return False, None
891
+ return True, f"event::{event_id}"
892
 
893
+ if mode == "thread_opened":
894
+ if is_pr:
895
+ return False, None
896
+ if not author_name or author_name.lower() != username.lower():
897
+ return False, None
898
+ return True, f"{repo_id}::thread::{discussion_num}"
899
 
900
+ if mode == "discussion_activity":
901
+ if is_pr:
902
+ return False, None
903
+ return True, f"event::{event_id}"
 
 
 
 
904
 
905
+ return False, None
 
 
 
 
906
 
907
+ def extract_date_for_mode(event: dict, discussion: dict):
908
+ event_ts = parse_ts(event.get("time"))
909
+ created_ts = parse_ts(discussion.get("createdAt"))
910
 
911
+ if mode in {"pr_opened", "thread_opened"}:
912
+ if created_ts is None:
913
+ return None
914
+ return created_ts.tz_localize(None).date()
 
 
915
 
916
+ if mode in {"pr_activity", "discussion_activity"}:
917
+ if event_ts is None:
918
+ return None
919
+ if created_ts is not None:
920
+ delta_seconds = abs((event_ts - created_ts).total_seconds())
921
+ if delta_seconds <= OPEN_EVENT_EPSILON_SECONDS:
922
+ return None
923
+ return event_ts.tz_localize(None).date()
924
 
925
+ return None
926
 
927
+ try:
928
+ while True:
929
+ if time.time() - start > MAX_DURATION:
930
+ logs.append("Stopped due to timeout.")
931
+ break
932
 
933
+ if pages_fetched >= MAX_PAGES:
934
+ logs.append("Stopped due to max pages.")
935
+ break
936
 
937
+ recent_activity_rate_limiter.wait()
 
 
 
 
 
 
 
938
 
939
+ params = {
940
+ "limit": limit,
941
+ "activityType": "discussion",
942
+ "feedType": "user",
943
+ "entity": username,
944
+ }
945
+ if cursor:
946
+ params["cursor"] = cursor
947
+ else:
948
+ params["skip"] = skip
949
 
950
+ response = session.get(
951
+ "https://huggingface.co/api/recent-activity",
952
+ params=params,
953
+ timeout=20,
954
+ )
955
+ response.raise_for_status()
956
 
957
+ payload = response.json()
958
+ pages_fetched += 1
 
 
959
 
960
+ events, next_cursor = normalize_payload(payload)
 
961
 
962
+ if not events:
963
+ logs.append("No more events.")
964
+ break
965
 
966
+ stop_early = False
 
 
 
 
 
 
967
 
968
+ for event in events:
969
+ if not isinstance(event, dict):
970
+ continue
971
 
972
+ event_ts = parse_ts(event.get("time"))
973
+ if event_ts is not None and event_ts.year < selected_year:
974
+ stop_early = True
975
+ break
 
976
 
977
+ discussion = event.get("discussionData") or {}
978
+ if not isinstance(discussion, dict):
979
+ continue
 
980
 
981
+ keep, unique_id = event_matches_mode(event, discussion)
982
+ if not keep or not unique_id:
983
+ continue
984
 
985
+ dt = extract_date_for_mode(event, discussion)
986
+ if dt is None or dt.year != selected_year:
987
+ continue
988
 
989
+ if unique_id in seen_ids:
990
+ continue
 
 
 
 
 
 
 
 
 
991
 
992
+ seen_ids.add(unique_id)
993
+ dates.append(dt)
 
 
 
994
 
995
+ if stop_early:
996
+ logs.append("Stopped early after reaching feed events older than selected year.")
997
+ break
998
 
999
+ if next_cursor:
1000
+ cursor = next_cursor
1001
+ else:
1002
+ skip += limit
 
 
1003
 
1004
+ logs.append(f"Fetched {pages_fetched} page(s) from recent-activity.")
1005
+ logs.append(f"Found {len(dates)} unique {mode} event(s) in {selected_year}.")
1006
 
1007
+ return {
1008
+ "title": title,
1009
+ "fetched": True,
1010
+ "dates": dates,
1011
+ "count": len(dates),
1012
+ "logs": "\n".join(logs),
1013
+ }
1014
 
1015
+ except Exception as e:
1016
+ return {
1017
+ "title": title,
1018
+ "fetched": True,
1019
+ "dates": [],
1020
+ "count": 0,
1021
+ "logs": f"{title} fetch failed: {str(e)}",
1022
+ }
1023
 
1024
 
1025
+ def fetch_all_community_activity(username: str, selected_year: int):
 
 
 
 
 
 
1026
  username = (username or "").strip()
1027
  selected_year = int(selected_year)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1028
 
1029
+ cache_key = (username.lower(), selected_year, "community")
1030
+ cached = get_cache(cache_key)
1031
+ if cached:
1032
+ return cached
1033
 
1034
+ if not username:
1035
+ state = empty_community_state(selected_year)
1036
+ state["fetched"] = True
1037
+ state["logs"] = "No username provided."
1038
+ set_cache(cache_key, state)
1039
+ return state
 
 
 
 
1040
 
1041
+ pr_opened = fetch_user_discussion_activity(username, selected_year, "pr_opened")
1042
+ pr_activity = fetch_user_discussion_activity(username, selected_year, "pr_activity")
1043
+ thread_opened = fetch_user_discussion_activity(username, selected_year, "thread_opened")
1044
+ discussion_activity = fetch_user_discussion_activity(username, selected_year, "discussion_activity")
1045
 
1046
+ combined_logs = "\n\n".join(
1047
+ [
1048
+ "[Pull Requests]\n" + pr_opened["logs"],
1049
+ "[PR Activity]\n" + pr_activity["logs"],
1050
+ "[Community Threads]\n" + thread_opened["logs"],
1051
+ "[Community Activity]\n" + discussion_activity["logs"],
1052
+ ]
1053
+ )
1054
 
1055
+ result = {
1056
+ "username": username,
1057
+ "selected_year": selected_year,
1058
+ "fetched": True,
1059
+ "pr_opened": pr_opened,
1060
+ "pr_activity": pr_activity,
1061
+ "thread_opened": thread_opened,
1062
+ "discussion_activity": discussion_activity,
1063
+ "logs": combined_logs,
1064
+ }
1065
 
1066
+ set_cache(cache_key, result)
1067
+ return result
1068
 
1069
 
1070
  # ============================================================
1071
  # Render helpers
1072
  # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1073
  def render_empty_chart(title: str):
1074
  safe_title = html.escape(title)
1075
  return f"""
 
1259
 
1260
  def render_bucketed_chart(section_key: str, section_state: dict | None, mode: str, username: str, year: int):
1261
  cfg = get_bucketed_section_config(section_key)
1262
+ chart_title = cfg["chart_title"]
1263
+ default_mode = cfg["default_mode"]
1264
 
1265
  if not section_state or not section_state.get("fetched"):
1266
+ return render_empty_chart(chart_title)
1267
 
 
 
1268
  mode = mode or default_mode
1269
+ bucket_keys = list(cfg["bucket_titles"].keys())
1270
 
1271
  if mode == "all":
1272
  merged = []
1273
+ for key in bucket_keys:
1274
+ merged.extend(section_state[key]["dates"])
1275
+ subtitle = f"{len(merged)} total {chart_title.lower()} event(s) by {username} in {year}"
1276
  else:
1277
  merged = list(section_state.get(mode, {}).get("dates", []))
1278
+ bucket_title = cfg["bucket_titles"].get(mode, mode)
1279
  subtitle = f"{len(merged)} {bucket_title.lower()} by {username} in {year}"
1280
 
1281
  df = pd.DataFrame(merged, columns=["date"])
 
1283
  return render_contrib_chart(
1284
  df,
1285
  year,
1286
+ chart_title,
1287
  palette_name=cfg["palette_name"],
1288
  subtitle=subtitle,
1289
  )
 
1536
 
1537
 
1538
  # ============================================================
1539
+ # Stable return-based fetch / reset logic
1540
  # ============================================================
1541
  def prepare_repo_fetch_ui(
1542
  username,
 
1636
  return render_app_state(app_state, visibility_mode, profile)
1637
 
1638
 
 
 
 
1639
  def maybe_reset_community_section(
1640
  current_community_state,
1641
  username,
 
1718
  )
1719
 
1720
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1721
  def maybe_reset_bucketed_section(
1722
  section_key: str,
1723
  current_state,
 
1787
  )
1788
 
1789
 
1790
+ def prepare_community_fetch_ui(username, selected_year, profile: gr.OAuthProfile | None):
1791
+ username = (username or "").strip()
1792
+ if username:
1793
+ identity = validate_username(username, profile)
1794
+ if not identity.get("exists"):
1795
+ return render_community_invalid_banner(username, identity.get("error"))
1796
+ return render_community_fetching_banner(username, selected_year, profile)
1797
+
1798
+
1799
  def prepare_bucketed_fetch_ui(section_key: str, username, selected_year, profile: gr.OAuthProfile | None):
1800
  username = (username or "").strip()
1801
  if username:
 
1805
  return render_bucketed_fetching_banner(section_key, username, selected_year, profile)
1806
 
1807
 
1808
+ def fetch_and_render_community(
1809
+ current_state,
1810
+ username,
1811
+ selected_year,
1812
+ pr_mode,
1813
+ discussion_mode,
1814
+ profile: gr.OAuthProfile | None,
1815
+ ):
1816
+ username = (username or "").strip()
1817
+ selected_year = int(selected_year)
1818
+ pr_mode = pr_mode or "both"
1819
+ discussion_mode = discussion_mode or "both"
1820
+
1821
+ identity = validate_username(username, profile)
1822
+ if username and not identity.get("exists"):
1823
+ empty_state = empty_community_state(selected_year)
1824
+ empty_state["username"] = username
1825
+ empty_state["selected_year"] = selected_year
1826
+ empty_state["logs"] = identity.get("error") or f"Username or org '{username}' was not found."
1827
+ return (
1828
+ empty_state,
1829
+ render_community_invalid_banner(username, identity.get("error")),
1830
+ render_empty_chart("Pull Requests"),
1831
+ render_empty_chart("Community Discussions"),
1832
+ empty_state["logs"],
1833
+ gr.update(value="both", visible="hidden"),
1834
+ gr.update(value="both", visible="hidden"),
1835
+ )
1836
+
1837
+ state = fetch_all_community_activity(username, selected_year)
1838
+
1839
+ pr_chart = render_pr_combined_chart(
1840
+ state,
1841
+ pr_mode,
1842
+ username=username,
1843
+ year=selected_year,
1844
+ )
1845
+
1846
+ discussion_chart = render_discussion_combined_chart(
1847
+ state,
1848
+ discussion_mode,
1849
+ username=username,
1850
+ year=selected_year,
1851
+ )
1852
+
1853
+ return (
1854
+ state,
1855
+ render_community_loaded_banner(state, profile),
1856
+ pr_chart,
1857
+ discussion_chart,
1858
+ state["logs"],
1859
+ gr.update(value=pr_mode, visible=True),
1860
+ gr.update(value=discussion_mode, visible=True),
1861
+ )
1862
+
1863
+
1864
  def fetch_and_render_bucketed_section(
1865
  section_key: str,
1866
  current_state,
 
1908
  )
1909
 
1910
 
1911
+ def rerender_pr_from_state(community_state, pr_mode):
1912
+ if not community_state:
1913
+ return render_empty_chart("Pull Requests")
1914
+ return render_pr_combined_chart(
1915
+ community_state,
1916
+ pr_mode or "both",
1917
+ username=(community_state.get("username") or ""),
1918
+ year=int(community_state.get("selected_year") or datetime.now().year),
1919
+ )
1920
+
1921
+
1922
+ def rerender_discussions_from_state(community_state, discussion_mode):
1923
+ if not community_state:
1924
+ return render_empty_chart("Community Discussions")
1925
+ return render_discussion_combined_chart(
1926
+ community_state,
1927
+ discussion_mode or "both",
1928
+ username=(community_state.get("username") or ""),
1929
+ year=int(community_state.get("selected_year") or datetime.now().year),
1930
+ )
1931
+
1932
+
1933
  def rerender_bucketed_from_state(section_key: str, section_state, mode):
1934
  cfg = get_bucketed_section_config(section_key)
1935
  if not section_state:
 
1944
 
1945
 
1946
  # ============================================================
1947
+ # Factory helpers for Gradio-compatible handlers
1948
  # ============================================================
1949
  def make_prepare_bucketed_fetch_ui(section_key: str):
1950
  def _fn(username, selected_year, profile: gr.OAuthProfile | None):
 
2585
 
2586
  year_options = list(range(datetime.now().year, 2017, -1))
2587
 
2588
+ upvotes_cfg = get_bucketed_section_config("upvotes")
2589
+ likes_cfg = get_bucketed_section_config("likes")
2590
+ papers_cfg = get_bucketed_section_config("papers")
2591
+
2592
  with gr.Blocks(title="HF Contributions") as demo:
2593
  app_state = gr.State(value=None)
2594
  community_state = gr.State(value=None)
 
2701
 
2702
  with gr.Column(elem_classes="toggles-container"):
2703
  upvotes_view_mode = gr.Radio(
2704
+ choices=build_radio_choices(upvotes_cfg["mode_labels"]),
2705
+ value=upvotes_cfg["default_mode"],
2706
+ label=upvotes_cfg["radio_label"],
2707
  visible="hidden",
2708
  elem_classes=["gh-toggle-radio"],
2709
  elem_id="upvotes-view-toggle",
2710
  )
2711
 
2712
+ upvotes_chart = gr.HTML(render_empty_chart(upvotes_cfg["chart_title"]))
2713
 
2714
  upvotes_logs_box = gr.Textbox(
2715
  label="Upvotes Fetch Logs",
2716
  interactive=False,
2717
  lines=10,
2718
+ value=upvotes_cfg["empty_logs_text"],
2719
  )
2720
 
2721
  gr.Markdown("## Likes activity")
 
2727
 
2728
  with gr.Column(elem_classes="toggles-container"):
2729
  likes_view_mode = gr.Radio(
2730
+ choices=build_radio_choices(likes_cfg["mode_labels"]),
2731
+ value=likes_cfg["default_mode"],
2732
+ label=likes_cfg["radio_label"],
2733
  visible="hidden",
2734
  elem_classes=["gh-toggle-radio"],
2735
  elem_id="likes-view-toggle",
2736
  )
2737
 
2738
+ likes_chart = gr.HTML(render_empty_chart(likes_cfg["chart_title"]))
2739
 
2740
  likes_logs_box = gr.Textbox(
2741
  label="Likes Fetch Logs",
2742
  interactive=False,
2743
  lines=10,
2744
+ value=likes_cfg["empty_logs_text"],
2745
  )
2746
 
2747
  gr.Markdown("## Papers activity")
 
2753
 
2754
  with gr.Column(elem_classes="toggles-container"):
2755
  papers_view_mode = gr.Radio(
2756
+ choices=build_radio_choices(papers_cfg["mode_labels"]),
2757
+ value=papers_cfg["default_mode"],
2758
+ label=papers_cfg["radio_label"],
2759
  visible="hidden",
2760
  elem_classes=["gh-toggle-radio"],
2761
  elem_id="papers-view-toggle",
2762
  )
2763
 
2764
+ papers_chart = gr.HTML(render_empty_chart(papers_cfg["chart_title"]))
2765
 
2766
  papers_logs_box = gr.Textbox(
2767
  label="Papers Fetch Logs",
2768
  interactive=False,
2769
  lines=10,
2770
+ value=papers_cfg["empty_logs_text"],
2771
  )
2772
 
2773
  repo_fetch_outputs = [