bloklake 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.
- bloklake-0.1.0/.gitignore +6 -0
- bloklake-0.1.0/LICENSE +21 -0
- bloklake-0.1.0/PKG-INFO +161 -0
- bloklake-0.1.0/README.md +144 -0
- bloklake-0.1.0/pyproject.toml +32 -0
- bloklake-0.1.0/scripts/generate.py +110 -0
- bloklake-0.1.0/scripts/schema.json +814 -0
- bloklake-0.1.0/src/bloklake/__init__.py +16 -0
- bloklake-0.1.0/src/bloklake/client.py +254 -0
- bloklake-0.1.0/src/bloklake/decode.py +67 -0
- bloklake-0.1.0/src/bloklake/errors.py +25 -0
- bloklake-0.1.0/src/bloklake/fields.py +317 -0
- bloklake-0.1.0/src/bloklake/py.typed +0 -0
- bloklake-0.1.0/src/bloklake/types.py +135 -0
- bloklake-0.1.0/tests/fixtures/eth-all-fields.arrows +0 -0
- bloklake-0.1.0/tests/fixtures/eth-all-fields.json +1 -0
- bloklake-0.1.0/tests/fixtures/eth-all-fields.request.json +1 -0
- bloklake-0.1.0/tests/fixtures/eth-empty.arrows +0 -0
- bloklake-0.1.0/tests/fixtures/eth-empty.json +1 -0
- bloklake-0.1.0/tests/fixtures/eth-empty.request.json +1 -0
- bloklake-0.1.0/tests/fixtures/eth-logs-only.arrows +0 -0
- bloklake-0.1.0/tests/fixtures/eth-logs-only.json +1 -0
- bloklake-0.1.0/tests/fixtures/eth-logs-only.request.json +1 -0
- bloklake-0.1.0/tests/fixtures/rh-all-fields.arrows +0 -0
- bloklake-0.1.0/tests/fixtures/rh-all-fields.json +1 -0
- bloklake-0.1.0/tests/fixtures/rh-all-fields.request.json +1 -0
- bloklake-0.1.0/tests/fixtures/synthetic-short.arrows +0 -0
- bloklake-0.1.0/tests/fixtures/synthetic-u256.arrows +0 -0
- bloklake-0.1.0/tests/fixtures/synthetic-u256.json +46 -0
- bloklake-0.1.0/tests/test_client.py +155 -0
- bloklake-0.1.0/tests/test_decode.py +52 -0
- bloklake-0.1.0/tests/test_live.py +71 -0
bloklake-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bloklake
|
|
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.
|
bloklake-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bloklake
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bloklake API client: typed queries, Apache Arrow transport, JSON-like rows.
|
|
5
|
+
Project-URL: Documentation, https://docs.bloklake.com
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Typing :: Typed
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: pyarrow>=14
|
|
12
|
+
Requires-Dist: requests>=2.31
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# bloklake
|
|
19
|
+
|
|
20
|
+
The official Python client for the [Bloklake](https://docs.bloklake.com) API.
|
|
21
|
+
|
|
22
|
+
- **Typed queries**: filters, field names and rows are `TypedDict`s and `Literal`s, so your
|
|
23
|
+
editor and mypy catch a misspelled field.
|
|
24
|
+
- **Arrow on the wire, JSON-like rows in your code**: responses travel as Apache Arrow (about
|
|
25
|
+
2–2.5× smaller than JSON, so cheaper) and are decoded here into plain dicts with the same
|
|
26
|
+
values `/query/json` returns.
|
|
27
|
+
- **Pagination, retries and integrity built in**: follows `next_block`, reuses prepared
|
|
28
|
+
queries, retries rate limits and busy servers, and refuses truncated responses.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install bloklake
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Python 3.9+. Depends on `pyarrow` and `requests`.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from bloklake import Bloklake
|
|
40
|
+
|
|
41
|
+
bl = Bloklake(chain="eth") # API key from BLOKLAKE_API_KEY
|
|
42
|
+
|
|
43
|
+
page = bl.query({
|
|
44
|
+
"from_block": 19_426_000,
|
|
45
|
+
"to_block": 19_426_009,
|
|
46
|
+
"logs": [{
|
|
47
|
+
"address": ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"],
|
|
48
|
+
"topic0": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
|
|
49
|
+
}],
|
|
50
|
+
"fields": {"log": ["block_number", "transaction_hash", "topic1", "topic2", "data"]},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
for log in page.logs:
|
|
54
|
+
print(log["block_number"], log["data"])
|
|
55
|
+
print(page.meta.next_block)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Write the query in snake_case, as above, or in the API's camelCase (`"fromBlock"`,
|
|
59
|
+
`"blockNumber"`, as in the HTTP API and curl examples); the client sends camelCase either way.
|
|
60
|
+
Give each key one way only. Rows always come back in snake_case (`log["block_number"]`).
|
|
61
|
+
|
|
62
|
+
## Reading a whole range
|
|
63
|
+
|
|
64
|
+
`paginate` follows `next_block` until `to_block` (or the chain head when you leave `to_block`
|
|
65
|
+
out). After the first page it sends only the prepared-query id, and falls back to the full
|
|
66
|
+
query if the id expired.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
for page in bl.paginate({
|
|
70
|
+
"from_block": 19_000_000,
|
|
71
|
+
"to_block": 19_100_000,
|
|
72
|
+
"logs": [{"topic0": [TRANSFER]}],
|
|
73
|
+
"fields": {"log": ["block_number", "address", "topic1", "topic2", "data"]},
|
|
74
|
+
}):
|
|
75
|
+
store(page.logs)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`collect(query)` returns all pages merged into one `Page`. Use it for ranges that fit in memory.
|
|
79
|
+
|
|
80
|
+
## Results
|
|
81
|
+
|
|
82
|
+
A `Page` has `logs`, `transactions` and `blocks` lists (empty when not requested) and `meta`
|
|
83
|
+
(`from_block`, `to_block`, `next_block`, `objects`, `archive_height`, `finalized_block`,
|
|
84
|
+
`to_block_hash`, `query_id`, `unavailable_fields`).
|
|
85
|
+
|
|
86
|
+
Rows use the API's column names (`block_number`, `transaction_hash`) and its JSON encoding:
|
|
87
|
+
|
|
88
|
+
| Field kind | Value |
|
|
89
|
+
|---|---|
|
|
90
|
+
| block numbers, indexes, gas, timestamps | `int` |
|
|
91
|
+
| amounts (`value`, gas prices, fees, uint256) | decimal `str` (`int(x)` to compute) |
|
|
92
|
+
| hashes, addresses, topics, `input`, `data` | `0x` hex `str` |
|
|
93
|
+
| `withdrawals`, `access_list`, `authorization_list` | lists of dicts |
|
|
94
|
+
| not available or not applicable | `None` |
|
|
95
|
+
|
|
96
|
+
To get a dataframe: `pandas.DataFrame(page.logs)` or `polars.DataFrame(page.logs)`. If you'd
|
|
97
|
+
rather keep the raw Arrow columns, use the API directly as shown in
|
|
98
|
+
[Reading Arrow](https://docs.bloklake.com/responses/reading-arrow).
|
|
99
|
+
|
|
100
|
+
## Other calls
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
s = bl.create_set("address", pools) # idempotent: same values, same id
|
|
104
|
+
bl.query({"logs": [{"address_set": s.set_id, "topic0": [SWAP]}], "fields": {"log": ["address", "data"]}})
|
|
105
|
+
bl.get_set(s.set_id)
|
|
106
|
+
bl.delete_set(s.set_id)
|
|
107
|
+
|
|
108
|
+
bl.prepare({"logs": [...], "fields": {...}}) # Prepared(query_id, expires_in)
|
|
109
|
+
bl.status() # ChainStatus(chain_id, first_block, archive_height, finalized_block), free
|
|
110
|
+
bl.schema() # every field and whether this chain has it, free
|
|
111
|
+
bl.account() # plan, rps, balance or usage, free
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Errors
|
|
115
|
+
|
|
116
|
+
Failures raise `BloklakeError` with `status` (0 for client-side failures), `code`
|
|
117
|
+
(`invalid_query`, `rate_limited`, `balance_exhausted`, `truncated`, …), `retry_after` and
|
|
118
|
+
`request_id`. The client retries `429`, `503`, `5xx`, network errors and truncated responses
|
|
119
|
+
itself, up to `max_retries` (default 4), honoring `Retry-After`. Other errors need a fix and are
|
|
120
|
+
raised at once. See [Errors](https://docs.bloklake.com/api-reference/errors).
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from bloklake import BloklakeError
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
bl.query(q)
|
|
127
|
+
except BloklakeError as e:
|
|
128
|
+
if e.code == "balance_exhausted":
|
|
129
|
+
alert_someone()
|
|
130
|
+
else:
|
|
131
|
+
raise
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Options
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
Bloklake(
|
|
138
|
+
api_key="bl_live_…", # default: BLOKLAKE_API_KEY
|
|
139
|
+
chain="rh", # or base_url="https://api.bloklake.com/v1/chains/rh"
|
|
140
|
+
max_retries=4,
|
|
141
|
+
timeout=120.0,
|
|
142
|
+
session=requests.Session(), # proxies, custom adapters, tests
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pip install -e ".[dev]"
|
|
150
|
+
pytest # unit and fixture tests
|
|
151
|
+
mypy src/bloklake # strict
|
|
152
|
+
BLOKLAKE_TEST_URL=https://api.bloklake.com/v1/chains/eth BLOKLAKE_API_KEY=… pytest # plus live tests
|
|
153
|
+
python scripts/generate.py # regenerate src/bloklake/fields.py after updating scripts/schema.json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The fixture tests decode recorded Arrow responses and compare them with the server's own
|
|
157
|
+
`/query/json` output for the same requests, field by field.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
bloklake-0.1.0/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# bloklake
|
|
2
|
+
|
|
3
|
+
The official Python client for the [Bloklake](https://docs.bloklake.com) API.
|
|
4
|
+
|
|
5
|
+
- **Typed queries**: filters, field names and rows are `TypedDict`s and `Literal`s, so your
|
|
6
|
+
editor and mypy catch a misspelled field.
|
|
7
|
+
- **Arrow on the wire, JSON-like rows in your code**: responses travel as Apache Arrow (about
|
|
8
|
+
2–2.5× smaller than JSON, so cheaper) and are decoded here into plain dicts with the same
|
|
9
|
+
values `/query/json` returns.
|
|
10
|
+
- **Pagination, retries and integrity built in**: follows `next_block`, reuses prepared
|
|
11
|
+
queries, retries rate limits and busy servers, and refuses truncated responses.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install bloklake
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Python 3.9+. Depends on `pyarrow` and `requests`.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from bloklake import Bloklake
|
|
23
|
+
|
|
24
|
+
bl = Bloklake(chain="eth") # API key from BLOKLAKE_API_KEY
|
|
25
|
+
|
|
26
|
+
page = bl.query({
|
|
27
|
+
"from_block": 19_426_000,
|
|
28
|
+
"to_block": 19_426_009,
|
|
29
|
+
"logs": [{
|
|
30
|
+
"address": ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"],
|
|
31
|
+
"topic0": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
|
|
32
|
+
}],
|
|
33
|
+
"fields": {"log": ["block_number", "transaction_hash", "topic1", "topic2", "data"]},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
for log in page.logs:
|
|
37
|
+
print(log["block_number"], log["data"])
|
|
38
|
+
print(page.meta.next_block)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Write the query in snake_case, as above, or in the API's camelCase (`"fromBlock"`,
|
|
42
|
+
`"blockNumber"`, as in the HTTP API and curl examples); the client sends camelCase either way.
|
|
43
|
+
Give each key one way only. Rows always come back in snake_case (`log["block_number"]`).
|
|
44
|
+
|
|
45
|
+
## Reading a whole range
|
|
46
|
+
|
|
47
|
+
`paginate` follows `next_block` until `to_block` (or the chain head when you leave `to_block`
|
|
48
|
+
out). After the first page it sends only the prepared-query id, and falls back to the full
|
|
49
|
+
query if the id expired.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
for page in bl.paginate({
|
|
53
|
+
"from_block": 19_000_000,
|
|
54
|
+
"to_block": 19_100_000,
|
|
55
|
+
"logs": [{"topic0": [TRANSFER]}],
|
|
56
|
+
"fields": {"log": ["block_number", "address", "topic1", "topic2", "data"]},
|
|
57
|
+
}):
|
|
58
|
+
store(page.logs)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`collect(query)` returns all pages merged into one `Page`. Use it for ranges that fit in memory.
|
|
62
|
+
|
|
63
|
+
## Results
|
|
64
|
+
|
|
65
|
+
A `Page` has `logs`, `transactions` and `blocks` lists (empty when not requested) and `meta`
|
|
66
|
+
(`from_block`, `to_block`, `next_block`, `objects`, `archive_height`, `finalized_block`,
|
|
67
|
+
`to_block_hash`, `query_id`, `unavailable_fields`).
|
|
68
|
+
|
|
69
|
+
Rows use the API's column names (`block_number`, `transaction_hash`) and its JSON encoding:
|
|
70
|
+
|
|
71
|
+
| Field kind | Value |
|
|
72
|
+
|---|---|
|
|
73
|
+
| block numbers, indexes, gas, timestamps | `int` |
|
|
74
|
+
| amounts (`value`, gas prices, fees, uint256) | decimal `str` (`int(x)` to compute) |
|
|
75
|
+
| hashes, addresses, topics, `input`, `data` | `0x` hex `str` |
|
|
76
|
+
| `withdrawals`, `access_list`, `authorization_list` | lists of dicts |
|
|
77
|
+
| not available or not applicable | `None` |
|
|
78
|
+
|
|
79
|
+
To get a dataframe: `pandas.DataFrame(page.logs)` or `polars.DataFrame(page.logs)`. If you'd
|
|
80
|
+
rather keep the raw Arrow columns, use the API directly as shown in
|
|
81
|
+
[Reading Arrow](https://docs.bloklake.com/responses/reading-arrow).
|
|
82
|
+
|
|
83
|
+
## Other calls
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
s = bl.create_set("address", pools) # idempotent: same values, same id
|
|
87
|
+
bl.query({"logs": [{"address_set": s.set_id, "topic0": [SWAP]}], "fields": {"log": ["address", "data"]}})
|
|
88
|
+
bl.get_set(s.set_id)
|
|
89
|
+
bl.delete_set(s.set_id)
|
|
90
|
+
|
|
91
|
+
bl.prepare({"logs": [...], "fields": {...}}) # Prepared(query_id, expires_in)
|
|
92
|
+
bl.status() # ChainStatus(chain_id, first_block, archive_height, finalized_block), free
|
|
93
|
+
bl.schema() # every field and whether this chain has it, free
|
|
94
|
+
bl.account() # plan, rps, balance or usage, free
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Errors
|
|
98
|
+
|
|
99
|
+
Failures raise `BloklakeError` with `status` (0 for client-side failures), `code`
|
|
100
|
+
(`invalid_query`, `rate_limited`, `balance_exhausted`, `truncated`, …), `retry_after` and
|
|
101
|
+
`request_id`. The client retries `429`, `503`, `5xx`, network errors and truncated responses
|
|
102
|
+
itself, up to `max_retries` (default 4), honoring `Retry-After`. Other errors need a fix and are
|
|
103
|
+
raised at once. See [Errors](https://docs.bloklake.com/api-reference/errors).
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from bloklake import BloklakeError
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
bl.query(q)
|
|
110
|
+
except BloklakeError as e:
|
|
111
|
+
if e.code == "balance_exhausted":
|
|
112
|
+
alert_someone()
|
|
113
|
+
else:
|
|
114
|
+
raise
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Options
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
Bloklake(
|
|
121
|
+
api_key="bl_live_…", # default: BLOKLAKE_API_KEY
|
|
122
|
+
chain="rh", # or base_url="https://api.bloklake.com/v1/chains/rh"
|
|
123
|
+
max_retries=4,
|
|
124
|
+
timeout=120.0,
|
|
125
|
+
session=requests.Session(), # proxies, custom adapters, tests
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install -e ".[dev]"
|
|
133
|
+
pytest # unit and fixture tests
|
|
134
|
+
mypy src/bloklake # strict
|
|
135
|
+
BLOKLAKE_TEST_URL=https://api.bloklake.com/v1/chains/eth BLOKLAKE_API_KEY=… pytest # plus live tests
|
|
136
|
+
python scripts/generate.py # regenerate src/bloklake/fields.py after updating scripts/schema.json
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The fixture tests decode recorded Arrow responses and compare them with the server's own
|
|
140
|
+
`/query/json` output for the same requests, field by field.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bloklake"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Bloklake API client: typed queries, Apache Arrow transport, JSON-like rows."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = ["pyarrow>=14", "requests>=2.31"]
|
|
13
|
+
classifiers = ["Programming Language :: Python :: 3", "Typing :: Typed"]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = ["pytest>=8", "mypy>=1.10"]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Documentation = "https://docs.bloklake.com"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/bloklake"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
26
|
+
|
|
27
|
+
[tool.mypy]
|
|
28
|
+
strict = true
|
|
29
|
+
|
|
30
|
+
[[tool.mypy.overrides]]
|
|
31
|
+
module = ["pyarrow", "pyarrow.*"]
|
|
32
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Generate src/bloklake/fields.py from scripts/schema.json (a copy of GET /v1/schema).
|
|
2
|
+
|
|
3
|
+
Run `python scripts/generate.py` after refreshing schema.json when the API adds fields.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
10
|
+
schema = json.loads((ROOT / "scripts" / "schema.json").read_text())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def camel_case(name: str) -> str:
|
|
14
|
+
"""``block_number`` -> ``blockNumber``; must match ``bloklake.client._camel``."""
|
|
15
|
+
return re.sub(r"_([a-z0-9])", lambda m: m.group(1).upper(), name)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Requests may name a field by its column name (``block_number``); the client turns it into the
|
|
19
|
+
# request name. That only works while every request name is its column name in camelCase.
|
|
20
|
+
for rt, fs in schema["row_types"].items():
|
|
21
|
+
for f in fs:
|
|
22
|
+
if camel_case(f["name"]) != f["dsl_name"]:
|
|
23
|
+
raise SystemExit(f'{rt}.{f["name"]}: request name {f["dsl_name"]} is not the column name in camelCase')
|
|
24
|
+
|
|
25
|
+
# uint256 amounts: 32 bytes like a hash, but decimal strings in JSON (the server's catalogue)
|
|
26
|
+
UINT256 = ["max_refund", "submission_fee_refund", "deposit_value", "retry_value", "max_submission_fee", "l1_base_fee"]
|
|
27
|
+
STRUCT_NAMES = {"withdrawals": "Withdrawal", "access_list": "AccessListEntry", "authorization_list": "Authorization"}
|
|
28
|
+
structs: dict[str, str] = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse(s: str):
|
|
32
|
+
pos = 0
|
|
33
|
+
|
|
34
|
+
def ws():
|
|
35
|
+
nonlocal pos
|
|
36
|
+
while pos < len(s) and s[pos] == " ":
|
|
37
|
+
pos += 1
|
|
38
|
+
|
|
39
|
+
def ident():
|
|
40
|
+
nonlocal pos
|
|
41
|
+
m = re.match(r"[A-Za-z0-9_]+", s[pos:])
|
|
42
|
+
pos += len(m.group(0))
|
|
43
|
+
return m.group(0)
|
|
44
|
+
|
|
45
|
+
def typ():
|
|
46
|
+
nonlocal pos
|
|
47
|
+
ws()
|
|
48
|
+
name = ident()
|
|
49
|
+
if name == "FixedSizeBinary":
|
|
50
|
+
pos += 1; n = ident(); pos += 1
|
|
51
|
+
return ("fsb", int(n))
|
|
52
|
+
if name == "List":
|
|
53
|
+
pos += 1; t = typ(); pos += 1
|
|
54
|
+
return ("list", t)
|
|
55
|
+
if name == "Struct":
|
|
56
|
+
pos += 1; fields = []
|
|
57
|
+
while True:
|
|
58
|
+
ws(); n = ident(); ws(); pos += 1; t = typ(); fields.append((n, t)); ws()
|
|
59
|
+
if s[pos] == ",":
|
|
60
|
+
pos += 1; continue
|
|
61
|
+
pos += 1; break
|
|
62
|
+
return ("struct", fields)
|
|
63
|
+
return ("prim", name)
|
|
64
|
+
|
|
65
|
+
return typ()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def py_type(t, column: str) -> str:
|
|
69
|
+
kind = t[0]
|
|
70
|
+
if kind == "prim":
|
|
71
|
+
return "int" if t[1].startswith("UInt") else "str"
|
|
72
|
+
if kind == "fsb":
|
|
73
|
+
return "str"
|
|
74
|
+
if kind == "list":
|
|
75
|
+
return f"List[{py_type(t[1], column)}]"
|
|
76
|
+
name = STRUCT_NAMES.get(column, f"{column}_entry")
|
|
77
|
+
if name not in structs:
|
|
78
|
+
body = ", ".join(f'"{n}": {py_type(ft, n)}' for n, ft in t[1])
|
|
79
|
+
structs[name] = f'{name} = TypedDict("{name}", {{{body}}})'
|
|
80
|
+
return name
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
out = ['"""Generated by scripts/generate.py from scripts/schema.json (GET /v1/schema). Do not edit."""',
|
|
84
|
+
"from typing import FrozenSet, List, Literal, Mapping, Optional, TypedDict", ""]
|
|
85
|
+
rows = []
|
|
86
|
+
for key, field_alias, row in (("logs", "LogField", "Log"), ("transactions", "TransactionField", "Transaction"), ("blocks", "BlockField", "Block")):
|
|
87
|
+
fields = schema["row_types"][key]
|
|
88
|
+
names = [f["dsl_name"] for f in fields] + (["topics"] if key == "logs" else [])
|
|
89
|
+
# The same fields by column name, for snake_case requests (``block_number``).
|
|
90
|
+
names += [f["name"] for f in fields if f["name"] != f["dsl_name"]]
|
|
91
|
+
rows.append(f"{field_alias} = Literal[\n" + "".join(f' "{n}",\n' for n in names) + "]")
|
|
92
|
+
rows.append(f'"""Field names for `fields.{ {"logs": "log", "transactions": "transaction", "blocks": "block"}[key] }`: '
|
|
93
|
+
f'the request name (camelCase) or the column name (snake_case)."""')
|
|
94
|
+
rows.append("")
|
|
95
|
+
entries = []
|
|
96
|
+
for f in fields:
|
|
97
|
+
t = py_type(parse(f["arrow_type"]), f["name"])
|
|
98
|
+
if f["nullable"]:
|
|
99
|
+
t = f"Optional[{t}]"
|
|
100
|
+
entries.append(f' "{f["name"]}": {t}, # {f["arrow_type"]}')
|
|
101
|
+
rows.append(f'{row} = TypedDict(\n "{row}",\n {{\n' + "\n".join(entries) + "\n },\n total=False,\n)")
|
|
102
|
+
rows.append(f'"""A {key[:-1]} row: keys are present only for the fields you selected."""')
|
|
103
|
+
rows.append("")
|
|
104
|
+
out += list(structs.values()) + [""] + rows
|
|
105
|
+
out.append("UINT256_COLUMNS: Mapping[str, FrozenSet[str]] = {")
|
|
106
|
+
out.append(f' "transactions": frozenset({sorted(UINT256)!r}),')
|
|
107
|
+
out.append("}")
|
|
108
|
+
out.append('"""Response keys that hold uint256 amounts (decimal strings), per row type."""')
|
|
109
|
+
(ROOT / "src" / "bloklake" / "fields.py").write_text("\n".join(out) + "\n")
|
|
110
|
+
print("wrote src/bloklake/fields.py")
|