Ctrl K
ComunidadGuías…

scripts/update_registry_metrics.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Refresh the numbers Awesome Clips Kitty shows: stats/metrics.json in the catalog.

    python scripts/update_registry_metrics.py              # read every number, write metrics.json
    python scripts/update_registry_metrics.py --dry-run    # print what would be written

Each number comes from its own source and is stored apart from the others:

- GitHub, for every repository the catalog names: stars, the last push,
  whether it is archived, whether Discussions are on, and how many
  discussions there are (that one needs a token: GITHUB_TOKEN or GH_TOKEN).
- Hugging Face, for every model an entry or a listing names: downloads in
  the last 30 days (Hugging Face's own figure) and likes.
- Clips Kitty installs, from the install counter: when registry/catalog.yaml
  names a counter address of the form
  https://github.com/<owner>/<repo>/releases/download/<tag>/{asset},
  the download count of each listing's file in that release.

Which repositories and models to read comes from index.json, so run
scripts/build_registry_index.py first when entries have changed, and again
afterwards so the index carries the new numbers. A number that can't be read
keeps its last value; nothing is ever made up. The app never asks GitHub or
Hugging Face for these itself.
"""

from __future__ import annotations

import argparse
import json
import os
import re
import sys
import time
import urllib.parse
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))

GITHUB_API = "https://api.github.com"
HF_API = "https://huggingface.co/api/models/"
RELEASE_RE = re.compile(r"^https://github\.com/([^/]+)/([^/]+)/releases/download/([^/]+)/\{asset\}$")
USER_AGENT = "Awesome-Clips-Kitty-metrics"


def get_json(url: str, *, token: str | None = None, body: dict | None = None):
    headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
    if token and url.startswith(GITHUB_API):
        headers["Authorization"] = f"Bearer {token}"
    data = json.dumps(body).encode() if body is not None else None
    if data:
        headers["Content-Type"] = "application/json"
    request = urllib.request.Request(url, data=data, headers=headers)
    with urllib.request.urlopen(request, timeout=30) as response:
        return json.loads(response.read(8 * 1024 * 1024))


def wanted(index: dict) -> tuple[list[str], list[str]]:
    """The GitHub repositories ("owner/repo") and Hugging Face model ids the
    index names, each once, sorted."""
    from plugins import catalog

    repos, models = set(), set()
    for p in index.get("plugins") or []:
        repo = catalog.github_repo(p.get("repository"))
        if repo:
            repos.add(repo)
        for m in p.get("models") or []:
            if isinstance(m, dict) and m.get("source") == "huggingface" and isinstance(m.get("id"), str):
                models.add(m["id"])
    for e in index.get("catalog") or []:
        source = e.get("source") or {}
        repo = catalog.github_repo(source.get("github"))
        if repo:
            repos.add(repo)
        if isinstance(source.get("huggingface"), str):
            models.add(source["huggingface"])
        for m in e.get("models") or []:
            if isinstance(m, dict) and isinstance(m.get("huggingface"), str):
                models.add(m["huggingface"])
    return sorted(repos), sorted(m for m in models if catalog.HF_ID_RE.match(m))


def github_numbers(repo: str, *, fetch, token: str | None) -> dict:
    data = fetch(f"{GITHUB_API}/repos/{repo}", token=token)
    out = {"stars": int(data["stargazers_count"]), "pushed_at": str(data.get("pushed_at") or "")[:10],
           "archived": bool(data.get("archived")), "has_discussions": bool(data.get("has_discussions"))}
    if out["has_discussions"] and token:
        owner, name = repo.split("/", 1)
        query = ("query($owner: String!, $name: String!) "
                 "{ repository(owner: $owner, name: $name) { discussions { totalCount } } }")
        answer = fetch(f"{GITHUB_API}/graphql", token=token,
                       body={"query": query, "variables": {"owner": owner, "name": name}})
        count = (((answer or {}).get("data") or {}).get("repository") or {}).get("discussions", {}).get("totalCount")
        if isinstance(count, int):
            out["discussions"] = count
    return out


def hf_numbers(model_id: str, *, fetch) -> dict:
    data = fetch(HF_API + urllib.parse.quote(model_id, safe="/"))
    out = {"downloads": int(data.get("downloads") or 0), "likes": int(data.get("likes") or 0)}
    if data.get("lastModified"):
        out["last_modified"] = str(data["lastModified"])[:10]
    return out


def install_counts(template: str | None, *, fetch, token: str | None) -> dict[str, int] | None:
    """{listing id: downloads of its counter file}, or None when the counter
    isn't a GitHub release (or there is none)."""
    m = RELEASE_RE.match(template or "")
    if not m:
        return None
    owner, repo, tag = m.groups()
    release = fetch(f"{GITHUB_API}/repos/{owner}/{repo}/releases/tags/{urllib.parse.quote(tag, safe='')}",
                    token=token)
    out = {}
    for asset in release.get("assets") or []:
        name = str(asset.get("name") or "")
        if name.endswith(".count") and "__" in name:
            out[name[: -len(".count")].replace("__", "/", 1)] = int(asset.get("download_count") or 0)
    return out


def update(index: dict, previous: dict, *, fetch=get_json, token: str | None = None, now: str | None = None,
           log=print) -> dict:
    """New metrics, keeping the previous value of anything that can't be read."""
    repos, models = wanted(index)
    old_gh, old_hf = previous.get("github") or {}, previous.get("huggingface") or {}
    out = {"generated_at": now or time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
           "github": {}, "huggingface": {}, "installs": dict(previous.get("installs") or {})}
    for repo in repos:
        try:
            out["github"][repo] = github_numbers(repo, fetch=fetch, token=token)
        except Exception as e:  # any failure keeps the last value
            log(f"GitHub {repo}: kept the last value ({e})")
            if repo in old_gh:
                out["github"][repo] = old_gh[repo]
    for model in models:
        try:
            out["huggingface"][model] = hf_numbers(model, fetch=fetch)
        except Exception as e:
            log(f"Hugging Face {model}: kept the last value ({e})")
            if model in old_hf:
                out["huggingface"][model] = old_hf[model]
    template = (index.get("counter") or {}).get("install")
    try:
        counts = install_counts(template, fetch=fetch, token=token)
    except Exception as e:
        log(f"install counter: kept the last values ({e})")
        counts = None
    if counts is not None:
        listed = {p["id"] for p in index.get("plugins") or []}
        out["installs"].update({pid: n for pid, n in counts.items() if pid in listed})
    out["installs"] = dict(sorted(out["installs"].items()))
    return out


def main(argv=None) -> int:
    from plugins import registry

    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--catalog", type=Path, default=registry.catalog_path(), help="the catalog folder")
    ap.add_argument("--dry-run", action="store_true", help="print the result instead of writing it")
    args = ap.parse_args(argv)

    index = json.loads((args.catalog / "index.json").read_text(encoding="utf-8"))
    path = args.catalog / "stats" / "metrics.json"
    try:
        previous = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, ValueError):
        previous = {}
    token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") or None
    metrics = update(index, previous if isinstance(previous, dict) else {}, token=token)
    text = json.dumps(metrics, indent=1, sort_keys=True, ensure_ascii=False) + "\n"
    if args.dry_run:
        print(text, end="")
        return 0
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")
    print(f"wrote {path}: {len(metrics['github'])} repositories, {len(metrics['huggingface'])} models, "
          f"{len(metrics['installs'])} install counts")
    return 0


if __name__ == "__main__":
    sys.exit(main())