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.
@@ -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/
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/openchainbench.svg)](https://pypi.org/project/openchainbench/)
41
+ [![Python versions](https://img.shields.io/pypi/pyversions/openchainbench.svg)](https://pypi.org/project/openchainbench/)
42
+ [![Downloads](https://img.shields.io/pypi/dm/openchainbench.svg)](https://pypi.org/project/openchainbench/)
43
+ [![License](https://img.shields.io/pypi/l/openchainbench.svg)](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
+ [![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,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