notamitgamer

It's my profile man, there is ...
Log | Files | Refs | README

commit 2fa0b5f45d8893140b7f47ed87c483a5f58186f7
parent 4352a1bae1d99311c813070b7b8b8a299b1904eb
Author: Amit Dutta <mail@amit.is-a.dev>
Date:   Sat, 27 Jun 2026 10:19:22 +0530

Refactor update_maintainers.py for improved clarity
Diffstat:
M.github/scripts/update_maintainers.py | 214++++++++++++++++++++++++++++++++++++-------------------------------------------
1 file changed, 97 insertions(+), 117 deletions(-)

diff --git a/.github/scripts/update_maintainers.py b/.github/scripts/update_maintainers.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 + import json import os import time @@ -9,7 +10,6 @@ import requests # ── Config ──────────────────────────────────────────────────────────────────── REPO = "is-a-dev/register" -REPO_START = (2020, 10) CACHE_FILE = "pr_cache.json" OUTPUT_FILE = "maintainers.json" @@ -26,33 +26,51 @@ HEADERS = { "Authorization": f"Bearer {GITHUB_TOKEN}", } -# ── Date helpers ────────────────────────────────────────────────────────────── +# ── Helpers ─────────────────────────────────────────────────────────────────── def now_utc() -> datetime: return datetime.now(timezone.utc) -def next_month(y: int, m: int) -> tuple[int, int]: - return (y + 1, 1) if m == 12 else (y, m + 1) -def prev_month(y: int, m: int) -> tuple[int, int]: - return (y - 1, 12) if m == 1 else (y, m - 1) +def fetch_contributor_stats() -> list: + """ + Fetch /stats/contributors. GitHub computes this asynchronously — + if it returns 202, we wait and retry until we get 200. + """ + url = f"https://api.github.com/repos/{REPO}/stats/contributors" + for attempt in range(10): + r = requests.get(url, headers=HEADERS, timeout=30) + if r.status_code == 202: + print(f" GitHub is computing stats, waiting 5s … (attempt {attempt+1})", flush=True) + time.sleep(5) + continue + if r.status_code == 204: + print(" No stats available yet.", flush=True) + return [] + r.raise_for_status() + return r.json() + raise RuntimeError("GitHub stats endpoint kept returning 202 after 10 attempts") -def month_key(y: int, m: int) -> str: - return f"{y}-{m:02d}" -def month_range_str(y: int, m: int) -> tuple[str, str]: - """Returns ISO timestamps for start/end of a month.""" - ny, nm = next_month(y, m) - return f"{y}-{m:02d}-01T00:00:00Z", f"{ny}-{nm:02d}-01T00:00:00Z" +def week_to_month_key(ts: int) -> str: + """Convert a Unix week timestamp to YYYY-MM key of the month it falls in.""" + dt = datetime.fromtimestamp(ts, tz=timezone.utc) + return f"{dt.year}-{dt.month:02d}" -def all_months() -> list[tuple[int, int]]: + +def this_month_key() -> str: now = now_utc() - months = [] - y, m = REPO_START - while (y, m) <= (now.year, now.month): - months.append((y, m)) - y, m = next_month(y, m) - return months + return f"{now.year}-{now.month:02d}" + + +def last_month_key() -> str: + now = now_utc() + y, m = now.year, now.month + if m == 1: + y, m = y - 1, 12 + else: + m -= 1 + return f"{y}-{m:02d}" # ── Cache ───────────────────────────────────────────────────────────────────── @@ -62,94 +80,11 @@ def load_cache() -> dict: return json.load(f) return {} + def save_cache(cache: dict): with open(CACHE_FILE, "w") as f: json.dump(cache, f, indent=2) -# ── GitHub Commits API ──────────────────────────────────────────────────────── - -def count_commits(username: str, since: str, until: str) -> int: - """Count commits by a user in a date range using pagination.""" - url = f"https://api.github.com/repos/{REPO}/commits" - count = 0 - page = 1 - while True: - while True: - r = requests.get(url, headers=HEADERS, params={ - "author": username, - "since": since, - "until": until, - "per_page": 100, - "page": page, - }, timeout=30) - - remaining = int(r.headers.get("X-RateLimit-Remaining", 10)) - reset_at = int(r.headers.get("X-RateLimit-Reset", time.time() + 60)) - - if r.status_code in (403, 429) or remaining == 0: - wait = max(reset_at - int(time.time()), 5) - print(f" Rate limited – sleeping {wait}s …", flush=True) - time.sleep(wait + 2) - continue - - r.raise_for_status() - - if remaining < 10: - wait = max(reset_at - int(time.time()), 5) - print(f" Low requests ({remaining} left) – sleeping {wait}s …", flush=True) - time.sleep(wait + 2) - break - - items = r.json() - count += len(items) - if len(items) < 100: - break - page += 1 - time.sleep(0.3) - - return count - -# ── Per-maintainer update ───────────────────────────────────────────────────── - -def update_maintainer(username: str, cache: dict) -> dict: - print(f"\n── {username} ──", flush=True) - - user_cache = cache.get(username, {}) - now = now_utc() - cur_key = month_key(now.year, now.month) - - for (y, m) in all_months(): - key = month_key(y, m) - if key in user_cache and key != cur_key: - print(f" {key}: {user_cache[key]} (cached)", flush=True) - continue - - since, until = month_range_str(y, m) - count = count_commits(username, since, until) - user_cache[key] = count - print(f" {key}: {count} (fetched)", flush=True) - - # Save after every month so progress isn't lost on interruption - cache[username] = user_cache - save_cache(cache) - - py, pm = prev_month(now.year, now.month) - all_time = sum(user_cache.values()) - this_month = user_cache.get(cur_key, 0) - last_month = user_cache.get(month_key(py, pm), 0) - - print(f" ✓ all={all_time} this_month={this_month} last_month={last_month}", flush=True) - - return { - "username": username, - "stats": { - "all_time": all_time, - "this_month": this_month, - "last_month": last_month, - }, - "last_updated": now_utc().isoformat(), - } - # ── Main ────────────────────────────────────────────────────────────────────── def main(): @@ -157,25 +92,70 @@ def main(): raise RuntimeError("GITHUB_TOKEN not set") cache = load_cache() - print(f"Loaded cache for {len(cache)} maintainer(s).\n", flush=True) + print("Fetching contributor stats (single API call) …", flush=True) + stats = fetch_contributor_stats() + + if not stats: + print("No stats returned, aborting.", flush=True) + return + + maintainers_lower = {m.lower(): m for m in MAINTAINERS} + this_key = this_month_key() + last_key = last_month_key() + + # Build monthly totals per maintainer from the weekly data + # weekly_data: { "username": { "YYYY-MM": total_commits } } + weekly_data = {} + for contributor in stats: + login = (contributor.get("author") or {}).get("login", "") + if not login or login.lower() not in maintainers_lower: + continue + + canonical = maintainers_lower[login.lower()] + monthly = {} + for week in contributor.get("weeks", []): + commits = week.get("c", 0) + if commits == 0: + continue + mk = week_to_month_key(week["w"]) + monthly[mk] = monthly.get(mk, 0) + commits + + weekly_data[canonical] = monthly + print(f" {canonical}: {sum(monthly.values())} total commits across {len(monthly)} active months", flush=True) - results = {} + # Merge into cache — update only months that changed + for username, monthly in weekly_data.items(): + user_cache = cache.get(username, {}) + for mk, count in monthly.items(): + user_cache[mk] = count # overwrite (stats endpoint is authoritative) + cache[username] = user_cache + + save_cache(cache) + print(f"\n✓ Cache saved to {CACHE_FILE}", flush=True) + + # Build output + output = [] for username in MAINTAINERS: - try: - results[username] = update_maintainer(username, cache) - except Exception as e: - print(f" ✗ {username} failed: {e}", flush=True) - results[username] = { - "username": username, - "stats": {"all_time": 0, "this_month": 0, "last_month": 0}, - "last_updated": now_utc().isoformat(), - } - - output = [results[u] for u in MAINTAINERS if u in results] + user_cache = cache.get(username, {}) + all_time = sum(user_cache.values()) + this_month = user_cache.get(this_key, 0) + last_month = user_cache.get(last_key, 0) + + print(f" {username}: all={all_time} this_month={this_month} last_month={last_month}", flush=True) + output.append({ + "username": username, + "stats": { + "all_time": all_time, + "this_month": this_month, + "last_month": last_month, + }, + "last_updated": now_utc().isoformat(), + }) + with open(OUTPUT_FILE, "w") as f: json.dump(output, f, indent=2) - print(f"\n✓ Written to {OUTPUT_FILE}", flush=True) + print(f"✓ Written to {OUTPUT_FILE}", flush=True) if __name__ == "__main__":
© notamitgamer • Site Built: 2026-07-21 13:58:23 UTC • git-mirror commit: 1037f62 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror