astreum 0.2.26__py3-none-any.whl → 0.2.27__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/models/message.py +64 -0
- astreum/node.py +40 -151
- {astreum-0.2.26.dist-info → astreum-0.2.27.dist-info}/METADATA +1 -1
- {astreum-0.2.26.dist-info → astreum-0.2.27.dist-info}/RECORD +7 -6
- {astreum-0.2.26.dist-info → astreum-0.2.27.dist-info}/WHEEL +0 -0
- {astreum-0.2.26.dist-info → astreum-0.2.27.dist-info}/licenses/LICENSE +0 -0
- {astreum-0.2.26.dist-info → astreum-0.2.27.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
3
|
+
|
|
4
|
+
class MessageTopic(IntEnum):
|
|
5
|
+
PING = 0
|
|
6
|
+
OBJECT_REQUEST = 1
|
|
7
|
+
OBJECT_RESPONSE = 2
|
|
8
|
+
ROUTE_REQUEST = 3
|
|
9
|
+
ROUTE_RESPONSE = 4
|
|
10
|
+
|
|
11
|
+
class Message:
|
|
12
|
+
handshake: bool
|
|
13
|
+
sender: X25519PublicKey
|
|
14
|
+
|
|
15
|
+
topic: MessageTopic
|
|
16
|
+
content: bytes
|
|
17
|
+
|
|
18
|
+
def to_bytes(self):
|
|
19
|
+
if self.handshake:
|
|
20
|
+
# handshake byte (1) + raw public key bytes
|
|
21
|
+
return bytes([1]) + self.sender.public_bytes(
|
|
22
|
+
encoding=serialization.Encoding.Raw,
|
|
23
|
+
format=serialization.PublicFormat.Raw
|
|
24
|
+
)
|
|
25
|
+
else:
|
|
26
|
+
# normal message: 0 + topic + content
|
|
27
|
+
return bytes([0, self.topic.value]) + self.content
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_bytes(cls, data: bytes) -> "Message":
|
|
31
|
+
if len(data) < 1:
|
|
32
|
+
raise ValueError("Cannot parse Message: no data")
|
|
33
|
+
flag = data[0]
|
|
34
|
+
# create empty instance
|
|
35
|
+
msg = cls.__new__(cls)
|
|
36
|
+
|
|
37
|
+
if flag == 1:
|
|
38
|
+
# handshake message: the rest is the peer’s public key
|
|
39
|
+
key_bytes = data[1:]
|
|
40
|
+
try:
|
|
41
|
+
sender = X25519PublicKey.from_public_bytes(key_bytes)
|
|
42
|
+
except ValueError:
|
|
43
|
+
raise ValueError("Invalid public key bytes")
|
|
44
|
+
msg.handshake = True
|
|
45
|
+
msg.sender = sender
|
|
46
|
+
msg.topic = None
|
|
47
|
+
msg.content = b''
|
|
48
|
+
elif flag == 0:
|
|
49
|
+
# normal message: next byte is topic, rest is content
|
|
50
|
+
if len(data) < 2:
|
|
51
|
+
raise ValueError("Cannot parse Message: missing topic byte")
|
|
52
|
+
topic_val = data[1]
|
|
53
|
+
try:
|
|
54
|
+
topic = MessageTopic(topic_val)
|
|
55
|
+
except ValueError:
|
|
56
|
+
raise ValueError(f"Unknown MessageTopic: {topic_val}")
|
|
57
|
+
msg.handshake = False
|
|
58
|
+
msg.sender = None
|
|
59
|
+
msg.topic = topic
|
|
60
|
+
msg.content = data[2:]
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"Invalid handshake flag: {flag}")
|
|
63
|
+
|
|
64
|
+
return msg
|
astreum/node.py
CHANGED
|
@@ -15,6 +15,7 @@ from .crypto import ed25519, x25519
|
|
|
15
15
|
from enum import IntEnum
|
|
16
16
|
import blake3
|
|
17
17
|
import struct
|
|
18
|
+
from .models.message import Message, MessageTopic
|
|
18
19
|
|
|
19
20
|
class ObjectRequestType(IntEnum):
|
|
20
21
|
OBJECT_GET = 0
|
|
@@ -61,106 +62,11 @@ class ObjectResponse:
|
|
|
61
62
|
type_val, data_val, hash_val = decode(data)
|
|
62
63
|
return cls(type=ObjectResponseType(type_val[0]), data=data_val, hash=hash_val)
|
|
63
64
|
|
|
64
|
-
class MessageTopic(IntEnum):
|
|
65
|
-
PING = 0
|
|
66
|
-
OBJECT_REQUEST = 1
|
|
67
|
-
OBJECT_RESPONSE = 2
|
|
68
|
-
ROUTE_REQUEST = 3
|
|
69
|
-
ROUTE_RESPONSE = 4
|
|
70
|
-
|
|
71
|
-
class Message:
|
|
72
|
-
body: bytes
|
|
73
|
-
topic: MessageTopic
|
|
74
|
-
|
|
75
|
-
def to_bytes(self):
|
|
76
|
-
return encode([self.body, [self.topic.value]])
|
|
77
|
-
|
|
78
|
-
@classmethod
|
|
79
|
-
def from_bytes(cls, data: bytes):
|
|
80
|
-
body, topic = decode(data)
|
|
81
|
-
return cls(body=body, topic=MessageTopic(topic[0]))
|
|
82
|
-
|
|
83
|
-
class Envelope:
|
|
84
|
-
encrypted: bool
|
|
85
|
-
message: Message
|
|
86
|
-
nonce: int
|
|
87
|
-
sender: X25519PublicKey
|
|
88
|
-
timestamp: datetime
|
|
89
|
-
|
|
90
|
-
def __init__(self, message: Message, sender: X25519PublicKey, encrypted: bool = False, nonce: int = 0, timestamp: Union[int, datetime, None] = None, difficulty: int = 1):
|
|
91
|
-
self.encrypted = encrypted
|
|
92
|
-
encrypted_bytes = b'\x01' if self.encrypted else b'\x00'
|
|
93
|
-
|
|
94
|
-
self.message = message
|
|
95
|
-
message_bytes = message.to_bytes()
|
|
96
|
-
|
|
97
|
-
self.sender = sender
|
|
98
|
-
self.sender_bytes = sender.public_bytes()
|
|
99
|
-
|
|
100
|
-
self.nonce = nonce
|
|
101
|
-
|
|
102
|
-
if timestamp is None:
|
|
103
|
-
self.timestamp = datetime.now(timezone.utc)
|
|
104
|
-
timestamp_int = int(self.timestamp.timestamp())
|
|
105
|
-
elif isinstance(timestamp, int):
|
|
106
|
-
self.timestamp = datetime.fromtimestamp(timestamp, timezone.utc)
|
|
107
|
-
timestamp_int = timestamp
|
|
108
|
-
elif isinstance(timestamp, datetime):
|
|
109
|
-
self.timestamp = timestamp
|
|
110
|
-
timestamp_int = int(timestamp.timestamp())
|
|
111
|
-
else:
|
|
112
|
-
raise TypeError("Timestamp must be an int (Unix timestamp), datetime object, or None")
|
|
113
|
-
|
|
114
|
-
def count_leading_zero_bits(data: bytes) -> int:
|
|
115
|
-
count = 0
|
|
116
|
-
for b in data:
|
|
117
|
-
if b == 0:
|
|
118
|
-
count += 8
|
|
119
|
-
else:
|
|
120
|
-
count += 8 - b.bit_length()
|
|
121
|
-
break
|
|
122
|
-
return count
|
|
123
|
-
|
|
124
|
-
while True:
|
|
125
|
-
envelope_bytes = encode([
|
|
126
|
-
encrypted_bytes,
|
|
127
|
-
message_bytes,
|
|
128
|
-
self.nonce,
|
|
129
|
-
self.sender_bytes,
|
|
130
|
-
timestamp_int
|
|
131
|
-
])
|
|
132
|
-
envelope_hash = blake3.blake3(envelope_bytes).digest()
|
|
133
|
-
if count_leading_zero_bits(envelope_hash) >= difficulty:
|
|
134
|
-
self.hash = envelope_hash
|
|
135
|
-
break
|
|
136
|
-
self.nonce += 1
|
|
137
|
-
|
|
138
|
-
def to_bytes(self):
|
|
139
|
-
encrypted_bytes = b'\x01' if self.encrypted else b'\x00'
|
|
140
|
-
|
|
141
|
-
return encode([
|
|
142
|
-
encrypted_bytes,
|
|
143
|
-
self.message.to_bytes(),
|
|
144
|
-
self.nonce,
|
|
145
|
-
self.sender.public_bytes(),
|
|
146
|
-
int(self.timestamp.timestamp())
|
|
147
|
-
])
|
|
148
|
-
|
|
149
|
-
@classmethod
|
|
150
|
-
def from_bytes(cls, data: bytes):
|
|
151
|
-
encrypted_bytes, message_bytes, nonce, sender_bytes, timestamp_int = decode(data)
|
|
152
|
-
return cls(
|
|
153
|
-
encrypted=(encrypted_bytes == b'\x01'),
|
|
154
|
-
message=Message.from_bytes(message_bytes),
|
|
155
|
-
nonce=nonce,
|
|
156
|
-
sender=X25519PublicKey.from_public_bytes(sender_bytes),
|
|
157
|
-
timestamp=datetime.fromtimestamp(timestamp_int, timezone.utc)
|
|
158
|
-
)
|
|
159
|
-
|
|
160
65
|
class Peer:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
66
|
+
shared_key: bytes
|
|
67
|
+
timestamp: datetime
|
|
68
|
+
def __init__(self, my_sec_key: X25519PrivateKey, peer_pub_key: X25519PublicKey):
|
|
69
|
+
self.shared_key = my_sec_key.exchange(peer_pub_key)
|
|
164
70
|
self.timestamp = datetime.now(timezone.utc)
|
|
165
71
|
|
|
166
72
|
class Route:
|
|
@@ -419,6 +325,7 @@ class Node:
|
|
|
419
325
|
self.peer_manager_thread.start()
|
|
420
326
|
|
|
421
327
|
self.peers = Dict[X25519PublicKey, Peer]
|
|
328
|
+
self.addresses = Dict[Tuple[str, int], X25519PublicKey]
|
|
422
329
|
|
|
423
330
|
if 'bootstrap' in config:
|
|
424
331
|
for addr in config['bootstrap']:
|
|
@@ -451,9 +358,8 @@ class Node:
|
|
|
451
358
|
# find the nearest peer route node to the hash and send an object request
|
|
452
359
|
closest_peer = self._get_closest_local_peer(hash)
|
|
453
360
|
if closest_peer:
|
|
454
|
-
object_request_message = Message(topic=MessageTopic.OBJECT_REQUEST,
|
|
455
|
-
|
|
456
|
-
self.outgoing_queue.put((object_request_envelope.to_bytes(), self.peers[closest_peer].address))
|
|
361
|
+
object_request_message = Message(topic=MessageTopic.OBJECT_REQUEST, content=hash)
|
|
362
|
+
self.outgoing_queue.put((object_request_message.to_bytes(), self.peers[closest_peer].address))
|
|
457
363
|
|
|
458
364
|
# wait for upto self.storage_get_relay_timeout seconds for the object to be stored/until local_object_get returns something
|
|
459
365
|
start_time = time.time()
|
|
@@ -481,30 +387,31 @@ class Node:
|
|
|
481
387
|
while True:
|
|
482
388
|
try:
|
|
483
389
|
data, addr = self.incoming_queue.get()
|
|
484
|
-
|
|
485
|
-
match
|
|
390
|
+
message = Message.from_bytes(data)
|
|
391
|
+
match message.topic:
|
|
486
392
|
case MessageTopic.PING:
|
|
487
|
-
|
|
488
|
-
|
|
393
|
+
peer_pub_key = self.addresses.get(addr)
|
|
394
|
+
if peer_pub_key in self.peers:
|
|
395
|
+
self.peers[peer_pub_key].timestamp = datetime.now(timezone.utc)
|
|
489
396
|
continue
|
|
490
397
|
|
|
491
|
-
is_validator_flag = decode(
|
|
398
|
+
is_validator_flag = decode(message.body)
|
|
492
399
|
|
|
493
|
-
if
|
|
400
|
+
if peer_pub_key not in self.peers:
|
|
494
401
|
self._send_ping(addr)
|
|
495
402
|
|
|
496
|
-
peer = Peer(self.relay_secret_key,
|
|
403
|
+
peer = Peer(my_sec_key=self.relay_secret_key, peer_pub_key=peer_pub_key)
|
|
497
404
|
self.peers[peer.sender] = peer
|
|
498
|
-
self.peer_route.add_peer(
|
|
405
|
+
self.peer_route.add_peer(peer_pub_key)
|
|
499
406
|
if is_validator_flag == [1]:
|
|
500
|
-
self.validation_route.add_peer(
|
|
407
|
+
self.validation_route.add_peer(peer_pub_key)
|
|
501
408
|
|
|
502
409
|
if peer.timestamp < datetime.now(timezone.utc) - timedelta(minutes=5.0):
|
|
503
410
|
self._send_ping(addr)
|
|
504
411
|
|
|
505
412
|
case MessageTopic.OBJECT_REQUEST:
|
|
506
413
|
try:
|
|
507
|
-
object_request = ObjectRequest.from_bytes(
|
|
414
|
+
object_request = ObjectRequest.from_bytes(message.body)
|
|
508
415
|
|
|
509
416
|
match object_request.type:
|
|
510
417
|
# -------------- OBJECT_GET --------------
|
|
@@ -519,9 +426,8 @@ class Node:
|
|
|
519
426
|
data=local_data,
|
|
520
427
|
hash=object_hash
|
|
521
428
|
)
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
self.outgoing_queue.put((env.to_bytes(), addr))
|
|
429
|
+
obj_res_msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
|
|
430
|
+
self.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
|
|
525
431
|
return # done
|
|
526
432
|
|
|
527
433
|
# 2. If we know a provider, tell the requester.
|
|
@@ -534,9 +440,8 @@ class Node:
|
|
|
534
440
|
data=provider_bytes,
|
|
535
441
|
hash=object_hash
|
|
536
442
|
)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
self.outgoing_queue.put((env.to_bytes(), addr))
|
|
443
|
+
obj_res_msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
|
|
444
|
+
self.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
|
|
540
445
|
return # done
|
|
541
446
|
|
|
542
447
|
# 3. Otherwise, direct the requester to a peer nearer to the hash.
|
|
@@ -555,14 +460,13 @@ class Node:
|
|
|
555
460
|
data=peer_info,
|
|
556
461
|
hash=object_hash
|
|
557
462
|
)
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
self.outgoing_queue.put((env.to_bytes(), addr))
|
|
463
|
+
obj_res_msg = Message(topic=MessageTopic.OBJECT_RESPONSE, body=resp.to_bytes())
|
|
464
|
+
self.outgoing_queue.put((obj_res_msg.to_bytes(), addr))
|
|
561
465
|
|
|
562
466
|
# -------------- OBJECT_PUT --------------
|
|
563
467
|
case ObjectRequestType.OBJECT_PUT:
|
|
564
468
|
# Ensure the hash is present / correct.
|
|
565
|
-
obj_hash = object_request.hash or
|
|
469
|
+
obj_hash = object_request.hash or blake3.blake3(object_request.data).digest()
|
|
566
470
|
|
|
567
471
|
nearest = self._get_closest_local_peer(obj_hash)
|
|
568
472
|
# If a strictly nearer peer exists, forward the PUT.
|
|
@@ -572,13 +476,13 @@ class Node:
|
|
|
572
476
|
data=object_request.data,
|
|
573
477
|
hash=obj_hash
|
|
574
478
|
)
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
self.outgoing_queue.put((fwd_env.to_bytes(), nearest[1].address))
|
|
479
|
+
obj_req_msg = Message(topic=MessageTopic.OBJECT_REQUEST, body=fwd_req.to_bytes())
|
|
480
|
+
self.outgoing_queue.put((obj_req_msg.to_bytes(), nearest[1].address))
|
|
578
481
|
else:
|
|
579
482
|
# We are closest → remember who can provide the object.
|
|
483
|
+
peer_pub_key = self.addresses.get(addr)
|
|
580
484
|
provider_record = encode([
|
|
581
|
-
|
|
485
|
+
peer_pub_key.public_bytes(),
|
|
582
486
|
encode_ip_address(*addr)
|
|
583
487
|
])
|
|
584
488
|
if not hasattr(self, "storage_index") or not isinstance(self.storage_index, dict):
|
|
@@ -590,13 +494,13 @@ class Node:
|
|
|
590
494
|
|
|
591
495
|
case MessageTopic.OBJECT_RESPONSE:
|
|
592
496
|
try:
|
|
593
|
-
object_response = ObjectResponse.from_bytes(
|
|
497
|
+
object_response = ObjectResponse.from_bytes(message.body)
|
|
594
498
|
if object_response.hash not in self.object_request_queue:
|
|
595
499
|
continue
|
|
596
500
|
|
|
597
501
|
match object_response.type:
|
|
598
502
|
case ObjectResponseType.OBJECT_FOUND:
|
|
599
|
-
if object_response.hash !=
|
|
503
|
+
if object_response.hash != blake3.blake3(object_response.data).digest():
|
|
600
504
|
continue
|
|
601
505
|
self.object_request_queue.remove(object_response.hash)
|
|
602
506
|
self._local_object_put(object_response.hash, object_response.data)
|
|
@@ -604,9 +508,8 @@ class Node:
|
|
|
604
508
|
case ObjectResponseType.OBJECT_PROVIDER:
|
|
605
509
|
_provider_public_key, provider_address = decode(object_response.data)
|
|
606
510
|
provider_ip, provider_port = decode_ip_address(provider_address)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
self.outgoing_queue.put((object_request_envelope.to_bytes(), (provider_ip, provider_port)))
|
|
511
|
+
obj_req_msg = Message(topic=MessageTopic.OBJECT_REQUEST, body=object_hash)
|
|
512
|
+
self.outgoing_queue.put((obj_req_msg.to_bytes(), (provider_ip, provider_port)))
|
|
610
513
|
|
|
611
514
|
case ObjectResponseType.OBJECT_NEAREST_PEER:
|
|
612
515
|
# -- decode the peer info sent back
|
|
@@ -630,23 +533,10 @@ class Node:
|
|
|
630
533
|
if self._is_closer_than_local_peers(
|
|
631
534
|
object_response.hash, nearest_peer_public_key
|
|
632
535
|
):
|
|
633
|
-
nearest_peer_ip, nearest_peer_port = decode_ip_address(
|
|
634
|
-
|
|
635
|
-
)
|
|
636
|
-
|
|
637
|
-
topic=MessageTopic.OBJECT_REQUEST,
|
|
638
|
-
body=object_response.hash,
|
|
639
|
-
)
|
|
640
|
-
object_request_envelope = Envelope(
|
|
641
|
-
message=object_request_message,
|
|
642
|
-
sender=self.relay_public_key,
|
|
643
|
-
)
|
|
644
|
-
self.outgoing_queue.put(
|
|
645
|
-
(
|
|
646
|
-
object_request_envelope.to_bytes(),
|
|
647
|
-
(nearest_peer_ip, nearest_peer_port),
|
|
648
|
-
)
|
|
649
|
-
)
|
|
536
|
+
nearest_peer_ip, nearest_peer_port = decode_ip_address(nearest_peer_address)
|
|
537
|
+
obj_req_msg = Message(topic=MessageTopic.OBJECT_REQUEST, content=object_response.hash)
|
|
538
|
+
self.outgoing_queue.put((obj_req_msg.to_bytes(), (nearest_peer_ip, nearest_peer_port),)
|
|
539
|
+
)
|
|
650
540
|
|
|
651
541
|
|
|
652
542
|
except Exception as e:
|
|
@@ -678,9 +568,8 @@ class Node:
|
|
|
678
568
|
|
|
679
569
|
def _send_ping(self, addr: Tuple[str, int]):
|
|
680
570
|
is_validator_flag = encode([1] if self.validation_secret_key else [0])
|
|
681
|
-
ping_message = Message(topic=MessageTopic.PING,
|
|
682
|
-
|
|
683
|
-
self.outgoing_queue.put((ping_envelope.to_bytes(), addr))
|
|
571
|
+
ping_message = Message(topic=MessageTopic.PING, content=is_validator_flag)
|
|
572
|
+
self.outgoing_queue.put((ping_message.to_bytes(), addr))
|
|
684
573
|
|
|
685
574
|
def _get_closest_local_peer(self, hash: bytes) -> Optional[Tuple[X25519PublicKey, Peer]]:
|
|
686
575
|
# Find the globally closest peer using XOR distance
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: astreum
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.27
|
|
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,6 +1,6 @@
|
|
|
1
1
|
astreum/__init__.py,sha256=y2Ok3EY_FstcmlVASr80lGR_0w-dH-SXDCCQFmL6uwA,28
|
|
2
2
|
astreum/format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
|
|
3
|
-
astreum/node.py,sha256=
|
|
3
|
+
astreum/node.py,sha256=DZyFQLQcdcNw-Vl3JrJzNbDKAiEqSbNQhyOLeGWGXz4,41967
|
|
4
4
|
astreum/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
astreum/crypto/ed25519.py,sha256=FRnvlN0kZlxn4j-sJKl-C9tqiz_0z4LZyXLj3KIj1TQ,1760
|
|
6
6
|
astreum/crypto/quadratic_form.py,sha256=pJgbORey2NTWbQNhdyvrjy_6yjORudQ67jBz2ScHptg,4037
|
|
@@ -14,10 +14,11 @@ astreum/models/account.py,sha256=sHujGSwtV13rvOGJ5LZXuMrJ4F9XUdvyuWKz-zJ9lkE,298
|
|
|
14
14
|
astreum/models/accounts.py,sha256=aFSEWlq6zRf65-KGAdNGqEJyNVY3fpKhx8y1vU6sgSc,1164
|
|
15
15
|
astreum/models/block.py,sha256=-5j7uO0woVtNi0h52__e7AxpDQSVhzKUhr6Qc-2xZsE,17870
|
|
16
16
|
astreum/models/merkle.py,sha256=lvWJa9nmrBL0n_2h_uNqpB_9a5s5Hn1FceRLx0IZIVQ,6778
|
|
17
|
+
astreum/models/message.py,sha256=vv8yx-ndVYjCmPM4gXRVMToCTlKY_mflPu0uKsb9iiE,2117
|
|
17
18
|
astreum/models/patricia.py,sha256=ohmXrcaz7Ae561tyC4u4iPOkQPkKr8N0IWJek4upFIg,13392
|
|
18
19
|
astreum/models/transaction.py,sha256=MkLL5YX18kIf9-O4LBaZ4eWjkXDAaYIrDcDehbDZoqg,3038
|
|
19
|
-
astreum-0.2.
|
|
20
|
-
astreum-0.2.
|
|
21
|
-
astreum-0.2.
|
|
22
|
-
astreum-0.2.
|
|
23
|
-
astreum-0.2.
|
|
20
|
+
astreum-0.2.27.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
|
|
21
|
+
astreum-0.2.27.dist-info/METADATA,sha256=nMaxqfSmJtxic-_mIXivDbg2WszmnGkDnniiX1hqDH4,5478
|
|
22
|
+
astreum-0.2.27.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
astreum-0.2.27.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
|
|
24
|
+
astreum-0.2.27.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|