algokit-utils 5.0.0a3__py3-none-any.whl

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.
Files changed (337) hide show
  1. algokit_abi/__init__.py +9 -0
  2. algokit_abi/_arc32_to_arc56.py +242 -0
  3. algokit_abi/_arc56_serde.py +161 -0
  4. algokit_abi/abi.py +667 -0
  5. algokit_abi/arc32.py +210 -0
  6. algokit_abi/arc56.py +821 -0
  7. algokit_abi/py.typed +0 -0
  8. algokit_algo25/__init__.py +38 -0
  9. algokit_algo25/_encoding.py +46 -0
  10. algokit_algo25/_wordlist.py +2065 -0
  11. algokit_algo25/exceptions.py +29 -0
  12. algokit_algo25/mnemonic.py +128 -0
  13. algokit_algo25/py.typed +0 -0
  14. algokit_algod_client/__init__.py +10 -0
  15. algokit_algod_client/client.py +1585 -0
  16. algokit_algod_client/config.py +36 -0
  17. algokit_algod_client/exceptions.py +59 -0
  18. algokit_algod_client/models/__init__.py +229 -0
  19. algokit_algod_client/models/_account.py +150 -0
  20. algokit_algod_client/models/_account_application_response.py +25 -0
  21. algokit_algod_client/models/_account_asset_response.py +25 -0
  22. algokit_algod_client/models/_account_participation.py +53 -0
  23. algokit_algod_client/models/_account_state_delta.py +30 -0
  24. algokit_algod_client/models/_allocations_for_genesis_file.py +23 -0
  25. algokit_algod_client/models/_allocations_for_genesis_file_state_model.py +42 -0
  26. algokit_algod_client/models/_application.py +23 -0
  27. algokit_algod_client/models/_application_initial_states.py +37 -0
  28. algokit_algod_client/models/_application_kvstorage.py +29 -0
  29. algokit_algod_client/models/_application_local_state.py +33 -0
  30. algokit_algod_client/models/_application_params.py +63 -0
  31. algokit_algod_client/models/_application_state_operation.py +41 -0
  32. algokit_algod_client/models/_application_state_schema.py +22 -0
  33. algokit_algod_client/models/_asset.py +23 -0
  34. algokit_algod_client/models/_asset_holding.py +29 -0
  35. algokit_algod_client/models/_asset_params.py +102 -0
  36. algokit_algod_client/models/_avm_key_value.py +28 -0
  37. algokit_algod_client/models/_avm_value.py +32 -0
  38. algokit_algod_client/models/_block.py +363 -0
  39. algokit_algod_client/models/_block_hash_response.py +14 -0
  40. algokit_algod_client/models/_block_txids_response.py +14 -0
  41. algokit_algod_client/models/_box.py +36 -0
  42. algokit_algod_client/models/_box_descriptor.py +24 -0
  43. algokit_algod_client/models/_boxes_response.py +21 -0
  44. algokit_algod_client/models/_build_version_contains_the_current_algod_build_version_information.py +34 -0
  45. algokit_algod_client/models/_compile_response.py +24 -0
  46. algokit_algod_client/models/_disassemble_response.py +14 -0
  47. algokit_algod_client/models/_error_response.py +22 -0
  48. algokit_algod_client/models/_eval_delta.py +32 -0
  49. algokit_algod_client/models/_eval_delta_key_value.py +28 -0
  50. algokit_algod_client/models/_genesis_file_in_json.py +53 -0
  51. algokit_algod_client/models/_get_block_time_stamp_offset_response.py +14 -0
  52. algokit_algod_client/models/_get_sync_round_response.py +14 -0
  53. algokit_algod_client/models/_ledger_state_delta.py +389 -0
  54. algokit_algod_client/models/_light_block_header_proof.py +32 -0
  55. algokit_algod_client/models/_node_status_response.py +118 -0
  56. algokit_algod_client/models/_pending_transaction_response.py +91 -0
  57. algokit_algod_client/models/_pending_transactions_response.py +29 -0
  58. algokit_algod_client/models/_post_transactions_response.py +14 -0
  59. algokit_algod_client/models/_scratch_change.py +23 -0
  60. algokit_algod_client/models/_serde_helpers.py +241 -0
  61. algokit_algod_client/models/_simulate_initial_states.py +25 -0
  62. algokit_algod_client/models/_simulate_request.py +54 -0
  63. algokit_algod_client/models/_simulate_request_transaction_group.py +25 -0
  64. algokit_algod_client/models/_simulate_response.py +44 -0
  65. algokit_algod_client/models/_simulate_trace_config.py +30 -0
  66. algokit_algod_client/models/_simulate_transaction_group_result.py +46 -0
  67. algokit_algod_client/models/_simulate_transaction_result.py +41 -0
  68. algokit_algod_client/models/_simulate_unnamed_resources_accessed.py +64 -0
  69. algokit_algod_client/models/_simulation_eval_overrides.py +40 -0
  70. algokit_algod_client/models/_simulation_opcode_trace_unit.py +55 -0
  71. algokit_algod_client/models/_simulation_transaction_exec_trace.py +82 -0
  72. algokit_algod_client/models/_source_map.py +30 -0
  73. algokit_algod_client/models/_state_delta.py +6 -0
  74. algokit_algod_client/models/_state_proof.py +28 -0
  75. algokit_algod_client/models/_state_proof_message.py +44 -0
  76. algokit_algod_client/models/_supply_response.py +26 -0
  77. algokit_algod_client/models/_teal_key_value.py +28 -0
  78. algokit_algod_client/models/_teal_key_value_store.py +6 -0
  79. algokit_algod_client/models/_teal_value.py +32 -0
  80. algokit_algod_client/models/_transaction_group_ledger_state_deltas_for_round_response.py +21 -0
  81. algokit_algod_client/models/_transaction_parameters_response.py +45 -0
  82. algokit_algod_client/models/_transaction_proof.py +44 -0
  83. algokit_algod_client/models/_version_contains_the_current_algod_version.py +38 -0
  84. algokit_algod_client/models/suggested_params.py +42 -0
  85. algokit_algod_client/py.typed +1 -0
  86. algokit_algod_client/types.py +7 -0
  87. algokit_algosdk/__init__.py +38 -0
  88. algokit_algosdk/account.py +32 -0
  89. algokit_algosdk/app_access.py +228 -0
  90. algokit_algosdk/box_reference.py +100 -0
  91. algokit_algosdk/constants.py +147 -0
  92. algokit_algosdk/encoding.py +89 -0
  93. algokit_algosdk/error.py +180 -0
  94. algokit_algosdk/logic.py +61 -0
  95. algokit_algosdk/logicsig.py +218 -0
  96. algokit_algosdk/mnemonic.py +216 -0
  97. algokit_algosdk/multisig.py +161 -0
  98. algokit_algosdk/py.typed +0 -0
  99. algokit_algosdk/transaction.py +596 -0
  100. algokit_algosdk/wordlist.py +2054 -0
  101. algokit_common/__init__.py +50 -0
  102. algokit_common/address.py +34 -0
  103. algokit_common/constants.py +47 -0
  104. algokit_common/hashing.py +25 -0
  105. algokit_common/py.typed +0 -0
  106. algokit_common/serde/__init__.py +40 -0
  107. algokit_common/serde/_core.py +610 -0
  108. algokit_common/serde/_primitives.py +135 -0
  109. algokit_common/source_map.py +158 -0
  110. algokit_indexer_client/__init__.py +10 -0
  111. algokit_indexer_client/client.py +1456 -0
  112. algokit_indexer_client/config.py +36 -0
  113. algokit_indexer_client/exceptions.py +59 -0
  114. algokit_indexer_client/models/__init__.py +148 -0
  115. algokit_indexer_client/models/_account.py +161 -0
  116. algokit_indexer_client/models/_account_participation.py +53 -0
  117. algokit_indexer_client/models/_account_response.py +19 -0
  118. algokit_indexer_client/models/_account_state_delta.py +29 -0
  119. algokit_indexer_client/models/_accounts_response.py +29 -0
  120. algokit_indexer_client/models/_application.py +35 -0
  121. algokit_indexer_client/models/_application_local_state.py +45 -0
  122. algokit_indexer_client/models/_application_local_states_response.py +29 -0
  123. algokit_indexer_client/models/_application_log_data.py +28 -0
  124. algokit_indexer_client/models/_application_logs_response.py +33 -0
  125. algokit_indexer_client/models/_application_params.py +62 -0
  126. algokit_indexer_client/models/_application_response.py +20 -0
  127. algokit_indexer_client/models/_application_state_schema.py +22 -0
  128. algokit_indexer_client/models/_applications_response.py +29 -0
  129. algokit_indexer_client/models/_asset.py +35 -0
  130. algokit_indexer_client/models/_asset_balances_response.py +29 -0
  131. algokit_indexer_client/models/_asset_holding.py +41 -0
  132. algokit_indexer_client/models/_asset_holdings_response.py +29 -0
  133. algokit_indexer_client/models/_asset_params.py +102 -0
  134. algokit_indexer_client/models/_asset_response.py +19 -0
  135. algokit_indexer_client/models/_assets_response.py +29 -0
  136. algokit_indexer_client/models/_block.py +150 -0
  137. algokit_indexer_client/models/_block_headers_response.py +29 -0
  138. algokit_indexer_client/models/_block_rewards.py +38 -0
  139. algokit_indexer_client/models/_block_upgrade_state.py +34 -0
  140. algokit_indexer_client/models/_block_upgrade_vote.py +26 -0
  141. algokit_indexer_client/models/_box.py +36 -0
  142. algokit_indexer_client/models/_box_descriptor.py +24 -0
  143. algokit_indexer_client/models/_box_reference.py +28 -0
  144. algokit_indexer_client/models/_boxes_response.py +29 -0
  145. algokit_indexer_client/models/_error_response.py +18 -0
  146. algokit_indexer_client/models/_eval_delta.py +32 -0
  147. algokit_indexer_client/models/_eval_delta_key_value.py +28 -0
  148. algokit_indexer_client/models/_hash_factory.py +14 -0
  149. algokit_indexer_client/models/_hb_proof_fields.py +57 -0
  150. algokit_indexer_client/models/_health_check.py +42 -0
  151. algokit_indexer_client/models/_holding_ref.py +23 -0
  152. algokit_indexer_client/models/_indexer_state_proof_message.py +40 -0
  153. algokit_indexer_client/models/_locals_ref.py +23 -0
  154. algokit_indexer_client/models/_merkle_array_proof.py +29 -0
  155. algokit_indexer_client/models/_mini_asset_holding.py +38 -0
  156. algokit_indexer_client/models/_on_completion.py +25 -0
  157. algokit_indexer_client/models/_participation_updates.py +22 -0
  158. algokit_indexer_client/models/_resource_ref.py +42 -0
  159. algokit_indexer_client/models/_serde_helpers.py +241 -0
  160. algokit_indexer_client/models/_state_delta.py +6 -0
  161. algokit_indexer_client/models/_state_proof_fields.py +57 -0
  162. algokit_indexer_client/models/_state_proof_participant.py +20 -0
  163. algokit_indexer_client/models/_state_proof_reveal.py +25 -0
  164. algokit_indexer_client/models/_state_proof_sig_slot.py +20 -0
  165. algokit_indexer_client/models/_state_proof_signature.py +37 -0
  166. algokit_indexer_client/models/_state_proof_tracking.py +32 -0
  167. algokit_indexer_client/models/_state_proof_verifier.py +24 -0
  168. algokit_indexer_client/models/_state_schema.py +25 -0
  169. algokit_indexer_client/models/_teal_key_value.py +28 -0
  170. algokit_indexer_client/models/_teal_key_value_store.py +6 -0
  171. algokit_indexer_client/models/_teal_value.py +32 -0
  172. algokit_indexer_client/models/_transaction.py +213 -0
  173. algokit_indexer_client/models/_transaction_application.py +105 -0
  174. algokit_indexer_client/models/_transaction_asset_config.py +31 -0
  175. algokit_indexer_client/models/_transaction_asset_freeze.py +29 -0
  176. algokit_indexer_client/models/_transaction_asset_transfer.py +41 -0
  177. algokit_indexer_client/models/_transaction_heartbeat.py +52 -0
  178. algokit_indexer_client/models/_transaction_keyreg.py +59 -0
  179. algokit_indexer_client/models/_transaction_payment.py +33 -0
  180. algokit_indexer_client/models/_transaction_response.py +19 -0
  181. algokit_indexer_client/models/_transaction_signature.py +35 -0
  182. algokit_indexer_client/models/_transaction_signature_logicsig.py +59 -0
  183. algokit_indexer_client/models/_transaction_signature_multisig.py +36 -0
  184. algokit_indexer_client/models/_transaction_signature_multisig_subsignature.py +28 -0
  185. algokit_indexer_client/models/_transaction_state_proof.py +32 -0
  186. algokit_indexer_client/models/_transactions_response.py +29 -0
  187. algokit_indexer_client/py.typed +1 -0
  188. algokit_indexer_client/types.py +7 -0
  189. algokit_kmd_client/__init__.py +10 -0
  190. algokit_kmd_client/client.py +1240 -0
  191. algokit_kmd_client/config.py +36 -0
  192. algokit_kmd_client/exceptions.py +59 -0
  193. algokit_kmd_client/models/__init__.py +112 -0
  194. algokit_kmd_client/models/_classical_signatures.py +4 -0
  195. algokit_kmd_client/models/_create_wallet_request.py +30 -0
  196. algokit_kmd_client/models/_create_wallet_response.py +19 -0
  197. algokit_kmd_client/models/_delete_key_request.py +27 -0
  198. algokit_kmd_client/models/_delete_multisig_request.py +27 -0
  199. algokit_kmd_client/models/_digest_represents_a32_byte_value_holding_the256_bit_hash_digest.py +4 -0
  200. algokit_kmd_client/models/_ed25519_public_key.py +4 -0
  201. algokit_kmd_client/models/_export_key_request.py +27 -0
  202. algokit_kmd_client/models/_export_key_response.py +24 -0
  203. algokit_kmd_client/models/_export_master_key_request.py +22 -0
  204. algokit_kmd_client/models/_export_master_key_response.py +18 -0
  205. algokit_kmd_client/models/_export_multisig_request.py +23 -0
  206. algokit_kmd_client/models/_export_multisig_response.py +26 -0
  207. algokit_kmd_client/models/_generate_key_request.py +18 -0
  208. algokit_kmd_client/models/_generate_key_response.py +19 -0
  209. algokit_kmd_client/models/_import_key_request.py +28 -0
  210. algokit_kmd_client/models/_import_key_response.py +19 -0
  211. algokit_kmd_client/models/_import_multisig_request.py +30 -0
  212. algokit_kmd_client/models/_import_multisig_response.py +19 -0
  213. algokit_kmd_client/models/_init_wallet_handle_token_request.py +22 -0
  214. algokit_kmd_client/models/_init_wallet_handle_token_response.py +18 -0
  215. algokit_kmd_client/models/_list_keys_request.py +18 -0
  216. algokit_kmd_client/models/_list_keys_response.py +18 -0
  217. algokit_kmd_client/models/_list_multisig_request.py +18 -0
  218. algokit_kmd_client/models/_list_multisig_response.py +18 -0
  219. algokit_kmd_client/models/_list_wallets_request.py +11 -0
  220. algokit_kmd_client/models/_list_wallets_response.py +25 -0
  221. algokit_kmd_client/models/_master_derivation_key.py +4 -0
  222. algokit_kmd_client/models/_multisig_sig.py +33 -0
  223. algokit_kmd_client/models/_multisig_subsig.py +23 -0
  224. algokit_kmd_client/models/_public_key.py +4 -0
  225. algokit_kmd_client/models/_release_wallet_handle_token_request.py +18 -0
  226. algokit_kmd_client/models/_rename_wallet_request.py +26 -0
  227. algokit_kmd_client/models/_rename_wallet_response.py +19 -0
  228. algokit_kmd_client/models/_renew_wallet_handle_token_request.py +18 -0
  229. algokit_kmd_client/models/_renew_wallet_handle_token_response.py +19 -0
  230. algokit_kmd_client/models/_serde_helpers.py +241 -0
  231. algokit_kmd_client/models/_sign_multisig_response.py +24 -0
  232. algokit_kmd_client/models/_sign_multisig_txn_request.py +45 -0
  233. algokit_kmd_client/models/_sign_program_multisig_request.py +50 -0
  234. algokit_kmd_client/models/_sign_program_multisig_response.py +24 -0
  235. algokit_kmd_client/models/_sign_program_request.py +37 -0
  236. algokit_kmd_client/models/_sign_program_response.py +24 -0
  237. algokit_kmd_client/models/_sign_transaction_response.py +24 -0
  238. algokit_kmd_client/models/_sign_txn_request.py +36 -0
  239. algokit_kmd_client/models/_signature.py +4 -0
  240. algokit_kmd_client/models/_tx_type.py +4 -0
  241. algokit_kmd_client/models/_versions_request.py +11 -0
  242. algokit_kmd_client/models/_versions_response.py +19 -0
  243. algokit_kmd_client/models/_wallet.py +38 -0
  244. algokit_kmd_client/models/_wallet_handle.py +24 -0
  245. algokit_kmd_client/models/_wallet_info_request.py +18 -0
  246. algokit_kmd_client/models/_wallet_info_response.py +19 -0
  247. algokit_kmd_client/py.typed +1 -0
  248. algokit_kmd_client/types.py +7 -0
  249. algokit_transact/__init__.py +190 -0
  250. algokit_transact/codec/__init__.py +0 -0
  251. algokit_transact/codec/msgpack.py +11 -0
  252. algokit_transact/codec/serde.py +7 -0
  253. algokit_transact/codec/signed.py +57 -0
  254. algokit_transact/codec/transaction.py +65 -0
  255. algokit_transact/exceptions.py +17 -0
  256. algokit_transact/logicsig.py +220 -0
  257. algokit_transact/models/__init__.py +0 -0
  258. algokit_transact/models/app_call.py +447 -0
  259. algokit_transact/models/asset_config.py +19 -0
  260. algokit_transact/models/asset_freeze.py +11 -0
  261. algokit_transact/models/asset_transfer.py +13 -0
  262. algokit_transact/models/common.py +17 -0
  263. algokit_transact/models/heartbeat.py +21 -0
  264. algokit_transact/models/key_registration.py +14 -0
  265. algokit_transact/models/payment.py +14 -0
  266. algokit_transact/models/signed_transaction.py +21 -0
  267. algokit_transact/models/state_proof.py +150 -0
  268. algokit_transact/models/transaction.py +88 -0
  269. algokit_transact/multisig.py +93 -0
  270. algokit_transact/ops/__init__.py +0 -0
  271. algokit_transact/ops/fees.py +47 -0
  272. algokit_transact/ops/group.py +28 -0
  273. algokit_transact/ops/ids.py +14 -0
  274. algokit_transact/ops/validate.py +503 -0
  275. algokit_transact/py.typed +0 -0
  276. algokit_transact/signer.py +195 -0
  277. algokit_transact/signing/__init__.py +0 -0
  278. algokit_transact/signing/logic_signature.py +19 -0
  279. algokit_transact/signing/multisig.py +84 -0
  280. algokit_transact/signing/types.py +39 -0
  281. algokit_transact/signing/validation.py +63 -0
  282. algokit_utils/__init__.py +23 -0
  283. algokit_utils/_debugging.py +304 -0
  284. algokit_utils/accounts/__init__.py +2 -0
  285. algokit_utils/accounts/account_manager.py +1051 -0
  286. algokit_utils/accounts/kmd_account_manager.py +206 -0
  287. algokit_utils/algo25.py +46 -0
  288. algokit_utils/algorand.py +383 -0
  289. algokit_utils/applications/__init__.py +7 -0
  290. algokit_utils/applications/abi.py +280 -0
  291. algokit_utils/applications/app_client.py +2193 -0
  292. algokit_utils/applications/app_deployer.py +788 -0
  293. algokit_utils/applications/app_factory.py +1140 -0
  294. algokit_utils/applications/app_manager.py +575 -0
  295. algokit_utils/applications/app_spec/__init__.py +6 -0
  296. algokit_utils/applications/enums.py +40 -0
  297. algokit_utils/assets/__init__.py +1 -0
  298. algokit_utils/assets/asset_manager.py +344 -0
  299. algokit_utils/clients/__init__.py +41 -0
  300. algokit_utils/clients/client_manager.py +756 -0
  301. algokit_utils/clients/dispenser_api_client.py +212 -0
  302. algokit_utils/common.py +40 -0
  303. algokit_utils/config.py +159 -0
  304. algokit_utils/errors/__init__.py +1 -0
  305. algokit_utils/errors/logic_error.py +160 -0
  306. algokit_utils/models/__init__.py +7 -0
  307. algokit_utils/models/account.py +12 -0
  308. algokit_utils/models/amount.py +198 -0
  309. algokit_utils/models/application.py +90 -0
  310. algokit_utils/models/network.py +29 -0
  311. algokit_utils/models/simulate.py +7 -0
  312. algokit_utils/models/state.py +53 -0
  313. algokit_utils/models/transaction.py +49 -0
  314. algokit_utils/protocols/__init__.py +3 -0
  315. algokit_utils/protocols/account.py +11 -0
  316. algokit_utils/protocols/signer.py +17 -0
  317. algokit_utils/protocols/typed_clients.py +110 -0
  318. algokit_utils/py.typed +0 -0
  319. algokit_utils/transact.py +195 -0
  320. algokit_utils/transactions/__init__.py +3 -0
  321. algokit_utils/transactions/builders/__init__.py +67 -0
  322. algokit_utils/transactions/builders/app.py +248 -0
  323. algokit_utils/transactions/builders/asset.py +256 -0
  324. algokit_utils/transactions/builders/common.py +263 -0
  325. algokit_utils/transactions/builders/keyreg.py +103 -0
  326. algokit_utils/transactions/builders/method_call.py +380 -0
  327. algokit_utils/transactions/builders/payment.py +43 -0
  328. algokit_utils/transactions/composer_resources.py +409 -0
  329. algokit_utils/transactions/fee_coverage.py +79 -0
  330. algokit_utils/transactions/helpers.py +9 -0
  331. algokit_utils/transactions/transaction_composer.py +1574 -0
  332. algokit_utils/transactions/transaction_creator.py +699 -0
  333. algokit_utils/transactions/transaction_sender.py +1240 -0
  334. algokit_utils/transactions/types.py +262 -0
  335. algokit_utils-5.0.0a3.dist-info/METADATA +105 -0
  336. algokit_utils-5.0.0a3.dist-info/RECORD +337 -0
  337. algokit_utils-5.0.0a3.dist-info/WHEEL +4 -0
