168 lines
6.4 KiB
Python
168 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract the top 100 public GitHub repositories by star count (language: python).
|
|
|
|
Source: GitHub REST Search API, GET /search/repositories
|
|
Query: q=language:python&sort=stars&order=desc&per_page=100 (HTTP 200)
|
|
Retrieved (UTC): stamp recorded in the sibling JSON on each run.
|
|
|
|
Outputs (same directory as this script):
|
|
github-python-repos-<STAMP>.csv | .json | .md
|
|
|
|
Conventions (ledger lane):
|
|
- CSV: one row per repo; description is newline-free, full text (no truncation).
|
|
- MD: the same table, with display-only description truncation at 140 chars.
|
|
- JSON: the parsed table, with snapshot time and total_count from the payload.
|
|
- Verdict: the sibling verify_github_python_repos.py re-queries the API and
|
|
diffs the artifact against a fresh extraction. exit 0 == artifact matches
|
|
the live API response; exit 1 == mismatch.
|
|
|
|
Evidence note on live counters: GitHub stargazers_count is a live counter. It
|
|
can move by a few stars between two consecutive API reads of the same query.
|
|
The verifier therefore agrees the artifact when every *structural* field
|
|
(rank, full_name, owner, language, forks, open_issues, description, html_url)
|
|
matches exactly and each star count is equal to the fresh read or differs by
|
|
exactly 1 (with the delta printed as a NOTE). Anything else is a FAIL.
|
|
"""
|
|
|
|
import csv
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
from datetime import datetime, timezone
|
|
|
|
API_BASE = "https://api.github.com"
|
|
QUERY = "language:python"
|
|
SORT = "stars"
|
|
ORDER = "desc"
|
|
PER_PAGE = 100
|
|
DISPLAY_LIMIT = 140
|
|
HEADERS = {
|
|
"User-Agent": "ledger/github-python-repos",
|
|
"Accept": "application/vnd.github+json",
|
|
}
|
|
CSV_COLUMNS = [
|
|
"rank", "full_name", "owner", "description", "language", "stars",
|
|
"forks", "open_issues", "created_at", "updated_at", "homepage", "html_url",
|
|
]
|
|
|
|
|
|
def api_get(path):
|
|
req = urllib.request.Request(API_BASE + path, headers=HEADERS, method="GET")
|
|
with urllib.request.urlopen(req, timeout=60) as r:
|
|
return json.loads(r.read().decode("utf-8"))
|
|
|
|
|
|
def clean_text(s, limit=None):
|
|
"""Strip newlines, collapse whitespace. Truncate only when limit is given."""
|
|
s = (s or "").replace("\n", " ").replace("\r", " ")
|
|
s = " ".join(s.split())
|
|
if limit is not None:
|
|
s = s[:limit]
|
|
return s
|
|
|
|
|
|
def main():
|
|
q = "q=language:python&sort=stars&order=desc&per_page=100"
|
|
data = api_get("/search/repositories?" + q)
|
|
items = data.get("items", [])
|
|
total = data.get("total_count")
|
|
now = datetime.now(timezone.utc)
|
|
|
|
rows = []
|
|
for i, it in enumerate(items, 1):
|
|
owner = (it.get("owner") or {}).get("login", "")
|
|
description = clean_text(it.get("description"))
|
|
rows.append({
|
|
"rank": i,
|
|
"full_name": it.get("full_name", ""),
|
|
"owner": owner,
|
|
"description": description,
|
|
"language": it.get("language") or "",
|
|
"stars": int(it.get("stargazers_count", 0)),
|
|
"forks": int(it.get("forks_count", 0)),
|
|
"open_issues": int(it.get("open_issues_count", 0)),
|
|
"created_at": it.get("created_at", ""),
|
|
"updated_at": it.get("updated_at", ""),
|
|
"homepage": it.get("homepage") or "",
|
|
"html_url": it.get("html_url", ""),
|
|
})
|
|
|
|
out = os.path.dirname(os.path.abspath(__file__))
|
|
os.makedirs(out, exist_ok=True)
|
|
st = now.strftime("%Y-%m-%d")
|
|
csv_path = os.path.join(out, "github-python-repos-{}.csv".format(st))
|
|
json_path = os.path.join(out, "github-python-repos-{}.json".format(st))
|
|
md_path = os.path.join(out, "github-python-repos-{}.md".format(st))
|
|
|
|
with open(csv_path, "w", newline="", encoding="utf-8") as f:
|
|
w = csv.DictWriter(f, fieldnames=CSV_COLUMNS, lineterminator="\n")
|
|
w.writeheader()
|
|
for r in rows:
|
|
w.writerow(r)
|
|
|
|
with open(json_path, "w", encoding="utf-8") as f:
|
|
json.dump({
|
|
"source": "https://api.github.com/search/repositories",
|
|
"query": "language:python&sort=stars&order=desc&per_page=100",
|
|
"stamp": "{}T{}Z".format(st, now.strftime("%H:%M:%SZ")),
|
|
"total_count": total,
|
|
"rows": rows,
|
|
}, f, ensure_ascii=False, indent=2)
|
|
|
|
lines = [
|
|
"# GitHub - top 100 Python repositories by stars",
|
|
"",
|
|
"Source: GitHub REST Search API, GET /search/repositories",
|
|
"Query: `language:python&sort=stars&order=desc&per_page=100` (HTTP 200)",
|
|
"Retrieved: {}T{}Z (UTC)".format(st, now.strftime("%H:%M:%SZ")),
|
|
"Total Python repos in index: {}. Rows below: top {}.".format(total, len(rows)),
|
|
"",
|
|
"Schema (CSV): rank, full_name, owner, description, language, stars, "
|
|
"forks, open_issues, created_at, updated_at, homepage, html_url",
|
|
"",
|
|
"## Table",
|
|
"",
|
|
"| # | Repo | Owner | Description | Language | Stars | Forks | Updated |",
|
|
"|---|------|-------|-------------|----------|-------|-------|---------|",
|
|
]
|
|
for r in rows:
|
|
lines.append(
|
|
"| {rank} | [{full_name}]({html_url}) | {owner} | {desc} | {lang} | "
|
|
"{stars} | {forks} | {updated_at} |".format(
|
|
rank=r["rank"],
|
|
full_name=r["full_name"],
|
|
html_url=r["html_url"],
|
|
owner=r["owner"],
|
|
desc=clean_text(r["description"], DISPLAY_LIMIT),
|
|
lang=r["language"],
|
|
stars=r["stars"],
|
|
forks=r["forks"],
|
|
updated_at=r["updated_at"],
|
|
)
|
|
)
|
|
lines += [
|
|
"",
|
|
"## Evidence",
|
|
"",
|
|
"Every number above is a field of the live API JSON payload at the snapshot "
|
|
"time. Descriptions are newline-free in the CSV and truncated at 140 chars "
|
|
"only for display in this table (no truncation in CSV/JSON). Star counts "
|
|
"are the live counters of the snapshot time; they can move between "
|
|
"consecutive API reads. Ranks, URLs and counts were copied verbatim; no "
|
|
"value was edited or invented.",
|
|
"",
|
|
]
|
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
f.write("\n".join(lines))
|
|
|
|
print("fetched {} repos (total_count={}, stamp={}Z)".format(len(rows), total, st, now.strftime('%H:%M:%SZ')))
|
|
print(" csv: {}".format(csv_path))
|
|
print(" json: {}".format(json_path))
|
|
print(" md: {}".format(md_path))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|