Skip to content

Commit f2df4f8

Browse files
Merge pull request #761 from ChainBench/chore/sync-main-into-dev
chore(dev): sync main → dev (bring SEO hotfixes + canonical fixes)
2 parents b36b457 + 549de50 commit f2df4f8

29 files changed

Lines changed: 2250 additions & 34 deletions

File tree

.github/workflows/pypi-publish.yml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: PyPI Publish
2+
3+
on:
4+
push:
5+
tags:
6+
- "python-v*"
7+
workflow_dispatch:
8+
inputs:
9+
ref:
10+
description: "Git ref to build (branch, tag or sha)"
11+
required: false
12+
default: "main"
13+
14+
jobs:
15+
build:
16+
name: Build sdist + wheel
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
ref: ${{ github.event.inputs.ref || github.ref }}
22+
- uses: actions/setup-python@v5
23+
with:
24+
python-version: "3.13"
25+
- name: Install build tooling
26+
run: python -m pip install --upgrade pip build
27+
- name: Build distributions
28+
working-directory: packages/python-client
29+
run: python -m build
30+
- name: Upload artifact
31+
uses: actions/upload-artifact@v4
32+
with:
33+
name: python-dist
34+
path: packages/python-client/dist/
35+
36+
smoke-test:
37+
name: Install + import smoke test
38+
needs: build
39+
runs-on: ubuntu-latest
40+
strategy:
41+
matrix:
42+
python-version: ["3.10", "3.11", "3.12", "3.13"]
43+
steps:
44+
- uses: actions/setup-python@v5
45+
with:
46+
python-version: ${{ matrix.python-version }}
47+
- uses: actions/download-artifact@v4
48+
with:
49+
name: python-dist
50+
path: dist
51+
- name: Install built wheel
52+
run: |
53+
python -m pip install --upgrade pip
54+
python -m pip install dist/openchainbench-*.whl
55+
- name: Import smoke test
56+
run: |
57+
python -c "import openchainbench; print(openchainbench.__version__)"
58+
59+
publish:
60+
name: Publish to PyPI (Trusted Publisher)
61+
needs: [build, smoke-test]
62+
if: startsWith(github.ref, 'refs/tags/python-v')
63+
runs-on: ubuntu-latest
64+
environment:
65+
name: pypi
66+
url: https://pypi.org/project/openchainbench/
67+
permissions:
68+
id-token: write
69+
steps:
70+
- uses: actions/download-artifact@v4
71+
with:
72+
name: python-dist
73+
path: dist
74+
- uses: pypa/gh-action-pypi-publish@release/v1
75+
with:
76+
packages-dir: dist
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Python Client CI
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "packages/python-client/**"
7+
- ".github/workflows/python-client-ci.yml"
8+
push:
9+
branches: [main, dev]
10+
paths:
11+
- "packages/python-client/**"
12+
- ".github/workflows/python-client-ci.yml"
13+
14+
jobs:
15+
test:
16+
runs-on: ubuntu-latest
17+
strategy:
18+
matrix:
19+
python-version: ["3.10", "3.11", "3.12", "3.13"]
20+
env:
21+
OCB_SKIP_INTEGRATION: "1"
22+
steps:
23+
- uses: actions/checkout@v4
24+
- uses: actions/setup-python@v5
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
- name: Install package + dev deps
28+
working-directory: packages/python-client
29+
run: |
30+
python -m pip install --upgrade pip
31+
python -m pip install -e ".[dev]"
32+
- name: Run unit tests
33+
working-directory: packages/python-client
34+
run: python -m pytest tests -m "not integration"
35+
- name: Build sdist + wheel
36+
working-directory: packages/python-client
37+
run: python -m build

