Skip to content

Commit 160ef23

Browse files
authored
Merge pull request #216 from opentensor/release/1.5.7
Release/1.5.7
2 parents 485415c + d1a8f80 commit 160ef23

File tree

8 files changed

+92
-41
lines changed

8 files changed

+92
-41
lines changed

.github/workflows/check-sdk-tests.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,16 @@ jobs:
6262
- name: Check out repository
6363
uses: actions/checkout@v4
6464

65-
- name: Get labels from PR
65+
- name: Skip label check for manual runs
6666
id: get-labels
67+
if: ${{ github.event_name == 'workflow_dispatch' }}
68+
run: |
69+
echo "Manual workflow dispatch detected, skipping PR label check."
70+
echo "run-sdk-tests=true" >> $GITHUB_OUTPUT
71+
72+
- name: Get labels from PR
73+
id: get-labels-pr
74+
if: ${{ github.event_name == 'pull_request' }}
6775
run: |
6876
sleep 5
6977
LABELS=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels --jq '.[].name')

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
# Changelog
2+
## 1.5.7 /2025-10-15
3+
* Updates the type hint on ws_shutdown_timer in RetryAsyncSubstrate by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/203
4+
* correct type hint by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/204
5+
* Clear asyncio.Queue after retrieval by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/206
6+
* Add the option to manually specify the Bittensor branch when running with `workflow_dispatch` by @basfroman in https://github.com/opentensor/async-substrate-interface/pull/208
7+
* Subscription Exception Handling by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/207
8+
* more efficient query map by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/211
9+
* Unique keys in request manager by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/212
10+
* Adds type annotations for Runtime by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/214
11+
* Edge case ss58 decoding in decode_query_map by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/213
12+
13+
**Full Changelog**: https://github.com/opentensor/async-substrate-interface/compare/v1.5.6...v1.5.7
14+
215
## 1.5.6 /2025-10-08
316
* Clean Up Error Handling by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/193
417
* Avoids ID of 'None' in queries by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/196

async_substrate_interface/async_substrate.py

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
RuntimeCache,
5656
SubstrateMixin,
5757
Preprocessed,
58+
RequestResults,
5859
)
5960
from async_substrate_interface.utils import (
6061
hex_to_bytes,
@@ -561,7 +562,7 @@ def __init__(
561562
self._received: dict[str, asyncio.Future] = {}
562563
self._received_subscriptions: dict[str, asyncio.Queue] = {}
563564
self._sending: Optional[asyncio.Queue] = None
564-
self._send_recv_task = None
565+
self._send_recv_task: Optional[asyncio.Task] = None
565566
self._inflight: dict[str, str] = {}
566567
self._attempts = 0
567568
self._lock = asyncio.Lock()
@@ -747,14 +748,15 @@ async def _start_receiving(self, ws: ClientConnection) -> Exception:
747748
elif isinstance(e, websockets.exceptions.ConnectionClosedOK):
748749
logger.debug("Websocket connection closed.")
749750
else:
750-
logger.debug(f"Timeout occurred. Reconnecting.")
751+
logger.debug(f"Timeout occurred.")
751752
return e
752753

753754
async def _start_sending(self, ws) -> Exception:
754755
to_send = None
755756
try:
756757
while True:
757758
to_send_ = await self._sending.get()
759+
self._sending.task_done()
758760
send_id = to_send_["id"]
759761
to_send = json.dumps(to_send_)
760762
async with self._lock:
@@ -779,7 +781,7 @@ async def _start_sending(self, ws) -> Exception:
779781
elif isinstance(e, websockets.exceptions.ConnectionClosedOK):
780782
logger.debug("Websocket connection closed.")
781783
else:
782-
logger.debug("Timeout occurred. Reconnecting.")
784+
logger.debug("Timeout occurred.")
783785
return e
784786

785787
async def send(self, payload: dict) -> str:
@@ -848,14 +850,19 @@ async def retrieve(self, item_id: str) -> Optional[dict]:
848850
return res
849851
else:
850852
try:
851-
return self._received_subscriptions[item_id].get_nowait()
853+
subscription = self._received_subscriptions[item_id].get_nowait()
854+
self._received_subscriptions[item_id].task_done()
855+
return subscription
852856
except asyncio.QueueEmpty:
853857
pass
854858
if self._send_recv_task is not None and self._send_recv_task.done():
855859
if not self._send_recv_task.cancelled():
856860
if isinstance((e := self._send_recv_task.exception()), Exception):
857861
logger.exception(f"Websocket sending exception: {e}")
858862
raise e
863+
elif isinstance((e := self._send_recv_task.result()), Exception):
864+
logger.exception(f"Websocket sending exception: {e}")
865+
raise e
859866
await asyncio.sleep(0.1)
860867
return None
861868

@@ -874,7 +881,7 @@ def __init__(
874881
retry_timeout: float = 60.0,
875882
_mock: bool = False,
876883
_log_raw_websockets: bool = False,
877-
ws_shutdown_timer: float = 5.0,
884+
ws_shutdown_timer: Optional[float] = 5.0,
878885
decode_ss58: bool = False,
879886
):
880887
"""
@@ -2385,9 +2392,12 @@ async def _make_rpc_request(
23852392
attempt: int = 1,
23862393
runtime: Optional[Runtime] = None,
23872394
force_legacy_decode: bool = False,
2388-
) -> RequestManager.RequestResults:
2395+
) -> RequestResults:
23892396
request_manager = RequestManager(payloads)
23902397

2398+
if len(set(x["id"] for x in payloads)) != len(payloads):
2399+
raise ValueError("Payloads must have unique ids")
2400+
23912401
subscription_added = False
23922402

23932403
async with self.ws as ws:
@@ -3663,34 +3673,41 @@ async def query_map(
36633673
self.decode_ss58,
36643674
)
36653675
else:
3666-
all_responses = []
3676+
# storage item and value scale type are not included here because this is batch-decoded in rust
36673677
page_batches = [
36683678
result_keys[i : i + page_size]
36693679
for i in range(0, len(result_keys), page_size)
36703680
]
36713681
changes = []
3672-
for batch_group in [
3673-
# run five concurrent batch pulls; could go higher, but it's good to be a good citizens
3674-
# of the ecosystem
3675-
page_batches[i : i + 5]
3676-
for i in range(0, len(page_batches), 5)
3677-
]:
3678-
all_responses.extend(
3679-
await asyncio.gather(
3680-
*[
3681-
self.rpc_request(
3682-
method="state_queryStorageAt",
3683-
params=[batch_keys, block_hash],
3684-
runtime=runtime,
3685-
)
3686-
for batch_keys in batch_group
3687-
]
3682+
payloads = []
3683+
for idx, page_batch in enumerate(page_batches):
3684+
payloads.append(
3685+
self.make_payload(
3686+
str(idx), "state_queryStorageAt", [page_batch, block_hash]
36883687
)
36893688
)
3690-
for response in all_responses:
3691-
for result_group in response["result"]:
3692-
changes.extend(result_group["changes"])
3693-
3689+
results: RequestResults = await self._make_rpc_request(
3690+
payloads, runtime=runtime
3691+
)
3692+
for result in results.values():
3693+
res = result[0]
3694+
if "error" in res:
3695+
err_msg = res["error"]["message"]
3696+
if (
3697+
"Client error: Api called for an unknown Block: State already discarded"
3698+
in err_msg
3699+
):
3700+
bh = err_msg.split("State already discarded for ")[
3701+
1
3702+
].strip()
3703+
raise StateDiscardedError(bh)
3704+
else:
3705+
raise SubstrateRequestException(err_msg)
3706+
elif "result" not in res:
3707+
raise SubstrateRequestException(res)
3708+
else:
3709+
for result_group in res["result"]:
3710+
changes.extend(result_group["changes"])
36943711
result = decode_query_map(
36953712
changes,
36963713
prefix,

async_substrate_interface/substrate_addons.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ def __init__(
264264
_mock: bool = False,
265265
_log_raw_websockets: bool = False,
266266
archive_nodes: Optional[list[str]] = None,
267-
ws_shutdown_timer: float = 5.0,
267+
ws_shutdown_timer: Optional[float] = 5.0,
268268
):
269269
fallback_chains = fallback_chains or []
270270
archive_nodes = archive_nodes or []

async_substrate_interface/sync_substrate.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
RequestManager,
3535
Preprocessed,
3636
ScaleObj,
37+
RequestResults,
3738
)
3839
from async_substrate_interface.utils import (
3940
hex_to_bytes,
@@ -1892,9 +1893,13 @@ def _make_rpc_request(
18921893
result_handler: Optional[ResultHandler] = None,
18931894
attempt: int = 1,
18941895
force_legacy_decode: bool = False,
1895-
) -> RequestManager.RequestResults:
1896+
) -> RequestResults:
18961897
request_manager = RequestManager(payloads)
18971898
_received = {}
1899+
1900+
if len(set(x["id"] for x in payloads)) != len(payloads):
1901+
raise ValueError("Payloads must have unique ids")
1902+
18981903
subscription_added = False
18991904

19001905
ws = self.connect(init=False if attempt == 1 else True)

async_substrate_interface/types.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
from datetime import datetime
77
from typing import Optional, Union, Any
88

9+
import scalecodec.types
910
from bt_decode import PortableRegistry, encode as encode_by_type_string
11+
from bt_decode.bt_decode import MetadataV15
1012
from scalecodec import ss58_encode, ss58_decode, is_valid_ss58_address
1113
from scalecodec.base import RuntimeConfigurationObject, ScaleBytes
1214
from scalecodec.type_registry import load_type_registry_preset
@@ -121,13 +123,13 @@ class Runtime:
121123
def __init__(
122124
self,
123125
chain: str,
124-
metadata,
125-
type_registry,
126+
metadata: scalecodec.types.GenericMetadataVersioned,
127+
type_registry: dict,
126128
runtime_config: Optional[RuntimeConfigurationObject] = None,
127-
metadata_v15=None,
128-
runtime_info=None,
129-
registry=None,
130-
ss58_format=SS58_FORMAT,
129+
metadata_v15: Optional[MetadataV15] = None,
130+
runtime_info: Optional[dict] = None,
131+
registry: Optional[PortableRegistry] = None,
132+
ss58_format: int = SS58_FORMAT,
131133
):
132134
self.ss58_format = ss58_format
133135
self.config = {}
@@ -369,9 +371,10 @@ def resolve_type_definition(type_id_):
369371
self.type_id_to_name = type_id_to_name
370372

371373

372-
class RequestManager:
373-
RequestResults = dict[Union[str, int], list[Union[ScaleType, dict]]]
374+
RequestResults = dict[Union[str, int], list[Union[ScaleType, dict]]]
375+
374376

377+
class RequestManager:
375378
def __init__(self, payloads):
376379
self.response_map = {}
377380
self.responses = defaultdict(

async_substrate_interface/utils/decoding.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ def concat_hash_len(key_hasher: str) -> int:
113113

114114
for item in result_group_changes:
115115
pre_decoded_keys.append(bytes.fromhex(item[0][len(prefix) :]))
116-
pre_decoded_values.append(hex_to_bytes_(item[1]))
116+
pre_decoded_values.append(
117+
hex_to_bytes_(item[1]) if item[1] is not None else b""
118+
)
117119
all_decoded = _decode_scale_list_with_runtime(
118120
pre_decoded_key_types + pre_decoded_value_types,
119121
pre_decoded_keys + pre_decoded_values,
@@ -133,7 +135,10 @@ def concat_hash_len(key_hasher: str) -> int:
133135
if len(param_types) - len(params) == 1:
134136
item_key = dk[1]
135137
if decode_ss58:
136-
if kts[kts.index(", ") + 2 : kts.index(")")] == "scale_info::0":
138+
if (
139+
isinstance(item_key[0], (tuple, list))
140+
and kts[kts.index(", ") + 2 : kts.index(")")] == "scale_info::0"
141+
):
137142
item_key = ss58_encode(bytes(item_key[0]), runtime.ss58_format)
138143
else:
139144
try:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "async-substrate-interface"
3-
version = "1.5.6"
3+
version = "1.5.7"
44
description = "Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface"
55
readme = "README.md"
66
license = { file = "LICENSE" }

0 commit comments

Comments
 (0)