notamitgamer

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

commit 4352a1bae1d99311c813070b7b8b8a299b1904eb
parent 637f5c6d17a09684dabbaf4fa318dcab1c7651a9
Author: Amit Dutta <mail@amit.is-a.dev>
Date:   Sat, 27 Jun 2026 10:12:56 +0530

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

diff --git a/.github/scripts/update_maintainers.py b/.github/scripts/update_maintainers.py @@ -3,14 +3,13 @@ import json import os import time from datetime import datetime, timezone -from concurrent.futures import ThreadPoolExecutor, as_completed import requests # ── Config ──────────────────────────────────────────────────────────────────── REPO = "is-a-dev/register" -REPO_START = (2020, 10) # October 2020 +REPO_START = (2020, 10) CACHE_FILE = "pr_cache.json" OUTPUT_FILE = "maintainers.json" @@ -42,11 +41,11 @@ 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}-01", f"{ny}-{nm:02d}-01" + return f"{y}-{m:02d}-01T00:00:00Z", f"{ny}-{nm:02d}-01T00:00:00Z" def all_months() -> list[tuple[int, int]]: - """All months from REPO_START up to and including current month.""" now = now_utc() months = [] y, m = REPO_START @@ -55,20 +54,9 @@ def all_months() -> list[tuple[int, int]]: y, m = next_month(y, m) return months -# ── Cache helpers ───────────────────────────────────────────────────────────── +# ── Cache ───────────────────────────────────────────────────────────────────── def load_cache() -> dict: - """ - Cache structure: - { - "USERNAME": { - "2020-10": 5, - "2020-11": 12, - ... - }, - ... - } - """ if os.path.exists(CACHE_FILE): with open(CACHE_FILE) as f: return json.load(f) @@ -78,54 +66,48 @@ def save_cache(cache: dict): with open(CACHE_FILE, "w") as f: json.dump(cache, f, indent=2) -# ── GitHub Search ───────────────────────────────────────────────────────────── +# ── GitHub Commits API ──────────────────────────────────────────────────────── -def fetch_ids(query: str) -> set[int]: - """Fetch all PR numbers matching a search query (handles pagination).""" - ids = set() - page = 1 +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( - "https://api.github.com/search/issues", - headers=HEADERS, - params={"q": query, "per_page": 100, "page": page}, - timeout=30, - ) - if r.status_code in (403, 429): - reset = int(r.headers.get("X-RateLimit-Reset", time.time() + 30)) - wait = max(reset - int(time.time()), 10) + 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) + time.sleep(wait + 2) continue - if r.status_code == 422: - return ids + 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 - data = r.json() - items = data.get("items", []) - for item in items: - ids.add(item["number"]) + items = r.json() + count += len(items) if len(items) < 100: break page += 1 - time.sleep(0.5) - return ids - - -def count_for_month(username: str, y: int, m: int) -> int: - """Count distinct closed PRs a maintainer interacted with in a given month.""" - start, end = month_range_str(y, m) - base = f"repo:{REPO} is:pr is:closed -author:{username}" - date = f"closed:{start}..{end}" + time.sleep(0.3) - rev_ids = fetch_ids(f"{base} reviewed-by:{username} {date}") - time.sleep(0.5) - com_ids = fetch_ids(f"{base} commenter:{username} {date}") - time.sleep(0.5) - - return len(rev_ids | com_ids) + return count # ── Per-maintainer update ───────────────────────────────────────────────────── @@ -138,22 +120,22 @@ def update_maintainer(username: str, cache: dict) -> dict: for (y, m) in all_months(): key = month_key(y, m) - # Always re-fetch current month (still growing). Skip all others if cached. if key in user_cache and key != cur_key: - print(f" {key}: cached={user_cache[key]} (skip)", flush=True) + print(f" {key}: {user_cache[key]} (cached)", flush=True) continue - count = count_for_month(username, y, m) + since, until = month_range_str(y, m) + count = count_commits(username, since, until) user_cache[key] = count - print(f" {key}: fetched={count}", flush=True) - - cache[username] = user_cache + print(f" {key}: {count} (fetched)", flush=True) - # Compute stats from cache - all_time = sum(user_cache.values()) + # 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) - this_month = user_cache.get(month_key(now.year, now.month), 0) + 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) @@ -175,33 +157,25 @@ def main(): raise RuntimeError("GITHUB_TOKEN not set") cache = load_cache() - print(f"Loaded cache for {len(cache)} maintainers.\n", flush=True) - - results = {} - # max_workers=2 to stay within Search API rate limits (30 req/min) - with ThreadPoolExecutor(max_workers=2) as ex: - futures = {ex.submit(update_maintainer, u, cache): u for u in MAINTAINERS} - for future in as_completed(futures): - u = futures[future] - try: - results[u] = future.result() - except Exception as e: - print(f" ✗ {u} failed: {e}", flush=True) - results[u] = { - "username": u, - "stats": {"all_time": 0, "this_month": 0, "last_month": 0}, - "last_updated": now_utc().isoformat(), - } - - # Save updated cache - save_cache(cache) - print(f"\n✓ Cache saved to {CACHE_FILE}", flush=True) - - # Write output + print(f"Loaded cache for {len(cache)} maintainer(s).\n", flush=True) + + results = {} + 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] with open(OUTPUT_FILE, "w") as f: json.dump(output, f, indent=2) - print(f"✓ Written to {OUTPUT_FILE}", flush=True) + + print(f"\n✓ 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