benchmarks/l1-finality.yml

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -191,18 +191,14 @@ providers:
191191
name: Gram
192192
tag: Masterchain commit, polled via tonapi.io
193193
formula: "Median wall-clock milliseconds for a Gram (formerly TON) masterchain block to commit under BAG consensus, polled via tonapi.io with a 3-seqno lookback, p50 over 24h."
194-
# Prom selectors straddle ton|gram during the harness relabel window.
195-
# The harness on Railway still emits chain="ton"; once it redeploys
196-
# with chain="gram", the regex keeps matching both. Drop the ton
197-
# branch after ~7 days of clean gram-labelled data.
198194
queries:
199-
p50: quantile_over_time(0.50, l1_finality_wallclock_lag_milliseconds{chain=~"ton|gram"}[24h])
200-
p90: quantile_over_time(0.90, l1_finality_wallclock_lag_milliseconds{chain=~"ton|gram"}[24h])
201-
p99: quantile_over_time(0.99, l1_finality_wallclock_lag_milliseconds{chain=~"ton|gram"}[24h])
202-
mean: avg_over_time(l1_finality_wallclock_lag_milliseconds{chain=~"ton|gram"}[24h])
203-
success: avg_over_time(l1_finality_wallclock_health{chain=~"ton|gram"}[24h])
204-
sample_size: increase(l1_finality_wallclock_samples_total{chain=~"ton|gram"}[24h])
205-
series: l1_finality_wallclock_lag_milliseconds{chain=~"ton|gram"}
195+
p50: quantile_over_time(0.50, l1_finality_wallclock_lag_milliseconds{chain="gram"}[24h])
196+
p90: quantile_over_time(0.90, l1_finality_wallclock_lag_milliseconds{chain="gram"}[24h])
197+
p99: quantile_over_time(0.99, l1_finality_wallclock_lag_milliseconds{chain="gram"}[24h])
198+
mean: avg_over_time(l1_finality_wallclock_lag_milliseconds{chain="gram"}[24h])
199+
success: avg_over_time(l1_finality_wallclock_health{chain="gram"}[24h])
200+
sample_size: increase(l1_finality_wallclock_samples_total{chain="gram"}[24h])
201+
series: l1_finality_wallclock_lag_milliseconds{chain="gram"}
206202

207203
- slug: stellar
208204
name: Stellar

benchmarks/network-fees.yml

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -270,16 +270,13 @@ providers:
270270
layer: l1
271271
tag: Hardcoded 0.005 GRAM typical wallet v4 transfer
272272
formula: "Conservative typical observed cost of a Gram (formerly TON) wallet transfer × GRAM USD price. Gram has no fee estimate RPC; this is the observed median."
273-
# Straddle the ton/gram chain label during the harness relabel
274-
# window. The harness Railway service currently emits chain="ton";
275-
# the regex keeps matching once it redeploys with chain="gram".
276273
queries:
277-
p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain=~"ton|gram",tier="single"}[24h])
278-
p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain=~"ton|gram",tier="single"}[24h])
279-
p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain=~"ton|gram",tier="single"}[24h])
280-
mean: avg_over_time(tx_fee_native_transfer_usd{chain=~"ton|gram",tier="single"}[24h])
281-
success: avg_over_time(tx_fee_health{chain=~"ton|gram"}[24h])
282-
series: tx_fee_native_transfer_usd{chain=~"ton|gram",tier="single"}
274+
p50: quantile_over_time(0.50, tx_fee_native_transfer_usd{chain="gram",tier="single"}[24h])
275+
p90: quantile_over_time(0.90, tx_fee_native_transfer_usd{chain="gram",tier="single"}[24h])
276+
p99: quantile_over_time(0.99, tx_fee_native_transfer_usd{chain="gram",tier="single"}[24h])
277+
mean: avg_over_time(tx_fee_native_transfer_usd{chain="gram",tier="single"}[24h])
278+
success: avg_over_time(tx_fee_health{chain="gram"}[24h])
279+
series: tx_fee_native_transfer_usd{chain="gram",tier="single"}
283280

284281
- slug: stellar
285282
name: Stellar

next.config.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,24 @@ const nextConfig: NextConfig = {
178178
destination: "/chains/gram",
179179
permanent: true,
180180
},
181+
// Scoped to the 3 benches that ship a gram (formerly ton)
182+
// per_chain_explainer. The previous catch-all `/benchmarks/:slug/ton`
183+
// fired for every bench, redirecting non-gram benches to a /gram
184+
// path that 404s (e.g. aggregator-head-lag/ton → aggregator-head-lag/gram → 404),
185+
// which Ahrefs and Google flag as a soft-404 redirect chain.
181186
{
182-
source: "/benchmarks/:slug/ton",
183-
destination: "/benchmarks/:slug/gram",
187+
source: "/benchmarks/l1-finality/ton",
188+
destination: "/benchmarks/l1-finality/gram",
189+
permanent: true,
190+
},
191+
{
192+
source: "/benchmarks/network-fees/ton",
193+
destination: "/benchmarks/network-fees/gram",
194+
permanent: true,
195+
},
196+
{
197+
source: "/benchmarks/wallet-labels-coverage/ton",
198+
destination: "/benchmarks/wallet-labels-coverage/gram",
184199
permanent: true,
185200
},
186201
...chainRedirects,

packages/python-client/.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.venv/
2+
dist/
3+
build/
4+
*.egg-info/
5+
__pycache__/
6+
*.py[cod]
7+
.pytest_cache/
8+
.coverage
9+
htmlcov/

packages/python-client/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 OpenChainBench contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Publishing `openchainbench` to PyPI
2+
3+
Releases are fully automated through GitHub Actions and PyPI Trusted
4+
Publishers. No long-lived API token is stored anywhere.
5+
6+
## One-time setup (PyPI Trusted Publisher)
7+
8+
1. Create an account on https://pypi.org if you do not have one.
9+
2. Reserve the project name by uploading the first release manually
10+
**or** request the project via the PyPI account settings. We do the
11+
pending publisher flow below so the very first release is automated.
12+
3. Go to https://pypi.org/manage/account/publishing/ and click
13+
**Add a new pending publisher**.
14+
4. Fill in:
15+
- PyPI project name: `openchainbench`
16+
- Owner: `ChainBench`
17+
- Repository name: `OpenChainBench`
18+
- Workflow name: `pypi-publish.yml`
19+
- Environment name: `pypi`
20+
5. Save. PyPI will accept the first upload from the matching GitHub
21+
workflow with no token.
22+
6. In GitHub, go to **Settings > Environments** for the repo and create
23+
the `pypi` environment. Add required reviewers if you want a manual
24+
gate before publish.
25+
26+
## Cutting a release
27+
28+
1. Bump the version in two places:
29+
- `packages/python-client/pyproject.toml` (`project.version`)
30+
- `packages/python-client/src/openchainbench/__init__.py` (`__version__`)
31+
- `packages/python-client/src/openchainbench/client.py` (`USER_AGENT`)
32+
2. Commit the bump on `main`.
33+
3. Tag and push:
34+
```bash
35+
git tag python-v0.1.0
36+
git push origin python-v0.1.0
37+
```
38+
4. The `PyPI Publish` workflow runs: build, smoke test on 3.10-3.13,
39+
then publish via the Trusted Publisher OIDC flow.
40+
5. Verify the release at https://pypi.org/project/openchainbench/.
41+
42+
## Local smoke build
43+
44+
```bash
45+
cd packages/python-client
46+
python -m pip install --upgrade build
47+
python -m build
48+
ls dist/
49+
```
50+
51+
This produces an `sdist` (`.tar.gz`) and a pure-Python `wheel` (`.whl`).
52+
The Trusted Publisher workflow runs the exact same command in CI.
53+
54+
## Tag namespace
55+
56+
We deliberately use `python-v*` (not bare `v*`) so OCB site tags such as
57+
`v1.0-dataset` never trigger a Python publish.

packages/python-client/README.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# openchainbench
2+
3+
Official Python client for [OpenChainBench](https://openchainbench.com), the live, reproducible benchmark suite for crypto infrastructure (RPC latency, bridge fees, perp venues, oracle deviation, and more).
4+
5+
[![PyPI version](https://img.shields.io/pypi/v/openchainbench.svg)](https://pypi.org/project/openchainbench/)
6+
[![Python versions](https://img.shields.io/pypi/pyversions/openchainbench.svg)](https://pypi.org/project/openchainbench/)
7+
[![Downloads](https://img.shields.io/pypi/dm/openchainbench.svg)](https://pypi.org/project/openchainbench/)
8+
[![License](https://img.shields.io/pypi/l/openchainbench.svg)](https://github.com/ChainBench/OpenChainBench/blob/main/LICENSE)
9+
10+
The data served by openchainbench.com is published under [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/). Attribute with a link back to the benchmark page (`pageUrl` on every payload).
11+
12+
## Install
13+
14+
```bash
15+
pip install openchainbench
16+
```
17+
18+
Requires Python 3.10+.
19+
20+
## Quick start
21+
22+
```python
23+
from openchainbench import OpenChainBench
24+
25+
with OpenChainBench() as ocb:
26+
for bench in ocb.list_benchmarks():
27+
if bench.leader:
28+
print(f"{bench.title}: {bench.leader.name} -> {bench.value}")
29+
```
30+
31+
### Fetch one benchmark
32+
33+
```python
34+
from openchainbench import OpenChainBench
35+
36+
with OpenChainBench() as ocb:
37+
bench = ocb.get_benchmark("bridge-fee")
38+
print(bench.headline)
39+
for row in bench.rankings[:3]:
40+
print(row.slug, row.ms.p50, row.success_rate)
41+
```
42+
43+
### Fetch a time series
44+
45+
```python
46+
from openchainbench import OpenChainBench
47+
48+
with OpenChainBench() as ocb:
49+
series = ocb.get_series("bridge-fee", range="24h")
50+
for provider in series.providers:
51+
print(provider.name, provider.values[-1])
52+
```
53+
54+
### Filter by chain or region
55+
56+
```python
57+
bench = ocb.get_benchmark("network-fees", chain="ethereum")
58+
series = ocb.get_series("network-fees", range="7d", chain="ethereum", region="eu-west")
59+
```
60+
61+
## Error handling
62+
63+
The client maps HTTP responses to a typed exception hierarchy so callers
64+
can react to intent rather than status codes.
65+
66+
```python
67+
from openchainbench import (
68+
OpenChainBench,
69+
NotFoundError,
70+
RateLimitError,
71+
APIUnavailableError,
72+
)
73+
74+
with OpenChainBench() as ocb:
75+
try:
76+
ocb.get_benchmark("not-a-real-slug")
77+
except NotFoundError:
78+
...
79+
except RateLimitError as exc:
80+
print(f"retry after {exc.retry_after_sec}s")
81+
except APIUnavailableError:
82+
# cold cache or Prom blackout, retry later
83+
...
84+
```
85+
86+
## API reference
87+
88+
| Method | Endpoint | Returns |
89+
|---|---|---|
90+
| `list_benchmarks()` | `GET /api/citable` | `list[BenchmarkSummary]` |
91+
| `fetch_citable_index()` | `GET /api/citable` | `CitableIndex` |
92+
| `get_benchmark(slug, *, chain=None, region=None)` | `GET /api/stat/<slug>` | `Benchmark` |
93+
| `get_series(slug, *, range="24h", chain=None, region=None, providers=None)` | `GET /api/series/<slug>` | `Series` |
94+
95+
All models are immutable dataclasses (`frozen=True`).
96+
97+
## Rate limits
98+
99+
The public API allows 60 requests per minute per IP. The client surfaces
100+
HTTP 429 as a `RateLimitError` with a `retry_after_sec` attribute.
101+
102+
## Citation
103+
104+
If you use the data in a paper, post, or product, please link the
105+
benchmark page. The license is CC-BY-4.0. Every payload includes a
106+
ready-to-paste `quote` field that already contains the attribution.
107+
108+
## Links
109+
110+
- Site: <https://openchainbench.com>
111+
- Docs: <https://openchainbench.com/docs>
112+
- Repository: <https://github.com/ChainBench/OpenChainBench>
113+
- Issues: <https://github.com/ChainBench/OpenChainBench/issues>
114+
115+
## License
116+
117+
MIT. The data fetched from the API stays under CC-BY-4.0; this client
118+
license only covers the code.

0 commit comments

Comments
 (0)