diff --git a/bitsharesbase/objects.py b/bitsharesbase/objects.py index 9d1c1613..e8cd8b97 100644 --- a/bitsharesbase/objects.py +++ b/bitsharesbase/objects.py @@ -456,3 +456,320 @@ def __init__(self, *args, **kwargs): else: raise ValueError("Unknown {}".format(self.__class__.name)) super().__init__(data, id) + + +class ChainParameterExtension(Extension): + class Htlc_options(GrapheneObject): + def __init__(self, *args, **kwargs): + super().__init__( + OrderedDict( + [ + ("max_timeout_secs", Uint32(kwargs["max_timeout_secs"])), + ("max_preimage_size", Uint32(kwargs["max_preimage_size"])), + ] + ) + ) + + class CustomAuthorityOptions(GrapheneObject): + def __init__(self, *args, **kwargs): + kwargs.update(args[0]) + super().__init__( + OrderedDict( + [ + ( + "max_custom_authority_lifetime_seconds", + Uint32(kwargs["max_custom_authority_lifetime_seconds"]), + ), + ( + "max_custom_authorities_per_account", + Uint32(kwargs["max_custom_authorities_per_account"]), + ), + ( + "max_custom_authorities_per_account_op", + Uint32(kwargs["max_custom_authorities_per_account_op"]), + ), + ( + "max_custom_authority_restrictions", + Uint32(kwargs["max_custom_authority_restrictions"]), + ), + ] + ) + ) + + def optional_uint16(x): + if x: + return Uint16(x) + + sorted_options = [ + ("updatable_htlc_options", Htlc_options), + ("custom_authority_options", CustomAuthorityOptions), + ("market_fee_network_percent", optional_uint16), + ("maker_fee_discount_percent", optional_uint16), + ] + + +class ChainParameters(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("block_interval", Uint8(kwargs["block_interval"])), + ( + "maintenance_interval", + Uint32(kwargs["maintenance_interval"]), + ), # uint32_t + ( + "maintenance_skip_slots", + Uint8(kwargs["maintenance_skip_slots"]), + ), # uint8_t + ( + "committee_proposal_review_period", + Uint32(kwargs["committee_proposal_review_period"]), + ), # uint32_t + ( + "maximum_transaction_size", + Uint32(kwargs["maximum_transaction_size"]), + ), # uint32_t + ( + "maximum_block_size", + Uint32(kwargs["maximum_block_size"]), + ), # uint32_t + ( + "maximum_time_until_expiration", + Uint32(kwargs["maximum_time_until_expiration"]), + ), # uint32_t + ( + "maximum_proposal_lifetime", + Uint32(kwargs["maximum_proposal_lifetime"]), + ), # uint32_t + ( + "maximum_asset_whitelist_authorities", + Uint8(kwargs["maximum_asset_whitelist_authorities"]), + ), # uint8_t + ( + "maximum_asset_feed_publishers", + Uint8(kwargs["maximum_asset_feed_publishers"]), + ), # uint8_t + ( + "maximum_witness_count", + Uint16(kwargs["maximum_witness_count"]), + ), # uint16_t + ( + "maximum_committee_count", + Uint16(kwargs["maximum_committee_count"]), + ), # uint16_t + ( + "maximum_authority_membership", + Uint16(kwargs["maximum_authority_membership"]), + ), # uint16_t + ( + "reserve_percent_of_fee", + Uint16(kwargs["reserve_percent_of_fee"]), + ), # uint16_t + ( + "network_percent_of_fee", + Uint16(kwargs["network_percent_of_fee"]), + ), # uint16_t + ( + "lifetime_referrer_percent_of_fee", + Uint16(kwargs["lifetime_referrer_percent_of_fee"]), + ), # uint16_t + ( + "cashback_vesting_period_seconds", + Uint32(kwargs["cashback_vesting_period_seconds"]), + ), # uint32_t + ( + "cashback_vesting_threshold", + Int64(kwargs["cashback_vesting_threshold"]), + ), # share_type + ( + "count_non_member_votes", + Bool(kwargs["count_non_member_votes"]), + ), # bool + ( + "allow_non_member_whitelists", + Bool(kwargs["allow_non_member_whitelists"]), + ), # bool + ( + "witness_pay_per_block", + Int64(kwargs["witness_pay_per_block"]), + ), # share_type + ( + "witness_pay_vesting_seconds", + Uint32(kwargs["witness_pay_vesting_seconds"]), + ), # uint32_t + ( + "worker_budget_per_day", + Int64(kwargs["worker_budget_per_day"]), + ), # share_type + ( + "max_predicate_opcode", + Uint16(kwargs["max_predicate_opcode"]), + ), # uint16_t + ( + "fee_liquidation_threshold", + Int64(kwargs["fee_liquidation_threshold"]), + ), # share_type + ( + "accounts_per_fee_scale", + Uint16(kwargs["accounts_per_fee_scale"]), + ), # uint16_t + ( + "account_fee_scale_bitshifts", + Uint8(kwargs["account_fee_scale_bitshifts"]), + ), # uint8_t + ( + "max_authority_depth", + Uint8(kwargs["max_authority_depth"]), + ), # uint8_t + ("extensions", ChainParameterExtension(kwargs["extensions"])), + ] + ) + ) + + +class VestingPolicy(Static_variant): + def __init__(self, o): + class Linear_vesting_policy_initializer(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ( + "begin_timestamp", + PointInTime(kwargs["begin_timestamp"]), + ), + ( + "vesting_cliff_seconds", + Uint32(kwargs["vesting_cliff_seconds"]), + ), + ( + "vesting_duration_seconds", + Uint32(kwargs["vesting_duration_seconds"]), + ), + ] + ) + ) + + class Cdd_vesting_policy_initializer(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("start_claim", PointInTime(kwargs["start_claim"])), + ("vesting_seconds", Uint32(kwargs["vesting_seconds"])), + ] + ) + ) + + class Instant_vesting_policy_initializer(GrapheneObject): + def __init__(self, *args, **kwargs): + super().__init__(OrderedDict([])) + + id = o[0] + if id == 0: + data = Linear_vesting_policy_initializer(o[1]) + elif id == 1: + data = Cdd_vesting_policy_initializer(o[1]) + elif id == 2: + data = Instant_vesting_policy_initializer(o[1]) + else: + raise ValueError("Unknown {}".format(self.__class__.name)) + super().__init__(data, id) + + +class RestrictionArgument(Static_variant): + def __init__(self, o): + raise NotImplementedError() + + # TODO: We need to implemented a class for each of these as the content + # of the static variant is the content of the restriction on this + # particular type - this will not produce nice code :-( + graphene_op_restriction_argument_variadic = { + "void_t", + "bool", + "int64_t", + "string", + "time_point_sec", + "public_key_type", + "fc::sha256", + "account_id_type", + "asset_id_type", + "force_settlement_id_type", + "committee_member_id_type", + "witness_id_type", + "limit_order_id_type", + "call_order_id_type", + "custom_id_type", + "proposal_id_type", + "withdraw_permission_id_type", + "vesting_balance_id_type", + "worker_id_type", + "balance_id_type", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "flat_set", + "vector", + "vector>", + "variant_assert_argument_type", + } + + class Argument(GrapheneObject): + def __init__(self, *args, **kwargs): + super().__init__(OrderedDict([])) + + id = o[0] + if len(graphene_op_restriction_argument_variadic) < id: + raise ValueError("Unknown {}".format(self.__class__.name)) + data = graphene_op_restriction_argument_variadic(id) + super().__init__(data, id) + + +class CustomRestriction(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("member_index", Uint32(kwargs["member_index"])), + ("restriction_type", Uint32(kwargs["restriction_type"])), + ("argument", RestrictionArgument(kwargs["argument"])), + ("extensions", Set([])), + ] + ) + ) diff --git a/bitsharesbase/operationids.py b/bitsharesbase/operationids.py index 208ad167..9a73b7f6 100644 --- a/bitsharesbase/operationids.py +++ b/bitsharesbase/operationids.py @@ -89,3 +89,12 @@ def getOperationName(id: str): return getOperationNameForId(id) else: raise ValueError + + +def getClassForOperation(operation_name: str): + classname = operation_name.lower().capitalize() + module = "bitsharesbase.operations" + fromlist = ["operations"] + module = __import__(module, fromlist=fromlist) + if hasattr(module, classname): + return getattr(module, classname) diff --git a/bitsharesbase/operations.py b/bitsharesbase/operations.py index 3957b5cf..35dfc153 100644 --- a/bitsharesbase/operations.py +++ b/bitsharesbase/operations.py @@ -45,6 +45,9 @@ Worker_initializer, isArgsThisClass, AssertPredicate, + ChainParameters, + VestingPolicy, + CustomRestriction, ) from .operationids import operations @@ -54,6 +57,10 @@ class_namemap = {} +class VirtualOperationException(Exception): + pass + + def fill_classmaps(): for name, ind in operations.items(): classname = name[0:1].upper() + name[1:] @@ -1043,7 +1050,15 @@ def __init__(self, *args, **kwargs): ) ) -ticket_type_strings = ['liquid', 'lock_180_days', 'lock_360_days', 'lock_720_days', 'lock_forever'] + +ticket_type_strings = [ + "liquid", + "lock_180_days", + "lock_360_days", + "lock_720_days", + "lock_forever", +] + class Ticket_create_operation(GrapheneObject): def __init__(self, *args, **kwargs): @@ -1070,6 +1085,7 @@ def __init__(self, *args, **kwargs): ) ) + class Ticket_update_operation(GrapheneObject): def __init__(self, *args, **kwargs): if isArgsThisClass(self, args): @@ -1119,7 +1135,10 @@ def __init__(self, *args, **kwargs): ("asset_b", ObjectId(kwargs["asset_b"], "asset")), ("share_asset", ObjectId(kwargs["share_asset"], "asset")), ("taker_fee_percent", Uint16(kwargs["taker_fee_percent"])), - ("withdrawal_fee_percent", Uint16(kwargs["withdrawal_fee_percent"])), + ( + "withdrawal_fee_percent", + Uint16(kwargs["withdrawal_fee_percent"]), + ), ("extensions", Set([])), ] ) @@ -1211,4 +1230,402 @@ def __init__(self, *args, **kwargs): ) +class Fill_order(GrapheneObject): + """Virtual operation.""" + + def __init__(self, *args, **kwargs): + raise VirtualOperationException() + + +class Account_transfer(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("fee", Asset(kwargs["fee"])), + ("account_id", ObjectId(kwargs["account_id"], "account")), + ("new_owner", ObjectId(kwargs["new_owner"], "account")), + ("extensions", Set([])), + ] + ) + ) + + +class Witness_create(GrapheneObject): + def __init__(self, *args, **kwargs): + prefix = kwargs.pop("prefix", default_prefix) + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("fee", Asset(kwargs["fee"])), + ( + "witness_account", + ObjectId(kwargs["witness_account"], "account"), + ), + ("url", String(kwargs["url"])), + ( + "block_signing_key", + PublicKey(kwargs["block_signing_key"], prefix=prefix), + ), + ("extensions", Set([])), + ] + ) + ) + + +class Proposal_delete(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ( + "fee_paying_account", + ObjectId(kwargs["fee_paying_account"], "account"), + ), + ( + "using_owner_authority", + Bool(kwargs["using_owner_authority"]), + ), + ("fee", Asset(kwargs["fee"])), + ("proposal", ObjectId(kwargs["proposal"], "proposal")), + ("extensions", Set([])), + ] + ) + ) + + +class Withdraw_permission_update(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("fee", Asset(kwargs["fee"])), + ( + "withdraw_from_account", + ObjectId(kwargs["withdraw_from_account"], "account"), + ), + ( + "authorized_account", + ObjectId(kwargs["authorized_account"], "account"), + ), + ( + "permission_to_update", + ObjectId( + kwargs["permission_to_update"], "withdraw_permission" + ), + ), + ("withdrawal_limit", Asset(kwargs["withdrawal_limit"])), + ( + "withdrawal_period_sec", + Uint32(kwargs["withdrawal_period_sec"]), + ), + ("period_start_time", PointInTime(kwargs["period_start_time"])), + ( + "periods_until_expiration", + Uint32(kwargs["periods_until_expiration"]), + ), + ] + ) + ) + + +class Withdraw_permission_claim(GrapheneObject): + def __init__(self, *args, **kwargs): + prefix = kwargs.pop("prefix", default_prefix) + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + if "memo" in kwargs and kwargs["memo"]: + if isinstance(kwargs["memo"], dict): + kwargs["memo"]["prefix"] = prefix + memo = Optional(Memo(**kwargs["memo"])) + else: + memo = Optional(Memo(kwargs["memo"])) + else: + memo = Optional(None) + super().__init__( + OrderedDict( + [ + ("fee", Asset(kwargs["fee"])), + ( + "permission_to_update", + ObjectId( + kwargs["permission_to_update"], "withdraw_permission" + ), + ), + ( + "withdraw_from_account", + ObjectId(kwargs["withdraw_from_account"], "account"), + ), + ( + "withdraw_to_account", + ObjectId(kwargs["withdraw_to_account"], "account"), + ), + ("amount_to_withdraw", Asset(kwargs["amount_to_withdraw"])), + ("memo", memo), + ] + ) + ) + + +class Withdraw_permission_delete(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("fee", Asset(kwargs["fee"])), + ( + "withdraw_from_account", + ObjectId(kwargs["withdraw_from_account"], "account"), + ), + ( + "authorized_account", + ObjectId(kwargs["authorized_account"], "account"), + ), + ( + "withdrawal_permission", + ObjectId( + kwargs["withdrawal_permission"], "withdraw_permission" + ), + ), + ] + ) + ) + + +class Committee_member_update(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("fee", Asset(kwargs["fee"])), + ( + "committee_member", + ObjectId(kwargs["committee_member"], "committee_member"), + ), + ( + "committee_member_account", + ObjectId(kwargs["committee_member_account"], "account"), + ), + ("new_url", String(kwargs["new_url"])), + ] + ) + ) + + +class Committee_member_update_global_parameters(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("fee", Asset(kwargs["fee"])), + ("new_parameters", ChainParameters(kwargs["new_parameters"])), + ] + ) + ) + + +class Vesting_balance_create(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + + super().__init__( + OrderedDict( + [ + ("fee", Asset(kwargs["fee"])), + ("creator", ObjectId(kwargs["creator"], "account")), + ("owner", ObjectId(kwargs["owner"], "account")), + ("amount", Asset(kwargs["amount"])), + ("policy", VestingPolicy(kwargs["policy"])), + ] + ) + ) + + +class Transfer_to_blind(GrapheneObject): + def __init__(self, *args, **kwargs): + raise NotImplementedError + + +class Transfer_from_blind(GrapheneObject): + def __init__(self, *args, **kwargs): + raise NotImplementedError + + +class Blind_transfer(GrapheneObject): + def __init__(self, *args, **kwargs): + raise NotImplementedError + + +class Asset_settle_cancel(GrapheneObject): + """Virtual Operation.""" + + def __init__(self, *args, **kwargs): + raise VirtualOperationException() + + +class Fba_distribute(GrapheneObject): + """Virtual Operation.""" + + def __init__(self, *args, **kwargs): + raise VirtualOperationException() + + +class Execute_bid(GrapheneObject): + """Virtual Operation.""" + + def __init__(self, *args, **kwargs): + raise VirtualOperationException() + + +class Htlc_redeemed(GrapheneObject): + """Virtual Operation.""" + + def __init__(self, *args, **kwargs): + raise VirtualOperationException() + + +class Htlc_refund(GrapheneObject): + """Virtual Operation.""" + + def __init__(self, *args, **kwargs): + raise VirtualOperationException() + + +class Custom_authority_create_operation(GrapheneObject): + def __init__(self, *args, **kwargs): + raise NotImplementedError() + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("fee", Asset(kwargs["fee"])), + ("account", ObjectId(kwargs["account"], "account")), + ("enabled", Bool(kwargs["enabled"])), + ("valid_from", PointInTime(kwargs["valid_from"])), + ("valid_to", PointInTime(kwargs["valid_to"])), + ("operation_type", Uint32(kwargs["operation_type"])), + ("auth", Permission(kwargs["auth"])), + ( + "restrictions", + Array( + [CustomRestriction(o) for o in kwargs["restrictions"]] + ), + ), + ("extensions", Set([])), + ] + ) + ) + + +class Custom_authority_update_operation(GrapheneObject): + def __init__(self, *args, **kwargs): + raise NotImplementedError() + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("fee", Asset(kwargs["fee"])), + ("account", ObjectId(kwargs["account"], "account")), + ( + "authority_to_update", + ObjectId(kwargs["authority_to_update"], "custom_authority"), + ), + ("new_enabled", Bool(kwargs["enabled"])), + ("new_valid_from", PointInTime(kwargs["valid_from"])), + ("new_valid_to", PointInTime(kwargs["valid_to"])), + ("new_auth", Permission(kwargs["auth"])), + ( + "restrictions_to_remove", + Array( + [Uint16(o) for o in kwargs["restrictions_to_remove"]] + ), + ), + ( + "restrictions_to_add", + Array( + [ + CustomRestriction(o) + for o in kwargs["restrictions_to_add"] + ] + ), + ), + ("extensions", Set([])), + ] + ) + ) + + +class Custom_authority_delete_operation(GrapheneObject): + def __init__(self, *args, **kwargs): + raise NotImplementedError() + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__( + OrderedDict( + [ + ("fee", Asset(kwargs["fee"])), + ("account", ObjectId(kwargs["account"], "account")), + ( + "authority_to_delete", + ObjectId(kwargs["authority_to_update"], "custom_authority"), + ), + ("extensions", Set([])), + ] + ) + ) + + fill_classmaps() diff --git a/tests/fixtures.py b/tests/fixtures.py index a655ba85..32e29379 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -22,7 +22,7 @@ # bitshares instance bitshares = BitShares( - "wss://bitshares.openledger.info/ws", keys=wifs, nobroadcast=True, num_retries=1 + "wss://eu.nodes.bitshares.ws", keys=wifs, nobroadcast=True, num_retries=1 ) config = bitshares.config diff --git a/tests/test_connection.py b/tests/test_connection.py index 0e0b0395..4d4fb80a 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -11,6 +11,10 @@ class Testcases(unittest.TestCase): + """ Deprecated tests - API servers partily unavailable + """ + + """ def test_bts1bts2(self): b1 = BitShares("wss://node.testnet.bitshares.eu", nobroadcast=True) @@ -45,3 +49,4 @@ def test_default_connection2(self): self.assertEqual(test["symbol"], "TEST") self.assertEqual(bts["symbol"], "BTS") + """ diff --git a/tests/test_objectcache.py b/tests/test_objectcache.py index 4fe7fb79..267d5234 100644 --- a/tests/test_objectcache.py +++ b/tests/test_objectcache.py @@ -15,7 +15,7 @@ def __init__(self, *args, **kwargs): def test_cache(self): cache = ObjectCache(default_expiration=1) - self.assertEqual(str(cache), "ObjectCache(n=0, default_expiration=1)") + self.assertEqual(str(cache), "ObjectCacheInMemory(default_expiration=1)") # Data cache["foo"] = "bar" diff --git a/tests/test_price.py b/tests/test_price.py index 53f3f21d..8cb59e90 100644 --- a/tests/test_price.py +++ b/tests/test_price.py @@ -5,12 +5,12 @@ from bitshares.price import Price from bitshares.asset import Asset import unittest +from .fixtures import bitshares class Testcases(unittest.TestCase): def __init__(self, *args, **kwargs): super(Testcases, self).__init__(*args, **kwargs) - bitshares = BitShares("wss://node.bitshares.eu", nobroadcast=True,) set_shared_bitshares_instance(bitshares) def test_init(self): diff --git a/tests/test_transactions.py b/tests/test_transactions.py index db95b2ea..62b8067c 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import random import unittest +import warnings from pprint import pprint from binascii import hexlify @@ -33,7 +34,6 @@ def doit(self, printWire=False): expiration=expiration, operations=ops, ) - pprint(tx.json()) tx = tx.sign([wif], chain=prefix) tx.verify([PrivateKey(wif).pubkey], prefix) txWire = hexlify(bytes(tx)).decode("ascii") @@ -55,6 +55,13 @@ def doit(self, printWire=False): # Compare expected result with test unit self.assertEqual(self.cm[:-130], txWire[:-130]) + def test_all_operations_implemented(self): + from bitsharesbase.operationids import ops, getClassForOperation + + for operation in ops: + if getClassForOperation(operation) is None: + warnings.warn(f"Operation {operation} missing!", Warning) + def test_call_update(self): self.op = operations.Call_order_update( **{ @@ -316,7 +323,7 @@ def test_create_account(self): "197cc69aa04c45e20a8c1c495629ca5765d8e458a18f0920bfaf9d0a" "909c01819cf887a66d06903af71fb07f0aac34600c733590984e" ) - self.doit(1) + self.doit(0) """ # TODO FIX THIS UNIT TEST