astreum 0.1.5__py3-none-any.whl → 0.1.6__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.
Potentially problematic release.
This version of astreum might be problematic. Click here for more details.
- astreum/__init__.py +1 -0
- astreum/machine/__init__.py +3 -4
- astreum/machine/environment.py +9 -2
- astreum/node/__init__.py +416 -0
- astreum/node/models.py +96 -0
- astreum/node/relay/__init__.py +248 -0
- astreum/node/relay/bucket.py +80 -0
- astreum/node/relay/envelope.py +280 -0
- astreum/node/relay/message.py +105 -0
- astreum/node/relay/peer.py +171 -0
- astreum/node/relay/route.py +125 -0
- astreum/utils/__init__.py +0 -0
- astreum/utils/bytes_format.py +75 -0
- {astreum-0.1.5.dist-info → astreum-0.1.6.dist-info}/METADATA +2 -2
- {astreum-0.1.5.dist-info → astreum-0.1.6.dist-info}/RECORD +18 -8
- {astreum-0.1.5.dist-info → astreum-0.1.6.dist-info}/WHEEL +1 -1
- {astreum-0.1.5.dist-info → astreum-0.1.6.dist-info}/LICENSE +0 -0
- {astreum-0.1.5.dist-info → astreum-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message related classes and utilities for Astreum node network.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import struct
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from astreum.utils.bytes_format import encode, decode
|
|
10
|
+
|
|
11
|
+
class Topic(Enum):
|
|
12
|
+
"""
|
|
13
|
+
Enum for different message topics in the Astreum network.
|
|
14
|
+
"""
|
|
15
|
+
OBJECT_REQUEST = auto()
|
|
16
|
+
OBJECT = auto()
|
|
17
|
+
PING = auto()
|
|
18
|
+
PONG = auto()
|
|
19
|
+
ROUTE = auto()
|
|
20
|
+
ROUTE_REQUEST = auto()
|
|
21
|
+
LATEST_BLOCK = auto()
|
|
22
|
+
BLOCK = auto()
|
|
23
|
+
LATEST_BLOCK_REQUEST = auto()
|
|
24
|
+
TRANSACTION = auto()
|
|
25
|
+
|
|
26
|
+
def to_bytes(self) -> bytes:
|
|
27
|
+
"""
|
|
28
|
+
Convert this Topic enum value to bytes.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
bytes: Single byte representing the topic
|
|
32
|
+
"""
|
|
33
|
+
return struct.pack('!B', self.value)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_bytes(cls, data: bytes) -> Optional['Topic']:
|
|
37
|
+
"""
|
|
38
|
+
Create a Topic from its serialized form.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
data (bytes): Serialized topic (single byte)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Optional[Topic]: The deserialized topic, or None if the data is invalid
|
|
45
|
+
"""
|
|
46
|
+
if not data or len(data) != 1:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
topic_value = struct.unpack('!B', data)[0]
|
|
51
|
+
return cls(topic_value)
|
|
52
|
+
except (struct.error, ValueError) as e:
|
|
53
|
+
print(f"Error deserializing topic: {e}")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Message:
|
|
58
|
+
"""
|
|
59
|
+
Represents a message in the Astreum network.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
body (bytes): The actual content of the message
|
|
63
|
+
topic (Topic): The topic/type of the message
|
|
64
|
+
"""
|
|
65
|
+
body: bytes
|
|
66
|
+
topic: Topic
|
|
67
|
+
|
|
68
|
+
def to_bytes(self) -> bytes:
|
|
69
|
+
"""
|
|
70
|
+
Convert this Message to bytes using bytes_format.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
bytes: Serialized message
|
|
74
|
+
"""
|
|
75
|
+
return encode([
|
|
76
|
+
self.topic.to_bytes(),
|
|
77
|
+
self.body
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_bytes(cls, data: bytes) -> Optional['Message']:
|
|
82
|
+
"""
|
|
83
|
+
Create a Message from its serialized form using bytes_format.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
data (bytes): Serialized message
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Optional[Message]: The deserialized message, or None if the data is invalid
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
parts = decode(data)
|
|
93
|
+
if len(parts) != 2:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
topic_data, body = parts
|
|
97
|
+
topic = Topic.from_bytes(topic_data)
|
|
98
|
+
|
|
99
|
+
if not topic:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
return cls(body=body, topic=topic)
|
|
103
|
+
except (ValueError, struct.error) as e:
|
|
104
|
+
print(f"Error deserializing message: {e}")
|
|
105
|
+
return None
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Peer management for Astreum node's Kademlia-style network.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Tuple, Optional
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Peer:
|
|
11
|
+
"""
|
|
12
|
+
Represents a peer in the Astreum network.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
address (Tuple[str, int]): The network address (host, port)
|
|
16
|
+
public_key (bytes): The public key of the peer
|
|
17
|
+
difficulty (int): The proof-of-work difficulty required for this peer
|
|
18
|
+
last_seen (int): Timestamp when the peer was last seen
|
|
19
|
+
failed_attempts (int): Number of consecutive failed communication attempts
|
|
20
|
+
"""
|
|
21
|
+
address: Tuple[str, int]
|
|
22
|
+
public_key: bytes
|
|
23
|
+
difficulty: int = 1
|
|
24
|
+
last_seen: int = 0
|
|
25
|
+
failed_attempts: int = 0
|
|
26
|
+
|
|
27
|
+
def __post_init__(self):
|
|
28
|
+
if self.last_seen == 0:
|
|
29
|
+
self.last_seen = int(time.time())
|
|
30
|
+
|
|
31
|
+
def update_last_seen(self):
|
|
32
|
+
"""Update the last seen timestamp to current time."""
|
|
33
|
+
self.last_seen = int(time.time())
|
|
34
|
+
self.failed_attempts = 0
|
|
35
|
+
|
|
36
|
+
def register_failed_attempt(self):
|
|
37
|
+
"""Register a failed communication attempt with this peer."""
|
|
38
|
+
self.failed_attempts += 1
|
|
39
|
+
|
|
40
|
+
def is_active(self, max_age: int = 3600, max_failures: int = 3) -> bool:
|
|
41
|
+
"""
|
|
42
|
+
Check if the peer is considered active.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
max_age (int): Maximum age in seconds before peer is considered inactive
|
|
46
|
+
max_failures (int): Maximum consecutive failures before peer is considered inactive
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
bool: True if the peer is active, False otherwise
|
|
50
|
+
"""
|
|
51
|
+
current_time = int(time.time())
|
|
52
|
+
return (
|
|
53
|
+
(current_time - self.last_seen) <= max_age and
|
|
54
|
+
self.failed_attempts < max_failures
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
class PeerManager:
|
|
58
|
+
"""
|
|
59
|
+
Manages a collection of peers and provides utilities for peer operations.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, our_node_id: bytes):
|
|
63
|
+
"""
|
|
64
|
+
Initialize a peer manager.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
our_node_id (bytes): Our node's unique identifier
|
|
68
|
+
"""
|
|
69
|
+
self.our_node_id = our_node_id
|
|
70
|
+
self.peers_by_id = {} # node_id: Peer
|
|
71
|
+
self.peers_by_address = {} # address: Peer
|
|
72
|
+
|
|
73
|
+
def add_or_update_peer(self, address: Tuple[str, int], public_key: bytes) -> Peer:
|
|
74
|
+
"""
|
|
75
|
+
Add a new peer or update an existing one.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
address (Tuple[str, int]): Network address (host, port)
|
|
79
|
+
public_key (bytes): The public key of the peer
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Peer: The added or updated peer
|
|
83
|
+
"""
|
|
84
|
+
# Check if we already know this peer by ID
|
|
85
|
+
if public_key in self.peers_by_id:
|
|
86
|
+
peer = self.peers_by_id[public_key]
|
|
87
|
+
# Update address if changed
|
|
88
|
+
if peer.address != address:
|
|
89
|
+
if peer.address in self.peers_by_address:
|
|
90
|
+
del self.peers_by_address[peer.address]
|
|
91
|
+
peer.address = address
|
|
92
|
+
self.peers_by_address[address] = peer
|
|
93
|
+
peer.update_last_seen()
|
|
94
|
+
return peer
|
|
95
|
+
|
|
96
|
+
# Check if address exists with different ID
|
|
97
|
+
if address in self.peers_by_address:
|
|
98
|
+
old_peer = self.peers_by_address[address]
|
|
99
|
+
if old_peer.public_key in self.peers_by_id:
|
|
100
|
+
del self.peers_by_id[old_peer.public_key]
|
|
101
|
+
|
|
102
|
+
# Create and add new peer
|
|
103
|
+
peer = Peer(address=address, public_key=public_key)
|
|
104
|
+
self.peers_by_id[public_key] = peer
|
|
105
|
+
self.peers_by_address[address] = peer
|
|
106
|
+
return peer
|
|
107
|
+
|
|
108
|
+
def get_peer_by_id(self, public_key: bytes) -> Optional[Peer]:
|
|
109
|
+
"""
|
|
110
|
+
Get a peer by its public key.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
public_key (bytes): The peer's public key
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Optional[Peer]: The peer if found, None otherwise
|
|
117
|
+
"""
|
|
118
|
+
return self.peers_by_id.get(public_key)
|
|
119
|
+
|
|
120
|
+
def get_peer_by_address(self, address: Tuple[str, int]) -> Optional[Peer]:
|
|
121
|
+
"""
|
|
122
|
+
Get a peer by its network address.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
address (Tuple[str, int]): The peer's network address
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Optional[Peer]: The peer if found, None otherwise
|
|
129
|
+
"""
|
|
130
|
+
return self.peers_by_address.get(address)
|
|
131
|
+
|
|
132
|
+
def remove_peer(self, public_key: bytes) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Remove a peer by its public key.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
public_key (bytes): The peer's public key
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
bool: True if the peer was removed, False otherwise
|
|
141
|
+
"""
|
|
142
|
+
if public_key in self.peers_by_id:
|
|
143
|
+
peer = self.peers_by_id[public_key]
|
|
144
|
+
del self.peers_by_id[public_key]
|
|
145
|
+
if peer.address in self.peers_by_address:
|
|
146
|
+
del self.peers_by_address[peer.address]
|
|
147
|
+
return True
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def calculate_distance(self, public_key: bytes) -> int:
|
|
151
|
+
"""
|
|
152
|
+
Calculate the XOR distance between our node ID and the given public key.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
public_key (bytes): The remote node's public key
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
int: XOR distance (0-255)
|
|
159
|
+
"""
|
|
160
|
+
# Assuming IDs are 256-bit (32 bytes)
|
|
161
|
+
for i in range(min(len(self.our_node_id), len(public_key))):
|
|
162
|
+
xor_byte = self.our_node_id[i] ^ public_key[i]
|
|
163
|
+
if xor_byte == 0:
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# Find the most significant bit
|
|
167
|
+
for bit in range(7, -1, -1):
|
|
168
|
+
if (xor_byte >> bit) & 1:
|
|
169
|
+
return (i * 8) + (7 - bit)
|
|
170
|
+
|
|
171
|
+
return 255 # Default maximum distance
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kademlia-style routing table implementation for Astreum node.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Dict, Set, Tuple, Optional
|
|
6
|
+
from .bucket import KBucket
|
|
7
|
+
from .peer import Peer, PeerManager
|
|
8
|
+
|
|
9
|
+
class RouteTable:
|
|
10
|
+
"""
|
|
11
|
+
Kademlia-style routing table using k-buckets.
|
|
12
|
+
|
|
13
|
+
The routing table consists of k-buckets, each covering a specific range of distances.
|
|
14
|
+
Each k-bucket is a list of nodes with specific IDs in a certain distance range from ourselves.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config: dict, our_node_id: bytes):
|
|
18
|
+
"""
|
|
19
|
+
Initialize the routing table.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
config (dict): Configuration dictionary
|
|
23
|
+
our_node_id (bytes): Our node's unique identifier
|
|
24
|
+
"""
|
|
25
|
+
self.our_node_id = our_node_id
|
|
26
|
+
self.bucket_size = config.get('max_peers_per_bucket', 20)
|
|
27
|
+
self.buckets: Dict[int, KBucket] = {}
|
|
28
|
+
self.peer_manager = PeerManager(our_node_id)
|
|
29
|
+
|
|
30
|
+
def add_peer(self, peer: Peer) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Add a peer to the appropriate k-bucket based on distance.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
peer (Peer): The peer to add
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
bool: True if the peer was added, False otherwise
|
|
39
|
+
"""
|
|
40
|
+
distance = self.peer_manager.calculate_distance(peer.public_key)
|
|
41
|
+
|
|
42
|
+
# Create bucket if it doesn't exist
|
|
43
|
+
if distance not in self.buckets:
|
|
44
|
+
self.buckets[distance] = KBucket(self.bucket_size)
|
|
45
|
+
|
|
46
|
+
# Add to bucket
|
|
47
|
+
return self.buckets[distance].add(peer.address)
|
|
48
|
+
|
|
49
|
+
def remove_peer(self, peer: Peer) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Remove a peer from its k-bucket.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
peer (Peer): The peer to remove
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
bool: True if the peer was removed, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
distance = self.peer_manager.calculate_distance(peer.public_key)
|
|
60
|
+
|
|
61
|
+
if distance in self.buckets:
|
|
62
|
+
return self.buckets[distance].remove(peer.address)
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def get_closest_peers(self, target_id: bytes, limit: int = 20) -> List[Peer]:
|
|
66
|
+
"""
|
|
67
|
+
Get the closest peers to a target ID.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
target_id (bytes): The target ID to find closest peers to
|
|
71
|
+
limit (int): Maximum number of peers to return
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List[Peer]: The closest peers
|
|
75
|
+
"""
|
|
76
|
+
# Calculate distances from all known peers to the target
|
|
77
|
+
peers_with_distance = []
|
|
78
|
+
|
|
79
|
+
for bucket in self.buckets.values():
|
|
80
|
+
for address in bucket.addresses:
|
|
81
|
+
peer = self.peer_manager.get_peer_by_address(address)
|
|
82
|
+
if peer:
|
|
83
|
+
# Calculate XOR distance between target and this peer
|
|
84
|
+
xor_distance = 0
|
|
85
|
+
for i in range(min(len(target_id), len(peer.public_key))):
|
|
86
|
+
xor_bit = target_id[i] ^ peer.public_key[i]
|
|
87
|
+
xor_distance = (xor_distance << 8) | xor_bit
|
|
88
|
+
|
|
89
|
+
peers_with_distance.append((peer, xor_distance))
|
|
90
|
+
|
|
91
|
+
# Sort by distance (closest first)
|
|
92
|
+
peers_with_distance.sort(key=lambda x: x[1])
|
|
93
|
+
|
|
94
|
+
# Return only the peers (without distances), up to the limit
|
|
95
|
+
return [p[0] for p in peers_with_distance[:limit]]
|
|
96
|
+
|
|
97
|
+
def get_bucket_stats(self) -> Dict[int, int]:
|
|
98
|
+
"""
|
|
99
|
+
Get statistics about the buckets in the routing table.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict[int, int]: Mapping of distance to number of peers in that bucket
|
|
103
|
+
"""
|
|
104
|
+
return {distance: len(bucket) for distance, bucket in self.buckets.items()}
|
|
105
|
+
|
|
106
|
+
def get_peers_in_bucket(self, distance: int) -> List[Peer]:
|
|
107
|
+
"""
|
|
108
|
+
Get all peers in a specific bucket.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
distance (int): Bucket distance
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List[Peer]: Peers in the bucket
|
|
115
|
+
"""
|
|
116
|
+
if distance not in self.buckets:
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
peers = []
|
|
120
|
+
for address in self.buckets[distance].addresses:
|
|
121
|
+
peer = self.peer_manager.get_peer_by_address(address)
|
|
122
|
+
if peer:
|
|
123
|
+
peers.append(peer)
|
|
124
|
+
|
|
125
|
+
return peers
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
def encode(iterable, encoder=lambda x: x):
|
|
2
|
+
"""
|
|
3
|
+
Encode an iterable of items into a single bytes object, applying the
|
|
4
|
+
provided encoder function to each item to convert it into bytes.
|
|
5
|
+
|
|
6
|
+
For each item (after applying encoder), we:
|
|
7
|
+
- Determine its length (n)
|
|
8
|
+
- Write a single byte to indicate how many bytes were used for the length:
|
|
9
|
+
0: n == 0
|
|
10
|
+
1: n <= 255
|
|
11
|
+
2: 256 <= n <= 65535
|
|
12
|
+
4: 65536 <= n <= 4294967295
|
|
13
|
+
8: n > 4294967295
|
|
14
|
+
- Write the little-endian encoding of n in the specified number of bytes (if n > 0)
|
|
15
|
+
- Append the item’s data
|
|
16
|
+
"""
|
|
17
|
+
result = bytearray()
|
|
18
|
+
for item in iterable:
|
|
19
|
+
item_bytes = encoder(item)
|
|
20
|
+
n = len(item_bytes)
|
|
21
|
+
if n > 4294967295:
|
|
22
|
+
result.append(8)
|
|
23
|
+
result.extend(n.to_bytes(8, byteorder='little'))
|
|
24
|
+
elif n > 65535:
|
|
25
|
+
result.append(4)
|
|
26
|
+
result.extend(n.to_bytes(4, byteorder='little'))
|
|
27
|
+
elif n > 255:
|
|
28
|
+
result.append(2)
|
|
29
|
+
result.extend(n.to_bytes(2, byteorder='little'))
|
|
30
|
+
elif n > 0:
|
|
31
|
+
result.append(1)
|
|
32
|
+
result.extend(n.to_bytes(1, byteorder='little'))
|
|
33
|
+
else:
|
|
34
|
+
result.append(0)
|
|
35
|
+
result.extend(item_bytes)
|
|
36
|
+
return bytes(result)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def decode(buffer, decoder=lambda x: x):
|
|
40
|
+
"""
|
|
41
|
+
Decode a bytes buffer into a list of items, applying the provided decoder
|
|
42
|
+
function to convert the bytes into the desired type.
|
|
43
|
+
|
|
44
|
+
The buffer is read sequentially:
|
|
45
|
+
- Read one byte that indicates how many bytes were used for the length.
|
|
46
|
+
- If this value is 0, then the item is empty.
|
|
47
|
+
- Otherwise, read that many bytes to get the item's length (as a little-endian integer).
|
|
48
|
+
- Then, slice the next 'length' bytes from the buffer to get the item data.
|
|
49
|
+
- Apply the decoder function to the item data before appending it.
|
|
50
|
+
|
|
51
|
+
By default, the decoder is the identity function, so items are returned as bytes.
|
|
52
|
+
"""
|
|
53
|
+
decoded_data = []
|
|
54
|
+
offset = 0
|
|
55
|
+
buf_len = len(buffer)
|
|
56
|
+
|
|
57
|
+
while offset < buf_len:
|
|
58
|
+
length_type = buffer[offset]
|
|
59
|
+
offset += 1
|
|
60
|
+
|
|
61
|
+
if length_type == 0:
|
|
62
|
+
n = 0
|
|
63
|
+
else:
|
|
64
|
+
if offset + length_type > buf_len:
|
|
65
|
+
raise ValueError("Buffer too short for length field")
|
|
66
|
+
n = int.from_bytes(buffer[offset: offset+length_type], byteorder='little')
|
|
67
|
+
offset += length_type
|
|
68
|
+
|
|
69
|
+
if offset + n > buf_len:
|
|
70
|
+
raise ValueError("Buffer is too short for item data")
|
|
71
|
+
item_data = buffer[offset:offset+n]
|
|
72
|
+
offset += n
|
|
73
|
+
decoded_data.append(decoder(item_data))
|
|
74
|
+
|
|
75
|
+
return decoded_data
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Python library to interact with the Astreum blockchain and its Lispeum virtual machine.
|
|
5
5
|
Author-email: "Roy R. O. Okello" <roy@stelar.xyz>
|
|
6
6
|
Project-URL: Homepage, https://github.com/astreum/lib
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
astreum/__init__.py,sha256
|
|
1
|
+
astreum/__init__.py,sha256=di8SwGUW1lNKUwvNWCjH9eLmz__sk1SbtuNZVjjpRe4,59
|
|
2
2
|
astreum/lispeum/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
astreum/lispeum/expression.py,sha256=3zOEoXFHpzEFUIi1clONY55WYAh5K0YkYhaLmtvQj0I,2939
|
|
4
4
|
astreum/lispeum/parser.py,sha256=SU8mjPj1ub4xQbU4CeX15HmKZAj4vI6TeefX2B72VCo,1191
|
|
@@ -16,11 +16,21 @@ astreum/lispeum/special/list/position.py,sha256=1EcD5fzfjUCUhHCk8iotFoWdeE9Z2Rjj
|
|
|
16
16
|
astreum/lispeum/special/list/remove.py,sha256=EPhkWFwyKG8HyWPheVvtxCvMaEGvODFUbx7h1fli_hg,690
|
|
17
17
|
astreum/lispeum/special/number/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
18
|
astreum/lispeum/special/number/addition.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
astreum/machine/__init__.py,sha256=
|
|
20
|
-
astreum/machine/environment.py,sha256=
|
|
19
|
+
astreum/machine/__init__.py,sha256=5Sx-_MEYFG-089q2G8JtLp9_ELiPYnYIXhagjy6C4w0,11048
|
|
20
|
+
astreum/machine/environment.py,sha256=K0084U6B7wwjrDZ9b2_7cEcbBzsB7UOy_Zpbrr7B3GY,834
|
|
21
21
|
astreum/machine/error.py,sha256=MvqBaZZt33rNELNhUJ2lER3TE3aS8WVqsWF2hz2AwoA,38
|
|
22
|
-
astreum
|
|
23
|
-
astreum
|
|
24
|
-
astreum
|
|
25
|
-
astreum
|
|
26
|
-
astreum
|
|
22
|
+
astreum/node/__init__.py,sha256=OUU7zcTp-eFv-4jB9JETfrei0wqx8CzozOaNfPHRm-0,18003
|
|
23
|
+
astreum/node/models.py,sha256=CBYuMJVD0uI5i6fWSFE2egkZXtFQHvR79kKTU4KP1WE,3086
|
|
24
|
+
astreum/node/relay/__init__.py,sha256=i_uD8ZMuA8Xa4vdzVAdiyd5uhIv-7-n9SBUG7WiL8mk,10024
|
|
25
|
+
astreum/node/relay/bucket.py,sha256=w0jzOrSUVT5WmRW_ggmtBcdf8sgZW-_MTeT4YQuhAvg,2211
|
|
26
|
+
astreum/node/relay/envelope.py,sha256=TfkynttoPX7smvMV7xEAdtIlfz-Z-EZjuhZ826csZxA,10078
|
|
27
|
+
astreum/node/relay/message.py,sha256=9j3Q5T5VVj47g0xqxUCWxtf2EFXutIPbj0mt1KbVw3M,2812
|
|
28
|
+
astreum/node/relay/peer.py,sha256=eOml9G4xMoQeR_BuONVgJvpvCwhQo-QXCpRhShAxz4Q,5721
|
|
29
|
+
astreum/node/relay/route.py,sha256=ZyWoTC2EXX-SU_80fFS0TP-lITH6e9mDeS-irsfcMp4,4303
|
|
30
|
+
astreum/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
+
astreum/utils/bytes_format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
|
|
32
|
+
astreum-0.1.6.dist-info/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
|
|
33
|
+
astreum-0.1.6.dist-info/METADATA,sha256=b_LZeCHI1_ST9e53ACed8__f9NwZkEUTA7qwSu9oep0,741
|
|
34
|
+
astreum-0.1.6.dist-info/WHEEL,sha256=EaM1zKIUYa7rQnxGiOCGhzJABRwy4WO57rWMR3_tj4I,91
|
|
35
|
+
astreum-0.1.6.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
|
|
36
|
+
astreum-0.1.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|