-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexport_gh_starred.py
More file actions
90 lines (77 loc) · 2.98 KB
/
Copy pathexport_gh_starred.py
File metadata and controls
90 lines (77 loc) · 2.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
"""Export a GitHub user's starred repositories to a compact JSON backup.
Usage: python3.14 export_gh_starred.py USERNAME
Set GITHUB_TOKEN to increase the GitHub API rate limit. Requires ``requests``.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from collections.abc import Mapping
from pathlib import Path
import requests
PER_PAGE = 100
REPOSITORY_KEYS = (
"name", "full_name", "html_url", "url", "description", "created_at",
"watchers_count", "stargazers_count", "language", "updated_at",
"pushed_at", "topics", "forks",
)
def filter_repo(repo: Mapping[str, object]) -> dict[str, object | None]:
"""Keep the fields needed by the backup format."""
filtered = {key: repo.get(key) for key in REPOSITORY_KEYS}
license_data = repo.get("license")
filtered["license_name"] = (
license_data.get("name") if isinstance(license_data, dict) else None
)
return filtered
def fetch_starred_repos(
username: str,
*,
token: str | None = None,
session: requests.Session | None = None,
) -> list[dict[str, object | None]]:
"""Fetch all starred repositories for *username*."""
client = session or requests.Session()
headers = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
if token:
headers["Authorization"] = f"Bearer {token}"
repositories: list[dict[str, object | None]] = []
page = 1
while True:
response = client.get(
f"https://api.github.com/users/{username}/starred",
params={"per_page": PER_PAGE, "page": page},
headers=headers,
timeout=30,
)
response.raise_for_status()
data = response.json()
if not isinstance(data, list):
raise ValueError("GitHub returned an unexpected response")
if not data:
break
repositories.extend(filter_repo(item) for item in data if isinstance(item, dict))
print(f"Downloaded pages: {page}; repositories: {len(repositories)}")
page += 1
time.sleep(0.5)
return repositories
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Export a user's starred GitHub repositories.")
parser.add_argument("username", help="GitHub username")
parser.add_argument("-o", "--output", type=Path, help="output JSON path")
args = parser.parse_args(argv)
output = args.output or Path(f"{args.username}_starred_backup.json")
try:
repositories = fetch_starred_repos(args.username, token=os.getenv("GITHUB_TOKEN"))
output.write_text(
json.dumps(repositories, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
except (OSError, ValueError, requests.RequestException) as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
print(f"Done. Saved {len(repositories)} repositories to {output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())