Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: PyPI Publish

on:
push:
tags:
- "python-v*"
workflow_dispatch:
inputs:
ref:
description: "Git ref to build (branch, tag or sha)"
required: false
default: "main"

jobs:
build:
name: Build sdist + wheel
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref || github.ref }}
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install build tooling
run: python -m pip install --upgrade pip build
- name: Build distributions
working-directory: packages/python-client
run: python -m build
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: python-dist
path: packages/python-client/dist/

smoke-test:
name: Install + import smoke test
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: actions/download-artifact@v4
with:
name: python-dist
path: dist
- name: Install built wheel
run: |
python -m pip install --upgrade pip
python -m pip install dist/openchainbench-*.whl
- name: Import smoke test
run: |
python -c "import openchainbench; print(openchainbench.__version__)"

publish:
name: Publish to PyPI (Trusted Publisher)
needs: [build, smoke-test]
if: startsWith(github.ref, 'refs/tags/python-v')
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/openchainbench/
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: python-dist
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist
37 changes: 37 additions & 0 deletions .github/workflows/python-client-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Python Client CI

on:
pull_request:
paths:
- "packages/python-client/**"
- ".github/workflows/python-client-ci.yml"
push:
branches: [main, dev]
paths:
- "packages/python-client/**"
- ".github/workflows/python-client-ci.yml"

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
env:
OCB_SKIP_INTEGRATION: "1"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install package + dev deps
working-directory: packages/python-client
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Run unit tests
working-directory: packages/python-client
run: python -m pytest tests -m "not integration"
- name: Build sdist + wheel
working-directory: packages/python-client
run: python -m build
9 changes: 9 additions & 0 deletions packages/python-client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.venv/
dist/
build/
*.egg-info/
__pycache__/
*.py[cod]
.pytest_cache/
.coverage
htmlcov/
21 changes: 21 additions & 0 deletions packages/python-client/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 OpenChainBench contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
57 changes: 57 additions & 0 deletions packages/python-client/PUBLISHING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Publishing `openchainbench` to PyPI

Releases are fully automated through GitHub Actions and PyPI Trusted
Publishers. No long-lived API token is stored anywhere.

## One-time setup (PyPI Trusted Publisher)

1. Create an account on https://pypi.org if you do not have one.
2. Reserve the project name by uploading the first release manually
**or** request the project via the PyPI account settings. We do the
pending publisher flow below so the very first release is automated.
3. Go to https://pypi.org/manage/account/publishing/ and click
**Add a new pending publisher**.
4. Fill in:
- PyPI project name: `openchainbench`
- Owner: `ChainBench`
- Repository name: `OpenChainBench`
- Workflow name: `pypi-publish.yml`
- Environment name: `pypi`
5. Save. PyPI will accept the first upload from the matching GitHub
workflow with no token.
6. In GitHub, go to **Settings > Environments** for the repo and create
the `pypi` environment. Add required reviewers if you want a manual
gate before publish.

## Cutting a release

1. Bump the version in two places:
- `packages/python-client/pyproject.toml` (`project.version`)
- `packages/python-client/src/openchainbench/__init__.py` (`__version__`)
- `packages/python-client/src/openchainbench/client.py` (`USER_AGENT`)
2. Commit the bump on `main`.
3. Tag and push:
```bash
git tag python-v0.1.0
git push origin python-v0.1.0
```
4. The `PyPI Publish` workflow runs: build, smoke test on 3.10-3.13,
then publish via the Trusted Publisher OIDC flow.
5. Verify the release at https://pypi.org/project/openchainbench/.

## Local smoke build

```bash
cd packages/python-client
python -m pip install --upgrade build
python -m build
ls dist/
```

This produces an `sdist` (`.tar.gz`) and a pure-Python `wheel` (`.whl`).
The Trusted Publisher workflow runs the exact same command in CI.

## Tag namespace

We deliberately use `python-v*` (not bare `v*`) so OCB site tags such as
`v1.0-dataset` never trigger a Python publish.
118 changes: 118 additions & 0 deletions packages/python-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# openchainbench

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).

[![PyPI version](https://img.shields.io/pypi/v/openchainbench.svg)](https://pypi.org/project/openchainbench/)
[![Python versions](https://img.shields.io/pypi/pyversions/openchainbench.svg)](https://pypi.org/project/openchainbench/)
[![Downloads](https://img.shields.io/pypi/dm/openchainbench.svg)](https://pypi.org/project/openchainbench/)
[![License](https://img.shields.io/pypi/l/openchainbench.svg)](https://github.com/ChainBench/OpenChainBench/blob/main/LICENSE)

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).

## Install

```bash
pip install openchainbench
```

Requires Python 3.10+.

## Quick start

```python
from openchainbench import OpenChainBench

with OpenChainBench() as ocb:
for bench in ocb.list_benchmarks():
if bench.leader:
print(f"{bench.title}: {bench.leader.name} -> {bench.value}")
```

### Fetch one benchmark

```python
from openchainbench import OpenChainBench

with OpenChainBench() as ocb:
bench = ocb.get_benchmark("bridge-fee")
print(bench.headline)
for row in bench.rankings[:3]:
print(row.slug, row.ms.p50, row.success_rate)
```

### Fetch a time series

```python
from openchainbench import OpenChainBench

with OpenChainBench() as ocb:
series = ocb.get_series("bridge-fee", range="24h")
for provider in series.providers:
print(provider.name, provider.values[-1])
```

### Filter by chain or region

```python
bench = ocb.get_benchmark("network-fees", chain="ethereum")
series = ocb.get_series("network-fees", range="7d", chain="ethereum", region="eu-west")
```

## Error handling

The client maps HTTP responses to a typed exception hierarchy so callers
can react to intent rather than status codes.

```python
from openchainbench import (
OpenChainBench,
NotFoundError,
RateLimitError,
APIUnavailableError,
)

with OpenChainBench() as ocb:
try:
ocb.get_benchmark("not-a-real-slug")
except NotFoundError:
...
except RateLimitError as exc:
print(f"retry after {exc.retry_after_sec}s")
except APIUnavailableError:
# cold cache or Prom blackout, retry later
...
```

## API reference

| Method | Endpoint | Returns |
|---|---|---|
| `list_benchmarks()` | `GET /api/citable` | `list[BenchmarkSummary]` |
| `fetch_citable_index()` | `GET /api/citable` | `CitableIndex` |
| `get_benchmark(slug, *, chain=None, region=None)` | `GET /api/stat/<slug>` | `Benchmark` |
| `get_series(slug, *, range="24h", chain=None, region=None, providers=None)` | `GET /api/series/<slug>` | `Series` |

All models are immutable dataclasses (`frozen=True`).

## Rate limits

The public API allows 60 requests per minute per IP. The client surfaces
HTTP 429 as a `RateLimitError` with a `retry_after_sec` attribute.

## Citation

If you use the data in a paper, post, or product, please link the
benchmark page. The license is CC-BY-4.0. Every payload includes a
ready-to-paste `quote` field that already contains the attribution.

## Links

- Site: <https://openchainbench.com>
- Docs: <https://openchainbench.com/docs>
- Repository: <https://github.com/ChainBench/OpenChainBench>
- Issues: <https://github.com/ChainBench/OpenChainBench/issues>

## License

MIT. The data fetched from the API stays under CC-BY-4.0; this client
license only covers the code.
74 changes: 74 additions & 0 deletions packages/python-client/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "openchainbench"
version = "0.1.0"
description = "Python client for OpenChainBench live crypto infrastructure benchmarks"
authors = [{ name = "OpenChainBench", email = "hello@openchainbench.com" }]
license = { text = "MIT" }
requires-python = ">=3.10"
readme = "README.md"
keywords = [
"openchainbench",
"blockchain",
"benchmark",
"rpc",
"crypto",
"ethereum",
"solana",
"infrastructure",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Internet",
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
]
dependencies = [
"httpx>=0.27",
]

[project.optional-dependencies]
dev = [
"pytest>=8",
"pytest-cov>=5",
"build>=1.2",
]

[project.urls]
Homepage = "https://openchainbench.com"
Documentation = "https://openchainbench.com/docs"
Repository = "https://github.com/ChainBench/OpenChainBench"
Issues = "https://github.com/ChainBench/OpenChainBench/issues"
Changelog = "https://github.com/ChainBench/OpenChainBench/releases"

[tool.hatch.build.targets.wheel]
packages = ["src/openchainbench"]

[tool.hatch.build.targets.sdist]
include = [
"src/openchainbench",
"tests",
"README.md",
"LICENSE",
"pyproject.toml",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra -q"
markers = [
"integration: hits the live openchainbench.com API (skipped when OCB_SKIP_INTEGRATION=1)",
]
Loading
Loading