Cache PyPI download metrics for patrols (#3387)

Co-authored-by: LauraGPT <[email protected]>
This commit is contained in:
zhifu gao
2026-07-23 08:45:12 +08:00
committed by GitHub
co-authored by LauraGPT
parent ddd0e74ccd
commit 8c299df62b
4 changed files with 133 additions and 7 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) with GraphQL node `D_kwDOIfjis84An-Vp` in the `Announcements` category. A 2026-07-23 GraphQL schema recheck found discussion mutations `createDiscussion`, `updateDiscussion`, `closeDiscussion`, `reopenDiscussion`, `deleteDiscussion`, and discussion-comment mutations, but still no `pinDiscussion` / `unpinDiscussion`; only `pinIssue`, `pinIssueComment`, and `pinEnvironment` are exposed. Pin manually in the GitHub Discussions UI or retry if GitHub adds a discussion pin mutation.
- [x] Triage all new issues within 48 hours. On 2026-07-23, all four repositories were patrolled with the GitHub token on `ind-gpu8`: `modelscope/FunASR`, `FunAudioLLM/Fun-ASR`, `FunAudioLLM/SenseVoice`, and `modelscope/FunClip` all had `0` open pull requests; Fun-ASR, SenseVoice, and FunClip had `0` open issues. The only open FunASR issues were already triaged: #3302 is labeled `question` + `needs feedback` and is waiting for the reporter to retest the merged hotword/postprocess fix, while #3104 is labeled `enhancement` + `needs maintainer decision` and is waiting on release-scope confirmation for RL training code. No unlabelled or unowned open issue remained in the 48-hour window.
- [x] Convert top 3 support questions into docs. On 2026-07-23, the latest 80 FunASR issues were triaged into three repeat support blockers: install / hub / model-id selection (#3321, #3045, #3042, #2973, #2976), llama.cpp / GGUF / CUDA-Vulkan runtime packages (#3298, #3297, #3296, #3289, #3243), and realtime / VAD / vLLM / server output behavior (#3101, #3109, #3038, #3031, #2968, #2965). `docs/troubleshooting.md` and `docs/troubleshooting_zh.md` now start with a Top support questions section that maps those three issue clusters to the existing install, model-download, OpenAI-compatible server, WebSocket, and llama.cpp / GGUF FAQ anchors.
- [ ] 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.
- [ ] 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`. On 2026-07-23, the live PyPIStats window was refreshed at `116,407` downloads in 7 days and `442,556` downloads in 30 days through `2026-07-21`; successful runs now write `/cpfs_speech/user/zhifu.gzf/.cache/funasr-ops/pypi-downloads-funasr.json`, and failed live runs fall back to that cache with status `stale_available` instead of dropping the download metric during SSL/429 outages. GitHub/PyPI tracking is now covered; docs traffic remains unavailable until an analytics credential or export is connected.
## Release-note template
+53 -4
View File
@@ -31,6 +31,7 @@ DEFAULT_BASELINE_STARS = 31224
DEFAULT_TARGET_ADDITIONAL_STARS = 20000
DEFAULT_TARGET_DATE = "2026-09-30"
OPS_GITHUB_TOKEN_PATH = Path("/cpfs_speech/user/zhifu.gzf/.config/funasr-ops/github_token")
DEFAULT_PYPI_DOWNLOAD_CACHE = Path("/cpfs_speech/user/zhifu.gzf/.cache/funasr-ops/pypi-downloads-funasr.json")
DEFAULT_INTEGRATION_PRS = [
"huggingface/transformers#46180",
"yuekaizhang/Fun-ASR-vllm#21",
@@ -328,6 +329,45 @@ def fetch_json(url: str, headers: Optional[Dict[str, str]] = None) -> Any:
raise RuntimeError(f"GET {url} failed: {exc.reason}") from exc
def pypi_download_cache_path(package: str) -> Path:
if package == DEFAULT_PACKAGE:
return DEFAULT_PYPI_DOWNLOAD_CACHE
return DEFAULT_PYPI_DOWNLOAD_CACHE.with_name(f"pypi-downloads-{package}.json")
def write_pypi_download_cache(package: str, metrics: Dict[str, Any]) -> None:
cache_path = pypi_download_cache_path(package)
try:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_text(
json.dumps({"package": package, "metrics": metrics}, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
except OSError:
pass
def read_pypi_download_cache(package: str, reason: str) -> Optional[Dict[str, Any]]:
cache_path = pypi_download_cache_path(package)
try:
cached = json.loads(cache_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
if cached.get("package") != package:
return None
metrics = cached.get("metrics")
if not isinstance(metrics, dict):
return None
required = {"latest_date", "downloads_last_7_days", "downloads_last_30_days", "source_url"}
if not required.issubset(metrics):
return None
stale = dict(metrics)
stale["status"] = "stale_available"
stale["cache_path"] = str(cache_path)
stale["reason"] = reason
return stale
def collect_pypi_download_metrics(package: str) -> Dict[str, Any]:
url = f"https://pypistats.org/api/packages/{package}/overall?mirrors=false"
try:
@@ -342,7 +382,7 @@ def collect_pypi_download_metrics(package: str) -> Dict[str, Any]:
rows.sort(key=lambda row: row["date"])
if not rows:
raise RuntimeError("PyPIStats returned no daily download rows")
return {
metrics = {
"source": "pypistats.org",
"source_url": f"https://pypistats.org/packages/{package}",
"latest_date": rows[-1]["date"],
@@ -350,12 +390,18 @@ def collect_pypi_download_metrics(package: str) -> Dict[str, Any]:
"downloads_last_30_days": sum(row["downloads"] for row in rows[-30:]),
"status": "available",
}
write_pypi_download_cache(package, metrics)
return metrics
except Exception as exc:
reason = str(exc)
cached = read_pypi_download_cache(package, reason)
if cached:
return cached
return {
"source": "pypistats.org",
"source_url": f"https://pypistats.org/packages/{package}",
"status": "unavailable",
"reason": str(exc),
"reason": reason,
}
@@ -791,11 +837,14 @@ def collect_ecosystem_metrics(
def format_pypi_downloads(pypi: Dict[str, Any]) -> str:
downloads = pypi.get("downloads") or {}
if downloads.get("status") == "available":
if downloads.get("status") in {"available", "stale_available"}:
cache_note = ""
if downloads.get("status") == "stale_available":
cache_note = f"; cached after live API failure: {downloads.get('reason')}"
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')})"
f"(through {downloads.get('latest_date')}, {downloads.get('source_url')}{cache_note})"
)
if downloads.get("status") == "unavailable":
return f"- PyPI downloads: unavailable via {downloads.get('source', 'PyPIStats')} ({downloads.get('reason')})"
+73 -2
View File
@@ -353,8 +353,9 @@ def test_collect_ecosystem_metrics_sums_repositories_and_target_gap(monkeypatch)
]
def test_collect_pypi_download_metrics_sums_recent_windows(monkeypatch):
def test_collect_pypi_download_metrics_sums_recent_windows(monkeypatch, tmp_path):
module = load_growth_metrics_module()
monkeypatch.setattr(module, "DEFAULT_PYPI_DOWNLOAD_CACHE", tmp_path / "pypi-downloads-funasr.json")
def fake_fetch_json(url, headers=None):
assert url == "https://pypistats.org/api/packages/funasr/overall?mirrors=false"
@@ -381,10 +382,44 @@ def test_collect_pypi_download_metrics_sums_recent_windows(monkeypatch):
"downloads_last_30_days": sum(range(2, 32)),
"status": "available",
}
cached = json.loads((tmp_path / "pypi-downloads-funasr.json").read_text())
assert cached["package"] == "funasr"
assert cached["metrics"] == metrics
def test_collect_pypi_download_metrics_reports_unavailable_without_failing(monkeypatch):
def test_collect_pypi_download_metrics_uses_cache_after_live_failure(monkeypatch, tmp_path):
module = load_growth_metrics_module()
cache_path = tmp_path / "pypi-downloads-funasr.json"
monkeypatch.setattr(module, "DEFAULT_PYPI_DOWNLOAD_CACHE", cache_path)
cache_path.write_text(json.dumps({
"package": "funasr",
"metrics": {
"source": "pypistats.org",
"source_url": "https://pypistats.org/packages/funasr",
"latest_date": "2026-07-21",
"downloads_last_7_days": 116407,
"downloads_last_30_days": 442556,
"status": "available",
},
}))
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"] == "stale_available"
assert metrics["downloads_last_7_days"] == 116407
assert metrics["downloads_last_30_days"] == 442556
assert metrics["cache_path"] == str(cache_path)
assert "HTTP 429" in metrics["reason"]
def test_collect_pypi_download_metrics_reports_unavailable_without_cache(monkeypatch, tmp_path):
module = load_growth_metrics_module()
monkeypatch.setattr(module, "DEFAULT_PYPI_DOWNLOAD_CACHE", tmp_path / "missing-cache.json")
def fake_fetch_json(url, headers=None):
raise RuntimeError("HTTP 429")
@@ -441,6 +476,42 @@ def test_collect_ecosystem_metrics_calculates_daily_target(monkeypatch):
assert metrics["ecosystem"]["required_daily_average"] == 181
def test_format_ecosystem_markdown_marks_cached_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",
"downloads": {
"status": "stale_available",
"latest_date": "2026-07-21",
"downloads_last_7_days": 116407,
"downloads_last_30_days": 442556,
"source_url": "https://pypistats.org/packages/funasr",
"reason": "HTTP 429",
},
},
}
output = module.format_ecosystem_markdown(metrics)
assert "- PyPI downloads: **116,407** last 7 days; **442,556** last 30 days" in output
assert "cached after live API failure: HTTP 429" in output
def test_format_ecosystem_markdown_includes_pypi_download_windows():
module = load_growth_metrics_module()
+6
View File
@@ -377,6 +377,12 @@ def test_growth_plan_records_metrics_tracking_progress():
"`downloads_last_30_days`",
"`collect_growth_metrics.py`",
"GitHub stars, open issues, and open pull requests",
"`116,407` downloads in 7 days",
"`442,556` downloads in 30 days",
"`2026-07-21`",
"pypi-downloads-funasr.json",
"`stale_available`",
"SSL/429 outages",
"docs traffic remains unavailable until an analytics credential or export is connected",
]
for marker in required_markers: