Track PyPI download windows

Add PyPIStats 7-day and 30-day download totals to the growth patrol while keeping the run tolerant of PyPIStats rate limits.
This commit is contained in:
zhifu gao
2026-07-23 07:56:07 +08:00
committed by GitHub
parent 874330a1f7
commit 9ec76b59ac
4 changed files with 146 additions and 1 deletions
+1 -1
View File
@@ -370,7 +370,7 @@ Paste the Markdown output into launch notes, weekly community updates, or releas
- [ ] Pin a GitHub discussion for the release. The release discussion exists at [`modelscope/FunASR#3376`](https://github.com/modelscope/FunASR/discussions/3376), but the GitHub GraphQL API currently exposes `createDiscussion` and `updateDiscussion` but no discussion pin mutation; pin manually in the GitHub Discussions UI or retry if GitHub exposes an API for discussion pinning.
- [ ] Triage all new issues within 48 hours.
- [ ] Convert top 3 support questions into docs.
- [ ] Track stars, PyPI downloads, issue volume, and docs traffic.
- [ ] Track stars, PyPI downloads, issue volume, and docs traffic. `collect_growth_metrics.py` now tracks GitHub stars, open issues, and open pull requests for the four core repositories and adds PyPIStats download windows for `funasr` as `downloads_last_7_days` and `downloads_last_30_days` when `pypistats.org` is reachable. The script keeps the patrol usable when PyPIStats is rate-limited by reporting downloads as unavailable instead of failing the run. GitHub/PyPI tracking is now covered; docs traffic remains unavailable until an analytics credential or export is connected.
## Release-note template
+48
View File
@@ -328,6 +328,37 @@ def fetch_json(url: str, headers: Optional[Dict[str, str]] = None) -> Any:
raise RuntimeError(f"GET {url} failed: {exc.reason}") from exc
def collect_pypi_download_metrics(package: str) -> Dict[str, Any]:
url = f"https://pypistats.org/api/packages/{package}/overall?mirrors=false"
try:
payload = fetch_json(url, {"User-Agent": "funasr-growth-metrics"})
rows = [
row
for row in payload.get("data", [])
if row.get("category") == "without_mirrors"
and row.get("date")
and isinstance(row.get("downloads"), int)
]
rows.sort(key=lambda row: row["date"])
if not rows:
raise RuntimeError("PyPIStats returned no daily download rows")
return {
"source": "pypistats.org",
"source_url": f"https://pypistats.org/packages/{package}",
"latest_date": rows[-1]["date"],
"downloads_last_7_days": sum(row["downloads"] for row in rows[-7:]),
"downloads_last_30_days": sum(row["downloads"] for row in rows[-30:]),
"status": "available",
}
except Exception as exc:
return {
"source": "pypistats.org",
"source_url": f"https://pypistats.org/packages/{package}",
"status": "unavailable",
"reason": str(exc),
}
def github_headers() -> Dict[str, str]:
headers = {
"Accept": "application/vnd.github+json",
@@ -710,6 +741,7 @@ def collect_metrics(repo: str, package: str) -> Dict[str, Any]:
"version": pypi.get("info", {}).get("version"),
"summary": pypi.get("info", {}).get("summary"),
"project_url": pypi.get("info", {}).get("project_url"),
"downloads": collect_pypi_download_metrics(package),
},
}
return metrics
@@ -752,10 +784,24 @@ def collect_ecosystem_metrics(
"version": pypi.get("info", {}).get("version"),
"summary": pypi.get("info", {}).get("summary"),
"project_url": pypi.get("info", {}).get("project_url"),
"downloads": collect_pypi_download_metrics(package),
},
}
def format_pypi_downloads(pypi: Dict[str, Any]) -> str:
downloads = pypi.get("downloads") or {}
if downloads.get("status") == "available":
return (
f"- PyPI downloads: **{downloads.get('downloads_last_7_days', 0):,}** last 7 days; "
f"**{downloads.get('downloads_last_30_days', 0):,}** last 30 days "
f"(through {downloads.get('latest_date')}, {downloads.get('source_url')})"
)
if downloads.get("status") == "unavailable":
return f"- PyPI downloads: unavailable via {downloads.get('source', 'PyPIStats')} ({downloads.get('reason')})"
return "- PyPI downloads: n/a"
def format_markdown(metrics: Dict[str, Any], star_goal: int) -> str:
github = metrics["github"]
pypi = metrics["pypi"]
@@ -772,6 +818,7 @@ def format_markdown(metrics: Dict[str, Any], star_goal: int) -> str:
if github.get("open_pull_requests") is not None
else "- Open pull requests: n/a",
f"- PyPI package: **{pypi.get('package')} {pypi.get('version')}**",
format_pypi_downloads(pypi),
f"- Last GitHub push: `{github.get('pushed_at')}`",
"",
"## Links",
@@ -797,6 +844,7 @@ def format_ecosystem_markdown(metrics: Dict[str, Any]) -> str:
f"- Target date: **{ecosystem['target_date']}** ({ecosystem['days_remaining']:,} days remaining)",
f"- Required daily average: **{ecosystem['required_daily_average']:,}** stars/day",
f"- PyPI package: **{pypi.get('package')} {pypi.get('version')}**",
format_pypi_downloads(pypi),
"",
"## Repositories",
"",
+81
View File
@@ -353,6 +353,51 @@ def test_collect_ecosystem_metrics_sums_repositories_and_target_gap(monkeypatch)
]
def test_collect_pypi_download_metrics_sums_recent_windows(monkeypatch):
module = load_growth_metrics_module()
def fake_fetch_json(url, headers=None):
assert url == "https://pypistats.org/api/packages/funasr/overall?mirrors=false"
return {
"data": [
{
"category": "without_mirrors",
"date": f"2026-07-{day:02d}",
"downloads": day,
}
for day in range(1, 32)
]
}
monkeypatch.setattr(module, "fetch_json", fake_fetch_json)
metrics = module.collect_pypi_download_metrics("funasr")
assert metrics == {
"source": "pypistats.org",
"source_url": "https://pypistats.org/packages/funasr",
"latest_date": "2026-07-31",
"downloads_last_7_days": sum(range(25, 32)),
"downloads_last_30_days": sum(range(2, 32)),
"status": "available",
}
def test_collect_pypi_download_metrics_reports_unavailable_without_failing(monkeypatch):
module = load_growth_metrics_module()
def fake_fetch_json(url, headers=None):
raise RuntimeError("HTTP 429")
monkeypatch.setattr(module, "fetch_json", fake_fetch_json)
metrics = module.collect_pypi_download_metrics("funasr")
assert metrics["status"] == "unavailable"
assert metrics["source"] == "pypistats.org"
assert "HTTP 429" in metrics["reason"]
def test_collect_ecosystem_metrics_calculates_daily_target(monkeypatch):
module = load_growth_metrics_module()
@@ -396,6 +441,42 @@ def test_collect_ecosystem_metrics_calculates_daily_target(monkeypatch):
assert metrics["ecosystem"]["required_daily_average"] == 181
def test_format_ecosystem_markdown_includes_pypi_download_windows():
module = load_growth_metrics_module()
metrics = {
"collected_at_utc": "2026-07-23T00:00:00+00:00",
"ecosystem": {
"repositories": [],
"total_stars": 35787,
"baseline_stars": 31224,
"target_additional_stars": 20000,
"added_stars": 4563,
"remaining_to_target": 15437,
"target_date": "2026-09-30",
"days_remaining": 70,
"required_daily_average": 221,
},
"pypi": {
"package": "funasr",
"version": "1.3.26",
"project_url": "https://pypi.org/project/funasr/",
"downloads": {
"status": "available",
"latest_date": "2026-07-22",
"downloads_last_7_days": 70123,
"downloads_last_30_days": 301234,
"source_url": "https://pypistats.org/packages/funasr",
},
},
}
output = module.format_ecosystem_markdown(metrics)
assert "- PyPI downloads: **70,123** last 7 days; **301,234** last 30 days" in output
assert "through 2026-07-22" in output
def test_collect_integration_metrics_summarizes_pull_request_checks(monkeypatch):
module = load_growth_metrics_module()
+16
View File
@@ -233,3 +233,19 @@ def test_growth_plan_records_homepage_entrypoint_completion():
assert marker in text
assert "[ ] Update homepage hero and docs entry points" not in text
def test_growth_plan_records_metrics_tracking_progress():
text = PLAN.read_text()
required_markers = [
"[ ] Track stars, PyPI downloads, issue volume, and docs traffic.",
"PyPIStats",
"`downloads_last_7_days`",
"`downloads_last_30_days`",
"`collect_growth_metrics.py`",
"GitHub stars, open issues, and open pull requests",
"docs traffic remains unavailable until an analytics credential or export is connected",
]
for marker in required_markers:
assert marker in text