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.
- algokit_abi/__init__.py +9 -0
- algokit_abi/_arc32_to_arc56.py +242 -0
- algokit_abi/_arc56_serde.py +161 -0
- algokit_abi/abi.py +667 -0
- algokit_abi/arc32.py +210 -0
- algokit_abi/arc56.py +821 -0
- algokit_abi/py.typed +0 -0
- algokit_algo25/__init__.py +38 -0
- algokit_algo25/_encoding.py +46 -0
- algokit_algo25/_wordlist.py +2065 -0
- algokit_algo25/exceptions.py +29 -0
- algokit_algo25/mnemonic.py +128 -0
- algokit_algo25/py.typed +0 -0
- algokit_algod_client/__init__.py +10 -0
- algokit_algod_client/client.py +1585 -0
- algokit_algod_client/config.py +36 -0
- algokit_algod_client/exceptions.py +59 -0
- algokit_algod_client/models/__init__.py +229 -0
- algokit_algod_client/models/_account.py +150 -0
- algokit_algod_client/models/_account_application_response.py +25 -0
- algokit_algod_client/models/_account_asset_response.py +25 -0
- algokit_algod_client/models/_account_participation.py +53 -0
- algokit_algod_client/models/_account_state_delta.py +30 -0
- algokit_algod_client/models/_allocations_for_genesis_file.py +23 -0
- algokit_algod_client/models/_allocations_for_genesis_file_state_model.py +42 -0
- algokit_algod_client/models/_application.py +23 -0
- algokit_algod_client/models/_application_initial_states.py +37 -0
- algokit_algod_client/models/_application_kvstorage.py +29 -0
- algokit_algod_client/models/_application_local_state.py +33 -0
- algokit_algod_client/models/_application_params.py +63 -0
- algokit_algod_client/models/_application_state_operation.py +41 -0
- algokit_algod_client/models/_application_state_schema.py +22 -0
- algokit_algod_client/models/_asset.py +23 -0
- algokit_algod_client/models/_asset_holding.py +29 -0
- algokit_algod_client/models/_asset_params.py +102 -0
- algokit_algod_client/models/_avm_key_value.py +28 -0
- algokit_algod_client/models/_avm_value.py +32 -0
- algokit_algod_client/models/_block.py +363 -0
- algokit_algod_client/models/_block_hash_response.py +14 -0
- algokit_algod_client/models/_block_txids_response.py +14 -0
- algokit_algod_client/models/_box.py +36 -0
- algokit_algod_client/models/_box_descriptor.py +24 -0
- algokit_algod_client/models/_boxes_response.py +21 -0
- algokit_algod_client/models/_build_version_contains_the_current_algod_build_version_information.py +34 -0
- algokit_algod_client/models/_compile_response.py +24 -0
- algokit_algod_client/models/_disassemble_response.py +14 -0
- algokit_algod_client/models/_error_response.py +22 -0
- algokit_algod_client/models/_eval_delta.py +32 -0
- algokit_algod_client/models/_eval_delta_key_value.py +28 -0
- algokit_algod_client/models/_genesis_file_in_json.py +53 -0
- algokit_algod_client/models/_get_block_time_stamp_offset_response.py +14 -0
- algokit_algod_client/models/_get_sync_round_response.py +14 -0
- algokit_algod_client/models/_ledger_state_delta.py +389 -0
- algokit_algod_client/models/_light_block_header_proof.py +32 -0
- algokit_algod_client/models/_node_status_response.py +118 -0
- algokit_algod_client/models/_pending_transaction_response.py +91 -0
- algokit_algod_client/models/_pending_transactions_response.py +29 -0
- algokit_algod_client/models/_post_transactions_response.py +14 -0
- algokit_algod_client/models/_scratch_change.py +23 -0
- algokit_algod_client/models/_serde_helpers.py +241 -0
- algokit_algod_client/models/_simulate_initial_states.py +25 -0
- algokit_algod_client/models/_simulate_request.py +54 -0
- algokit_algod_client/models/_simulate_request_transaction_group.py +25 -0
- algokit_algod_client/models/_simulate_response.py +44 -0
- algokit_algod_client/models/_simulate_trace_config.py +30 -0
- algokit_algod_client/models/_simulate_transaction_group_result.py +46 -0
- algokit_algod_client/models/_simulate_transaction_result.py +41 -0
- algokit_algod_client/models/_simulate_unnamed_resources_accessed.py +64 -0
- algokit_algod_client/models/_simulation_eval_overrides.py +40 -0
- algokit_algod_client/models/_simulation_opcode_trace_unit.py +55 -0
- algokit_algod_client/models/_simulation_transaction_exec_trace.py +82 -0
- algokit_algod_client/models/_source_map.py +30 -0
- algokit_algod_client/models/_state_delta.py +6 -0
- algokit_algod_client/models/_state_proof.py +28 -0
- algokit_algod_client/models/_state_proof_message.py +44 -0
- algokit_algod_client/models/_supply_response.py +26 -0
- algokit_algod_client/models/_teal_key_value.py +28 -0
- algokit_algod_client/models/_teal_key_value_store.py +6 -0
- algokit_algod_client/models/_teal_value.py +32 -0
- algokit_algod_client/models/_transaction_group_ledger_state_deltas_for_round_response.py +21 -0
- algokit_algod_client/models/_transaction_parameters_response.py +45 -0
- algokit_algod_client/models/_transaction_proof.py +44 -0
- algokit_algod_client/models/_version_contains_the_current_algod_version.py +38 -0
- algokit_algod_client/models/suggested_params.py +42 -0
- algokit_algod_client/py.typed +1 -0
- algokit_algod_client/types.py +7 -0
- algokit_algosdk/__init__.py +38 -0
- algokit_algosdk/account.py +32 -0
- algokit_algosdk/app_access.py +228 -0
- algokit_algosdk/box_reference.py +100 -0
- algokit_algosdk/constants.py +147 -0
- algokit_algosdk/encoding.py +89 -0
- algokit_algosdk/error.py +180 -0
- algokit_algosdk/logic.py +61 -0
- algokit_algosdk/logicsig.py +218 -0
- algokit_algosdk/mnemonic.py +216 -0
- algokit_algosdk/multisig.py +161 -0
- algokit_algosdk/py.typed +0 -0
- algokit_algosdk/transaction.py +596 -0
- algokit_algosdk/wordlist.py +2054 -0
- algokit_common/__init__.py +50 -0
- algokit_common/address.py +34 -0
- algokit_common/constants.py +47 -0
- algokit_common/hashing.py +25 -0
- algokit_common/py.typed +0 -0
- algokit_common/serde/__init__.py +40 -0
- algokit_common/serde/_core.py +610 -0
- algokit_common/serde/_primitives.py +135 -0
- algokit_common/source_map.py +158 -0
- algokit_indexer_client/__init__.py +10 -0
- algokit_indexer_client/client.py +1456 -0
- algokit_indexer_client/config.py +36 -0
- algokit_indexer_client/exceptions.py +59 -0
- algokit_indexer_client/models/__init__.py +148 -0
- algokit_indexer_client/models/_account.py +161 -0
- algokit_indexer_client/models/_account_participation.py +53 -0
- algokit_indexer_client/models/_account_response.py +19 -0
- algokit_indexer_client/models/_account_state_delta.py +29 -0
- algokit_indexer_client/models/_accounts_response.py +29 -0
- algokit_indexer_client/models/_application.py +35 -0
- algokit_indexer_client/models/_application_local_state.py +45 -0
- algokit_indexer_client/models/_application_local_states_response.py +29 -0
- algokit_indexer_client/models/_application_log_data.py +28 -0
- algokit_indexer_client/models/_application_logs_response.py +33 -0
- algokit_indexer_client/models/_application_params.py +62 -0
- algokit_indexer_client/models/_application_response.py +20 -0
- algokit_indexer_client/models/_application_state_schema.py +22 -0
- algokit_indexer_client/models/_applications_response.py +29 -0
- algokit_indexer_client/models/_asset.py +35 -0
- algokit_indexer_client/models/_asset_balances_response.py +29 -0
- algokit_indexer_client/models/_asset_holding.py +41 -0
- algokit_indexer_client/models/_asset_holdings_response.py +29 -0
- algokit_indexer_client/models/_asset_params.py +102 -0
- algokit_indexer_client/models/_asset_response.py +19 -0
- algokit_indexer_client/models/_assets_response.py +29 -0
- algokit_indexer_client/models/_block.py +150 -0
- algokit_indexer_client/models/_block_headers_response.py +29 -0
- algokit_indexer_client/models/_block_rewards.py +38 -0
- algokit_indexer_client/models/_block_upgrade_state.py +34 -0
- algokit_indexer_client/models/_block_upgrade_vote.py +26 -0
- algokit_indexer_client/models/_box.py +36 -0
- algokit_indexer_client/models/_box_descriptor.py +24 -0
- algokit_indexer_client/models/_box_reference.py +28 -0
- algokit_indexer_client/models/_boxes_response.py +29 -0
- algokit_indexer_client/models/_error_response.py +18 -0
- algokit_indexer_client/models/_eval_delta.py +32 -0
- algokit_indexer_client/models/_eval_delta_key_value.py +28 -0
- algokit_indexer_client/models/_hash_factory.py +14 -0
- algokit_indexer_client/models/_hb_proof_fields.py +57 -0
- algokit_indexer_client/models/_health_check.py +42 -0
- algokit_indexer_client/models/_holding_ref.py +23 -0
- algokit_indexer_client/models/_indexer_state_proof_message.py +40 -0
- algokit_indexer_client/models/_locals_ref.py +23 -0
- algokit_indexer_client/models/_merkle_array_proof.py +29 -0
- algokit_indexer_client/models/_mini_asset_holding.py +38 -0
- algokit_indexer_client/models/_on_completion.py +25 -0
- algokit_indexer_client/models/_participation_updates.py +22 -0
- algokit_indexer_client/models/_resource_ref.py +42 -0
- algokit_indexer_client/models/_serde_helpers.py +241 -0
- algokit_indexer_client/models/_state_delta.py +6 -0
- algokit_indexer_client/models/_state_proof_fields.py +57 -0
- algokit_indexer_client/models/_state_proof_participant.py +20 -0
- algokit_indexer_client/models/_state_proof_reveal.py +25 -0
- algokit_indexer_client/models/_state_proof_sig_slot.py +20 -0
- algokit_indexer_client/models/_state_proof_signature.py +37 -0
- algokit_indexer_client/models/_state_proof_tracking.py +32 -0
- algokit_indexer_client/models/_state_proof_verifier.py +24 -0
- algokit_indexer_client/models/_state_schema.py +25 -0
- algokit_indexer_client/models/_teal_key_value.py +28 -0
- algokit_indexer_client/models/_teal_key_value_store.py +6 -0
- algokit_indexer_client/models/_teal_value.py +32 -0
- algokit_indexer_client/models/_transaction.py +213 -0
- algokit_indexer_client/models/_transaction_application.py +105 -0
- algokit_indexer_client/models/_transaction_asset_config.py +31 -0
- algokit_indexer_client/models/_transaction_asset_freeze.py +29 -0
- algokit_indexer_client/models/_transaction_asset_transfer.py +41 -0
- algokit_indexer_client/models/_transaction_heartbeat.py +52 -0
- algokit_indexer_client/models/_transaction_keyreg.py +59 -0
- algokit_indexer_client/models/_transaction_payment.py +33 -0
- algokit_indexer_client/models/_transaction_response.py +19 -0
- algokit_indexer_client/models/_transaction_signature.py +35 -0
- algokit_indexer_client/models/_transaction_signature_logicsig.py +59 -0
- algokit_indexer_client/models/_transaction_signature_multisig.py +36 -0
- algokit_indexer_client/models/_transaction_signature_multisig_subsignature.py +28 -0
- algokit_indexer_client/models/_transaction_state_proof.py +32 -0
- algokit_indexer_client/models/_transactions_response.py +29 -0
- algokit_indexer_client/py.typed +1 -0
- algokit_indexer_client/types.py +7 -0
- algokit_kmd_client/__init__.py +10 -0
- algokit_kmd_client/client.py +1240 -0
- algokit_kmd_client/config.py +36 -0
- algokit_kmd_client/exceptions.py +59 -0
- algokit_kmd_client/models/__init__.py +112 -0
- algokit_kmd_client/models/_classical_signatures.py +4 -0
- algokit_kmd_client/models/_create_wallet_request.py +30 -0
- algokit_kmd_client/models/_create_wallet_response.py +19 -0
- algokit_kmd_client/models/_delete_key_request.py +27 -0
- algokit_kmd_client/models/_delete_multisig_request.py +27 -0
- algokit_kmd_client/models/_digest_represents_a32_byte_value_holding_the256_bit_hash_digest.py +4 -0
- algokit_kmd_client/models/_ed25519_public_key.py +4 -0
- algokit_kmd_client/models/_export_key_request.py +27 -0
- algokit_kmd_client/models/_export_key_response.py +24 -0
- algokit_kmd_client/models/_export_master_key_request.py +22 -0
- algokit_kmd_client/models/_export_master_key_response.py +18 -0
- algokit_kmd_client/models/_export_multisig_request.py +23 -0
- algokit_kmd_client/models/_export_multisig_response.py +26 -0
- algokit_kmd_client/models/_generate_key_request.py +18 -0
- algokit_kmd_client/models/_generate_key_response.py +19 -0
- algokit_kmd_client/models/_import_key_request.py +28 -0
- algokit_kmd_client/models/_import_key_response.py +19 -0
- algokit_kmd_client/models/_import_multisig_request.py +30 -0
- algokit_kmd_client/models/_import_multisig_response.py +19 -0
- algokit_kmd_client/models/_init_wallet_handle_token_request.py +22 -0
- algokit_kmd_client/models/_init_wallet_handle_token_response.py +18 -0
- algokit_kmd_client/models/_list_keys_request.py +18 -0
- algokit_kmd_client/models/_list_keys_response.py +18 -0
- algokit_kmd_client/models/_list_multisig_request.py +18 -0
- algokit_kmd_client/models/_list_multisig_response.py +18 -0
- algokit_kmd_client/models/_list_wallets_request.py +11 -0
- algokit_kmd_client/models/_list_wallets_response.py +25 -0
- algokit_kmd_client/models/_master_derivation_key.py +4 -0
- algokit_kmd_client/models/_multisig_sig.py +33 -0
- algokit_kmd_client/models/_multisig_subsig.py +23 -0
- algokit_kmd_client/models/_public_key.py +4 -0
- algokit_kmd_client/models/_release_wallet_handle_token_request.py +18 -0
- algokit_kmd_client/models/_rename_wallet_request.py +26 -0
- algokit_kmd_client/models/_rename_wallet_response.py +19 -0
- algokit_kmd_client/models/_renew_wallet_handle_token_request.py +18 -0
- algokit_kmd_client/models/_renew_wallet_handle_token_response.py +19 -0
- algokit_kmd_client/models/_serde_helpers.py +241 -0
- algokit_kmd_client/models/_sign_multisig_response.py +24 -0
- algokit_kmd_client/models/_sign_multisig_txn_request.py +45 -0
- algokit_kmd_client/models/_sign_program_multisig_request.py +50 -0
- algokit_kmd_client/models/_sign_program_multisig_response.py +24 -0
- algokit_kmd_client/models/_sign_program_request.py +37 -0
- algokit_kmd_client/models/_sign_program_response.py +24 -0
- algokit_kmd_client/models/_sign_transaction_response.py +24 -0
- algokit_kmd_client/models/_sign_txn_request.py +36 -0
- algokit_kmd_client/models/_signature.py +4 -0
- algokit_kmd_client/models/_tx_type.py +4 -0
- algokit_kmd_client/models/_versions_request.py +11 -0
- algokit_kmd_client/models/_versions_response.py +19 -0
- algokit_kmd_client/models/_wallet.py +38 -0
- algokit_kmd_client/models/_wallet_handle.py +24 -0
- algokit_kmd_client/models/_wallet_info_request.py +18 -0
- algokit_kmd_client/models/_wallet_info_response.py +19 -0
- algokit_kmd_client/py.typed +1 -0
- algokit_kmd_client/types.py +7 -0
- algokit_transact/__init__.py +190 -0
- algokit_transact/codec/__init__.py +0 -0
- algokit_transact/codec/msgpack.py +11 -0
- algokit_transact/codec/serde.py +7 -0
- algokit_transact/codec/signed.py +57 -0
- algokit_transact/codec/transaction.py +65 -0
- algokit_transact/exceptions.py +17 -0
- algokit_transact/logicsig.py +220 -0
- algokit_transact/models/__init__.py +0 -0
- algokit_transact/models/app_call.py +447 -0
- algokit_transact/models/asset_config.py +19 -0
- algokit_transact/models/asset_freeze.py +11 -0
- algokit_transact/models/asset_transfer.py +13 -0
- algokit_transact/models/common.py +17 -0
- algokit_transact/models/heartbeat.py +21 -0
- algokit_transact/models/key_registration.py +14 -0
- algokit_transact/models/payment.py +14 -0
- algokit_transact/models/signed_transaction.py +21 -0
- algokit_transact/models/state_proof.py +150 -0
- algokit_transact/models/transaction.py +88 -0
- algokit_transact/multisig.py +93 -0
- algokit_transact/ops/__init__.py +0 -0
- algokit_transact/ops/fees.py +47 -0
- algokit_transact/ops/group.py +28 -0
- algokit_transact/ops/ids.py +14 -0
- algokit_transact/ops/validate.py +503 -0
- algokit_transact/py.typed +0 -0
- algokit_transact/signer.py +195 -0
- algokit_transact/signing/__init__.py +0 -0
- algokit_transact/signing/logic_signature.py +19 -0
- algokit_transact/signing/multisig.py +84 -0
- algokit_transact/signing/types.py +39 -0
- algokit_transact/signing/validation.py +63 -0
- algokit_utils/__init__.py +23 -0
- algokit_utils/_debugging.py +304 -0
- algokit_utils/accounts/__init__.py +2 -0
- algokit_utils/accounts/account_manager.py +1051 -0
- algokit_utils/accounts/kmd_account_manager.py +206 -0
- algokit_utils/algo25.py +46 -0
- algokit_utils/algorand.py +383 -0
- algokit_utils/applications/__init__.py +7 -0
- algokit_utils/applications/abi.py +280 -0
- algokit_utils/applications/app_client.py +2193 -0
- algokit_utils/applications/app_deployer.py +788 -0
- algokit_utils/applications/app_factory.py +1140 -0
- algokit_utils/applications/app_manager.py +575 -0
- algokit_utils/applications/app_spec/__init__.py +6 -0
- algokit_utils/applications/enums.py +40 -0
- algokit_utils/assets/__init__.py +1 -0
- algokit_utils/assets/asset_manager.py +344 -0
- algokit_utils/clients/__init__.py +41 -0
- algokit_utils/clients/client_manager.py +756 -0
- algokit_utils/clients/dispenser_api_client.py +212 -0
- algokit_utils/common.py +40 -0
- algokit_utils/config.py +159 -0
- algokit_utils/errors/__init__.py +1 -0
- algokit_utils/errors/logic_error.py +160 -0
- algokit_utils/models/__init__.py +7 -0
- algokit_utils/models/account.py +12 -0
- algokit_utils/models/amount.py +198 -0
- algokit_utils/models/application.py +90 -0
- algokit_utils/models/network.py +29 -0
- algokit_utils/models/simulate.py +7 -0
- algokit_utils/models/state.py +53 -0
- algokit_utils/models/transaction.py +49 -0
- algokit_utils/protocols/__init__.py +3 -0
- algokit_utils/protocols/account.py +11 -0
- algokit_utils/protocols/signer.py +17 -0
- algokit_utils/protocols/typed_clients.py +110 -0
- algokit_utils/py.typed +0 -0
- algokit_utils/transact.py +195 -0
- algokit_utils/transactions/__init__.py +3 -0
- algokit_utils/transactions/builders/__init__.py +67 -0
- algokit_utils/transactions/builders/app.py +248 -0
- algokit_utils/transactions/builders/asset.py +256 -0
- algokit_utils/transactions/builders/common.py +263 -0
- algokit_utils/transactions/builders/keyreg.py +103 -0
- algokit_utils/transactions/builders/method_call.py +380 -0
- algokit_utils/transactions/builders/payment.py +43 -0
- algokit_utils/transactions/composer_resources.py +409 -0
- algokit_utils/transactions/fee_coverage.py +79 -0
- algokit_utils/transactions/helpers.py +9 -0
- algokit_utils/transactions/transaction_composer.py +1574 -0
- algokit_utils/transactions/transaction_creator.py +699 -0
- algokit_utils/transactions/transaction_sender.py +1240 -0
- algokit_utils/transactions/types.py +262 -0
- algokit_utils-5.0.0a3.dist-info/METADATA +105 -0
- algokit_utils-5.0.0a3.dist-info/RECORD +337 -0
- 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
|