canoser
Advanced tools
| Metadata-Version: 2.1 | ||
| Name: canoser | ||
| Version: 0.8.1 | ||
| Version: 0.8.2 | ||
| Summary: A python implementation of the LCS(Libra Canonical Serialization) for the Libra network. | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/yuan-xy/canoser-python.git |
@@ -34,4 +34,3 @@ README.md | ||
| test/test_struct.py | ||
| test/test_transaction_print.py | ||
| test/test_types.py | ||
| test/test_util.py |
@@ -22,3 +22,3 @@ from canoser.base import Base | ||
| if self.encode_len: | ||
| output += Uint32.encode(len(arr)) | ||
| output += Uint32.serialize_uint32_as_uleb128(len(arr)) | ||
| for item in arr: | ||
@@ -31,3 +31,3 @@ output += self.atype.encode(item) | ||
| if self.encode_len: | ||
| size = Uint32.decode(cursor) | ||
| size = Uint32.parse_uint32_from_uleb128(cursor) | ||
| if self.fixed_len is not None and size != self.fixed_len: | ||
@@ -34,0 +34,0 @@ raise TypeError(f"{size} is not equal to predefined value: {self.fixed_len}") |
+28
-2
@@ -19,3 +19,3 @@ import struct | ||
| if self.encode_len: | ||
| output += Uint32.encode(len(value)) | ||
| output += Uint32.serialize_uint32_as_uleb128(len(value)) | ||
| output += value | ||
@@ -27,3 +27,3 @@ return output | ||
| if self.encode_len: | ||
| size = Uint32.decode(cursor) | ||
| size = Uint32.parse_uint32_from_uleb128(cursor) | ||
| if self.fixed_len is not None and size != self.fixed_len: | ||
@@ -52,1 +52,27 @@ raise TypeError(f"{size} is not equal to predefined value: {self.fixed_len}") | ||
| class ByteArrayT(Base): | ||
| def encode(self, value): | ||
| output = b"" | ||
| output += Uint32.serialize_uint32_as_uleb128(len(value)) | ||
| output += bytes(value) | ||
| return output | ||
| def decode(self, cursor): | ||
| size = Uint32.parse_uint32_from_uleb128(cursor) | ||
| return bytearray(cursor.read_bytes(size)) | ||
| def check_value(self, value): | ||
| if not isinstance(value, bytearray): | ||
| raise TypeError('value {} is not bytearray'.format(value)) | ||
| def __eq__(self, other): | ||
| if not isinstance(other, ByteArrayT): | ||
| return False | ||
| return True | ||
| def to_json_serializable(cls, obj): | ||
| return obj.hex() | ||
+31
-0
@@ -0,1 +1,2 @@ | ||
| from __future__ import annotations | ||
| import struct | ||
@@ -139,2 +140,32 @@ from random import randint | ||
| @classmethod | ||
| def serialize_uint32_as_uleb128(cls, value: Uint32) -> bytes: | ||
| ret = bytearray() | ||
| while value >= 0x80: | ||
| # Write 7 (lowest) bits of data and set the 8th bit to 1. | ||
| byte = (value & 0x7f) | ||
| ret.append(byte | 0x80) | ||
| value >>= 7 | ||
| # Write the remaining bits of data and set the highest bit to 0. | ||
| ret.append(value) | ||
| return bytes(ret) | ||
| @classmethod | ||
| def parse_uint32_from_uleb128(cls, cursor: Cursor) -> Uint32: | ||
| max_shift = 28 | ||
| value = 0 | ||
| shift = 0 | ||
| while not cursor.is_finished(): | ||
| byte = cursor.read_u8() | ||
| val = byte & 0x7f | ||
| value |= (val << shift) | ||
| if val == byte: | ||
| return value | ||
| shift += 7 | ||
| if shift > max_shift: | ||
| break | ||
| bail(f"invalid ULEB128 representation for Uint32") | ||
| class Uint64(IntType): | ||
@@ -141,0 +172,0 @@ pack_str = "<Q" |
+2
-2
@@ -12,3 +12,3 @@ from canoser.base import Base | ||
| output = b"" | ||
| output += Uint32.encode(len(kvs)) | ||
| output += Uint32.serialize_uint32_as_uleb128(len(kvs)) | ||
| odict = {} | ||
@@ -24,3 +24,3 @@ for k, v in kvs.items(): | ||
| kvs = {} | ||
| size = Uint32.decode(cursor) | ||
| size = Uint32.parse_uint32_from_uleb128(cursor) | ||
| for _ in range(size): | ||
@@ -27,0 +27,0 @@ k = self.ktype.decode(cursor) |
@@ -76,3 +76,3 @@ from canoser.base import Base | ||
| def encode(cls, enum): | ||
| ret = Uint32.encode(enum.index) | ||
| ret = Uint32.serialize_uint32_as_uleb128(enum.index) | ||
| if enum.value_type is not None: | ||
@@ -84,3 +84,3 @@ ret += enum.value_type.encode(enum.value) | ||
| def decode(cls, cursor): | ||
| index = Uint32.decode(cursor) | ||
| index = Uint32.parse_uint32_from_uleb128(cursor) | ||
| _name, datatype = cls._enums[index] | ||
@@ -87,0 +87,0 @@ if datatype is not None: |
+2
-2
@@ -9,3 +9,3 @@ from canoser.int_type import Uint32 | ||
| utf8 = value.encode('utf-8') | ||
| output += Uint32.encode(len(utf8)) | ||
| output += Uint32.serialize_uint32_as_uleb128(len(utf8)) | ||
| output += utf8 | ||
@@ -16,3 +16,3 @@ return output | ||
| def decode(self, cursor): | ||
| strlen = Uint32.decode(cursor) | ||
| strlen = Uint32.parse_uint32_from_uleb128(cursor) | ||
| return str(cursor.read_bytes(strlen), encoding='utf-8') | ||
@@ -19,0 +19,0 @@ |
+3
-1
@@ -5,3 +5,3 @@ from canoser.int_type import Uint8 | ||
| from canoser.str_t import StrT | ||
| from canoser.bytes_t import BytesT | ||
| from canoser.bytes_t import BytesT, ByteArrayT | ||
| from canoser.bool_t import BoolT | ||
@@ -26,2 +26,4 @@ from canoser.array_t import ArrayT | ||
| return BytesT() | ||
| elif field_type == bytearray: | ||
| return ByteArrayT() | ||
| elif field_type == bool: | ||
@@ -28,0 +30,0 @@ return BoolT |
@@ -1,1 +0,1 @@ | ||
| version = "0.8.1" | ||
| version = "0.8.2" |
+1
-1
| Metadata-Version: 2.1 | ||
| Name: canoser | ||
| Version: 0.8.1 | ||
| Version: 0.8.2 | ||
| Summary: A python implementation of the LCS(Libra Canonical Serialization) for the Libra network. | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/yuan-xy/canoser-python.git |
+24
-1
| import setuptools | ||
| from canoser.version import version | ||
| import re | ||
| with open("canoser/version.py", "r") as fp: | ||
| try: | ||
| version = re.findall( | ||
| r"^version = \"([0-9\.]+)\"", fp.read(), re.M | ||
| )[0] | ||
| except IndexError: | ||
| raise RuntimeError("Unable to determine version.") | ||
| with open("README.md", "r") as fh: | ||
@@ -9,2 +19,13 @@ content = fh.read() | ||
| tests_require = [ | ||
| 'pytest', | ||
| 'hypothesis', | ||
| ] | ||
| install_requires = [ | ||
| ] | ||
| setuptools.setup( | ||
@@ -20,2 +41,4 @@ name="canoser", | ||
| packages=setuptools.find_packages(), | ||
| install_requires=install_requires, | ||
| tests_require=tests_require, | ||
| classifiers=[ | ||
@@ -22,0 +45,0 @@ "Programming Language :: Python :: 3", |
+12
-2
@@ -18,3 +18,3 @@ from canoser import * | ||
| def test_enocde_len(): | ||
| expected_output = bytes([32, 0, 0, 0]) + input | ||
| expected_output = bytes([32]) + input | ||
| actual_output = Address1.encode(input) | ||
@@ -35,2 +35,12 @@ assert expected_output == actual_output | ||
| addr2 = AddrStruct.deserialize(ser) | ||
| assert addrs.map == addr2.map | ||
| assert addrs.map == addr2.map | ||
| class BArrayStruct(Struct): | ||
| _fields = [('map', {Address2: bytearray})] | ||
| def test_bytearray(): | ||
| amap = {input: bytearray(b'ba')} | ||
| addrs = BArrayStruct(amap) | ||
| ser = addrs.serialize() | ||
| addr2 = BArrayStruct.deserialize(ser) | ||
| assert addrs.map == addr2.map |
@@ -16,5 +16,5 @@ from canoser import * | ||
| bstr = t12.serialize() | ||
| assert bstr == bytes([12]) + Uint32.encode(2) + t1.serialize() + t2.serialize() | ||
| assert bstr == bytes([12]) + Uint32.serialize_uint32_as_uleb128(2) + t1.serialize() + t2.serialize() | ||
| tt = Circular.deserialize(bstr) | ||
| assert tt == t12 | ||
| assert Circular.deserialize(t1.serialize()) == t1 |
@@ -39,4 +39,4 @@ from canoser import * | ||
| bs = Bools.encode(x) | ||
| assert bs == b'\x03\x00\x00\x00\x01\x00\x01' | ||
| assert bs == b'\x03\x01\x00\x01' | ||
| x2 = Bools.deserialize(bs) | ||
| assert x == x2 |
@@ -42,3 +42,3 @@ import pytest | ||
| assert t_arg.value_type == Uint64 | ||
| assert t_arg.serialize() == b"\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00" | ||
| assert t_arg.serialize() == b"\x00\x02\x00\x00\x00\x00\x00\x00\x00" | ||
| assert t_arg == TransactionArgument.deserialize(t_arg.serialize()) | ||
@@ -77,8 +77,8 @@ t_arg.value = 3 | ||
| assert Enum1.new_with_index_value(1, None) == Enum1('opt2') | ||
| assert Enum1.encode(e1) == b'\x00\x00\x00\x00\x01\x00\x00\x00\x05' | ||
| assert Enum1.encode(e2) == b'\x01\x00\x00\x00' | ||
| obj = Enum1.decode(Cursor(b'\x00\x00\x00\x00\x01\x00\x00\x00\x05')) | ||
| assert Enum1.encode(e1) == b'\x00\x01\x05' | ||
| assert Enum1.encode(e2) == b'\x01' | ||
| obj = Enum1.decode(Cursor(b'\x00\x01\x05')) | ||
| assert obj.index == 0 | ||
| assert obj.value == [5] | ||
| obj = Enum1.decode(Cursor(b'\x01\x00\x00\x00')) | ||
| obj = Enum1.decode(Cursor(b'\x01')) | ||
| assert obj.index == 1 | ||
@@ -99,3 +99,3 @@ assert obj.value == None | ||
| sx = x.serialize() | ||
| assert sx == b'\x00\x00\x00\x00' | ||
| assert sx == b'\x00' | ||
| x2 = EStruct.deserialize(sx) | ||
@@ -118,5 +118,5 @@ assert x.enum.index == x2.enum.index | ||
| sx = x.serialize() | ||
| assert sx == b'\x01\x00\x00\x00\x03\x00\x00\x00' +\ | ||
| b'\x02\x00\x00\x00' + b'\x02\x00\x00\x00ab' + b'\x01\x00\x00\x00c' +\ | ||
| b'\x01\x00\x00\x00' + b'\x01\x00\x00\x00d' + b'\x00\x00\x00\x00' | ||
| assert sx == b'\x01\x03' +\ | ||
| b'\x02' + b'\x02ab' + b'\x01c' +\ | ||
| b'\x01' + b'\x01d' + b'\x00' | ||
| x2 = EStruct2.deserialize(sx) | ||
@@ -123,0 +123,0 @@ assert x.enum.index == x2.enum.index |
@@ -7,9 +7,14 @@ import pytest | ||
| #copy form libra source code | ||
| TEST_VECTOR_1 = "ffffffffffffffff060000006463584d4237640000000000000009000000000102\ | ||
| 03040506070820000000050505050505050505050505050505050505050505050505\ | ||
| 05050505050505056300000001030000000100000001030000001615430300000000\ | ||
| 381503000000160a05040000001415596903000000c9175a" | ||
| TEST_VECTOR_1 = [ | ||
| 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x06, 0x64, 0x63, 0x58, 0x4d, 0x42, 0x37, | ||
| 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, | ||
| 0x06, 0x07, 0x08, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, | ||
| 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, | ||
| 0x05, 0x05, 0x05, 0x05, 0x05, 0x63, 0x00, 0x00, 0x00, 0x01, 0x03, 0x01, 0x01, 0x03, 0x16, | ||
| 0x15, 0x43, 0x03, 0x00, 0x38, 0x15, 0x03, 0x16, 0x0a, 0x05, 0x04, 0x14, 0x15, 0x59, 0x69, | ||
| 0x03, 0xc9, 0x17, 0x5a, | ||
| ] | ||
| class Addr(Struct): | ||
| _fields = [('addr', [Uint8, 32])] | ||
| _fields = [('addr', [Uint8, 32, False])] | ||
@@ -62,5 +67,5 @@ | ||
| str1 = foo.serialize() | ||
| str2 = bytes.fromhex(TEST_VECTOR_1) | ||
| str2 = bytes(TEST_VECTOR_1) | ||
| assert str1 == str2 | ||
| foo2 = Foo.deserialize(str1) | ||
| assert foo == foo2 |
@@ -29,3 +29,3 @@ from canoser import * | ||
| def test_enocde_len(): | ||
| expected_output0 = [32, 0, 0, 0] + expected_output | ||
| expected_output0 = [32] + expected_output | ||
| actual_output = Address1.encode(input) | ||
@@ -32,0 +32,0 @@ assert bytes(expected_output0) == actual_output |
@@ -52,3 +52,3 @@ import pytest | ||
| sx = x.serialize() | ||
| assert b"\3\0\0\0\1\0\1" == sx | ||
| assert b"\3\1\0\1" == sx | ||
| x2 = ArrayS.deserialize(sx) | ||
@@ -59,3 +59,3 @@ assert x.array == x2.array | ||
| with pytest.raises(TypeError): | ||
| ArrayS.deserialize(b"\3\0\0\0\1\0\2") | ||
| ArrayS.deserialize(b"\3\1\0\2") | ||
| x = ArrayS([]) | ||
@@ -145,3 +145,3 @@ with pytest.raises(TypeError): | ||
| sx = x.serialize() | ||
| assert sx == b'\x03\x00\x00\x00\x61\x62\x63\x01\x00\x02\x00' | ||
| assert sx == b'\x03\x61\x62\x63\x01\x00\x02\x00' | ||
| x2 = TupleS.deserialize(sx) | ||
@@ -148,0 +148,0 @@ assert x.tp == x2.tp |
@@ -174,13 +174,13 @@ from canoser import * | ||
| arrt = ArrayT(Uint8, 2) | ||
| assert arrt.encode([1, 2]) == b'\x02\x00\x00\x00\x01\x02' | ||
| arr = arrt.decode(Cursor(b'\x02\x00\x00\x00\x01\x02')) | ||
| assert arrt.encode([1, 2]) == b'\x02\x01\x02' | ||
| arr = arrt.decode(Cursor(b'\x02\x01\x02')) | ||
| assert arr == [1, 2] | ||
| with pytest.raises(TypeError): | ||
| arrt.decode(Cursor(b'\x01\x00\x00\x00\x01\x02')) | ||
| arrt.decode(Cursor(b'\x01\x01\x02')) | ||
| with pytest.raises(TypeError): | ||
| arrt.decode(Cursor(b'\x03\x00\x00\x00\x01\x02')) | ||
| arrt.decode(Cursor(b'\x03\x01\x02')) | ||
| def test_deserialize_int_array(): | ||
| arrt = ArrayT(BoolT, 2) | ||
| bools = arrt.decode(Cursor([2,0,0,0,1,0])) | ||
| bools = arrt.decode(Cursor([2,1,0])) | ||
| assert bools == [True, False] | ||
@@ -190,4 +190,4 @@ | ||
| tuplet = TupleT(StrT, Uint8, BoolT) | ||
| assert tuplet.encode(("abc", 1, False)) == b'\x03\x00\x00\x00\x61\x62\x63\x01\x00' | ||
| ret = tuplet.decode(Cursor(b'\x03\x00\x00\x00\x61\x62\x63\x01\x00')) | ||
| assert tuplet.encode(("abc", 1, False)) == b'\x03\x61\x62\x63\x01\x00' | ||
| ret = tuplet.decode(Cursor(b'\x03\x61\x62\x63\x01\x00')) | ||
| assert ret == ("abc", 1, False) | ||
@@ -194,0 +194,0 @@ |
| import pytest | ||
| import pdb | ||
| from canoser import * | ||
| from datetime import datetime | ||
| import struct | ||
| import json | ||
| # Test Example in https://github.com/libra/libra/tree/master/common/canonical_serialization/README.md | ||
| ADDRESS_LENGTH = 32 | ||
| ED25519_PUBLIC_KEY_LENGTH = 32 | ||
| ED25519_SIGNATURE_LENGTH = 64 | ||
| class Address(DelegateT): | ||
| delegate_type = [Uint8, ADDRESS_LENGTH] | ||
| class ByteArray(DelegateT): | ||
| delegate_type = [Uint8] | ||
| class TransactionArgument(RustEnum): | ||
| _enums = [ | ||
| ('U64', Uint64), | ||
| ('Address', Address), | ||
| ('String', str), | ||
| ('ByteArray', ByteArray) | ||
| ] | ||
| class WriteOp(RustEnum): | ||
| _enums = [ | ||
| ('Deletion', None), | ||
| ('Value', ByteArray) | ||
| ] | ||
| class AccessPath(Struct): | ||
| _fields = [ | ||
| ('address', Address), | ||
| ('path', ByteArray) | ||
| ] | ||
| class Program(Struct): | ||
| _fields = [ | ||
| ('code', [Uint8]), | ||
| ('args', [TransactionArgument]), | ||
| ('modules', [[Uint8]]) | ||
| ] | ||
| # `WriteSet` contains all access paths that one transaction modifies. Each of them is a `WriteOp` | ||
| # where `Value(val)` means that serialized representation should be updated to `val`, and | ||
| # `Deletion` means that we are going to delete this access path. | ||
| class WriteSet(Struct): | ||
| _fields = [ | ||
| ('write_set', [(AccessPath, WriteOp)]) | ||
| ] | ||
| class Module(Struct): | ||
| _fields = [ | ||
| ('code', [Uint8]) | ||
| ] | ||
| class Script(Struct): | ||
| _fields = [ | ||
| ('code', [Uint8]), | ||
| ('args', [TransactionArgument]) | ||
| ] | ||
| class TransactionPayload(RustEnum): | ||
| _enums = [ | ||
| ('Program', Program), | ||
| ('WriteSet', WriteSet), | ||
| ('Script', Script), | ||
| ('Module', Module) | ||
| ] | ||
| class RawTransaction(Struct): | ||
| _fields = [ | ||
| ('sender', Address), | ||
| ('sequence_number', Uint64), | ||
| ('payload', TransactionPayload), | ||
| ('max_gas_amount', Uint64), | ||
| ('gas_unit_price', Uint64), | ||
| ('expiration_time', Uint64) | ||
| ] | ||
| def test_readme_example1(): | ||
| lcs = '0200000020000000A71D76FAA2D2D5C3224EC3D41DEB293973564A791E55C6782BA76C2BF0495F9A2100000001217DA6C6B3E19F1825CFB2676DAECCE3BF3DE03CF26647C78DF00B371B25CC970000000020000000C4C63F80C74B11263E421EBF8486A4E398D0DBC09FA7D4F62CCDB309F3AEA81F0900000001217DA6C6B3E19F180100000004000000CAFED00D' | ||
| tx = WriteSet.deserialize(bytes.fromhex(lcs)) | ||
| print(tx) | ||
| def test_readme_example2(): | ||
| lcs = '00000000040000006D6F766502000000020000000900000043414645204430304402000000090000006361666520643030640300000001000000CA02000000FED0010000000D' | ||
| tx = TransactionPayload.deserialize(bytes.fromhex(lcs)) | ||
| print(tx) | ||
| def test_readme_example3(): | ||
| lcs = '010000000200000020000000A71D76FAA2D2D5C3224EC3D41DEB293973564A791E55C6782BA76C2BF0495F9A2100000001217DA6C6B3E19F1825CFB2676DAECCE3BF3DE03CF26647C78DF00B371B25CC970000000020000000C4C63F80C74B11263E421EBF8486A4E398D0DBC09FA7D4F62CCDB309F3AEA81F0900000001217DA6C6B3E19F180100000004000000CAFED00D' | ||
| tx = TransactionPayload.deserialize(bytes.fromhex(lcs)) | ||
| print(tx) | ||
| def test_readme_example4(): | ||
| lcs = '200000003A24A61E05D129CACE9E0EFC8BC9E33831FEC9A9BE66F50FD352A2638A49B9EE200000000000000000000000040000006D6F766502000000020000000900000043414645204430304402000000090000006361666520643030640300000001000000CA02000000FED0010000000D1027000000000000204E0000000000008051010000000000' | ||
| tx = RawTransaction.deserialize(bytes.fromhex(lcs)) | ||
| assert tx.__str__() == """{ | ||
| "sender": "3a24a61e05d129cace9e0efc8bc9e33831fec9a9be66f50fd352a2638a49b9ee", | ||
| "sequence_number": 32, | ||
| "payload": { | ||
| "Program": { | ||
| "code": "6d6f7665", | ||
| "args": [ | ||
| { | ||
| "String": "CAFE D00D" | ||
| }, | ||
| { | ||
| "String": "cafe d00d" | ||
| } | ||
| ], | ||
| "modules": [ | ||
| "ca", | ||
| "fed0", | ||
| "0d" | ||
| ] | ||
| } | ||
| }, | ||
| "max_gas_amount": 10000, | ||
| "gas_unit_price": 20000, | ||
| "expiration_time": 86400 | ||
| }""" | ||
| def test_readme_example5(): | ||
| lcs = '20000000C3398A599A6F3B9F30B635AF29F2BA046D3A752C26E9D0647B9647D1F4C04AD42000000000000000010000000200000020000000A71D76FAA2D2D5C3224EC3D41DEB293973564A791E55C6782BA76C2BF0495F9A2100000001217DA6C6B3E19F1825CFB2676DAECCE3BF3DE03CF26647C78DF00B371B25CC970000000020000000C4C63F80C74B11263E421EBF8486A4E398D0DBC09FA7D4F62CCDB309F3AEA81F0900000001217DA6C6B3E19F180100000004000000CAFED00D00000000000000000000000000000000FFFFFFFFFFFFFFFF' | ||
| tx = RawTransaction.deserialize(bytes.fromhex(lcs)) | ||
| assert tx.__str__() == """{ | ||
| "sender": "c3398a599a6f3b9f30b635af29f2ba046d3a752c26e9d0647b9647d1f4c04ad4", | ||
| "sequence_number": 32, | ||
| "payload": { | ||
| "WriteSet": { | ||
| "write_set": [ | ||
| [ | ||
| { | ||
| "address": "a71d76faa2d2d5c3224ec3d41deb293973564a791e55c6782ba76c2bf0495f9a", | ||
| "path": "01217da6c6b3e19f1825cfb2676daecce3bf3de03cf26647c78df00b371b25cc97" | ||
| }, | ||
| "Deletion" | ||
| ], | ||
| [ | ||
| { | ||
| "address": "c4c63f80c74b11263e421ebf8486a4e398d0dbc09fa7d4f62ccdb309f3aea81f", | ||
| "path": "01217da6c6b3e19f18" | ||
| }, | ||
| { | ||
| "Value": "cafed00d" | ||
| } | ||
| ] | ||
| ] | ||
| } | ||
| }, | ||
| "max_gas_amount": 0, | ||
| "gas_unit_price": 0, | ||
| "expiration_time": 18446744073709551615 | ||
| }""" | ||
| amap = tx.to_json_serializable() | ||
| assert tx.to_json(indent=2) == json.dumps(amap, sort_keys=False, indent=2) | ||
| assert tx.to_json(indent=2) == tx.__str__() |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
106636
-2.63%37
-2.63%1569
-4.39%