From 8c299df62ba4af7d33e031011b4e2e21336cbea7 Mon Sep 17 00:00:00 2001 From: zhifu gao Date: Thu, 23 Jul 2026 08:45:12 +0800 Subject: [PATCH] Cache PyPI download metrics for patrols (#3387) Co-authored-by: LauraGPT --- docs/community_growth_20k.md | 2 +- scripts/collect_growth_metrics.py | 57 +++++++++++++++++++-- tests/test_collect_growth_metrics.py | 75 +++++++++++++++++++++++++++- tests/test_growth_plan_doc.py | 6 +++ 4 files changed, 133 insertions(+), 7 deletions(-) diff --git a/docs/community_growth_20k.md b/docs/community_growth_20k.md index 9b6117388..5c9557fcc 100644 --- a/docs/community_growth_20k.md +++ b/docs/community_growth_20k.md @@ -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 diff --git a/scripts/collect_growth_metrics.py b/scripts/collect_growth_metrics.py index 685a7ee64..66d1948fd 100755 --- a/scripts/collect_growth_metrics.py +++ b/scripts/collect_growth_metrics.py @@ -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')})" diff --git a/tests/test_collect_growth_metrics.py b/tests/test_collect_growth_metrics.py index 372fe86a7..09df2851e 100644 --- a/tests/test_collect_growth_metrics.py +++ b/tests/test_collect_growth_metrics.py @@ -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() diff --git a/tests/test_growth_plan_doc.py b/tests/test_growth_plan_doc.py index 594401c94..8f5988409 100644 --- a/tests/test_growth_plan_doc.py +++ b/tests/test_growth_plan_doc.py @@ -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: