openchainbench 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openchainbench-0.1.0/.gitignore +9 -0
- openchainbench-0.1.0/LICENSE +21 -0
- openchainbench-0.1.0/PKG-INFO +153 -0
- openchainbench-0.1.0/README.md +118 -0
- openchainbench-0.1.0/pyproject.toml +74 -0
- openchainbench-0.1.0/src/openchainbench/__init__.py +48 -0
- openchainbench-0.1.0/src/openchainbench/client.py +202 -0
- openchainbench-0.1.0/src/openchainbench/exceptions.py +41 -0
- openchainbench-0.1.0/src/openchainbench/models.py +319 -0
- openchainbench-0.1.0/src/openchainbench/py.typed +0 -0
- openchainbench-0.1.0/tests/__init__.py +0 -0
- openchainbench-0.1.0/tests/fixtures/citable.json +46 -0
- openchainbench-0.1.0/tests/fixtures/series.json +24 -0
- openchainbench-0.1.0/tests/fixtures/stat.json +49 -0
- openchainbench-0.1.0/tests/test_client.py +184 -0
- openchainbench-0.1.0/tests/test_models.py +133 -0
|
@@ -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.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openchainbench
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for OpenChainBench live crypto infrastructure benchmarks
|
|
5
|
+
Project-URL: Homepage, https://openchainbench.com
|
|
6
|
+
Project-URL: Documentation, https://openchainbench.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/ChainBench/OpenChainBench
|
|
8
|
+
Project-URL: Issues, https://github.com/ChainBench/OpenChainBench/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/ChainBench/OpenChainBench/releases
|
|
10
|
+
Author-email: OpenChainBench <hello@openchainbench.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: benchmark,blockchain,crypto,ethereum,infrastructure,openchainbench,rpc,solana
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Internet
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: httpx>=0.27
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# openchainbench
|
|
37
|
+
|
|
38
|
+
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).
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/openchainbench/)
|
|
41
|
+
[](https://pypi.org/project/openchainbench/)
|
|
42
|
+
[](https://pypi.org/project/openchainbench/)
|
|
43
|
+
[](https://github.com/ChainBench/OpenChainBench/blob/main/LICENSE)
|
|
44
|
+
|
|
45
|
+
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).
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install openchainbench
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Requires Python 3.10+.
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from openchainbench import OpenChainBench
|
|
59
|
+
|
|
60
|
+
with OpenChainBench() as ocb:
|
|
61
|
+
for bench in ocb.list_benchmarks():
|
|
62
|
+
if bench.leader:
|
|
63
|
+
print(f"{bench.title}: {bench.leader.name} -> {bench.value}")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Fetch one benchmark
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from openchainbench import OpenChainBench
|
|
70
|
+
|
|
71
|
+
with OpenChainBench() as ocb:
|
|
72
|
+
bench = ocb.get_benchmark("bridge-fee")
|
|
73
|
+
print(bench.headline)
|
|
74
|
+
for row in bench.rankings[:3]:
|
|
75
|
+
print(row.slug, row.ms.p50, row.success_rate)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Fetch a time series
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from openchainbench import OpenChainBench
|
|
82
|
+
|
|
83
|
+
with OpenChainBench() as ocb:
|
|
84
|
+
series = ocb.get_series("bridge-fee", range="24h")
|
|
85
|
+
for provider in series.providers:
|
|
86
|
+
print(provider.name, provider.values[-1])
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Filter by chain or region
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
bench = ocb.get_benchmark("network-fees", chain="ethereum")
|
|
93
|
+
series = ocb.get_series("network-fees", range="7d", chain="ethereum", region="eu-west")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Error handling
|
|
97
|
+
|
|
98
|
+
The client maps HTTP responses to a typed exception hierarchy so callers
|
|
99
|
+
can react to intent rather than status codes.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from openchainbench import (
|
|
103
|
+
OpenChainBench,
|
|
104
|
+
NotFoundError,
|
|
105
|
+
RateLimitError,
|
|
106
|
+
APIUnavailableError,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
with OpenChainBench() as ocb:
|
|
110
|
+
try:
|
|
111
|
+
ocb.get_benchmark("not-a-real-slug")
|
|
112
|
+
except NotFoundError:
|
|
113
|
+
...
|
|
114
|
+
except RateLimitError as exc:
|
|
115
|
+
print(f"retry after {exc.retry_after_sec}s")
|
|
116
|
+
except APIUnavailableError:
|
|
117
|
+
# cold cache or Prom blackout, retry later
|
|
118
|
+
...
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## API reference
|
|
122
|
+
|
|
123
|
+
| Method | Endpoint | Returns |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `list_benchmarks()` | `GET /api/citable` | `list[BenchmarkSummary]` |
|
|
126
|
+
| `fetch_citable_index()` | `GET /api/citable` | `CitableIndex` |
|
|
127
|
+
| `get_benchmark(slug, *, chain=None, region=None)` | `GET /api/stat/<slug>` | `Benchmark` |
|
|
128
|
+
| `get_series(slug, *, range="24h", chain=None, region=None, providers=None)` | `GET /api/series/<slug>` | `Series` |
|
|
129
|
+
|
|
130
|
+
All models are immutable dataclasses (`frozen=True`).
|
|
131
|
+
|
|
132
|
+
## Rate limits
|
|
133
|
+
|
|
134
|
+
The public API allows 60 requests per minute per IP. The client surfaces
|
|
135
|
+
HTTP 429 as a `RateLimitError` with a `retry_after_sec` attribute.
|
|
136
|
+
|
|
137
|
+
## Citation
|
|
138
|
+
|
|
139
|
+
If you use the data in a paper, post, or product, please link the
|
|
140
|
+
benchmark page. The license is CC-BY-4.0. Every payload includes a
|
|
141
|
+
ready-to-paste `quote` field that already contains the attribution.
|
|
142
|
+
|
|
143
|
+
## Links
|
|
144
|
+
|
|
145
|
+
- Site: <https://openchainbench.com>
|
|
146
|
+
- Docs: <https://openchainbench.com/docs>
|
|
147
|
+
- Repository: <https://github.com/ChainBench/OpenChainBench>
|
|
148
|
+
- Issues: <https://github.com/ChainBench/OpenChainBench/issues>
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT. The data fetched from the API stays under CC-BY-4.0; this client
|
|
153
|
+
license only covers the code.
|
|
@@ -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
|
+
[](https://pypi.org/project/openchainbench/)
|
|
6
|
+
[](https://pypi.org/project/openchainbench/)
|
|
7
|
+
[](https://pypi.org/project/openchainbench/)
|
|
8
|
+
[](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,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "openchainbench"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client for OpenChainBench live crypto infrastructure benchmarks"
|
|
9
|
+
authors = [{ name = "OpenChainBench", email = "hello@openchainbench.com" }]
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
keywords = [
|
|
14
|
+
"openchainbench",
|
|
15
|
+
"blockchain",
|
|
16
|
+
"benchmark",
|
|
17
|
+
"rpc",
|
|
18
|
+
"crypto",
|
|
19
|
+
"ethereum",
|
|
20
|
+
"solana",
|
|
21
|
+
"infrastructure",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Topic :: Internet",
|
|
36
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
37
|
+
"Typing :: Typed",
|
|
38
|
+
]
|
|
39
|
+
dependencies = [
|
|
40
|
+
"httpx>=0.27",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest>=8",
|
|
46
|
+
"pytest-cov>=5",
|
|
47
|
+
"build>=1.2",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Homepage = "https://openchainbench.com"
|
|
52
|
+
Documentation = "https://openchainbench.com/docs"
|
|
53
|
+
Repository = "https://github.com/ChainBench/OpenChainBench"
|
|
54
|
+
Issues = "https://github.com/ChainBench/OpenChainBench/issues"
|
|
55
|
+
Changelog = "https://github.com/ChainBench/OpenChainBench/releases"
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["src/openchainbench"]
|
|
59
|
+
|
|
60
|
+
[tool.hatch.build.targets.sdist]
|
|
61
|
+
include = [
|
|
62
|
+
"src/openchainbench",
|
|
63
|
+
"tests",
|
|
64
|
+
"README.md",
|
|
65
|
+
"LICENSE",
|
|
66
|
+
"pyproject.toml",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
addopts = "-ra -q"
|
|
72
|
+
markers = [
|
|
73
|
+
"integration: hits the live openchainbench.com API (skipped when OCB_SKIP_INTEGRATION=1)",
|
|
74
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Official Python client for OpenChainBench.
|
|
2
|
+
|
|
3
|
+
OpenChainBench (https://openchainbench.com) publishes live, reproducible
|
|
4
|
+
benchmarks of crypto infrastructure: RPC latency, bridge fees, perp venue
|
|
5
|
+
performance, oracle deviation, and more. This package wraps the public,
|
|
6
|
+
CC-BY-4.0 licensed JSON API so that Python applications, notebooks, and
|
|
7
|
+
agents can cite live numbers without scraping HTML.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .client import DEFAULT_BASE_URL, OpenChainBench
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
APIUnavailableError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
OpenChainBenchError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
)
|
|
17
|
+
from .models import (
|
|
18
|
+
Benchmark,
|
|
19
|
+
BenchmarkSummary,
|
|
20
|
+
CitableIndex,
|
|
21
|
+
Latency,
|
|
22
|
+
Leader,
|
|
23
|
+
ProviderResult,
|
|
24
|
+
Series,
|
|
25
|
+
SeriesProvider,
|
|
26
|
+
Source,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"DEFAULT_BASE_URL",
|
|
33
|
+
"OpenChainBench",
|
|
34
|
+
"OpenChainBenchError",
|
|
35
|
+
"NotFoundError",
|
|
36
|
+
"RateLimitError",
|
|
37
|
+
"APIUnavailableError",
|
|
38
|
+
"Benchmark",
|
|
39
|
+
"BenchmarkSummary",
|
|
40
|
+
"CitableIndex",
|
|
41
|
+
"Latency",
|
|
42
|
+
"Leader",
|
|
43
|
+
"ProviderResult",
|
|
44
|
+
"Series",
|
|
45
|
+
"SeriesProvider",
|
|
46
|
+
"Source",
|
|
47
|
+
"__version__",
|
|
48
|
+
]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Synchronous HTTP client for the public OpenChainBench API.
|
|
2
|
+
|
|
3
|
+
Wraps three endpoints:
|
|
4
|
+
|
|
5
|
+
* ``GET /api/citable`` -> :meth:`OpenChainBench.list_benchmarks`
|
|
6
|
+
* ``GET /api/stat/<slug>`` -> :meth:`OpenChainBench.get_benchmark`
|
|
7
|
+
* ``GET /api/series/<slug>?range=...`` -> :meth:`OpenChainBench.get_series`
|
|
8
|
+
|
|
9
|
+
The client keeps a long-lived ``httpx.Client`` and supports the context
|
|
10
|
+
manager protocol. Errors are mapped to a typed hierarchy in
|
|
11
|
+
:mod:`openchainbench.exceptions` so callers can ``except`` on intent rather
|
|
12
|
+
than HTTP status codes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, List, Optional
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
from .exceptions import (
|
|
22
|
+
APIUnavailableError,
|
|
23
|
+
NotFoundError,
|
|
24
|
+
OpenChainBenchError,
|
|
25
|
+
RateLimitError,
|
|
26
|
+
)
|
|
27
|
+
from .models import Benchmark, BenchmarkSummary, CitableIndex, Series
|
|
28
|
+
|
|
29
|
+
DEFAULT_BASE_URL = "https://openchainbench.com"
|
|
30
|
+
DEFAULT_TIMEOUT = 30.0
|
|
31
|
+
USER_AGENT = "openchainbench-python/0.1.0"
|
|
32
|
+
|
|
33
|
+
_SUPPORTED_RANGES = ("24h", "7d", "30d")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parse_retry_after(value: Optional[str]) -> Optional[int]:
|
|
37
|
+
if not value:
|
|
38
|
+
return None
|
|
39
|
+
try:
|
|
40
|
+
return int(value)
|
|
41
|
+
except (TypeError, ValueError):
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
46
|
+
if response.status_code < 400:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
body: Any
|
|
50
|
+
try:
|
|
51
|
+
body = response.json()
|
|
52
|
+
except ValueError:
|
|
53
|
+
body = {}
|
|
54
|
+
|
|
55
|
+
message = (
|
|
56
|
+
body.get("error")
|
|
57
|
+
if isinstance(body, dict) and body.get("error")
|
|
58
|
+
else f"HTTP {response.status_code} from {response.request.url}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if response.status_code == 404:
|
|
62
|
+
raise NotFoundError(str(message))
|
|
63
|
+
if response.status_code == 429:
|
|
64
|
+
retry = _parse_retry_after(response.headers.get("retry-after"))
|
|
65
|
+
if retry is None and isinstance(body, dict):
|
|
66
|
+
retry = body.get("retryAfterSec")
|
|
67
|
+
raise RateLimitError(str(message), retry_after_sec=retry)
|
|
68
|
+
if response.status_code == 503:
|
|
69
|
+
retry = _parse_retry_after(response.headers.get("retry-after"))
|
|
70
|
+
if retry is None and isinstance(body, dict):
|
|
71
|
+
retry = body.get("retryAfterSec")
|
|
72
|
+
raise APIUnavailableError(str(message), retry_after_sec=retry)
|
|
73
|
+
|
|
74
|
+
raise OpenChainBenchError(str(message), status_code=response.status_code)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class OpenChainBench:
|
|
78
|
+
"""Synchronous client for openchainbench.com.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> from openchainbench import OpenChainBench
|
|
82
|
+
>>> with OpenChainBench() as ocb:
|
|
83
|
+
... for bench in ocb.list_benchmarks():
|
|
84
|
+
... print(bench.slug, bench.value)
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
90
|
+
*,
|
|
91
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
92
|
+
user_agent: str = USER_AGENT,
|
|
93
|
+
client: Optional[httpx.Client] = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
self._owns_client = client is None
|
|
96
|
+
self._client = client or httpx.Client(
|
|
97
|
+
base_url=base_url.rstrip("/"),
|
|
98
|
+
timeout=timeout,
|
|
99
|
+
headers={
|
|
100
|
+
"User-Agent": user_agent,
|
|
101
|
+
"Accept": "application/json",
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def list_benchmarks(self) -> List[BenchmarkSummary]:
|
|
106
|
+
"""Return every live benchmark with its current headline figure.
|
|
107
|
+
|
|
108
|
+
Wraps the ``CitableIndex`` envelope and returns the list directly
|
|
109
|
+
for ergonomic iteration. Use :meth:`fetch_citable_index` if you
|
|
110
|
+
need the site metadata as well.
|
|
111
|
+
"""
|
|
112
|
+
return list(self.fetch_citable_index().benchmarks)
|
|
113
|
+
|
|
114
|
+
def fetch_citable_index(self) -> CitableIndex:
|
|
115
|
+
"""Return the full ``/api/citable`` payload including site metadata."""
|
|
116
|
+
data = self._get("/api/citable")
|
|
117
|
+
return CitableIndex.from_dict(data)
|
|
118
|
+
|
|
119
|
+
def get_benchmark(
|
|
120
|
+
self,
|
|
121
|
+
slug: str,
|
|
122
|
+
*,
|
|
123
|
+
chain: Optional[str] = None,
|
|
124
|
+
region: Optional[str] = None,
|
|
125
|
+
) -> Benchmark:
|
|
126
|
+
"""Return the full benchmark detail.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
slug: Benchmark slug, e.g. ``"bridge-fee"``.
|
|
130
|
+
chain: Optional chain filter (e.g. ``"ethereum"``).
|
|
131
|
+
region: Optional region filter (e.g. ``"eu-west"``).
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
NotFoundError: The slug does not exist or is not live.
|
|
135
|
+
"""
|
|
136
|
+
params: dict[str, str] = {}
|
|
137
|
+
if chain:
|
|
138
|
+
params["chain"] = chain
|
|
139
|
+
if region:
|
|
140
|
+
params["region"] = region
|
|
141
|
+
data = self._get(f"/api/stat/{slug}", params=params or None)
|
|
142
|
+
return Benchmark.from_dict(data)
|
|
143
|
+
|
|
144
|
+
def get_series(
|
|
145
|
+
self,
|
|
146
|
+
slug: str,
|
|
147
|
+
*,
|
|
148
|
+
range: str = "24h",
|
|
149
|
+
chain: Optional[str] = None,
|
|
150
|
+
region: Optional[str] = None,
|
|
151
|
+
providers: Optional[List[str]] = None,
|
|
152
|
+
) -> Series:
|
|
153
|
+
"""Return the per-provider time series for a benchmark.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
slug: Benchmark slug.
|
|
157
|
+
range: One of ``"24h"``, ``"7d"``, ``"30d"``.
|
|
158
|
+
chain: Optional chain filter.
|
|
159
|
+
region: Optional region filter.
|
|
160
|
+
providers: Optional list of provider slugs to restrict the result to.
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
ValueError: ``range`` is not supported.
|
|
164
|
+
NotFoundError: The slug does not exist or has no data for ``range``.
|
|
165
|
+
"""
|
|
166
|
+
if range not in _SUPPORTED_RANGES:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"range must be one of {_SUPPORTED_RANGES}, got {range!r}"
|
|
169
|
+
)
|
|
170
|
+
params: dict[str, str] = {"range": range}
|
|
171
|
+
if chain:
|
|
172
|
+
params["chain"] = chain
|
|
173
|
+
if region:
|
|
174
|
+
params["region"] = region
|
|
175
|
+
if providers:
|
|
176
|
+
params["providers"] = ",".join(providers)
|
|
177
|
+
data = self._get(f"/api/series/{slug}", params=params)
|
|
178
|
+
return Series.from_dict(data)
|
|
179
|
+
|
|
180
|
+
def close(self) -> None:
|
|
181
|
+
"""Close the underlying HTTP client (if owned)."""
|
|
182
|
+
if self._owns_client:
|
|
183
|
+
self._client.close()
|
|
184
|
+
|
|
185
|
+
def __enter__(self) -> "OpenChainBench":
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
def __exit__(self, *exc_info: Any) -> None:
|
|
189
|
+
self.close()
|
|
190
|
+
|
|
191
|
+
def _get(self, path: str, *, params: Optional[dict[str, str]] = None) -> Any:
|
|
192
|
+
try:
|
|
193
|
+
response = self._client.get(path, params=params)
|
|
194
|
+
except httpx.HTTPError as exc:
|
|
195
|
+
raise OpenChainBenchError(f"HTTP request failed: {exc}") from exc
|
|
196
|
+
_raise_for_status(response)
|
|
197
|
+
try:
|
|
198
|
+
return response.json()
|
|
199
|
+
except ValueError as exc:
|
|
200
|
+
raise OpenChainBenchError(
|
|
201
|
+
f"Response was not valid JSON: {exc}"
|
|
202
|
+
) from exc
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Exception hierarchy for the OpenChainBench client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OpenChainBenchError(Exception):
|
|
9
|
+
"""Base error for every failure surfaced by this client."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, *, status_code: Optional[int] = None) -> None:
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotFoundError(OpenChainBenchError):
|
|
17
|
+
"""Raised when a benchmark slug or range does not exist."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str) -> None:
|
|
20
|
+
super().__init__(message, status_code=404)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RateLimitError(OpenChainBenchError):
|
|
24
|
+
"""Raised when the API returns HTTP 429.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
retry_after_sec: Seconds the caller should wait before retrying,
|
|
28
|
+
parsed from the ``Retry-After`` header or the response body.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, *, retry_after_sec: Optional[int] = None) -> None:
|
|
32
|
+
super().__init__(message, status_code=429)
|
|
33
|
+
self.retry_after_sec = retry_after_sec
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class APIUnavailableError(OpenChainBenchError):
|
|
37
|
+
"""Raised when the API returns HTTP 503 (no live snapshot available)."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str, *, retry_after_sec: Optional[int] = None) -> None:
|
|
40
|
+
super().__init__(message, status_code=503)
|
|
41
|
+
self.retry_after_sec = retry_after_sec
|