Skip to content

Commit ce452e0

Browse files
committed
refactor: refines msgpack decoding in api generator and boolean fields in block model
Improves msgpack decoding in Algod, Indexer and KMD clients by handling byte keys and values. This prevents decoding errors when encountering non-UTF-8 byte sequences. Additionally, adds decoding for boolean fields in block models to correctly interpret raw values as booleans. This addresses issues with inconsistent data representation.
1 parent a531367 commit ce452e0

File tree

14 files changed

+202
-173
lines changed

14 files changed

+202
-173
lines changed

api/oas-generator/src/oas_generator/renderer/templates/client.py.j2

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ class {{ client.class_name }}:
248248
return response.content
249249
content_type = response.headers.get("content-type", "application/json")
250250
if "msgpack" in content_type:
251-
data = msgpack.unpackb(response.content, raw=False, strict_map_key=False)
251+
data = msgpack.unpackb(response.content, raw=True, strict_map_key=False)
252252
data = self._normalize_msgpack(data)
253253
elif content_type.startswith("application/json"):
254254
data = response.json()
@@ -264,7 +264,23 @@ class {{ client.class_name }}:
264264

265265
def _normalize_msgpack(self, value: object) -> object:
266266
if isinstance(value, dict):
267-
return {key: self._normalize_msgpack(item) for key, item in value.items()}
267+
normalized: dict[object, object] = {}
268+
for key, item in value.items():
269+
normalized[self._coerce_msgpack_key(key)] = self._normalize_msgpack(item)
270+
return normalized
268271
if isinstance(value, list):
269272
return [self._normalize_msgpack(item) for item in value]
273+
if isinstance(value, bytes):
274+
try:
275+
return value.decode("utf-8")
276+
except UnicodeDecodeError:
277+
return value
270278
return value
279+
280+
def _coerce_msgpack_key(self, key: object) -> object:
281+
if isinstance(key, bytes):
282+
try:
283+
return key.decode("utf-8")
284+
except UnicodeDecodeError:
285+
return key
286+
return key

api/oas-generator/src/oas_generator/renderer/templates/models/_serde_helpers.py.j2

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ from typing import Callable, TypeAlias, TypeVar
88

99
from algokit_common.serde import from_wire, to_wire
1010

11-
T = TypeVar("T")
12-
E = TypeVar("E", bound=Enum)
13-
KT = TypeVar("KT")
11+
DecodedT = TypeVar("DecodedT")
12+
EnumValueT = TypeVar("EnumValueT", bound=Enum)
13+
MapKeyT = TypeVar("MapKeyT")
1414
BytesLike: TypeAlias = bytes | bytearray | memoryview
1515

1616

