feat(metrics): publish registry summary (#275)
This commit is contained in:
@@ -43,7 +43,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add metrics/comfy-registry-downloads.csv metrics/README.md
|
||||
git add metrics/comfy-registry-downloads.csv metrics/README.md metrics/summary.json
|
||||
if git diff --cached --quiet; then
|
||||
echo "No metrics changes to commit."
|
||||
exit 0
|
||||
|
||||
@@ -13,6 +13,10 @@ The format is inspired by Keep a Changelog and SemVer.
|
||||
commit SHA, and enabled one grouped weekly Dependabot update for those pins.
|
||||
|
||||
### Added
|
||||
- **Registry metrics summary.** The daily Comfy Registry collector now writes
|
||||
`metrics/summary.json` beside its CSV history, giving downstream consumers a
|
||||
small stable contract for the v0.3.0 downloads, daily-average, peak-day, and
|
||||
latest-version summary.
|
||||
- **Independent output type for published setups.** A `Koolook_PublishRouter`
|
||||
is auto-detected as the setup's **output selector**: whichever writer branches
|
||||
you wire become the app's "Output type" options, and the external user can pick
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Contract tests for the public Registry metrics summary."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = REPO_ROOT / "tools" / "collect_registry_metrics.py"
|
||||
SPEC = importlib.util.spec_from_file_location("collect_registry_metrics", SCRIPT)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
collector = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = collector
|
||||
SPEC.loader.exec_module(collector)
|
||||
|
||||
|
||||
def _row(date: str, downloads: int, delta: int, version: str = "0.4.4") -> dict[str, str]:
|
||||
return {
|
||||
"date": date,
|
||||
"timestamp_utc": f"{date}T08:00:00Z",
|
||||
"downloads_total": str(downloads),
|
||||
"daily_delta": str(delta),
|
||||
"rolling_7d_avg": "0.0",
|
||||
"latest_version": version,
|
||||
"latest_status": "Active",
|
||||
"pending_versions": "",
|
||||
"active_versions": version,
|
||||
}
|
||||
|
||||
|
||||
def test_summary_matches_the_v030_landing_page_contract():
|
||||
rows = [
|
||||
_row("2026-08-04", 25315, 15),
|
||||
_row("2026-08-05", 26242, 927),
|
||||
_row("2026-08-06", 26870, 628),
|
||||
]
|
||||
|
||||
assert collector._make_summary(rows) == {
|
||||
"asOf": "2026-08-06",
|
||||
"sinceVersion": "0.3.0",
|
||||
"downloadsSince": 26775,
|
||||
"days": 90,
|
||||
"avgPerDay": 297.5,
|
||||
"peakDate": "2026-08-05",
|
||||
"peakDelta": 927,
|
||||
"latestVersion": "0.4.4",
|
||||
}
|
||||
|
||||
|
||||
def test_summary_is_pretty_printed_json(tmp_path: Path):
|
||||
summary = collector._make_summary([_row("2026-08-06", 26870, 628)])
|
||||
output = tmp_path / "summary.json"
|
||||
|
||||
collector._write_summary(output, summary)
|
||||
|
||||
assert json.loads(output.read_text(encoding="utf-8")) == summary
|
||||
assert output.read_text(encoding="utf-8").endswith("\n")
|
||||
|
||||
|
||||
def test_registry_metrics_workflow_commits_the_summary():
|
||||
workflow = (REPO_ROOT / ".github" / "workflows" / "registry-metrics.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert "metrics/summary.json" in workflow
|
||||
@@ -15,7 +15,7 @@ import sys
|
||||
import urllib.request
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -33,6 +33,9 @@ CSV_FIELDS = [
|
||||
"pending_versions",
|
||||
"active_versions",
|
||||
]
|
||||
SUMMARY_BASELINE_DATE = "2026-05-08"
|
||||
SUMMARY_BASELINE_DOWNLOADS = 95
|
||||
SUMMARY_SINCE_VERSION = "0.3.0"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -141,6 +144,41 @@ def _upsert_today(rows: list[dict[str, str]], snapshot: Snapshot) -> list[dict[s
|
||||
return [*kept, snapshot.as_row()]
|
||||
|
||||
|
||||
def _make_summary(rows: list[dict[str, str]]) -> dict[str, str | int | float]:
|
||||
"""Build the stable, machine-readable landing-page summary.
|
||||
|
||||
The Registry did not expose per-version downloads at the v0.3.0 release,
|
||||
so its maintainer-recorded 95-download total on 2026-05-08 is the fixed
|
||||
baseline for the public "since v0.3.0" metric.
|
||||
"""
|
||||
if not rows:
|
||||
raise ValueError("cannot summarize an empty metrics history")
|
||||
|
||||
latest = rows[-1]
|
||||
latest_date = date.fromisoformat(latest["date"])
|
||||
baseline_date = date.fromisoformat(SUMMARY_BASELINE_DATE)
|
||||
days = (latest_date - baseline_date).days
|
||||
if days <= 0:
|
||||
raise ValueError("latest metrics snapshot must be after the summary baseline")
|
||||
|
||||
downloads_since = int(latest["downloads_total"]) - SUMMARY_BASELINE_DOWNLOADS
|
||||
peak = max(rows, key=lambda row: int(row.get("daily_delta") or 0))
|
||||
return {
|
||||
"asOf": latest["date"],
|
||||
"sinceVersion": SUMMARY_SINCE_VERSION,
|
||||
"downloadsSince": downloads_since,
|
||||
"days": days,
|
||||
"avgPerDay": round(downloads_since / days, 1),
|
||||
"peakDate": peak["date"],
|
||||
"peakDelta": int(peak.get("daily_delta") or 0),
|
||||
"latestVersion": latest["latest_version"],
|
||||
}
|
||||
|
||||
|
||||
def _write_summary(path: Path, summary: dict[str, str | int | float]) -> None:
|
||||
path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _render_overview(node_id: str, rows: list[dict[str, str]]) -> str:
|
||||
if not rows:
|
||||
return f"# Comfy Registry Metrics: {node_id}\n\nNo snapshots recorded yet.\n"
|
||||
@@ -184,6 +222,7 @@ def main() -> int:
|
||||
output_dir = Path(args.output_dir)
|
||||
csv_path = output_dir / "comfy-registry-downloads.csv"
|
||||
overview_path = output_dir / "README.md"
|
||||
summary_path = output_dir / "summary.json"
|
||||
|
||||
node = _fetch_json(f"{API_ROOT}/nodes/{args.node}")
|
||||
versions = _fetch_json(f"{API_ROOT}/nodes/{args.node}/versions")
|
||||
@@ -193,6 +232,7 @@ def main() -> int:
|
||||
|
||||
_write_rows(csv_path, updated_rows)
|
||||
overview_path.write_text(_render_overview(args.node, updated_rows), encoding="utf-8")
|
||||
_write_summary(summary_path, _make_summary(updated_rows))
|
||||
|
||||
print(
|
||||
f"{snapshot.date}: downloads={snapshot.downloads_total} "
|
||||
|
||||
Reference in New Issue
Block a user