@@ -0,0 +1,1585 @@
1
+ # AUTO-GENERATED: oas_generator
2
+ import random
3
+ import time
4
+ from base64 import b64encode
5
+ from collections.abc import Sequence
6
+ from dataclasses import is_dataclass
7
+ from typing import Any, Literal, TypeVar, overload
8
+
9
+ import httpx
10
+ import msgpack
11
+
12
+ from algokit_common.serde import from_wire, to_wire
13
+
14
+ from . import models
15
+ from .config import ClientConfig
16
+ from .exceptions import UnexpectedStatusError
17
+ from .types import Headers
18
+
19
+ # HTTP status codes that warrant a retry (aligned with algokit-utils-ts)
20
+ _RETRY_STATUS_CODES: frozenset[int] = frozenset({408, 413, 429, 500, 502, 503, 504})
21
+ # Network error codes that warrant a retry (aligned with algokit-utils-ts)
22
+ _RETRY_ERROR_CODES: frozenset[str] = frozenset(
23
+ {
24
+ "ETIMEDOUT",
25
+ "ECONNRESET",
26
+ "EADDRINUSE",
27
+ "ECONNREFUSED",
28
+ "EPIPE",
29
+ "ENOTFOUND",
30
+ "ENETUNREACH",
31
+ "EAI_AGAIN",
32
+ "EPROTO",
33
+ }
34
+ )
35
+ _MAX_BACKOFF_MS: float = 10_000.0
36
+ _DEFAULT_MAX_TRIES: int = 5
37
+
38
+ ModelT = TypeVar("ModelT")
39
+ ListModelT = TypeVar("ListModelT")
40
+ PrimitiveT = TypeVar("PrimitiveT")
41
+
42
+ # Prefixed markers used when converting unhashable msgpack map keys into hashable tuples
43
+ _UNHASHABLE_PREFIXES: dict[str, str] = {
44
+ "dict": "__dict_key__",
45
+ "list": "__list_key__",
46
+ "set": "__set_key__",
47
+ "generic": "__unhashable__",
48
+ }
49
+
50
+
51
+ class AlgodClient:
52
+ def __init__(self, config: ClientConfig | None = None, *, http_client: httpx.Client | None = None) -> None:
53
+ self._config = config or ClientConfig()
54
+ # Track whether a custom HTTP client was provided to avoid retry conflicts
55
+ self._uses_custom_client = http_client is not None
56
+ self._client = http_client or httpx.Client(
57
+ base_url=self._config.base_url,
58
+ timeout=self._config.timeout,
59
+ verify=self._config.verify,
60
+ )
61
+
62
+ def close(self) -> None:
63
+ self._client.close()
64
+
65
+ def _calculate_max_tries(self) -> int:
66
+ """Calculate maximum number of tries from config.max_retries."""
67
+ max_retries = self._config.max_retries
68
+ if not isinstance(max_retries, int) or max_retries < 0:
69
+ return _DEFAULT_MAX_TRIES
70
+ return max_retries + 1
71
+
72
+ def _should_retry(self, error: Exception | None, status_code: int | None, attempt: int, max_tries: int) -> bool:
73
+ """Determine if a request should be retried based on error/status and attempt count."""
74
+ if attempt >= max_tries:
75
+ return False
76
+
77
+ # Check HTTP status code
78
+ if status_code is not None and status_code in _RETRY_STATUS_CODES:
79
+ return True
80
+
81
+ # Check network error codes (aligned with algokit-utils-ts)
82
+ if error is not None:
83
+ error_code = self._extract_error_code(error)
84
+ if error_code and error_code in _RETRY_ERROR_CODES:
85
+ return True
86
+
87
+ return False
88
+
89
+ def _extract_error_code(self, error: BaseException) -> str | None:
90
+ """Extract error code from exception, checking common attributes."""
91
+ # Check for 'code' attribute (common in OS/network errors)
92
+ if hasattr(error, "code") and isinstance(error.code, str):
93
+ return error.code
94
+ # Check for errno attribute
95
+ if hasattr(error, "errno") and error.errno is not None:
96
+ import errno as errno_module
97
+
98
+ try:
99
+ return errno_module.errorcode.get(error.errno)
100
+ except (TypeError, AttributeError):
101
+ pass
102
+ # Check __cause__ for wrapped errors
103
+ if error.__cause__ is not None:
104
+ return self._extract_error_code(error.__cause__)
105
+ return None
106
+
107
+ def _request_with_retry(self, request_kwargs: dict[str, Any]) -> httpx.Response:
108
+ """Execute request with exponential backoff retry for transient failures.
109
+
110
+ When a custom HTTP client is provided, retries are disabled to avoid
111
+ conflicts with any retry mechanism the custom client may implement.
112
+ """
113
+ # Disable retries when using a custom HTTP client to avoid conflicts
114
+ # with the client's own retry mechanism
115
+ if self._uses_custom_client:
116
+ return self._client.request(**request_kwargs)
117
+
118
+ max_tries = self._calculate_max_tries()
119
+ attempt = 1
120
+ last_error: Exception | None = None
121
+
122
+ while attempt <= max_tries:
123
+ status_code: int | None = None
124
+ try:
125
+ response = self._client.request(**request_kwargs)
126
+ status_code = response.status_code
127
+ if not self._should_retry(None, status_code, attempt, max_tries):
128
+ return response
129
+ except httpx.TransportError as exc:
130
+ last_error = exc
131
+ if not self._should_retry(exc, None, attempt, max_tries):
132
+ raise
133
+
134
+ if attempt == 1:
135
+ backoff_ms = 0.0
136
+ else:
137
+ base_backoff = min(1000.0 * (2 ** (attempt - 1)), _MAX_BACKOFF_MS)
138
+ jitter = 0.5 + random.random() # Random value between 0.5 and 1.5
139
+ backoff_ms = base_backoff * jitter
140
+ if backoff_ms > 0:
141
+ time.sleep(backoff_ms / 1000.0)
142
+ attempt += 1
143
+
144
+ # Should not reach here, but satisfy type checker
145
+ if last_error:
146
+ raise last_error
147
+ raise RuntimeError(f"Request failed after {max_tries} attempt(s)")
148
+
149
+ # public
150
+
151
+ def _get_application_box_by_name(
152
+ self,
153
+ application_id: int,
154
+ name: str,
155
+ ) -> models.Box:
156
+ """
157
+ Get box information for a given application.
158
+ """
159
+
160
+ path = "/v2/applications/{application-id}/box"
161
+ path = path.replace("{application-id}", str(application_id))
162
+
163
+ params: dict[str, Any] = {}
164
+ headers: Headers = self._config.resolve_headers()
165
+ if name is not None:
166
+ params["name"] = name
167
+
168
+ accept_value: str | None = None
169
+
170
+ headers.setdefault("accept", accept_value or "application/json")
171
+ request_kwargs: dict[str, Any] = {
172
+ "method": "GET",
173
+ "url": path,
174
+ "params": params,
175
+ "headers": headers,
176
+ }
177
+
178
+ response = self._request_with_retry(request_kwargs)
179
+ if response.is_success:
180
+ return self._decode_response(response, model=models.Box)
181
+
182
+ raise UnexpectedStatusError(response.status_code, response.text)
183
+
184
+ def _raw_transaction(
185
+ self,
186
+ body: bytes,
187
+ ) -> models.PostTransactionsResponse:
188
+ """
189
+ Broadcasts a raw transaction or transaction group to the network.
190
+ """
191
+
192
+ path = "/v2/transactions"
193
+ params: dict[str, Any] = {}
194
+ headers: Headers = self._config.resolve_headers()
195
+
196
+ accept_value: str | None = None
197
+
198
+ body_media_types = ["application/x-binary"]
199
+
200
+ headers.setdefault("accept", accept_value or "application/json")
201
+ request_kwargs: dict[str, Any] = {
202
+ "method": "POST",
203
+ "url": path,
204
+ "params": params,
205
+ "headers": headers,
206
+ }
207
+
208
+ if body is not None:
209
+ self._assign_body(
210
+ request_kwargs,
211
+ body,
212
+ {
213
+ "is_binary": True,
214
+ },
215
+ body_media_types,
216
+ )
217
+
218
+ response = self._request_with_retry(request_kwargs)
219
+ if response.is_success:
220
+ return self._decode_response(response, model=models.PostTransactionsResponse)
221
+
222
+ raise UnexpectedStatusError(response.status_code, response.text)
223
+
224
+ def _transaction_params(
225
+ self,
226
+ ) -> models.TransactionParametersResponse:
227
+ """
228
+ Get parameters for constructing a new transaction
229
+ """
230
+
231
+ path = "/v2/transactions/params"
232
+ params: dict[str, Any] = {}
233
+ headers: Headers = self._config.resolve_headers()
234
+
235
+ accept_value: str | None = None
236
+
237
+ headers.setdefault("accept", accept_value or "application/json")
238
+ request_kwargs: dict[str, Any] = {
239
+ "method": "GET",
240
+ "url": path,
241
+ "params": params,
242
+ "headers": headers,
243
+ }
244
+
245
+ response = self._request_with_retry(request_kwargs)
246
+ if response.is_success:
247
+ return self._decode_response(response, model=models.TransactionParametersResponse)
248
+
249
+ raise UnexpectedStatusError(response.status_code, response.text)
250
+
251
+ def account_application_information(
252
+ self,
253
+ address: str,
254
+ application_id: int,
255
+ *,
256
+ response_format: Literal["json", "msgpack"] | None = None,
257
+ ) -> models.AccountApplicationResponse:
258
+ """
259
+ Get account information about a given app.
260
+ """
261
+
262
+ path = "/v2/accounts/{address}/applications/{application-id}"
263
+ path = path.replace("{address}", str(address))
264
+
265
+ path = path.replace("{application-id}", str(application_id))
266
+
267
+ params: dict[str, Any] = {}
268
+ headers: Headers = self._config.resolve_headers()
269
+
270
+ accept_value: str | None = None
271
+
272
+ selected_format = response_format
273
+
274
+ if selected_format == "msgpack":
275
+ params["format"] = "msgpack"
276
+ accept_value = "application/msgpack"
277
+
278
+ headers.setdefault("accept", accept_value or "application/json")
279
+ request_kwargs: dict[str, Any] = {
280
+ "method": "GET",
281
+ "url": path,
282
+ "params": params,
283
+ "headers": headers,
284
+ }
285
+
286
+ response = self._request_with_retry(request_kwargs)
287
+ if response.is_success:
288
+ return self._decode_response(response, model=models.AccountApplicationResponse)
289
+
290
+ raise UnexpectedStatusError(response.status_code, response.text)
291
+
292
+ def account_asset_information(
293
+ self,
294
+ address: str,
295
+ asset_id: int,
296
+ ) -> models.AccountAssetResponse:
297
+ """
298
+ Get account information about a given asset.
299
+ """
300
+
301
+ path = "/v2/accounts/{address}/assets/{asset-id}"
302
+ path = path.replace("{address}", str(address))
303
+
304
+ path = path.replace("{asset-id}", str(asset_id))
305
+
306
+ params: dict[str, Any] = {}
307
+ headers: Headers = self._config.resolve_headers()
308
+
309
+ accept_value: str | None = None
310
+
311
+ headers.setdefault("accept", accept_value or "application/json")
312
+ request_kwargs: dict[str, Any] = {
313
+ "method": "GET",
314
+ "url": path,
315
+ "params": params,
316
+ "headers": headers,
317
+ }
318
+
319
+ response = self._request_with_retry(request_kwargs)
320
+ if response.is_success:
321
+ return self._decode_response(response, model=models.AccountAssetResponse)
322
+
323
+ raise UnexpectedStatusError(response.status_code, response.text)
324
+
325
+ def account_information(
326
+ self,
327
+ address: str,
328
+ *,
329
+ exclude: str | None = None,
330
+ ) -> models.Account:
331
+ """
332
+ Get account information.
333
+ """
334
+
335
+ path = "/v2/accounts/{address}"
336
+ path = path.replace("{address}", str(address))
337
+
338
+ params: dict[str, Any] = {}
339
+ headers: Headers = self._config.resolve_headers()
340
+ if exclude is not None:
341
+ params["exclude"] = exclude
342
+
343
+ accept_value: str | None = None
344
+
345
+ headers.setdefault("accept", accept_value or "application/json")
346
+ request_kwargs: dict[str, Any] = {
347
+ "method": "GET",
348
+ "url": path,
349
+ "params": params,
350
+ "headers": headers,
351
+ }
352
+
353
+ response = self._request_with_retry(request_kwargs)
354
+ if response.is_success:
355
+ return self._decode_response(response, model=models.Account)
356
+
357
+ raise UnexpectedStatusError(response.status_code, response.text)
358
+
359
+ def get_application_boxes(
360
+ self,
361
+ application_id: int,
362
+ *,
363
+ max_: int | None = None,
364
+ ) -> models.BoxesResponse:
365
+ """
366
+ Get all box names for a given application.
367
+ """
368
+
369
+ path = "/v2/applications/{application-id}/boxes"
370
+ path = path.replace("{application-id}", str(application_id))
371
+
372
+ params: dict[str, Any] = {}
373
+ headers: Headers = self._config.resolve_headers()
374
+ if max_ is not None:
375
+ params["max"] = max_
376
+
377
+ accept_value: str | None = None
378
+
379
+ headers.setdefault("accept", accept_value or "application/json")
380
+ request_kwargs: dict[str, Any] = {
381
+ "method": "GET",
382
+ "url": path,
383
+ "params": params,
384
+ "headers": headers,
385
+ }
386
+
387
+ response = self._request_with_retry(request_kwargs)
388
+ if response.is_success:
389
+ return self._decode_response(response, model=models.BoxesResponse)
390
+
391
+ raise UnexpectedStatusError(response.status_code, response.text)
392
+
393
+ def get_application_by_id(
394
+ self,
395
+ application_id: int,
396
+ ) -> models.Application:
397
+ """
398
+ Get application information.
399
+ """
400
+
401
+ path = "/v2/applications/{application-id}"
402
+ path = path.replace("{application-id}", str(application_id))
403
+
404
+ params: dict[str, Any] = {}
405
+ headers: Headers = self._config.resolve_headers()
406
+
407
+ accept_value: str | None = None
408
+
409
+ headers.setdefault("accept", accept_value or "application/json")
410
+ request_kwargs: dict[str, Any] = {
411
+ "method": "GET",
412
+ "url": path,
413
+ "params": params,
414
+ "headers": headers,
415
+ }
416
+
417
+ response = self._request_with_retry(request_kwargs)
418
+ if response.is_success:
419
+ return self._decode_response(response, model=models.Application)
420
+
421
+ raise UnexpectedStatusError(response.status_code, response.text)
422
+
423
+ def get_asset_by_id(
424
+ self,
425
+ asset_id: int,
426
+ ) -> models.Asset:
427
+ """
428
+ Get asset information.
429
+ """
430
+
431
+ path = "/v2/assets/{asset-id}"
432
+ path = path.replace("{asset-id}", str(asset_id))
433
+
434
+ params: dict[str, Any] = {}
435
+ headers: Headers = self._config.resolve_headers()
436
+
437
+ accept_value: str | None = None
438
+
439
+ headers.setdefault("accept", accept_value or "application/json")
440
+ request_kwargs: dict[str, Any] = {
441
+ "method": "GET",
442
+ "url": path,
443
+ "params": params,
444
+ "headers": headers,
445
+ }
446
+
447
+ response = self._request_with_retry(request_kwargs)
448
+ if response.is_success:
449
+ return self._decode_response(response, model=models.Asset)
450
+
451
+ raise UnexpectedStatusError(response.status_code, response.text)
452
+
453
+ def get_block(
454
+ self,
455
+ round_: int,
456
+ *,
457
+ header_only: bool | None = None,
458
+ ) -> models.BlockResponse:
459
+ """
460
+ Get the block for the given round.
461
+ """
462
+
463
+ path = "/v2/blocks/{round}"
464
+ path = path.replace("{round}", str(round_))
465
+
466
+ params: dict[str, Any] = {}
467
+ headers: Headers = self._config.resolve_headers()
468
+ if header_only is not None:
469
+ params["header-only"] = header_only
470
+
471
+ accept_value: str | None = None
472
+
473
+ params["format"] = "msgpack"
474
+ accept_value = "application/msgpack"
475
+
476
+ headers.setdefault("accept", accept_value or "application/msgpack")
477
+ request_kwargs: dict[str, Any] = {
478
+ "method": "GET",
479
+ "url": path,
480
+ "params": params,
481
+ "headers": headers,
482
+ }
483
+
484
+ response = self._request_with_retry(request_kwargs)
485
+ if response.is_success:
486
+ return self._decode_response(response, model=models.BlockResponse)
487
+
488
+ raise UnexpectedStatusError(response.status_code, response.text)
489
+
490
+ def get_block_hash(
491
+ self,
492
+ round_: int,
493
+ ) -> models.BlockHashResponse:
494
+ """
495
+ Get the block hash for the block on the given round.
496
+ """
497
+
498
+ path = "/v2/blocks/{round}/hash"
499
+ path = path.replace("{round}", str(round_))
500
+
501
+ params: dict[str, Any] = {}
502
+ headers: Headers = self._config.resolve_headers()
503
+
504
+ accept_value: str | None = None
505
+
506
+ headers.setdefault("accept", accept_value or "application/json")
507
+ request_kwargs: dict[str, Any] = {
508
+ "method": "GET",
509
+ "url": path,
510
+ "params": params,
511
+ "headers": headers,
512
+ }
513
+
514
+ response = self._request_with_retry(request_kwargs)
515
+ if response.is_success:
516
+ return self._decode_response(response, model=models.BlockHashResponse)
517
+
518
+ raise UnexpectedStatusError(response.status_code, response.text)
519
+
520
+ def get_block_time_stamp_offset(
521
+ self,
522
+ ) -> models.GetBlockTimeStampOffsetResponse:
523
+ """
524
+ Returns the timestamp offset. Timestamp offsets can only be set in dev mode.
525
+ """
526
+
527
+ path = "/v2/devmode/blocks/offset"
528
+ params: dict[str, Any] = {}
529
+ headers: Headers = self._config.resolve_headers()
530
+
531
+ accept_value: str | None = None
532
+
533
+ headers.setdefault("accept", accept_value or "application/json")
534
+ request_kwargs: dict[str, Any] = {
535
+ "method": "GET",
536
+ "url": path,
537
+ "params": params,
538
+ "headers": headers,
539
+ }
540
+
541
+ response = self._request_with_retry(request_kwargs)
542
+ if response.is_success:
543
+ return self._decode_response(response, model=models.GetBlockTimeStampOffsetResponse)
544
+
545
+ raise UnexpectedStatusError(response.status_code, response.text)
546
+
547
+ def get_block_tx_ids(
548
+ self,
549
+ round_: int,
550
+ ) -> models.BlockTxidsResponse:
551
+ """
552
+ Get the top level transaction IDs for the block on the given round.
553
+ """
554
+
555
+ path = "/v2/blocks/{round}/txids"
556
+ path = path.replace("{round}", str(round_))
557
+
558
+ params: dict[str, Any] = {}
559
+ headers: Headers = self._config.resolve_headers()
560
+
561
+ accept_value: str | None = None
562
+
563
+ headers.setdefault("accept", accept_value or "application/json")
564
+ request_kwargs: dict[str, Any] = {
565
+ "method": "GET",
566
+ "url": path,
567
+ "params": params,
568
+ "headers": headers,
569
+ }
570
+
571
+ response = self._request_with_retry(request_kwargs)
572
+ if response.is_success:
573
+ return self._decode_response(response, model=models.BlockTxidsResponse)
574
+
575
+ raise UnexpectedStatusError(response.status_code, response.text)
576
+
577
+ def get_genesis(
578
+ self,
579
+ ) -> models.GenesisFileInJson:
580
+ """
581
+ Gets the genesis information.
582
+ """
583
+
584
+ path = "/genesis"
585
+ params: dict[str, Any] = {}
586
+ headers: Headers = self._config.resolve_headers()
587
+
588
+ accept_value: str | None = None
589
+
590
+ headers.setdefault("accept", accept_value or "application/json")
591
+ request_kwargs: dict[str, Any] = {
592
+ "method": "GET",
593
+ "url": path,
594
+ "params": params,
595
+ "headers": headers,
596
+ }
597
+
598
+ response = self._request_with_retry(request_kwargs)
599
+ if response.is_success:
600
+ return self._decode_response(response, model=models.GenesisFileInJson)
601
+
602
+ raise UnexpectedStatusError(response.status_code, response.text)
603
+
604
+ def get_ledger_state_delta(
605
+ self,
606
+ round_: int,
607
+ ) -> models.LedgerStateDelta:
608
+ """
609
+ Get a LedgerStateDelta object for a given round
610
+ """
611
+
612
+ path = "/v2/deltas/{round}"
613
+ path = path.replace("{round}", str(round_))
614
+
615
+ params: dict[str, Any] = {}
616
+ headers: Headers = self._config.resolve_headers()
617
+
618
+ accept_value: str | None = None
619
+
620
+ params["format"] = "msgpack"
621
+ accept_value = "application/msgpack"
622
+
623
+ headers.setdefault("accept", accept_value or "application/msgpack")
624
+ request_kwargs: dict[str, Any] = {
625
+ "method": "GET",
626
+ "url": path,
627
+ "params": params,
628
+ "headers": headers,
629
+ }
630
+
631
+ response = self._request_with_retry(request_kwargs)
632
+ if response.is_success:
633
+ return self._decode_response(response, model=models.LedgerStateDelta)
634
+
635
+ raise UnexpectedStatusError(response.status_code, response.text)
636
+
637
+ def get_ledger_state_delta_for_transaction_group(
638
+ self,
639
+ id_: str,
640
+ ) -> models.LedgerStateDelta:
641
+ """
642
+ Get a LedgerStateDelta object for a given transaction group
643
+ """
644
+
645
+ path = "/v2/deltas/txn/group/{id}"
646
+ path = path.replace("{id}", str(id_))
647
+
648
+ params: dict[str, Any] = {}
649
+ headers: Headers = self._config.resolve_headers()
650
+
651
+ accept_value: str | None = None
652
+
653
+ params["format"] = "msgpack"
654
+ accept_value = "application/msgpack"
655
+
656
+ headers.setdefault("accept", accept_value or "application/msgpack")
657
+ request_kwargs: dict[str, Any] = {
658
+ "method": "GET",
659
+ "url": path,
660
+ "params": params,
661
+ "headers": headers,
662
+ }
663
+
664
+ response = self._request_with_retry(request_kwargs)
665
+ if response.is_success:
666
+ return self._decode_response(response, model=models.LedgerStateDelta)
667
+
668
+ raise UnexpectedStatusError(response.status_code, response.text)
669
+
670
+ def get_light_block_header_proof(
671
+ self,
672
+ round_: int,
673
+ ) -> models.LightBlockHeaderProof:
674
+ """
675
+ Gets a proof for a given light block header inside a state proof commitment
676
+ """
677
+
678
+ path = "/v2/blocks/{round}/lightheader/proof"
679
+ path = path.replace("{round}", str(round_))
680
+
681
+ params: dict[str, Any] = {}
682
+ headers: Headers = self._config.resolve_headers()
683
+
684
+ accept_value: str | None = None
685
+
686
+ headers.setdefault("accept", accept_value or "application/json")
687
+ request_kwargs: dict[str, Any] = {
688
+ "method": "GET",
689
+ "url": path,
690
+ "params": params,
691
+ "headers": headers,
692
+ }
693
+
694
+ response = self._request_with_retry(request_kwargs)
695
+ if response.is_success:
696
+ return self._decode_response(response, model=models.LightBlockHeaderProof)
697
+
698
+ raise UnexpectedStatusError(response.status_code, response.text)
699
+
700
+ def get_pending_transactions(
701
+ self,
702
+ *,
703
+ max_: int | None = None,
704
+ ) -> models.PendingTransactionsResponse:
705
+ """
706
+ Get a list of unconfirmed transactions currently in the transaction pool.
707
+ """
708
+
709
+ path = "/v2/transactions/pending"
710
+ params: dict[str, Any] = {}
711
+ headers: Headers = self._config.resolve_headers()
712
+ if max_ is not None:
713
+ params["max"] = max_
714
+
715
+ accept_value: str | None = None
716
+
717
+ params["format"] = "msgpack"
718
+ accept_value = "application/msgpack"
719
+
720
+ headers.setdefault("accept", accept_value or "application/msgpack")
721
+ request_kwargs: dict[str, Any] = {
722
+ "method": "GET",
723
+ "url": path,
724
+ "params": params,
725
+ "headers": headers,
726
+ }
727
+
728
+ response = self._request_with_retry(request_kwargs)
729
+ if response.is_success:
730
+ return self._decode_response(response, model=models.PendingTransactionsResponse)
731
+
732
+ raise UnexpectedStatusError(response.status_code, response.text)
733
+
734
+ def get_pending_transactions_by_address(
735
+ self,
736
+ address: str,
737
+ *,
738
+ max_: int | None = None,
739
+ ) -> models.PendingTransactionsResponse:
740
+ """
741
+ Get a list of unconfirmed transactions currently in the transaction pool by address.
742
+ """
743
+
744
+ path = "/v2/accounts/{address}/transactions/pending"
745
+ path = path.replace("{address}", str(address))
746
+
747
+ params: dict[str, Any] = {}
748
+ headers: Headers = self._config.resolve_headers()
749
+ if max_ is not None:
750
+ params["max"] = max_
751
+
752
+ accept_value: str | None = None
753
+
754
+ params["format"] = "msgpack"
755
+ accept_value = "application/msgpack"
756
+
757
+ headers.setdefault("accept", accept_value or "application/msgpack")
758
+ request_kwargs: dict[str, Any] = {
759
+ "method": "GET",
760
+ "url": path,
761
+ "params": params,
762
+ "headers": headers,
763
+ }
764
+
765
+ response = self._request_with_retry(request_kwargs)
766
+ if response.is_success:
767
+ return self._decode_response(response, model=models.PendingTransactionsResponse)
768
+
769
+ raise UnexpectedStatusError(response.status_code, response.text)
770
+
771
+ def get_ready(
772
+ self,
773
+ ) -> None:
774
+ """
775
+ Returns OK if healthy and fully caught up.
776
+ """
777
+
778
+ path = "/ready"
779
+ params: dict[str, Any] = {}
780
+ headers: Headers = self._config.resolve_headers()
781
+
782
+ accept_value: str | None = None
783
+
784
+ headers.setdefault("accept", accept_value or "application/json")
785
+ request_kwargs: dict[str, Any] = {
786
+ "method": "GET",
787
+ "url": path,
788
+ "params": params,
789
+ "headers": headers,
790
+ }
791
+
792
+ response = self._request_with_retry(request_kwargs)
793
+ if response.is_success:
794
+ return
795
+
796
+ raise UnexpectedStatusError(response.status_code, response.text)
797
+
798
+ def get_state_proof(
799
+ self,
800
+ round_: int,
801
+ ) -> models.StateProof:
802
+ """
803
+ Get a state proof that covers a given round
804
+ """
805
+
806
+ path = "/v2/stateproofs/{round}"
807
+ path = path.replace("{round}", str(round_))
808
+
809
+ params: dict[str, Any] = {}
810
+ headers: Headers = self._config.resolve_headers()
811
+
812
+ accept_value: str | None = None
813
+
814
+ headers.setdefault("accept", accept_value or "application/json")
815
+ request_kwargs: dict[str, Any] = {
816
+ "method": "GET",
817
+ "url": path,
818
+ "params": params,
819
+ "headers": headers,
820
+ }
821
+
822
+ response = self._request_with_retry(request_kwargs)
823
+ if response.is_success:
824
+ return self._decode_response(response, model=models.StateProof)
825
+
826
+ raise UnexpectedStatusError(response.status_code, response.text)
827
+
828
+ def get_status(
829
+ self,
830
+ ) -> models.NodeStatusResponse:
831
+ """
832
+ Gets the current node status.
833
+ """
834
+
835
+ path = "/v2/status"
836
+ params: dict[str, Any] = {}
837
+ headers: Headers = self._config.resolve_headers()
838
+
839
+ accept_value: str | None = None
840
+
841
+ headers.setdefault("accept", accept_value or "application/json")
842
+ request_kwargs: dict[str, Any] = {
843
+ "method": "GET",
844
+ "url": path,
845
+ "params": params,
846
+ "headers": headers,
847
+ }
848
+
849
+ response = self._request_with_retry(request_kwargs)
850
+ if response.is_success:
851
+ return self._decode_response(response, model=models.NodeStatusResponse)
852
+
853
+ raise UnexpectedStatusError(response.status_code, response.text)
854
+
855
+ def get_supply(
856
+ self,
857
+ ) -> models.SupplyResponse:
858
+ """
859
+ Get the current supply reported by the ledger.
860
+ """
861
+
862
+ path = "/v2/ledger/supply"
863
+ params: dict[str, Any] = {}
864
+ headers: Headers = self._config.resolve_headers()
865
+
866
+ accept_value: str | None = None
867
+
868
+ headers.setdefault("accept", accept_value or "application/json")
869
+ request_kwargs: dict[str, Any] = {
870
+ "method": "GET",
871
+ "url": path,
872
+ "params": params,
873
+ "headers": headers,
874
+ }
875
+
876
+ response = self._request_with_retry(request_kwargs)
877
+ if response.is_success:
878
+ return self._decode_response(response, model=models.SupplyResponse)
879
+
880
+ raise UnexpectedStatusError(response.status_code, response.text)
881
+
882
+ def get_sync_round(
883
+ self,
884
+ ) -> models.GetSyncRoundResponse:
885
+ """
886
+ Returns the minimum sync round the ledger is keeping in cache.
887
+ """
888
+
889
+ path = "/v2/ledger/sync"
890
+ params: dict[str, Any] = {}
891
+ headers: Headers = self._config.resolve_headers()
892
+
893
+ accept_value: str | None = None
894
+
895
+ headers.setdefault("accept", accept_value or "application/json")
896
+ request_kwargs: dict[str, Any] = {
897
+ "method": "GET",
898
+ "url": path,
899
+ "params": params,
900
+ "headers": headers,
901
+ }
902
+
903
+ response = self._request_with_retry(request_kwargs)
904
+ if response.is_success:
905
+ return self._decode_response(response, model=models.GetSyncRoundResponse)
906
+
907
+ raise UnexpectedStatusError(response.status_code, response.text)
908
+
909
+ def get_transaction_group_ledger_state_deltas_for_round(
910
+ self,
911
+ round_: int,
912
+ ) -> models.GetTransactionGroupLedgerStateDeltasForRound:
913
+ """
914
+ Get LedgerStateDelta objects for all transaction groups in a given round
915
+ """
916
+
917
+ path = "/v2/deltas/{round}/txn/group"
918
+ path = path.replace("{round}", str(round_))
919
+
920
+ params: dict[str, Any] = {}
921
+ headers: Headers = self._config.resolve_headers()
922
+
923
+ accept_value: str | None = None
924
+
925
+ params["format"] = "msgpack"
926
+ accept_value = "application/msgpack"
927
+
928
+ headers.setdefault("accept", accept_value or "application/msgpack")
929
+ request_kwargs: dict[str, Any] = {
930
+ "method": "GET",
931
+ "url": path,
932
+ "params": params,
933
+ "headers": headers,
934
+ }
935
+
936
+ response = self._request_with_retry(request_kwargs)
937
+ if response.is_success:
938
+ return self._decode_response(response, model=models.GetTransactionGroupLedgerStateDeltasForRound)
939
+
940
+ raise UnexpectedStatusError(response.status_code, response.text)
941
+
942
+ def get_transaction_proof(
943
+ self,
944
+ round_: int,
945
+ txid: str,
946
+ *,
947
+ response_format: Literal["json", "msgpack"] | None = None,
948
+ hashtype: str | None = None,
949
+ ) -> models.TransactionProof:
950
+ """
951
+ Get a proof for a transaction in a block.
952
+ """
953
+
954
+ path = "/v2/blocks/{round}/transactions/{txid}/proof"
955
+ path = path.replace("{round}", str(round_))
956
+
957
+ path = path.replace("{txid}", str(txid))
958
+
959
+ params: dict[str, Any] = {}
960
+ headers: Headers = self._config.resolve_headers()
961
+ if hashtype is not None:
962
+ params["hashtype"] = hashtype
963
+
964
+ accept_value: str | None = None
965
+
966
+ selected_format = response_format
967
+
968
+ if selected_format == "msgpack":
969
+ params["format"] = "msgpack"
970
+ accept_value = "application/msgpack"
971
+
972
+ headers.setdefault("accept", accept_value or "application/json")
973
+ request_kwargs: dict[str, Any] = {
974
+ "method": "GET",
975
+ "url": path,
976
+ "params": params,
977
+ "headers": headers,
978
+ }
979
+
980
+ response = self._request_with_retry(request_kwargs)
981
+ if response.is_success:
982
+ return self._decode_response(response, model=models.TransactionProof)
983
+
984
+ raise UnexpectedStatusError(response.status_code, response.text)
985
+
986
+ def get_version(
987
+ self,
988
+ ) -> models.VersionContainsTheCurrentAlgodVersion:
989
+ """
990
+ Retrieves the supported API versions, binary build versions, and genesis information.
991
+ """
992
+
993
+ path = "/versions"
994
+ params: dict[str, Any] = {}
995
+ headers: Headers = self._config.resolve_headers()
996
+
997
+ accept_value: str | None = None
998
+
999
+ headers.setdefault("accept", accept_value or "application/json")
1000
+ request_kwargs: dict[str, Any] = {
1001
+ "method": "GET",
1002
+ "url": path,
1003
+ "params": params,
1004
+ "headers": headers,
1005
+ }
1006
+
1007
+ response = self._request_with_retry(request_kwargs)
1008
+ if response.is_success:
1009
+ return self._decode_response(response, model=models.VersionContainsTheCurrentAlgodVersion)
1010
+
1011
+ raise UnexpectedStatusError(response.status_code, response.text)
1012
+
1013
+ def health_check(
1014
+ self,
1015
+ ) -> None:
1016
+ """
1017
+ Returns OK if healthy.
1018
+ """
1019
+
1020
+ path = "/health"
1021
+ params: dict[str, Any] = {}
1022
+ headers: Headers = self._config.resolve_headers()
1023
+
1024
+ accept_value: str | None = None
1025
+
1026
+ headers.setdefault("accept", accept_value or "application/json")
1027
+ request_kwargs: dict[str, Any] = {
1028
+ "method": "GET",
1029
+ "url": path,
1030
+ "params": params,
1031
+ "headers": headers,
1032
+ }
1033
+
1034
+ response = self._request_with_retry(request_kwargs)
1035
+ if response.is_success:
1036
+ return
1037
+
1038
+ raise UnexpectedStatusError(response.status_code, response.text)
1039
+
1040
+ def pending_transaction_information(
1041
+ self,
1042
+ txid: str,
1043
+ ) -> models.PendingTransactionResponse:
1044
+ """
1045
+ Get a specific pending transaction.
1046
+ """
1047
+
1048
+ path = "/v2/transactions/pending/{txid}"
1049
+ path = path.replace("{txid}", str(txid))
1050
+
1051
+ params: dict[str, Any] = {}
1052
+ headers: Headers = self._config.resolve_headers()
1053
+
1054
+ accept_value: str | None = None
1055
+
1056
+ params["format"] = "msgpack"
1057
+ accept_value = "application/msgpack"
1058
+
1059
+ headers.setdefault("accept", accept_value or "application/msgpack")
1060
+ request_kwargs: dict[str, Any] = {
1061
+ "method": "GET",
1062
+ "url": path,
1063
+ "params": params,
1064
+ "headers": headers,
1065
+ }
1066
+
1067
+ response = self._request_with_retry(request_kwargs)
1068
+ if response.is_success:
1069
+ return self._decode_response(response, model=models.PendingTransactionResponse)
1070
+
1071
+ raise UnexpectedStatusError(response.status_code, response.text)
1072
+
1073
+ def set_block_time_stamp_offset(
1074
+ self,
1075
+ offset: int,
1076
+ ) -> None:
1077
+ """
1078
+ Given a timestamp offset in seconds, adds the offset to every subsequent block header's
1079
+ timestamp.
1080
+ """
1081
+
1082
+ path = "/v2/devmode/blocks/offset/{offset}"
1083
+ path = path.replace("{offset}", str(offset))
1084
+
1085
+ params: dict[str, Any] = {}
1086
+ headers: Headers = self._config.resolve_headers()
1087
+
1088
+ accept_value: str | None = None
1089
+
1090
+ headers.setdefault("accept", accept_value or "application/json")
1091
+ request_kwargs: dict[str, Any] = {
1092
+ "method": "POST",
1093
+ "url": path,
1094
+ "params": params,
1095
+ "headers": headers,
1096
+ }
1097
+
1098
+ response = self._request_with_retry(request_kwargs)
1099
+ if response.is_success:
1100
+ return
1101
+
1102
+ raise UnexpectedStatusError(response.status_code, response.text)
1103
+
1104
+ def set_sync_round(
1105
+ self,
1106
+ round_: int,
1107
+ ) -> None:
1108
+ """
1109
+ Given a round, tells the ledger to keep that round in its cache.
1110
+ """
1111
+
1112
+ path = "/v2/ledger/sync/{round}"
1113
+ path = path.replace("{round}", str(round_))
1114
+
1115
+ params: dict[str, Any] = {}
1116
+ headers: Headers = self._config.resolve_headers()
1117
+
1118
+ accept_value: str | None = None
1119
+
1120
+ headers.setdefault("accept", accept_value or "application/json")
1121
+ request_kwargs: dict[str, Any] = {
1122
+ "method": "POST",
1123
+ "url": path,
1124
+ "params": params,
1125
+ "headers": headers,
1126
+ }
1127
+
1128
+ response = self._request_with_retry(request_kwargs)
1129
+ if response.is_success:
1130
+ return
1131
+
1132
+ raise UnexpectedStatusError(response.status_code, response.text)
1133
+
1134
+ def simulate_transactions(
1135
+ self,
1136
+ body: models.SimulateRequest,
1137
+ ) -> models.SimulateResponse:
1138
+ """
1139
+ Simulates a raw transaction or transaction group as it would be evaluated on the
1140
+ network. The simulation will use blockchain state from the latest committed round.
1141
+ """
1142
+
1143
+ path = "/v2/transactions/simulate"
1144
+ params: dict[str, Any] = {}
1145
+ headers: Headers = self._config.resolve_headers()
1146
+
1147
+ accept_value: str | None = None
1148
+
1149
+ body_media_types = ["application/msgpack"]
1150
+
1151
+ params["format"] = "msgpack"
1152
+ accept_value = "application/msgpack"
1153
+
1154
+ if "application/msgpack" in body_media_types:
1155
+ body_media_types = ["application/msgpack"]
1156
+
1157
+ headers.setdefault("accept", accept_value or "application/msgpack")
1158
+ request_kwargs: dict[str, Any] = {
1159
+ "method": "POST",
1160
+ "url": path,
1161
+ "params": params,
1162
+ "headers": headers,
1163
+ }
1164
+
1165
+ if body is not None:
1166
+ self._assign_body(
1167
+ request_kwargs,
1168
+ body,
1169
+ {
1170
+ "model": "SimulateRequest",
1171
+ },
1172
+ body_media_types,
1173
+ )
1174
+
1175
+ response = self._request_with_retry(request_kwargs)
1176
+ if response.is_success:
1177
+ return self._decode_response(response, model=models.SimulateResponse)
1178
+
1179
+ raise UnexpectedStatusError(response.status_code, response.text)
1180
+
1181
+ def status_after_block(
1182
+ self,
1183
+ round_: int,
1184
+ ) -> models.NodeStatusResponse:
1185
+ """
1186
+ Gets the node status after waiting for a round after the given round.
1187
+ """
1188
+
1189
+ path = "/v2/status/wait-for-block-after/{round}"
1190
+ path = path.replace("{round}", str(round_))
1191
+
1192
+ params: dict[str, Any] = {}
1193
+ headers: Headers = self._config.resolve_headers()
1194
+
1195
+ accept_value: str | None = None
1196
+
1197
+ headers.setdefault("accept", accept_value or "application/json")
1198
+ request_kwargs: dict[str, Any] = {
1199
+ "method": "GET",
1200
+ "url": path,
1201
+ "params": params,
1202
+ "headers": headers,
1203
+ }
1204
+
1205
+ response = self._request_with_retry(request_kwargs)
1206
+ if response.is_success:
1207
+ return self._decode_response(response, model=models.NodeStatusResponse)
1208
+
1209
+ raise UnexpectedStatusError(response.status_code, response.text)
1210
+
1211
+ def teal_compile(
1212
+ self,
1213
+ body: bytes,
1214
+ *,
1215
+ sourcemap: bool | None = None,
1216
+ ) -> models.CompileResponse:
1217
+ """
1218
+ Compile TEAL source code to binary, produce its hash
1219
+ """
1220
+
1221
+ path = "/v2/teal/compile"
1222
+ params: dict[str, Any] = {}
1223
+ headers: Headers = self._config.resolve_headers()
1224
+ if sourcemap is not None:
1225
+ params["sourcemap"] = sourcemap
1226
+
1227
+ accept_value: str | None = None
1228
+
1229
+ body_media_types = ["text/plain"]
1230
+
1231
+ headers.setdefault("accept", accept_value or "application/json")
1232
+ request_kwargs: dict[str, Any] = {
1233
+ "method": "POST",
1234
+ "url": path,
1235
+ "params": params,
1236
+ "headers": headers,
1237
+ }
1238
+
1239
+ if body is not None:
1240
+ self._assign_body(
1241
+ request_kwargs,
1242
+ body,
1243
+ {
1244
+ "is_binary": True,
1245
+ },
1246
+ body_media_types,
1247
+ )
1248
+
1249
+ response = self._request_with_retry(request_kwargs)
1250
+ if response.is_success:
1251
+ return self._decode_response(response, model=models.CompileResponse)
1252
+
1253
+ raise UnexpectedStatusError(response.status_code, response.text)
1254
+
1255
+ def teal_disassemble(
1256
+ self,
1257
+ body: bytes,
1258
+ ) -> models.DisassembleResponse:
1259
+ """
1260
+ Disassemble program bytes into the TEAL source code.
1261
+ """
1262
+
1263
+ path = "/v2/teal/disassemble"
1264
+ params: dict[str, Any] = {}
1265
+ headers: Headers = self._config.resolve_headers()
1266
+
1267
+ accept_value: str | None = None
1268
+
1269
+ body_media_types = ["application/x-binary"]
1270
+
1271
+ headers.setdefault("accept", accept_value or "application/json")
1272
+ request_kwargs: dict[str, Any] = {
1273
+ "method": "POST",
1274
+ "url": path,
1275
+ "params": params,
1276
+ "headers": headers,
1277
+ }
1278
+
1279
+ if body is not None:
1280
+ self._assign_body(
1281
+ request_kwargs,
1282
+ body,
1283
+ {
1284
+ "is_binary": True,
1285
+ },
1286
+ body_media_types,
1287
+ )
1288
+
1289
+ response = self._request_with_retry(request_kwargs)
1290
+ if response.is_success:
1291
+ return self._decode_response(response, model=models.DisassembleResponse)
1292
+
1293
+ raise UnexpectedStatusError(response.status_code, response.text)
1294
+
1295
+ def unset_sync_round(
1296
+ self,
1297
+ ) -> None:
1298
+ """
1299
+ Removes minimum sync round restriction from the ledger.
1300
+ """
1301
+
1302
+ path = "/v2/ledger/sync"
1303
+ params: dict[str, Any] = {}
1304
+ headers: Headers = self._config.resolve_headers()
1305
+
1306
+ accept_value: str | None = None
1307
+
1308
+ headers.setdefault("accept", accept_value or "application/json")
1309
+ request_kwargs: dict[str, Any] = {
1310
+ "method": "DELETE",
1311
+ "url": path,
1312
+ "params": params,
1313
+ "headers": headers,
1314
+ }
1315
+
1316
+ response = self._request_with_retry(request_kwargs)
1317
+ if response.is_success:
1318
+ return
1319
+
1320
+ raise UnexpectedStatusError(response.status_code, response.text)
1321
+
1322
+ def send_raw_transaction(
1323
+ self,
1324
+ stx_or_stxs: bytes | bytearray | memoryview | Sequence[bytes | bytearray | memoryview],
1325
+ ) -> models.PostTransactionsResponse:
1326
+ """
1327
+ Send a signed transaction or array of signed transactions to the network.
1328
+ """
1329
+
1330
+ payload: bytes
1331
+ if isinstance(stx_or_stxs, bytes | bytearray | memoryview):
1332
+ payload = bytes(stx_or_stxs)
1333
+ elif isinstance(stx_or_stxs, Sequence):
1334
+ segments: list[bytes] = []
1335
+ for value in stx_or_stxs:
1336
+ if not isinstance(value, bytes | bytearray | memoryview):
1337
+ raise TypeError("All sequence elements must be bytes-like")
1338
+ segments.append(bytes(value))
1339
+ payload = b"".join(segments)
1340
+ else:
1341
+ raise TypeError("stx_or_stxs must be bytes or a sequence of bytes-like values")
1342
+
1343
+ return self._raw_transaction(payload)
1344
+
1345
+ def get_application_box_by_name(
1346
+ self,
1347
+ application_id: int,
1348
+ box_name: bytes | bytearray | memoryview | str,
1349
+ ) -> models.Box:
1350
+ """
1351
+ Given an application ID and box name, return the corresponding box details.
1352
+ """
1353
+
1354
+ box_bytes = box_name.encode() if isinstance(box_name, str) else bytes(box_name)
1355
+ encoded_name = "b64:" + b64encode(box_bytes).decode("ascii")
1356
+ return self._get_application_box_by_name(application_id, name=encoded_name)
1357
+
1358
+ def suggested_params(self) -> models.SuggestedParams:
1359
+ """
1360
+ Return the common parameters required for assembling a transaction.
1361
+ """
1362
+
1363
+ txn_params = self._transaction_params()
1364
+ last_round = txn_params.last_round
1365
+ return models.SuggestedParams(
1366
+ consensus_version=txn_params.consensus_version,
1367
+ fee=txn_params.fee,
1368
+ genesis_hash=txn_params.genesis_hash,
1369
+ genesis_id=txn_params.genesis_id,
1370
+ min_fee=txn_params.min_fee,
1371
+ flat_fee=False,
1372
+ first_valid=last_round,
1373
+ last_valid=last_round + 1000,
1374
+ )
1375
+
1376
+ def _assign_body(
1377
+ self,
1378
+ request_kwargs: dict[str, Any],
1379
+ payload: object,
1380
+ descriptor: dict[str, object],
1381
+ media_types: list[str],
1382
+ ) -> None:
1383
+ encoded = self._encode_payload(payload, descriptor)
1384
+ binary_types = {"application/x-binary", "application/octet-stream"}
1385
+ if bool(descriptor.get("is_binary")) or any(mt in binary_types for mt in media_types):
1386
+ if encoded is None:
1387
+ return
1388
+ request_kwargs["content"] = encoded
1389
+ if media_types:
1390
+ request_kwargs.setdefault("headers", {})["content-type"] = media_types[0]
1391
+ else:
1392
+ request_kwargs.setdefault("headers", {})["content-type"] = "application/octet-stream"
1393
+ elif "application/json" in media_types:
1394
+ request_kwargs["json"] = encoded
1395
+ elif "application/msgpack" in media_types:
1396
+ request_kwargs["content"] = msgpack.packb(encoded, use_bin_type=True)
1397
+ request_kwargs.setdefault("headers", {})["content-type"] = "application/msgpack"
1398
+ else:
1399
+ request_kwargs["json"] = encoded
1400
+
1401
+ def _encode_payload(self, payload: object, descriptor: dict[str, object]) -> object:
1402
+ if payload is None:
1403
+ return None
1404
+ if is_dataclass(payload):
1405
+ return to_wire(payload)
1406
+ list_model = descriptor.get("list_model")
1407
+ if list_model and isinstance(payload, list):
1408
+ return [to_wire(item) if is_dataclass(item) else item for item in payload]
1409
+ return payload
1410
+
1411
+ @overload
1412
+ def _decode_response(
1413
+ self,
1414
+ response: httpx.Response,
1415
+ *,
1416
+ model: type[ModelT],
1417
+ is_binary: bool = False,
1418
+ raw_msgpack: bool = False,
1419
+ ) -> ModelT: ...
1420
+
1421
+ @overload
1422
+ def _decode_response(
1423
+ self,
1424
+ response: httpx.Response,
1425
+ *,
1426
+ list_model: type[ListModelT],
1427
+ is_binary: bool = False,
1428
+ raw_msgpack: bool = False,
1429
+ ) -> list[ListModelT]: ...
1430
+
1431
+ @overload
1432
+ def _decode_response(
1433
+ self,
1434
+ response: httpx.Response,
1435
+ *,
1436
+ type_: type[PrimitiveT],
1437
+ is_binary: bool = False,
1438
+ raw_msgpack: bool = False,
1439
+ ) -> PrimitiveT: ...
1440
+
1441
+ @overload
1442
+ def _decode_response(
1443
+ self,
1444
+ response: httpx.Response,
1445
+ *,
1446
+ is_binary: Literal[True],
1447
+ raw_msgpack: bool = False,
1448
+ ) -> bytes: ...
1449
+
1450
+ @overload
1451
+ def _decode_response(
1452
+ self,
1453
+ response: httpx.Response,
1454
+ *,
1455
+ raw_msgpack: Literal[True],
1456
+ ) -> bytes: ...
1457
+
1458
+ @overload
1459
+ def _decode_response(
1460
+ self,
1461
+ response: httpx.Response,
1462
+ *,
1463
+ type_: None = None,
1464
+ is_binary: bool = False,
1465
+ raw_msgpack: bool = False,
1466
+ ) -> object: ...
1467
+
1468
+ def _decode_response(
1469
+ self,
1470
+ response: httpx.Response,
1471
+ *,
1472
+ model: type[Any] | None = None,
1473
+ list_model: type[Any] | None = None,
1474
+ type_: type[Any] | None = None,
1475
+ is_binary: bool = False,
1476
+ raw_msgpack: bool = False,
1477
+ ) -> object:
1478
+ if is_binary or raw_msgpack:
1479
+ return response.content
1480
+ content_type = response.headers.get("content-type", "application/json")
1481
+ if "msgpack" in content_type:
1482
+ # Handle msgpack unpacking with support for unhashable keys
1483
+ # Use Unpacker for more control over the unpacking process
1484
+ unpacker = msgpack.Unpacker(
1485
+ raw=True,
1486
+ strict_map_key=False,
1487
+ object_pairs_hook=self._msgpack_pairs_hook,
1488
+ )
1489
+ unpacker.feed(response.content)
1490
+ try:
1491
+ data = unpacker.unpack()
1492
+ except TypeError:
1493
+ # If unpacking fails due to unhashable keys, try without the hook
1494
+ # and handle in normalization
1495
+ unpacker = msgpack.Unpacker(raw=True, strict_map_key=False)
1496
+ unpacker.feed(response.content)
1497
+ data = unpacker.unpack()
1498
+ data = self._normalize_msgpack(data)
1499
+ elif content_type.startswith("application/json"):
1500
+ data = response.json()
1501
+ else:
1502
+ data = response.text
1503
+ if model is not None:
1504
+ return from_wire(model, data)
1505
+ if list_model is not None:
1506
+ return [from_wire(list_model, item) for item in data]
1507
+ if type_ is not None:
1508
+ return data
1509
+ return data
1510
+
1511
+ def _normalize_msgpack(self, value: object) -> object:
1512
+ # Handle pairs returned from msgpack_pairs_hook when keys are unhashable
1513
+ _pair_length = 2
1514
+ if isinstance(value, list) and value and isinstance(value[0], tuple | list) and len(value[0]) == _pair_length:
1515
+ # Convert to dict with normalized keys
1516
+ pairs_dict: dict[object, object] = {}
1517
+ for pair in value:
1518
+ if isinstance(pair, tuple | list) and len(pair) == _pair_length:
1519
+ k, v = pair
1520
+ # For unhashable keys (like dict keys), use a tuple representation
1521
+ try:
1522
+ normalized_key = self._coerce_msgpack_key(k)
1523
+ pairs_dict[normalized_key] = self._normalize_msgpack(v)
1524
+ except TypeError:
1525
+ # Key is unhashable - use tuple representation
1526
+ normalized_key = ("__unhashable__", id(k), str(k))
1527
+ pairs_dict[normalized_key] = self._normalize_msgpack(v)
1528
+ return pairs_dict
1529
+ if isinstance(value, dict):
1530
+ # Safely normalize maps: coerce string/bytes keys, but tolerate complex/unhashable keys
1531
+ try:
1532
+ normalized_dict: dict[object, object] = {}
1533
+ for key, item in value.items():
1534
+ normalized_dict[self._coerce_msgpack_key(key)] = self._normalize_msgpack(item)
1535
+ return normalized_dict
1536
+ except TypeError:
1537
+ # Some maps can decode to object/dict keys; keep original keys and
1538
+ # only normalize values to avoid "unhashable type: 'dict'" errors.
1539
+ for k, item in list(value.items()):
1540
+ value[k] = self._normalize_msgpack(item)
1541
+ return value
1542
+ if isinstance(value, list):
1543
+ return [self._normalize_msgpack(item) for item in value]
1544
+ return value
1545
+
1546
+ def _coerce_msgpack_key(self, key: object) -> object:
1547
+ if isinstance(key, bytes):
1548
+ try:
1549
+ return key.decode("utf-8")
1550
+ except UnicodeDecodeError:
1551
+ return key
1552
+ return key
1553
+
1554
+ def _msgpack_pairs_hook(self, pairs: list[tuple[object, object]] | list[list[object]]) -> dict[object, object]:
1555
+ # Convert pairs to dict, handling unhashable keys by converting them to hashable tuples
1556
+ out: dict[object, object] = {}
1557
+ _hashable_type_tuple = (str, int, float, bool, type(None), bytes)
1558
+
1559
+ for k, v in pairs:
1560
+ if isinstance(k, dict | list | set):
1561
+ # Convert unhashable key to hashable tuple
1562
+ hashable_key: tuple[str, object]
1563
+ if isinstance(k, dict):
1564
+ try:
1565
+ hashable_key = (_UNHASHABLE_PREFIXES["dict"], tuple(sorted(k.items())))
1566
+ except TypeError:
1567
+ hashable_key = (_UNHASHABLE_PREFIXES["dict"], str(k))
1568
+ elif isinstance(k, list):
1569
+ prefix = _UNHASHABLE_PREFIXES["list"]
1570
+ hashable_key = (prefix, tuple(k) if all(isinstance(x, _hashable_type_tuple) for x in k) else str(k))
1571
+ else: # set
1572
+ prefix = _UNHASHABLE_PREFIXES["set"]
1573
+ if all(isinstance(x, _hashable_type_tuple) for x in k):
1574
+ hashable_key = (prefix, tuple(sorted(k)))
1575
+ else:
1576
+ hashable_key = (prefix, str(k))
1577
+ out[hashable_key] = v
1578
+ else:
1579
+ # Key should be hashable, use as-is
1580
+ try:
1581
+ out[k] = v
1582
+ except TypeError:
1583
+ # Unexpected unhashable type, convert to tuple
1584
+ out[(_UNHASHABLE_PREFIXES["generic"], str(type(k).__name__), str(k))] = v
1585
+ return out