@@ -32,7 +32,7 @@ def decode_bytes_base64(raw: object) -> bytes:
3232
if isinstance(raw, str):
3333
try:
3434
return base64.b64decode(raw.encode("ascii"), validate=True)
35-
except (BinasciiError, UnicodeEncodeError) as exc:
35+
except (BinasciiError, UnicodeEncodeError):
3636
try:
3737
return raw.encode("utf-8")
3838
except UnicodeEncodeError as fallback_exc:
@@ -77,11 +77,11 @@ def encode_model_sequence(values: Iterable[object] | None) -> list[dict[str, obj
7777
return encoded or None
7878

7979

80-
def decode_model_sequence(cls_factory: Callable[[], type[T]], raw: object) -> list[T] | None:
80+
def decode_model_sequence(cls_factory: Callable[[], type[DecodedT]], raw: object) -> list[DecodedT] | None:
8181
if not isinstance(raw, list):
8282
return None
8383
cls = cls_factory()
84-
decoded: list[T] = []
84+
decoded: list[DecodedT] = []
8585
for item in raw:
8686
if isinstance(item, Mapping):
8787
decoded.append(from_wire(cls, item))
@@ -99,11 +99,11 @@ def encode_enum_sequence(values: Iterable[object] | None) -> list[object] | None
9999
return encoded or None
100100

101101

102-
def decode_enum_sequence(enum_factory: Callable[[], type[E]], raw: object) -> list[E] | None:
102+
def decode_enum_sequence(enum_factory: Callable[[], type[EnumValueT]], raw: object) -> list[EnumValueT] | None:
103103
if not isinstance(raw, list):
104104
return None
105105
enum_cls = enum_factory()
106-
decoded: list[E] = []
106+
decoded: list[EnumValueT] = []
107107
for item in raw:
108108
try:
109109
decoded.append(enum_cls(item))
@@ -113,7 +113,7 @@ def decode_enum_sequence(enum_factory: Callable[[], type[E]], raw: object) -> li
113113

114114

115115
def encode_model_mapping(
116-
factory: Callable[[], type[T]],
116+
factory: Callable[[], type[DecodedT]],
117117
mapping: Mapping[object, object] | None,
118118
*,
119119
key_encoder: Callable[[object], str] | None = None,
@@ -140,24 +140,30 @@ def encode_model_mapping(
140140

141141

142142
def decode_model_mapping(
143-
factory: Callable[[], type[T]],
143+
factory: Callable[[], type[DecodedT]],
144144
raw: object,
145145
*,
146-
key_decoder: Callable[[object], KT] | None = None,
147-
) -> dict[KT, T] | None:
146+
key_decoder: Callable[[object], MapKeyT] | None = None,
147+
) -> dict[MapKeyT, DecodedT] | None:
148148
if not isinstance(raw, Mapping):
149149
return None
150150
cls = factory()
151-
decoded: dict[KT, T] = {}
151+
decoded: dict[MapKeyT, DecodedT] = {}
152152
for key, value in raw.items():
153153
if isinstance(value, Mapping):
154154
decoded_key = key_decoder(key) if key_decoder is not None else key
155155
decoded[decoded_key] = from_wire(cls, value)
156156
return decoded or None
157157

158158

159+
def decode_optional_bool(raw: object) -> bool | None:
160+
if raw is None:
161+
return None
162+
return bool(raw)
163+
164+
159165
def mapping_encoder(
160-
factory: Callable[[], type[T]],
166+
factory: Callable[[], type[DecodedT]],
161167
*,
162168
key_encoder: Callable[[object], str] | None = None,
163169
) -> Callable[[Mapping[object, object] | None], dict[str, object] | None]:
@@ -168,11 +174,11 @@ def mapping_encoder(
168174

169175

170176
def mapping_decoder(
171-
factory: Callable[[], type[T]],
177+
factory: Callable[[], type[DecodedT]],
172178
*,
173-
key_decoder: Callable[[object], KT] | None = None,
174-
) -> Callable[[object], dict[KT, T] | None]:
175-
def _decode(raw: object) -> dict[KT, T] | None:
179+
key_decoder: Callable[[object], MapKeyT] | None = None,
180+
) -> Callable[[object], dict[MapKeyT, DecodedT] | None]:
181+
def _decode(raw: object) -> dict[MapKeyT, DecodedT] | None:
176182
return decode_model_mapping(factory, raw, key_decoder=key_decoder)
177183

178184
return _decode

api/oas-generator/src/oas_generator/renderer/templates/models/block.py.j2

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ from ._serde_helpers import (
1111
decode_bytes_base64,
1212
decode_model_mapping,
1313
decode_model_sequence,
14+
decode_optional_bool,
1415
encode_bytes_base64,
1516
encode_model_mapping,
1617
encode_model_sequence,
@@ -68,14 +69,22 @@ def _decode_block_state_delta(raw: object) -> BlockStateDelta | None:
6869
return decoded or None
6970

7071

72+
def _encode_local_delta_index_key(key: object) -> str:
73+
if isinstance(key, bool):
74+
return str(int(key))
75+
if isinstance(key, int):
76+
return str(key)
77+
return str(int(key))
78+
79+
7180
def _encode_local_deltas(mapping: Mapping[int, BlockStateDelta] | None) -> dict[str, object] | None:
7281
if mapping is None:
7382
return None
7483
out: dict[str, object] = {}
7584
for key, value in mapping.items():
7685
encoded = _encode_block_state_delta(value)
7786
if encoded:
78-
out[str(int(key))] = encoded
87+
out[_encode_local_delta_index_key(key)] = encoded
7988
return out or None
8089

8190

@@ -156,8 +165,8 @@ class SignedTxnInBlock:
156165
)
157166
config_asset: int | None = field(default=None, metadata=wire("caid"))
158167
application_id: int | None = field(default=None, metadata=wire("apid"))
159-
has_genesis_id: bool | None = field(default=None, metadata=wire("hgi"))
160-
has_genesis_hash: bool | None = field(default=None, metadata=wire("hgh"))
168+
has_genesis_id: bool | None = field(default=None, metadata=wire("hgi", decode=decode_optional_bool))
169+
has_genesis_hash: bool | None = field(default=None, metadata=wire("hgh", decode=decode_optional_bool))
161170

162171

163172
@dataclass(slots=True)

api/specs/compatibility.md

Lines changed: 0 additions & 79 deletions
This file was deleted.

src/algokit_algod_client/client.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1937,7 +1937,7 @@ def _decode_response(
19371937
return response.content
19381938
content_type = response.headers.get("content-type", "application/json")
19391939
if "msgpack" in content_type:
1940-
data = msgpack.unpackb(response.content, raw=False, strict_map_key=False)
1940+
data = msgpack.unpackb(response.content, raw=True, strict_map_key=False)
19411941
data = self._normalize_msgpack(data)
19421942
elif content_type.startswith("application/json"):
19431943
data = response.json()
@@ -1953,7 +1953,23 @@ def _decode_response(
19531953

19541954
def _normalize_msgpack(self, value: object) -> object:
19551955
if isinstance(value, dict):
1956-
return {key: self._normalize_msgpack(item) for key, item in value.items()}
1956+
normalized: dict[object, object] = {}
1957+
for key, item in value.items():
1958+
normalized[self._coerce_msgpack_key(key)] = self._normalize_msgpack(item)
1959+
return normalized
19571960
if isinstance(value, list):
19581961
return [self._normalize_msgpack(item) for item in value]
1962+
if isinstance(value, bytes):
1963+
try:
1964+
return value.decode("utf-8")
1965+
except UnicodeDecodeError:
1966+
return value
19591967
return value
1968+
1969+
def _coerce_msgpack_key(self, key: object) -> object:
1970+
if isinstance(key, bytes):
1971+
try:
1972+
return key.decode("utf-8")
1973+
except UnicodeDecodeError:
1974+
return key
1975+
return key

0 commit comments

Comments
 (0)