Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -402,56 +402,130 @@ def build_failed_bucket(title: str):
|
|
| 402 |
|
| 403 |
|
| 404 |
# ============================================================
|
| 405 |
-
#
|
| 406 |
# ============================================================
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
"
|
| 410 |
-
"
|
| 411 |
-
"
|
| 412 |
-
"
|
| 413 |
-
"
|
| 414 |
-
"
|
| 415 |
-
"
|
| 416 |
-
"
|
| 417 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
|
| 419 |
|
| 420 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
username: str,
|
| 422 |
selected_year: int,
|
| 423 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
):
|
| 425 |
username = (username or "").strip()
|
| 426 |
selected_year = int(selected_year)
|
| 427 |
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
"discussion_activity": "Community Activity",
|
| 433 |
-
}
|
| 434 |
-
title = title_map.get(mode, "Community")
|
| 435 |
|
| 436 |
if not username:
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
"logs": "No username provided.",
|
| 443 |
-
}
|
| 444 |
|
| 445 |
-
|
| 446 |
-
|
|
|
|
|
|
|
| 447 |
skip = 0
|
| 448 |
limit = 20
|
| 449 |
-
cursor = None
|
| 450 |
pages_fetched = 0
|
| 451 |
|
| 452 |
MAX_PAGES = 40
|
| 453 |
-
MAX_DURATION =
|
| 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":
|
| 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
|
|
|
|
| 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 |
-
|
| 602 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
stop_early = True
|
| 604 |
break
|
| 605 |
|
| 606 |
-
|
| 607 |
-
if not isinstance(discussion, dict):
|
| 608 |
continue
|
| 609 |
|
| 610 |
-
|
| 611 |
-
|
|
|
|
| 612 |
continue
|
| 613 |
|
| 614 |
-
|
| 615 |
-
if dt is None or dt.year != selected_year:
|
| 616 |
continue
|
| 617 |
|
| 618 |
-
if unique_id in
|
| 619 |
continue
|
| 620 |
|
| 621 |
-
|
| 622 |
-
|
| 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 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
return {
|
| 637 |
-
"title": title,
|
| 638 |
"fetched": True,
|
| 639 |
-
"
|
| 640 |
-
"count": len(dates),
|
| 641 |
-
"logs": "\n".join(logs),
|
| 642 |
}
|
| 643 |
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
}
|
| 652 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
|
| 654 |
-
|
| 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 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 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 |
-
|
| 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 |
-
|
| 851 |
-
"upvotes":
|
| 852 |
-
|
| 853 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 864 |
bucket_titles=cfg["bucket_titles"],
|
| 865 |
bucket_log_labels=cfg["bucket_log_labels"],
|
| 866 |
empty_logs_text=cfg["empty_logs_text"],
|
| 867 |
-
classifier=
|
| 868 |
)
|
| 869 |
|
| 870 |
|
|
@@ -878,432 +773,303 @@ def empty_generic_section_state(section_key: str, selected_year: int):
|
|
| 878 |
|
| 879 |
|
| 880 |
# ============================================================
|
| 881 |
-
#
|
| 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
|
| 984 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 985 |
|
| 986 |
|
| 987 |
-
def
|
| 988 |
username: str,
|
| 989 |
-
|
|
|
|
| 990 |
):
|
| 991 |
username = (username or "").strip()
|
| 992 |
-
|
| 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 |
-
|
| 1018 |
-
|
| 1019 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1020 |
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
|
|
|
|
|
|
|
|
|
| 1026 |
|
| 1027 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1028 |
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
|
| 1034 |
-
|
| 1035 |
-
parts.append("Viewing <b>your activity</b>")
|
| 1036 |
-
else:
|
| 1037 |
-
parts.append(f"Viewing <b>{html.escape(username)}</b>")
|
| 1038 |
|
| 1039 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1040 |
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
parts.append("Public repos only")
|
| 1045 |
|
| 1046 |
-
|
|
|
|
|
|
|
|
|
|
| 1047 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1048 |
|
| 1049 |
-
|
| 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 |
-
|
| 1059 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
|
| 1061 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1062 |
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
|
|
|
|
|
|
| 1067 |
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
parts.append(str(selected_year))
|
| 1075 |
-
parts.append("Checking public repos")
|
| 1076 |
|
| 1077 |
-
|
|
|
|
|
|
|
|
|
|
| 1078 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1079 |
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
):
|
| 1085 |
-
username = (username or "").strip()
|
| 1086 |
-
selected_year = int(selected_year)
|
| 1087 |
-
viewer = getattr(profile, "username", None) if profile else None
|
| 1088 |
|
| 1089 |
-
|
| 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 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
parts.append(f"Ready to fetch community activity for <b>{html.escape(username)}</b>")
|
| 1104 |
-
parts.append(str(selected_year))
|
| 1105 |
|
| 1106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1107 |
|
|
|
|
| 1108 |
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
|
|
|
| 1113 |
|
|
|
|
|
|
|
|
|
|
| 1114 |
|
| 1115 |
-
|
| 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 |
-
|
| 1125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1126 |
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
|
|
|
| 1132 |
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
else:
|
| 1136 |
-
parts.append(f"Fetching community activity for <b>{html.escape(username)}</b>")
|
| 1137 |
|
| 1138 |
-
|
| 1139 |
-
return render_status_badge(" · ".join(parts))
|
| 1140 |
|
|
|
|
|
|
|
|
|
|
| 1141 |
|
| 1142 |
-
|
| 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 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
parts.append("Not logged in")
|
| 1159 |
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
parts.append(f"Viewing community activity for <b>{html.escape(username)}</b>")
|
| 1164 |
|
| 1165 |
-
|
| 1166 |
-
|
|
|
|
| 1167 |
|
|
|
|
|
|
|
|
|
|
| 1168 |
|
| 1169 |
-
|
| 1170 |
-
|
| 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 |
-
|
| 1182 |
-
|
| 1183 |
-
parts.append(f"Logged in as <b>{html.escape(viewer)}</b>")
|
| 1184 |
-
else:
|
| 1185 |
-
parts.append("Not logged in")
|
| 1186 |
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
parts.append(f"Ready to fetch {subject} for <b>{html.escape(username)}</b>")
|
| 1196 |
-
parts.append(str(selected_year))
|
| 1197 |
|
| 1198 |
-
|
|
|
|
| 1199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1200 |
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
|
| 1205 |
-
|
|
|
|
|
|
|
|
|
|
| 1206 |
|
| 1207 |
|
| 1208 |
-
def
|
| 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 |
-
|
| 1239 |
-
|
| 1240 |
-
|
| 1241 |
-
|
| 1242 |
-
)
|
| 1243 |
-
|
| 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 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
|
| 1254 |
-
|
| 1255 |
-
|
| 1256 |
-
|
| 1257 |
-
|
| 1258 |
-
|
|
|
|
|
|
|
|
|
|
| 1259 |
|
| 1260 |
-
|
| 1261 |
-
|
| 1262 |
-
|
| 1263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1264 |
|
| 1265 |
-
|
| 1266 |
-
return
|
| 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(
|
| 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
|
| 1507 |
-
merged.extend(section_state[
|
| 1508 |
-
subtitle = f"{len(merged)} total {
|
| 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 |
-
|
| 1520 |
palette_name=cfg["palette_name"],
|
| 1521 |
subtitle=subtitle,
|
| 1522 |
)
|
|
@@ -1769,7 +1536,7 @@ def render_app_state(
|
|
| 1769 |
|
| 1770 |
|
| 1771 |
# ============================================================
|
| 1772 |
-
#
|
| 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=
|
| 2940 |
-
value=
|
| 2941 |
-
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(
|
| 2948 |
|
| 2949 |
upvotes_logs_box = gr.Textbox(
|
| 2950 |
label="Upvotes Fetch Logs",
|
| 2951 |
interactive=False,
|
| 2952 |
lines=10,
|
| 2953 |
-
value=
|
| 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=
|
| 2966 |
-
value=
|
| 2967 |
-
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(
|
| 2974 |
|
| 2975 |
likes_logs_box = gr.Textbox(
|
| 2976 |
label="Likes Fetch Logs",
|
| 2977 |
interactive=False,
|
| 2978 |
lines=10,
|
| 2979 |
-
value=
|
| 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=
|
| 2992 |
-
value=
|
| 2993 |
-
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(
|
| 3000 |
|
| 3001 |
papers_logs_box = gr.Textbox(
|
| 3002 |
label="Papers Fetch Logs",
|
| 3003 |
interactive=False,
|
| 3004 |
lines=10,
|
| 3005 |
-
value=
|
| 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 = [
|