From 3a91a20e96d45a42745319beb613b90010be68f2 Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Mon, 22 Jun 2026 18:21:39 +0200 Subject: [PATCH] feat(hf-space): leaderboard app for the OCB dataset Gradio app reading the daily parquet snapshot live from HF. Four tabs: today's leaderboard, per-chain leaders, provider rankings, about. Deploy workflow pushes to the OpenChainBench/leaderboard Space on main pushes that touch scripts/hf_space/. --- .github/workflows/hf-space-deploy.yml | 82 +++++++ .gitignore | 3 +- scripts/hf_space/README.md | 52 +++++ scripts/hf_space/app.py | 304 ++++++++++++++++++++++++++ scripts/hf_space/requirements.txt | 5 + 5 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/hf-space-deploy.yml create mode 100644 scripts/hf_space/README.md create mode 100644 scripts/hf_space/app.py create mode 100644 scripts/hf_space/requirements.txt diff --git a/.github/workflows/hf-space-deploy.yml b/.github/workflows/hf-space-deploy.yml new file mode 100644 index 00000000..7d9a2bfb --- /dev/null +++ b/.github/workflows/hf-space-deploy.yml @@ -0,0 +1,82 @@ +name: HF Space deploy + +# Pushes scripts/hf_space/ to the HF Space at +# https://huggingface.co/spaces/OpenChainBench/leaderboard. +# +# Trigger: +# - push to main that touches scripts/hf_space/** +# - workflow_dispatch for manual redeploys +# +# Required secrets: +# HF_TOKEN write-scoped token for the Space repo (same token as the +# dataset publisher). + +on: + push: + branches: [main] + paths: + - "scripts/hf_space/**" + - ".github/workflows/hf-space-deploy.yml" + workflow_dispatch: + +concurrency: + group: hf-space-deploy + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_SPACE_REPO_ID: OpenChainBench/leaderboard + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install huggingface_hub + run: pip install "huggingface_hub>=0.26" + + - name: Upload to HF Space + env: + GIT_SHA: ${{ github.sha }} + run: | + python - <<'PY' + import os + from huggingface_hub import HfApi + + token = os.environ["HF_TOKEN"] + repo_id = os.environ["HF_SPACE_REPO_ID"] + sha = os.environ.get("GIT_SHA", "manual") + api = HfApi(token=token) + api.create_repo( + repo_id=repo_id, + repo_type="space", + space_sdk="gradio", + exist_ok=True, + private=False, + ) + info = api.upload_folder( + folder_path="scripts/hf_space", + repo_id=repo_id, + repo_type="space", + commit_message=f"deploy from {sha}", + ) + print("uploaded:", getattr(info, "oid", "ok")) + PY + + - name: Summary + if: always() + run: | + { + echo "## HF Space deploy ${{ job.status }}" + echo "" + echo "**Space:** https://huggingface.co/spaces/${HF_SPACE_REPO_ID}" + echo "**Commit:** ${{ github.sha }}" + echo "**Time:** $(date -u +%FT%TZ)" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 5ca4a66c..b40a263f 100644 --- a/.gitignore +++ b/.gitignore @@ -62,7 +62,8 @@ harnesses/*/monitor harnesses/*/cmd/script/script harnesses/*/cmd/monitor/monitor -# Python venvs for the HF publisher (created locally for dry-runs) +# Python venvs for the HF publisher and Space (created locally for dry-runs) scripts/hf_publisher/.venv/ +scripts/hf_space/.venv/ **/__pycache__/ *.pyc diff --git a/scripts/hf_space/README.md b/scripts/hf_space/README.md new file mode 100644 index 00000000..c862d70a --- /dev/null +++ b/scripts/hf_space/README.md @@ -0,0 +1,52 @@ +--- +title: OpenChainBench Leaderboard +emoji: 📊 +colorFrom: indigo +colorTo: blue +sdk: gradio +sdk_version: 4.44.1 +python_version: "3.11" +app_file: app.py +license: cc-by-4.0 +pinned: false +short_description: Interactive leaderboard for OpenChainBench crypto infra benchmarks +--- + +# OpenChainBench leaderboard + +A small Gradio app that reads the daily parquet snapshot from the +[OpenChainBench/benchmarks](https://huggingface.co/datasets/OpenChainBench/benchmarks) +dataset and lets you browse it. Four tabs: today's leaderboard, per-chain +leaders, per-provider rankings, and an about page. + +The dataset itself is the source of truth. This Space is a viewer on top +of it. If you want raw access, query the parquet files directly with +`polars`, `duckdb`, `pandas`, or any tool that speaks parquet. + +```python +import polars as pl + +df = pl.scan_parquet( + "hf://datasets/OpenChainBench/benchmarks/headlines/**/*.parquet", + hive_partitioning=True, +) +print(df.collect().head()) +``` + +For the full website with methodology, per-bench detail pages, and the +historical view, head to [openchainbench.com](https://openchainbench.com). + +## Local dev + +```bash +pip install -r requirements.txt +python app.py +``` + +Open http://127.0.0.1:7860. + +## License + +Code in this Space is part of the OpenChainBench repo. The dataset is +released under CC-BY-4.0. Attribution: link back to openchainbench.com or +the dataset page. diff --git a/scripts/hf_space/app.py b/scripts/hf_space/app.py new file mode 100644 index 00000000..c60a4f59 --- /dev/null +++ b/scripts/hf_space/app.py @@ -0,0 +1,304 @@ +""" +Gradio Space for the OpenChainBench public dataset. + +Loads parquet partitions directly from the HF dataset at +hf://datasets/OpenChainBench/benchmarks via polars, surfaces a +sortable / filterable leaderboard, per-chain leaders, and per-provider +rankings. No local cache, no auth, no state. Each tab refresh re-reads +the latest snapshot from HF, which is cheap because polars only scans +the columns it needs. + +Run locally: + pip install -r requirements.txt + python app.py + +The HF Space picks up `app_file: app.py` from README.md frontmatter. +""" + +from __future__ import annotations + +import functools +import logging +from typing import Any + +import gradio as gr +import polars as pl + +logger = logging.getLogger("ocb_space") +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") + +DATASET_REPO = "OpenChainBench/benchmarks" +DATASET_URL = f"https://huggingface.co/datasets/{DATASET_REPO}" +SITE_URL = "https://openchainbench.com" +GITHUB_URL = "https://github.com/ChainBench/OpenChainBench" + +FOOTER = ( + f"Data sourced from {DATASET_URL} (CC-BY-4.0). Updated daily." +) + +# Hive partition layout: /snapshot_date=YYYY-MM-DD/part-0.parquet. +# Globbing the partitions and reading only the most recent snapshot_date +# keeps the scan small even as the dataset accumulates history. +HF_BASE = f"hf://datasets/{DATASET_REPO}" + + +@functools.lru_cache(maxsize=1) +def latest_snapshot_date() -> str: + """Pick the most recent snapshot_date present in headlines. + + Scans the partition column only, no row data is materialized. Result + is cached for the lifetime of the process so every tab call reuses + the same date. + """ + lf = pl.scan_parquet(f"{HF_BASE}/headlines/**/*.parquet", hive_partitioning=True) + dates = lf.select("snapshot_date").unique().collect() + latest = dates["snapshot_date"].max() + if latest is None: + raise RuntimeError("no snapshots found in headlines/") + logger.info("latest snapshot: %s", latest) + return str(latest) + + +def _read_table(table: str, snapshot: str) -> pl.DataFrame: + lf = pl.scan_parquet( + f"{HF_BASE}/{table}/**/*.parquet", hive_partitioning=True + ).filter(pl.col("snapshot_date") == snapshot) + return lf.collect() + + +@functools.lru_cache(maxsize=1) +def headlines_df() -> pl.DataFrame: + return _read_table("headlines", latest_snapshot_date()) + + +@functools.lru_cache(maxsize=1) +def providers_df() -> pl.DataFrame: + return _read_table("providers", latest_snapshot_date()) + + +@functools.lru_cache(maxsize=1) +def chain_leaders_df() -> pl.DataFrame: + return _read_table("chain_leaders", latest_snapshot_date()) + + +def _categories() -> list[str]: + df = headlines_df() + if "category" not in df.columns: + return ["All"] + cats = sorted({c for c in df["category"].to_list() if c}) + return ["All", *cats] + + +def _bench_slugs() -> list[str]: + df = headlines_df() + return sorted({s for s in df["slug"].to_list() if s}) + + +def _bench_choices_for_chains() -> list[str]: + df = chain_leaders_df() + if df.is_empty(): + return ["All"] + return ["All", *sorted({s for s in df["bench_slug"].to_list() if s})] + + +def _chain_choices() -> list[str]: + df = chain_leaders_df() + if df.is_empty(): + return ["All"] + return ["All", *sorted({s for s in df["chain"].to_list() if s})] + + +def view_headlines(category: str) -> Any: + df = headlines_df() + if category and category != "All": + df = df.filter(pl.col("category") == category) + + # The detail URL pattern on openchainbench.com is /benchmarks/. + # We render the title as a markdown link so clicking opens the page + # in a new tab. + pdf = ( + df.select( + [ + pl.col("title").alias("Bench"), + pl.col("slug"), + pl.col("category").alias("Category"), + pl.col("metric").alias("Metric"), + pl.col("unit").alias("Unit"), + pl.col("leader_name").alias("Leader"), + pl.col("leader_value").alias("Leader value"), + pl.col("bench_sample_size").alias("Sample size"), + pl.col("as_of").alias("As of"), + ] + ) + .sort("Bench") + .to_pandas() + ) + pdf["Bench"] = pdf.apply( + lambda r: f"[{r['Bench']}]({SITE_URL}/benchmarks/{r['slug']})", axis=1 + ) + pdf = pdf.drop(columns=["slug"]) + return pdf + + +def view_chain_leaders(bench: str, chain: str) -> Any: + df = chain_leaders_df() + if df.is_empty(): + return df.to_pandas() + if bench and bench != "All": + df = df.filter(pl.col("bench_slug") == bench) + if chain and chain != "All": + df = df.filter(pl.col("chain") == chain) + return ( + df.select( + [ + pl.col("bench_slug").alias("Bench"), + pl.col("chain").alias("Chain"), + pl.col("leader_name").alias("Leader"), + pl.col("leader_value").alias("Leader value"), + pl.col("worst_name").alias("Worst"), + pl.col("worst_value").alias("Worst value"), + ] + ) + .sort(["Bench", "Chain"]) + .to_pandas() + ) + + +def view_providers(bench: str) -> Any: + df = providers_df() + if not bench: + return df.head(0).to_pandas() + df = df.filter(pl.col("bench_slug") == bench) + return ( + df.select( + [ + pl.col("provider_name").alias("Provider"), + pl.col("provider_type").alias("Type"), + pl.col("p50").alias("p50"), + pl.col("p90").alias("p90"), + pl.col("p99").alias("p99"), + pl.col("success_rate").alias("Success rate"), + pl.col("provider_sample_size").alias("Sample size"), + pl.col("is_leader").alias("Leader?"), + ] + ) + .sort("p50", nulls_last=True) + .to_pandas() + ) + + +ABOUT_MD = f""" +## OpenChainBench + +Public benchmarks for crypto infrastructure: RPCs, oracles, bridges, aggregators, +prediction markets, and more. The full leaderboard, methodology, and per-bench +detail live at [openchainbench.com]({SITE_URL}). + +This Space is a thin viewer over the daily parquet snapshot published to +[{DATASET_REPO}]({DATASET_URL}). Every tab reads directly from the dataset, so +the numbers you see here match the dataset exactly. + +### Links +- Website: [{SITE_URL}]({SITE_URL}) +- Dataset: [{DATASET_URL}]({DATASET_URL}) +- GitHub: [{GITHUB_URL}]({GITHUB_URL}) + +### License + +The dataset is released under **CC-BY-4.0**. Attribution required: link +back to {SITE_URL} or the dataset page. + +### Citation + +```bibtex +@misc{{openchainbench2026, + title = {{OpenChainBench: Public benchmarks for crypto infrastructure}}, + author = {{OpenChainBench contributors}}, + year = {{2026}}, + url = {{{DATASET_URL}}}, + note = {{CC-BY-4.0}} +}} +``` +""" + + +def build_app() -> gr.Blocks: + snapshot = latest_snapshot_date() + title = f"OpenChainBench leaderboard ({snapshot})" + + with gr.Blocks(title=title, theme=gr.themes.Soft()) as demo: + gr.Markdown(f"# {title}") + gr.Markdown( + "Sortable view of the daily snapshot. Click a bench title to open " + f"its page on {SITE_URL}." + ) + + with gr.Tabs(): + with gr.Tab("Today's leaderboard"): + cat = gr.Dropdown( + choices=_categories(), + value="All", + label="Category", + ) + table = gr.Dataframe( + value=view_headlines("All"), + interactive=False, + wrap=True, + datatype=["markdown", "str", "str", "str", "str", "number", "number", "str"], + ) + cat.change(view_headlines, inputs=cat, outputs=table) + + with gr.Tab("Per-chain leaders"): + with gr.Row(): + bench_dd = gr.Dropdown( + choices=_bench_choices_for_chains(), + value="All", + label="Bench", + ) + chain_dd = gr.Dropdown( + choices=_chain_choices(), + value="All", + label="Chain", + ) + chains_table = gr.Dataframe( + value=view_chain_leaders("All", "All"), + interactive=False, + wrap=True, + ) + bench_dd.change( + view_chain_leaders, + inputs=[bench_dd, chain_dd], + outputs=chains_table, + ) + chain_dd.change( + view_chain_leaders, + inputs=[bench_dd, chain_dd], + outputs=chains_table, + ) + + with gr.Tab("Provider rankings"): + slugs = _bench_slugs() + default_slug = slugs[0] if slugs else None + prov_dd = gr.Dropdown( + choices=slugs, + value=default_slug, + label="Bench slug", + ) + prov_table = gr.Dataframe( + value=view_providers(default_slug) if default_slug else None, + interactive=False, + wrap=True, + ) + prov_dd.change(view_providers, inputs=prov_dd, outputs=prov_table) + + with gr.Tab("About"): + gr.Markdown(ABOUT_MD) + + gr.Markdown(f"---\n{FOOTER}") + + return demo + + +if __name__ == "__main__": + app = build_app() + app.launch(server_name="0.0.0.0", server_port=7860) diff --git a/scripts/hf_space/requirements.txt b/scripts/hf_space/requirements.txt new file mode 100644 index 00000000..bffab3e4 --- /dev/null +++ b/scripts/hf_space/requirements.txt @@ -0,0 +1,5 @@ +gradio==4.44.1 +polars>=1.41.0 +pandas==2.2.3 +pyarrow==17.0.0 +huggingface_hub==0.26.2