From 1b345cfacd6fabec1bbcc02b7e49d6a852df5f5e Mon Sep 17 00:00:00 2001 From: John Jannotti Date: Wed, 5 Jun 2024 11:28:54 -0400 Subject: [PATCH 1/5] Even in the face of urllib.error.HTTPError, return the json (#529) Previously, if we caught a urllib.error.HTTPError, we would try to parse the json response. If we succeeded we would return _only_ the `message` field of that response. If we failed, we're return the text of the response. By "return" here, I actually mean "pack up into an Exception object and throw it". Now, if succeed in parsing the response as json, we extend that exception object so that it also contains the JSON that we got. That way, client code can find extra goodies in the json["data"] field. Also added a utility to the examples so they can be easily run against a locally running `algod`. --- algosdk/error.py | 3 ++- algosdk/v2client/algod.py | 7 +++++-- examples/inspect-error.py | 20 ++++++++++++++++++++ examples/utils.py | 17 +++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100755 examples/inspect-error.py diff --git a/algosdk/error.py b/algosdk/error.py index 6de6afee..78ae7562 100644 --- a/algosdk/error.py +++ b/algosdk/error.py @@ -178,9 +178,10 @@ def __init__(self, msg): class AlgodHTTPError(Exception): - def __init__(self, msg, code=None): + def __init__(self, msg, code=None, data=None): super().__init__(msg) self.code = code + self.data = data class AlgodResponseError(Exception): diff --git a/algosdk/v2client/algod.py b/algosdk/v2client/algod.py index 431f7715..f39aa77e 100644 --- a/algosdk/v2client/algod.py +++ b/algosdk/v2client/algod.py @@ -107,10 +107,13 @@ def algod_request( except urllib.error.HTTPError as e: code = e.code es = e.read().decode("utf-8") + m = e # If json.loads() fails, we'll return e itself + j = {} try: - e = json.loads(es)["message"] + j = json.loads(es) + m = j["message"] finally: - raise error.AlgodHTTPError(e, code) + raise error.AlgodHTTPError(m, code, j.get("data")) if response_format == "json": try: return json.load(resp) diff --git a/examples/inspect-error.py b/examples/inspect-error.py new file mode 100755 index 00000000..427db724 --- /dev/null +++ b/examples/inspect-error.py @@ -0,0 +1,20 @@ +from algosdk import error, transaction + +from utils import get_algod_client, algod_env + +algod = get_algod_client(*algod_env()) + +# This program is "#pragma version 5, +". It will fail because no arguments are on the stack. +lsig = transaction.LogicSigAccount(b"\x05\x08") +sender = lsig.address() +# Get suggested parameters +params = algod.suggested_params() + +amount = 10000 +txn = transaction.PaymentTxn(sender, params, sender, amount) +lstx = transaction.LogicSigTransaction(txn, lsig) +try: + txid = algod.send_transaction(lstx) + print("Impossible! Exception will have been thrown") +except error.AlgodHTTPError as e: + print(e.data) diff --git a/examples/utils.py b/examples/utils.py index d5c3a8bc..d760ec21 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -93,6 +93,23 @@ class SandboxAccount: signer: AccountTransactionSigner +def algod_env(): + algodata = os.environ.get("ALGORAND_DATA") + if not algodata: + return () + try: + token = ( + open(os.path.join(algodata, "algod.token"), "rt").read().strip() + ) + net = ( + "http://" + + open(os.path.join(algodata, "algod.net"), "rt").read().strip() + ) + return (net, token) + except FileNotFoundError: + return () + + def get_accounts( kmd_address: str = KMD_URL, kmd_token: str = KMD_TOKEN, From 18adf5efe21fbbcf0494d1d275bc14fe88ec9208 Mon Sep 17 00:00:00 2001 From: gmalouf Date: Wed, 5 Jun 2024 15:29:28 +0000 Subject: [PATCH 2/5] bump up version to v2.6.0 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14ec9cfa..26e29c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +# v2.6.0 + + + +## What's Changed +### Bugfixes +* fix: no timeout for urlopen issue #526 by @grzracz in https://github.com/algorand/py-algorand-sdk/pull/527 +* txns: Uses sp.min_fee if available by @jannotti in https://github.com/algorand/py-algorand-sdk/pull/530 +* fix: Fix initialization for `WrongAmountType` error by @algolog in https://github.com/algorand/py-algorand-sdk/pull/532 +* Fix: Fix indexer sync issue in cucumber tests by @jasonpaulos in https://github.com/algorand/py-algorand-sdk/pull/533 +### Enhancements +* Docs: Add missing pages for source map and dryrun results by @jasonpaulos in https://github.com/algorand/py-algorand-sdk/pull/520 +* DX: Keyreg bytes by @jannotti in https://github.com/algorand/py-algorand-sdk/pull/522 +* Testing: Add Python 3.12 to test matrix by @jasonpaulos in https://github.com/algorand/py-algorand-sdk/pull/534 +* Simulate: Support newer simulate options by @jasonpaulos in https://github.com/algorand/py-algorand-sdk/pull/537 +* Tests: Enable min-balance Cucumber tests. by @gmalouf in https://github.com/algorand/py-algorand-sdk/pull/539 +### Other +* Fix typographic error when printing offline participation transaction by @hsoerensen in https://github.com/algorand/py-algorand-sdk/pull/524 + +## New Contributors +* @hsoerensen made their first contribution in https://github.com/algorand/py-algorand-sdk/pull/524 +* @grzracz made their first contribution in https://github.com/algorand/py-algorand-sdk/pull/527 +* @algolog made their first contribution in https://github.com/algorand/py-algorand-sdk/pull/532 +* @gmalouf made their first contribution in https://github.com/algorand/py-algorand-sdk/pull/539 + +**Full Changelog**: https://github.com/algorand/py-algorand-sdk/compare/v2.5.0...v2.6.0 + # v2.5.0 diff --git a/setup.py b/setup.py index 70d6c854..f7fb11b3 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ description="Algorand SDK in Python", author="Algorand", author_email="pypiservice@algorand.com", - version="2.5.0", + version="2.6.0", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/algorand/py-algorand-sdk", From a7832dfa43d3f7bf46272b1d864397d07dbe73df Mon Sep 17 00:00:00 2001 From: Gary Malouf <982483+gmalouf@users.noreply.github.com> Date: Wed, 5 Jun 2024 14:22:26 -0400 Subject: [PATCH 3/5] Update .test-env to use Cucumber specs from master (#544) Update .test-env to use Cucumber specs from master rather than the now deprecated V2. --- .test-env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.test-env b/.test-env index a321a7f1..049289d7 100644 --- a/.test-env +++ b/.test-env @@ -1,6 +1,6 @@ # Configs for testing repo download: SDK_TESTING_URL="https://github.com/algorand/algorand-sdk-testing" -SDK_TESTING_BRANCH="V2" +SDK_TESTING_BRANCH="master" SDK_TESTING_HARNESS="test-harness" INSTALL_ONLY=0 From 45ea0b187232fe7a971f4deca5dadca94a17b72c Mon Sep 17 00:00:00 2001 From: Jason Paulos Date: Thu, 6 Jun 2024 14:36:11 -0400 Subject: [PATCH 4/5] Fix: Pass args to underlying `kmd_request` function, including timeout (#545) --- algosdk/kmd.py | 100 +++++++++++++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 40 deletions(-) diff --git a/algosdk/kmd.py b/algosdk/kmd.py index 70dc0edd..1f6848c3 100644 --- a/algosdk/kmd.py +++ b/algosdk/kmd.py @@ -1,5 +1,6 @@ import base64 import json +from typing import Any import urllib.error from urllib import parse from urllib.request import Request, urlopen @@ -66,7 +67,7 @@ def kmd_request(self, method, requrl, params=None, data=None, timeout=30): raise error.KMDHTTPError(e) return json.loads(resp.read().decode("utf-8")) - def versions(self): + def versions(self, **kwargs: Any): """ Get kmd versions. @@ -74,9 +75,9 @@ def versions(self): str[]: list of versions """ req = "/versions" - return self.kmd_request("GET", req)["versions"] + return self.kmd_request("GET", req, **kwargs)["versions"] - def list_wallets(self): + def list_wallets(self, **kwargs: Any): """ List all wallets hosted on node. @@ -84,14 +85,19 @@ def list_wallets(self): dict[]: list of dictionaries containing wallet information """ req = "/wallets" - res = self.kmd_request("GET", req) + res = self.kmd_request("GET", req, **kwargs) if "wallets" in res: return res["wallets"] else: return [] def create_wallet( - self, name, pswd, driver_name="sqlite", master_deriv_key=None + self, + name, + pswd, + driver_name="sqlite", + master_deriv_key=None, + **kwargs: Any ): """ Create a new wallet. @@ -113,9 +119,9 @@ def create_wallet( } if master_deriv_key: query["master_derivation_key"] = master_deriv_key - return self.kmd_request("POST", req, data=query)["wallet"] + return self.kmd_request("POST", req, data=query, **kwargs)["wallet"] - def get_wallet(self, handle): + def get_wallet(self, handle, **kwargs: Any): """ Get wallet information. @@ -127,9 +133,11 @@ def get_wallet(self, handle): """ req = "/wallet/info" query = {"wallet_handle_token": handle} - return self.kmd_request("POST", req, data=query)["wallet_handle"] + return self.kmd_request("POST", req, data=query, **kwargs)[ + "wallet_handle" + ] - def init_wallet_handle(self, id, password): + def init_wallet_handle(self, id, password, **kwargs: Any): """ Initialize a handle for the wallet. @@ -142,9 +150,11 @@ def init_wallet_handle(self, id, password): """ req = "/wallet/init" query = {"wallet_id": id, "wallet_password": password} - return self.kmd_request("POST", req, data=query)["wallet_handle_token"] + return self.kmd_request("POST", req, data=query, **kwargs)[ + "wallet_handle_token" + ] - def release_wallet_handle(self, handle): + def release_wallet_handle(self, handle, **kwargs: Any): """ Deactivate the handle for the wallet. @@ -156,10 +166,10 @@ def release_wallet_handle(self, handle): """ req = "/wallet/release" query = {"wallet_handle_token": handle} - result = self.kmd_request("POST", req, data=query) + result = self.kmd_request("POST", req, data=query, **kwargs) return result == {} - def renew_wallet_handle(self, handle): + def renew_wallet_handle(self, handle, **kwargs: Any): """ Renew the wallet handle. @@ -171,9 +181,11 @@ def renew_wallet_handle(self, handle): """ req = "/wallet/renew" query = {"wallet_handle_token": handle} - return self.kmd_request("POST", req, data=query)["wallet_handle"] + return self.kmd_request("POST", req, data=query, **kwargs)[ + "wallet_handle" + ] - def rename_wallet(self, id, password, new_name): + def rename_wallet(self, id, password, new_name, **kwargs: Any): """ Rename the wallet. @@ -191,9 +203,9 @@ def rename_wallet(self, id, password, new_name): "wallet_password": password, "wallet_name": new_name, } - return self.kmd_request("POST", req, data=query)["wallet"] + return self.kmd_request("POST", req, data=query, **kwargs)["wallet"] - def export_master_derivation_key(self, handle, password): + def export_master_derivation_key(self, handle, password, **kwargs: Any): """ Get the wallet's master derivation key. @@ -206,10 +218,10 @@ def export_master_derivation_key(self, handle, password): """ req = "/master-key/export" query = {"wallet_handle_token": handle, "wallet_password": password} - result = self.kmd_request("POST", req, data=query) + result = self.kmd_request("POST", req, data=query, **kwargs) return result["master_derivation_key"] - def import_key(self, handle, private_key): + def import_key(self, handle, private_key, **kwargs: Any): """ Import an account into the wallet. @@ -222,9 +234,9 @@ def import_key(self, handle, private_key): """ req = "/key/import" query = {"wallet_handle_token": handle, "private_key": private_key} - return self.kmd_request("POST", req, data=query)["address"] + return self.kmd_request("POST", req, data=query, **kwargs)["address"] - def export_key(self, handle, password, address): + def export_key(self, handle, password, address, **kwargs: Any): """ Return an account private key. @@ -242,9 +254,11 @@ def export_key(self, handle, password, address): "wallet_password": password, "address": address, } - return self.kmd_request("POST", req, data=query)["private_key"] + return self.kmd_request("POST", req, data=query, **kwargs)[ + "private_key" + ] - def generate_key(self, handle, display_mnemonic=True): + def generate_key(self, handle, display_mnemonic=True, **kwargs: Any): """ Generate a key in the wallet. @@ -258,9 +272,9 @@ def generate_key(self, handle, display_mnemonic=True): """ req = "/key" query = {"wallet_handle_token": handle} - return self.kmd_request("POST", req, data=query)["address"] + return self.kmd_request("POST", req, data=query, **kwargs)["address"] - def delete_key(self, handle, password, address): + def delete_key(self, handle, password, address, **kwargs: Any): """ Delete a key in the wallet. @@ -278,10 +292,10 @@ def delete_key(self, handle, password, address): "wallet_password": password, "address": address, } - result = self.kmd_request("DELETE", req, data=query) + result = self.kmd_request("DELETE", req, data=query, **kwargs) return result == {} - def list_keys(self, handle): + def list_keys(self, handle, **kwargs: Any): """ List all keys in the wallet. @@ -294,12 +308,14 @@ def list_keys(self, handle): req = "/key/list" query = {"wallet_handle_token": handle} - result = self.kmd_request("POST", req, data=query) + result = self.kmd_request("POST", req, data=query, **kwargs) if result: return result["addresses"] return [] - def sign_transaction(self, handle, password, txn, signing_address=None): + def sign_transaction( + self, handle, password, txn, signing_address=None, **kwargs: Any + ): """ Sign a transaction. @@ -322,11 +338,11 @@ def sign_transaction(self, handle, password, txn, signing_address=None): } if signing_address: query["public_key"] = signing_address - result = self.kmd_request("POST", req, data=query) + result = self.kmd_request("POST", req, data=query, **kwargs) result = result["signed_transaction"] return encoding.msgpack_decode(result) - def list_multisig(self, handle): + def list_multisig(self, handle, **kwargs: Any): """ List all multisig accounts in the wallet. @@ -338,12 +354,12 @@ def list_multisig(self, handle): """ req = "/multisig/list" query = {"wallet_handle_token": handle} - result = self.kmd_request("POST", req, data=query) + result = self.kmd_request("POST", req, data=query, **kwargs) if result == {}: return [] return result["addresses"] - def import_multisig(self, handle, multisig): + def import_multisig(self, handle, multisig, **kwargs: Any): """ Import a multisig account into the wallet. @@ -364,9 +380,9 @@ def import_multisig(self, handle, multisig): for s in multisig.subsigs ], } - return self.kmd_request("POST", req, data=query)["address"] + return self.kmd_request("POST", req, data=query, **kwargs)["address"] - def export_multisig(self, handle, address): + def export_multisig(self, handle, address, **kwargs: Any): """ Export a multisig account. @@ -379,7 +395,7 @@ def export_multisig(self, handle, address): """ req = "/multisig/export" query = {"wallet_handle_token": handle, "address": address} - result = self.kmd_request("POST", req, data=query) + result = self.kmd_request("POST", req, data=query, **kwargs) pks = result["pks"] pks = [encoding.encode_address(base64.b64decode(p)) for p in pks] msig = transaction.Multisig( @@ -387,7 +403,7 @@ def export_multisig(self, handle, address): ) return msig - def delete_multisig(self, handle, password, address): + def delete_multisig(self, handle, password, address, **kwargs: Any): """ Delete a multisig account. @@ -405,10 +421,12 @@ def delete_multisig(self, handle, password, address): "wallet_password": password, "address": address, } - result = self.kmd_request("DELETE", req, data=query) + result = self.kmd_request("DELETE", req, data=query, **kwargs) return result == {} - def sign_multisig_transaction(self, handle, password, public_key, mtx): + def sign_multisig_transaction( + self, handle, password, public_key, mtx, **kwargs: Any + ): """ Sign a multisig transaction for the given public key. @@ -439,7 +457,9 @@ def sign_multisig_transaction(self, handle, password, public_key, mtx): signer = base64.b64encode(encoding.decode_address(mtx.auth_addr)) query["signer"] = signer.decode() - result = self.kmd_request("POST", req, data=query)["multisig"] + result = self.kmd_request("POST", req, data=query, **kwargs)[ + "multisig" + ] msig = encoding.msgpack_decode(result) mtx.multisig = msig return mtx From 47d69fd73a953e6bfef541a6aab49fa830ef872f Mon Sep 17 00:00:00 2001 From: jasonpaulos Date: Thu, 6 Jun 2024 18:36:53 +0000 Subject: [PATCH 5/5] bump up version to v2.6.1 --- CHANGELOG.md | 12 ++++++++++++ setup.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e29c14..412efb7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +# v2.6.1 + + + +## What's Changed +### Bugfixes +* algod: Even in the face of urllib.error.HTTPError, return the json by @jannotti in https://github.com/algorand/py-algorand-sdk/pull/529 +* Fix: Pass args to underlying `kmd_request` function, including timeout by @jasonpaulos in https://github.com/algorand/py-algorand-sdk/pull/545 + + +**Full Changelog**: https://github.com/algorand/py-algorand-sdk/compare/v2.6.0...v2.6.1 + # v2.6.0 diff --git a/setup.py b/setup.py index f7fb11b3..a30301cf 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ description="Algorand SDK in Python", author="Algorand", author_email="pypiservice@algorand.com", - version="2.6.0", + version="2.6.1", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/algorand/py-algorand-sdk",