astreum 0.2.6__py3-none-any.whl → 0.2.8__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.

@@ -0,0 +1,123 @@
1
+ import math
2
+
3
+
4
+ def extended_gcd(a: int, b: int) -> tuple[int, int, int]:
5
+ """
6
+ Return (g, x, y) such that a*x + b*y = g = gcd(a, b).
7
+ """
8
+ if b == 0:
9
+ return (a, 1, 0)
10
+ g, x1, y1 = extended_gcd(b, a % b)
11
+ return (g, y1, x1 - (a // b) * y1)
12
+
13
+
14
+ def modinv(a: int, m: int) -> int:
15
+ """
16
+ Return modular inverse of a mod m.
17
+ """
18
+ g, x, _ = extended_gcd(a, m)
19
+ if g != 1:
20
+ raise ValueError(f"No modular inverse for {a} modulo {m}")
21
+ return x % m
22
+
23
+
24
+ def is_reduced(q: 'QuadraticForm') -> bool:
25
+ """
26
+ Check if the form q is in reduced (Gauss) form:
27
+ |b| <= a <= c, and if a == c then b >= 0.
28
+ """
29
+ return abs(q.b) <= q.a <= q.c and not (q.a == q.c and q.b < 0)
30
+
31
+
32
+ def is_primitive(a: int, b: int, c: int) -> bool:
33
+ """
34
+ Check if the form coefficients are coprime: gcd(a, b, c) == 1.
35
+ """
36
+ return math.gcd(math.gcd(a, b), c) == 1
37
+
38
+
39
+ class QuadraticForm:
40
+ """
41
+ Represents the binary quadratic form ax^2 + bxy + cy^2 with discriminant D,
42
+ stored in reduced, primitive form.
43
+ """
44
+
45
+ def __init__(self, a: int, b: int, c: int, D: int):
46
+ if b*b - 4*a*c != D:
47
+ raise ValueError(f"Discriminant mismatch: b^2 - 4ac = {b*b - 4*a*c}, expected {D}")
48
+ if not is_primitive(a, b, c):
49
+ raise ValueError("Form coefficients are not coprime (not primitive)")
50
+ self.a = a
51
+ self.b = b
52
+ self.c = c
53
+ self.D = D
54
+
55
+ def reduce(self) -> 'QuadraticForm':
56
+ """
57
+ Perform Gauss reduction until the form is reduced.
58
+ """
59
+ while not is_reduced(self):
60
+ self._gauss_step()
61
+ return self
62
+
63
+ def _gauss_step(self) -> None:
64
+ """
65
+ One iteration of Gauss reduction on the current form.
66
+ """
67
+ a, b, c = self.a, self.b, self.c
68
+ # Compute m = round(b / (2a)) using integer arithmetic
69
+ sign = 1 if b >= 0 else -1
70
+ m = (b + sign * a) // (2 * a)
71
+ # Update b and c
72
+ b_new = b - 2 * m * a
73
+ c_new = m * m * a - m * b + c
74
+ # Assign back
75
+ self.b = b_new
76
+ self.c = c_new
77
+ # Swap if needed to ensure a <= c and proper sign
78
+ if a > self.c or (a == self.c and self.b < 0):
79
+ self.a, self.b, self.c = self.c, -self.b, a
80
+
81
+ def __mul__(self, other: 'QuadraticForm') -> 'QuadraticForm':
82
+ """
83
+ Dirichlet (NUCOMP) composition of two forms of the same discriminant.
84
+ """
85
+ if self.D != other.D:
86
+ raise ValueError("Cannot compose forms with different discriminants")
87
+ a1, b1, c1 = self.a, self.b, self.c
88
+ a2, b2, c2 = other.a, other.b, other.c
89
+ D = self.D
90
+ # Compute g = gcd(a1, a2, (b1 + b2)//2)
91
+ k = (b1 + b2) // 2
92
+ g = math.gcd(math.gcd(a1, a2), k)
93
+ a1p = a1 // g
94
+ a2p = a2 // g
95
+ # Solve m * a1p ≡ (b2 - b1)//2 mod a2p
96
+ diff = (b2 - b1) // 2
97
+ inv = modinv(a1p, a2p)
98
+ m = (diff * inv) % a2p
99
+ # Compute composed coefficients
100
+ b3 = b1 + 2 * m * a1
101
+ a3 = a1 * a2p
102
+ c3 = (b3 * b3 - D) // (4 * a3)
103
+ return QuadraticForm(a3, b3, c3, D).reduce()
104
+
105
+ def to_bytes(self) -> bytes:
106
+ """
107
+ Serialize this form to bytes (a and b, big-endian, fixed width).
108
+ """
109
+ # Width: enough to hold |D| bitlength / 8 rounded up
110
+ width = (self.D.bit_length() + 15) // 8
111
+ return self.a.to_bytes(width, 'big', signed=True) + \
112
+ self.b.to_bytes(width, 'big', signed=True)
113
+
114
+ @classmethod
115
+ def from_bytes(cls, data: bytes, D: int) -> 'QuadraticForm':
116
+ """
117
+ Deserialize bytes back into a QuadraticForm for discriminant D.
118
+ """
119
+ width = len(data) // 2
120
+ a = int.from_bytes(data[:width], 'big', signed=True)
121
+ b = int.from_bytes(data[width:], 'big', signed=True)
122
+ c = (b*b - D) // (4 * a)
123
+ return cls(a, b, c, D).reduce()
@@ -0,0 +1,154 @@
1
+ import hashlib
2
+ from typing import Tuple
3
+ from quadratic_form import QuadraticForm
4
+
5
+ # --- Helper functions ---------------------------------------------------
6
+
7
+ def hash_to_int(*args: bytes) -> int:
8
+ """
9
+ Hash the concatenation of args (bytes) to a large integer using SHA-256.
10
+ """
11
+ h = hashlib.sha256()
12
+ for b in args:
13
+ h.update(b)
14
+ return int.from_bytes(h.digest(), 'big')
15
+
16
+ # --- Class-group VDF functions using QuadraticForm ----------------------
17
+
18
+ def group_mul(x: QuadraticForm, y: QuadraticForm) -> QuadraticForm:
19
+ """
20
+ Compose two class-group elements via QuadraticForm multiplication.
21
+ """
22
+ return (x * y)
23
+
24
+
25
+ def identity(D: int) -> QuadraticForm:
26
+ """
27
+ Return the identity element of the class group for discriminant D.
28
+ """
29
+ # For D ≡ 1 mod 4, identity form is (1, 1, (1-D)//4)
30
+ b0 = 1
31
+ c0 = (b0*b0 - D) // 4
32
+ return QuadraticForm(1, b0, c0, D)
33
+
34
+
35
+ def class_group_square(x: QuadraticForm) -> QuadraticForm:
36
+ """
37
+ One sequential squaring step in the class group.
38
+ """
39
+ return group_mul(x, x)
40
+
41
+
42
+ def group_exp(x: QuadraticForm, exponent: int) -> QuadraticForm:
43
+ """
44
+ Fast exponentiation in the class group by repeated squaring.
45
+ """
46
+ result = identity(x.D)
47
+ base = x
48
+ e = exponent
49
+ while e > 0:
50
+ if e & 1:
51
+ result = group_mul(result, base)
52
+ base = group_mul(base, base)
53
+ e >>= 1
54
+ return result
55
+
56
+ # --- Wesolowski proof and verify ----------------------------------------
57
+
58
+ def compute_wesolowski_proof(
59
+ x0: QuadraticForm,
60
+ y: QuadraticForm,
61
+ T: int
62
+ ) -> QuadraticForm:
63
+ """
64
+ Compute the Wesolowski proof π for VDF evaluation:
65
+ Solve 2^T = c * q + r, where
66
+ c = hash(x0 || y || T)
67
+ Return π = x0^q in the class group.
68
+ """
69
+ # Derive challenge c
70
+ h_bytes = serialize(x0) + serialize(y) + T.to_bytes((T.bit_length()+7)//8, 'big')
71
+ c = hash_to_int(h_bytes)
72
+ # Divide exponent
73
+ two_T = 1 << T
74
+ q, r = divmod(two_T, c)
75
+ # π = x0^q
76
+ return group_exp(x0, q)
77
+
78
+
79
+ def verify_wesolowski_proof(
80
+ x0: QuadraticForm,
81
+ y: QuadraticForm,
82
+ pi: QuadraticForm,
83
+ T: int
84
+ ) -> bool:
85
+ """
86
+ Verify π satisfies: π^c * x0^r == y.
87
+ """
88
+ h_bytes = serialize(x0) + serialize(y) + T.to_bytes((T.bit_length()+7)//8, 'big')
89
+ c = hash_to_int(h_bytes)
90
+ two_T = 1 << T
91
+ q, r = divmod(two_T, c)
92
+ lhs = group_mul(group_exp(pi, c), group_exp(x0, r))
93
+ return lhs == y
94
+
95
+ # --- Serialization helpers ----------------------------------------------
96
+
97
+ def serialize(x: QuadraticForm) -> bytes:
98
+ """
99
+ Serialize a QuadraticForm to bytes.
100
+ """
101
+ return x.to_bytes()
102
+
103
+
104
+ def deserialize(data: bytes, D: int) -> QuadraticForm:
105
+ """
106
+ Deserialize bytes into a QuadraticForm of discriminant D.
107
+ """
108
+ return QuadraticForm.from_bytes(data, D)
109
+
110
+ # --- Public VDF API -----------------------------------------------------
111
+
112
+ def generate(
113
+ old_output: bytes,
114
+ T: int,
115
+ D: int
116
+ ) -> Tuple[bytes, bytes]:
117
+ """
118
+ Evaluate the VDF by sequentially squaring the previous output 'T' times,
119
+ then produce a Wesolowski proof.
120
+
121
+ Returns:
122
+ new_output : serialized new VDF output (y)
123
+ proof : serialized proof (π)
124
+ """
125
+ # Decode previous output
126
+ x0 = deserialize(old_output, D)
127
+ # Sequential squarings
128
+ x = x0
129
+ for _ in range(T):
130
+ x = class_group_square(x)
131
+ # Serialize output
132
+ y_bytes = serialize(x)
133
+ # Compute proof
134
+ pi = compute_wesolowski_proof(x0, x, T)
135
+ proof_bytes = serialize(pi)
136
+ return y_bytes, proof_bytes
137
+
138
+
139
+ def verify(
140
+ old_output: bytes,
141
+ new_output: bytes,
142
+ proof: bytes,
143
+ T: int,
144
+ D: int
145
+ ) -> bool:
146
+ """
147
+ Verify the Wesolowski VDF proof.
148
+
149
+ Returns True if valid, False otherwise.
150
+ """
151
+ x0 = deserialize(old_output, D)
152
+ y = deserialize(new_output, D)
153
+ pi = deserialize(proof, D)
154
+ return verify_wesolowski_proof(x0, y, pi, T)
astreum/node.py CHANGED
@@ -289,23 +289,39 @@ class Expr:
289
289
  return f'(error "{self.message}" in {self.origin})'
290
290
 
291
291
  class Env:
292
- def __init__(self, parent_id: uuid.UUID = None):
293
- self.data: Dict[str, Expr] = {}
294
- self.parent_id = parent_id
295
-
296
- def put(self, name: str, value: Expr):
292
+ def __init__(
293
+ self,
294
+ data: Optional[Dict[str, Expr]] = None,
295
+ parent_id: Optional[uuid.UUID] = None,
296
+ max_exprs: Optional[int] = 8,
297
+ ):
298
+ self.data: Dict[str, Expr] = data if data is not None else {}
299
+ self.parent_id: Optional[uuid.UUID] = parent_id
300
+ self.max_exprs: Optional[int] = max_exprs
301
+
302
+ def put(self, name: str, value: Expr) -> None:
303
+ if (
304
+ self.max_exprs is not None
305
+ and name not in self.data
306
+ and len(self.data) >= self.max_exprs
307
+ ):
308
+ raise RuntimeError(
309
+ f"environment full: {len(self.data)} ≥ max_exprs={self.max_exprs}"
310
+ )
297
311
  self.data[name] = value
298
312
 
299
313
  def get(self, name: str) -> Optional[Expr]:
300
- if name in self.data:
301
- return self.data[name]
302
- elif self.parent is not None:
303
- return self.parent.get(name)
304
- else:
305
- return None
314
+ return self.data.get(name)
315
+
316
+ def pop(self, name: str) -> Optional[Expr]:
317
+ return self.data.pop(name, None)
306
318
 
307
- def __repr__(self):
308
- return f"Env({self.data})"
319
+ def __repr__(self) -> str:
320
+ return (
321
+ f"Env(size={len(self.data)}, "
322
+ f"max_exprs={self.max_exprs}, "
323
+ f"parent_id={self.parent_id})"
324
+ )
309
325
 
310
326
 
311
327
  class Node:
@@ -706,14 +722,19 @@ class Node:
706
722
  self.environments[env_id] = Env(parent_id=parent_id)
707
723
  return env_id
708
724
 
709
- def machine_get_or_create_environment(self, env_id: Optional[uuid.UUID] = None, parent_id: Optional[uuid.UUID] = None) -> uuid.UUID:
725
+ def machine_get_or_create_environment(
726
+ self,
727
+ env_id: Optional[uuid.UUID] = None,
728
+ parent_id: Optional[uuid.UUID] = None,
729
+ max_exprs: Optional[int] = None
730
+ ) -> uuid.UUID:
710
731
  with self.machine_environments_lock:
711
732
  if env_id is not None and env_id in self.environments:
712
733
  return env_id
713
734
  new_id = env_id if env_id is not None else uuid.uuid4()
714
735
  while new_id in self.environments:
715
736
  new_id = uuid.uuid4()
716
- self.environments[new_id] = Env(parent_id=parent_id)
737
+ self.environments[new_id] = Env(parent_id=parent_id, max_exprs=max_exprs)
717
738
  return new_id
718
739
 
719
740
  def machine_delete_environment(self, env_id: uuid.UUID) -> bool:
@@ -911,120 +932,90 @@ class Node:
911
932
  # env=env,
912
933
  # )
913
934
 
914
- # Integer
935
+ # Integer arithmetic primitives
915
936
  elif first.value == "+":
916
937
  args = expr.elements[1:]
917
- if len(args) == 0:
918
- return Expr.Error(message="'+' expects at least 1 argument", origin=expr)
919
- evaluated_args = []
920
- for arg in args:
921
- val = self.machine_expr_eval(env_id==env_id, expr=arg)
922
- if isinstance(val, Expr.Error):
923
- return val
924
- evaluated_args.append(val)
925
- if not all(isinstance(val, Expr.Integer) for val in evaluated_args):
926
- offending = next(val for val in evaluated_args if not isinstance(val, Expr.Integer))
927
- return Expr.Error(message="'+' only accepts integer operands", origin=offending)
928
- result = sum(val.value for val in evaluated_args)
938
+ if not args:
939
+ return Expr.Error("'+' expects at least 1 argument", origin=expr)
940
+ vals = [self.machine_expr_eval(env_id=env_id, expr=a) for a in args]
941
+ for v in vals:
942
+ if isinstance(v, Expr.Error): return v
943
+ if not isinstance(v, Expr.Integer):
944
+ return Expr.Error("'+' only accepts integer operands", origin=v)
945
+ return Expr.Integer(abs(vals[0].value) if len(vals) == 1
946
+ else sum(v.value for v in vals))
947
+
948
+ elif first.value == "-":
949
+ args = expr.elements[1:]
950
+ if not args:
951
+ return Expr.Error("'-' expects at least 1 argument", origin=expr)
952
+ vals = [self.machine_expr_eval(env_id=env_id, expr=a) for a in args]
953
+ for v in vals:
954
+ if isinstance(v, Expr.Error): return v
955
+ if not isinstance(v, Expr.Integer):
956
+ return Expr.Error("'-' only accepts integer operands", origin=v)
957
+ if len(vals) == 1:
958
+ return Expr.Integer(-vals[0].value)
959
+ result = vals[0].value
960
+ for v in vals[1:]:
961
+ result -= v.value
929
962
  return Expr.Integer(result)
930
-
931
- # # Subtraction
932
- # elif first.value == "-":
933
- # evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
934
-
935
- # # Check for non-integer arguments
936
- # if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
937
- # return Expr.Error(
938
- # category="TypeError",
939
- # message="All arguments to - must be integers"
940
- # )
941
-
942
- # # With only one argument, negate it
943
- # if len(evaluated_args) == 1:
944
- # return Expr.Integer(-evaluated_args[0].value)
945
-
946
- # # With multiple arguments, subtract all from the first
947
- # result = evaluated_args[0].value
948
- # for arg in evaluated_args[1:]:
949
- # result -= arg.value
950
-
951
- # return Expr.Integer(result)
952
-
953
- # # Multiplication
954
- # elif first.value == "*":
955
- # evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
956
963
 
957
- # # Check for non-integer arguments
958
- # if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
959
- # return Expr.Error(
960
- # category="TypeError",
961
- # message="All arguments to * must be integers"
962
- # )
963
-
964
- # # Multiply all values
965
- # result = 1
966
- # for arg in evaluated_args:
967
- # result *= arg.value
968
-
969
- # return Expr.Integer(result)
970
-
971
- # # Division (integer division)
972
- # elif first.value == "/":
973
- # evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
974
-
975
- # # Check for non-integer arguments
976
- # if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
977
- # return Expr.Error(
978
- # category="TypeError",
979
- # message="All arguments to / must be integers"
980
- # )
981
-
982
- # # Need exactly two arguments
983
- # if len(evaluated_args) != 2:
984
- # return Expr.Error(
985
- # category="ArgumentError",
986
- # message="The / operation requires exactly two arguments"
987
- # )
988
-
989
- # dividend = evaluated_args[0].value
990
- # divisor = evaluated_args[1].value
991
-
992
- # if divisor == 0:
993
- # return Expr.Error(
994
- # category="DivisionError",
995
- # message="Division by zero"
996
- # )
997
-
998
- # return Expr.Integer(dividend // divisor) # Integer division
999
-
1000
- # # Remainder (modulo)
1001
- # elif first.value == "%":
1002
- # evaluated_args = [self.evaluate_expression(arg, env) for arg in expr.elements[1:]]
964
+ elif first.value == "/":
965
+ args = expr.elements[1:]
966
+ if len(args) < 2:
967
+ return Expr.Error("'/' expects at least 2 arguments", origin=expr)
968
+ vals = [self.machine_expr_eval(env_id=env_id, expr=a) for a in args]
969
+ for v in vals:
970
+ if isinstance(v, Expr.Error): return v
971
+ if not isinstance(v, Expr.Integer):
972
+ return Expr.Error("'/' only accepts integer operands", origin=v)
973
+ result = vals[0].value
974
+ for v in vals[1:]:
975
+ if v.value == 0:
976
+ return Expr.Error("division by zero", origin=v)
977
+ if result % v.value:
978
+ return Expr.Error("non-exact division", origin=expr)
979
+ result //= v.value
980
+ return Expr.Integer(result)
1003
981
 
1004
- # # Check for non-integer arguments
1005
- # if not all(isinstance(arg, Expr.Integer) for arg in evaluated_args):
1006
- # return Expr.Error(
1007
- # category="TypeError",
1008
- # message="All arguments to % must be integers"
1009
- # )
1010
-
1011
- # # Need exactly two arguments
1012
- # if len(evaluated_args) != 2:
1013
- # return Expr.Error(
1014
- # category="ArgumentError",
1015
- # message="The % operation requires exactly two arguments"
1016
- # )
1017
-
1018
- # dividend = evaluated_args[0].value
1019
- # divisor = evaluated_args[1].value
1020
-
1021
- # if divisor == 0:
1022
- # return Expr.Error(
1023
- # category="DivisionError",
1024
- # message="Modulo by zero"
1025
- # )
1026
-
1027
- # return Expr.Integer(dividend % divisor)
982
+ elif first.value == "%":
983
+ if len(expr.elements) != 3:
984
+ return Expr.Error("'%' expects exactly 2 arguments", origin=expr)
985
+ a = self.machine_expr_eval(env_id=env_id, expr=expr.elements[1])
986
+ b = self.machine_expr_eval(env_id=env_id, expr=expr.elements[2])
987
+ for v in (a, b):
988
+ if isinstance(v, Expr.Error): return v
989
+ if not isinstance(v, Expr.Integer):
990
+ return Expr.Error("'%' only accepts integer operands", origin=v)
991
+ if b.value == 0:
992
+ return Expr.Error("division by zero", origin=expr.elements[2])
993
+ return Expr.Integer(a.value % b.value)
994
+
995
+ elif first.value in ("=", "!=", ">", "<", ">=", "<="):
996
+ args = expr.elements[1:]
997
+ if len(args) != 2:
998
+ return Expr.Error(f"'{first.value}' expects exactly 2 arguments", origin=expr)
999
+
1000
+ left = self.machine_expr_eval(env_id=env_id, expr=args[0])
1001
+ right = self.machine_expr_eval(env_id=env_id, expr=args[1])
1002
+
1003
+ for v in (left, right):
1004
+ if isinstance(v, Expr.Error):
1005
+ return v
1006
+ if not isinstance(v, Expr.Integer):
1007
+ return Expr.Error(f"'{first.value}' only accepts integer operands", origin=v)
1008
+
1009
+ a, b = left.value, right.value
1010
+ match first.value:
1011
+ case "=": res = a == b
1012
+ case "!=": res = a != b
1013
+ case ">": res = a > b
1014
+ case "<": res = a < b
1015
+ case ">=": res = a >= b
1016
+ case "<=": res = a <= b
1017
+
1018
+ return Expr.Boolean(res)
1028
1019
 
1029
1020
  else:
1030
1021
  evaluated_elements = [self.machine_expr_eval(env_id=env_id, expr=e) for e in expr.elements]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.6
3
+ Version: 0.2.8
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,18 +1,20 @@
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=dlgnGmVq5zn3XNcGr5CjWniOpBSkqvgrenOY4IJxiGc,46383
3
+ astreum/node.py,sha256=SswYgBq6-iJyD6dWbo3nf0_QrjNl_KifPSJsFMJ2z1Q,45941
4
4
  astreum/_node/__init__.py,sha256=7yz1YHo0DCUgUQvJf75qdUo_ocl5-XZRU-Vc2NhcvJs,18639
5
5
  astreum/_node/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  astreum/_node/storage/merkle.py,sha256=XCQBrHbwI0FuPTCUwHOy-Kva3uWbvCdw_-13hRPf1UI,10219
7
7
  astreum/_node/storage/patricia.py,sha256=tynxn_qETCU9X7yJdeh_0GHpC8Pzcoq4CWrSZlMUeRc,11546
8
8
  astreum/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  astreum/crypto/ed25519.py,sha256=FRnvlN0kZlxn4j-sJKl-C9tqiz_0z4LZyXLj3KIj1TQ,1760
10
+ astreum/crypto/quadratic_form.py,sha256=pJgbORey2NTWbQNhdyvrjy_6yjORudQ67jBz2ScHptg,4037
11
+ astreum/crypto/wesolowski.py,sha256=FDAX82L5cceR6DGTtUO57ZhcxpBNiskGrnLWnd_3BSw,4084
10
12
  astreum/crypto/x25519.py,sha256=i29v4BmwKRcbz9E7NKqFDQyxzFtJUqN0St9jd7GS1uA,1137
11
13
  astreum/lispeum/__init__.py,sha256=K-NDzIjtIsXzC9X7lnYvlvIaVxjFcY7WNsgLIE3DH3U,58
12
14
  astreum/lispeum/parser.py,sha256=jQRzZYvBuSg8t_bxsbt1-WcHaR_LPveHNX7Qlxhaw-M,1165
13
15
  astreum/lispeum/tokenizer.py,sha256=J-I7MEd0r2ZoVqxvRPlu-Afe2ZdM0tKXXhf1R4SxYTo,1429
14
- astreum-0.2.6.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
15
- astreum-0.2.6.dist-info/METADATA,sha256=NRBQqKmoHeFm5VY4HpBUmPlfmlVjrFAV4ax3NvBvJx0,5453
16
- astreum-0.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- astreum-0.2.6.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
18
- astreum-0.2.6.dist-info/RECORD,,
16
+ astreum-0.2.8.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
17
+ astreum-0.2.8.dist-info/METADATA,sha256=mXlFwukobFtoWXDbgQOr__JH_9xnTfMWoginX-KoR7k,5453
18
+ astreum-0.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ astreum-0.2.8.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
20
+ astreum-0.2.8.dist-info/RECORD,,