astreum 0.2.29__py3-none-any.whl → 0.2.61__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.
Files changed (58) hide show
  1. astreum/__init__.py +9 -1
  2. astreum/_communication/__init__.py +11 -0
  3. astreum/{models → _communication}/message.py +101 -64
  4. astreum/_communication/peer.py +23 -0
  5. astreum/_communication/ping.py +33 -0
  6. astreum/_communication/route.py +95 -0
  7. astreum/_communication/setup.py +322 -0
  8. astreum/_communication/util.py +42 -0
  9. astreum/_consensus/__init__.py +20 -0
  10. astreum/_consensus/account.py +95 -0
  11. astreum/_consensus/accounts.py +38 -0
  12. astreum/_consensus/block.py +311 -0
  13. astreum/_consensus/chain.py +66 -0
  14. astreum/_consensus/fork.py +100 -0
  15. astreum/_consensus/genesis.py +72 -0
  16. astreum/_consensus/receipt.py +136 -0
  17. astreum/_consensus/setup.py +115 -0
  18. astreum/_consensus/transaction.py +215 -0
  19. astreum/_consensus/workers/__init__.py +9 -0
  20. astreum/_consensus/workers/discovery.py +48 -0
  21. astreum/_consensus/workers/validation.py +125 -0
  22. astreum/_consensus/workers/verify.py +63 -0
  23. astreum/_lispeum/__init__.py +16 -0
  24. astreum/_lispeum/environment.py +13 -0
  25. astreum/_lispeum/expression.py +190 -0
  26. astreum/_lispeum/high_evaluation.py +236 -0
  27. astreum/_lispeum/low_evaluation.py +123 -0
  28. astreum/_lispeum/meter.py +18 -0
  29. astreum/_lispeum/parser.py +51 -0
  30. astreum/_lispeum/tokenizer.py +22 -0
  31. astreum/_node.py +198 -0
  32. astreum/_storage/__init__.py +7 -0
  33. astreum/_storage/atom.py +109 -0
  34. astreum/_storage/patricia.py +478 -0
  35. astreum/_storage/setup.py +35 -0
  36. astreum/models/block.py +48 -39
  37. astreum/node.py +755 -563
  38. astreum/utils/bytes.py +24 -0
  39. astreum/utils/integer.py +25 -0
  40. astreum/utils/logging.py +219 -0
  41. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/METADATA +50 -14
  42. astreum-0.2.61.dist-info/RECORD +57 -0
  43. astreum/lispeum/__init__.py +0 -2
  44. astreum/lispeum/environment.py +0 -40
  45. astreum/lispeum/expression.py +0 -86
  46. astreum/lispeum/parser.py +0 -41
  47. astreum/lispeum/tokenizer.py +0 -52
  48. astreum/models/account.py +0 -91
  49. astreum/models/accounts.py +0 -34
  50. astreum/models/transaction.py +0 -106
  51. astreum/relay/__init__.py +0 -0
  52. astreum/relay/peer.py +0 -9
  53. astreum/relay/route.py +0 -25
  54. astreum/relay/setup.py +0 -58
  55. astreum-0.2.29.dist-info/RECORD +0 -33
  56. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/WHEEL +0 -0
  57. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/licenses/LICENSE +0 -0
  58. {astreum-0.2.29.dist-info → astreum-0.2.61.dist-info}/top_level.txt +0 -0
astreum/utils/bytes.py ADDED
@@ -0,0 +1,24 @@
1
+ from typing import Optional
2
+
3
+
4
+ def hex_to_bytes(value: str, *, expected_length: Optional[int] = None) -> bytes:
5
+ """Convert a 0x-prefixed hex string into raw bytes."""
6
+ if not isinstance(value, str):
7
+ raise TypeError("hex value must be provided as a string")
8
+
9
+ if not value.startswith(("0x", "0X")):
10
+ raise ValueError("hex value must start with '0x'")
11
+
12
+ hex_digits = value[2:]
13
+ if len(hex_digits) % 2:
14
+ raise ValueError("hex value must have an even number of digits")
15
+
16
+ try:
17
+ result = bytes.fromhex(hex_digits)
18
+ except ValueError as exc:
19
+ raise ValueError("hex value contains non-hexadecimal characters") from exc
20
+
21
+ if expected_length is not None and len(result) != expected_length:
22
+ raise ValueError(f"hex value must decode to exactly {expected_length} bytes")
23
+
24
+ return result
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Union
4
+
5
+ ByteLike = Union[bytes, bytearray, memoryview]
6
+
7
+
8
+ def int_to_bytes(value: Optional[int]) -> bytes:
9
+ """Convert an integer to a little-endian byte string with minimal length."""
10
+ if value is None:
11
+ return b""
12
+ value = int(value)
13
+ if value == 0:
14
+ return b"\x00"
15
+ length = (value.bit_length() + 7) // 8
16
+ return value.to_bytes(length, "little", signed=False)
17
+
18
+
19
+ def bytes_to_int(data: Optional[ByteLike]) -> int:
20
+ """Convert a little-endian byte string to an integer."""
21
+ if not data:
22
+ return 0
23
+ if isinstance(data, memoryview):
24
+ data = data.tobytes()
25
+ return int.from_bytes(bytes(data), "little", signed=False)
@@ -0,0 +1,219 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import inspect
5
+ import gzip
6
+ import json
7
+ import logging
8
+ import logging.handlers
9
+ import os
10
+ import pathlib
11
+ import platform
12
+ import queue
13
+ import shutil
14
+ from datetime import datetime, timezone
15
+ from typing import Any, Dict, Optional
16
+
17
+ from blake3 import blake3
18
+
19
+ # Fixed identity for all loggers in this library
20
+ _ORG_NAME = "Astreum"
21
+ _PRODUCT_NAME = "lib-py"
22
+
23
+
24
+ def _safe_path(path_str: str) -> Optional[pathlib.Path]:
25
+ try:
26
+ return pathlib.Path(path_str).resolve()
27
+ except Exception:
28
+ try:
29
+ return pathlib.Path(path_str).absolute()
30
+ except Exception:
31
+ return None
32
+
33
+
34
+ def _hash_path(path: pathlib.Path) -> str:
35
+ try:
36
+ data = str(path).encode("utf-8", errors="ignore")
37
+ except Exception:
38
+ data = repr(path).encode("utf-8", errors="ignore")
39
+ return blake3(data).hexdigest()
40
+
41
+
42
+ def _find_caller_path() -> pathlib.Path:
43
+ stack = inspect.stack()
44
+ candidates: list[pathlib.Path] = []
45
+ for frame_info in stack[2:]:
46
+ filename = frame_info.filename
47
+ if not filename:
48
+ continue
49
+ path = _safe_path(filename)
50
+ if path is None:
51
+ continue
52
+ candidates.append(path)
53
+ if "astreum" not in path.parts:
54
+ return path
55
+
56
+ if candidates:
57
+ return candidates[0]
58
+ return pathlib.Path.cwd()
59
+
60
+
61
+ def _derive_instance_id() -> str:
62
+ return _hash_path(_find_caller_path())[:16]
63
+
64
+
65
+ def _log_root(org: str, product: str, instance_id: str) -> pathlib.Path:
66
+ """Resolve the base directory for logs using platform defaults."""
67
+ if platform.system() == "Windows":
68
+ base = os.getenv("LOCALAPPDATA") or str(pathlib.Path.home())
69
+ return pathlib.Path(base) / org / product / "logs" / instance_id
70
+
71
+ xdg_state = os.getenv("XDG_STATE_HOME")
72
+ base_path = pathlib.Path(xdg_state) if xdg_state else pathlib.Path.home() / ".local" / "state"
73
+ return base_path / org / product / "logs" / instance_id
74
+
75
+
76
+ class JSONFormatter(logging.Formatter):
77
+ """Log record formatter that emits JSON objects per line."""
78
+
79
+ def format(self, record: logging.LogRecord) -> str: # type: ignore[override]
80
+ payload: Dict[str, Any] = {
81
+ "ts": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
82
+ "level": record.levelname,
83
+ "logger": record.name,
84
+ "msg": record.getMessage(),
85
+ "pid": record.process,
86
+ "thread": record.threadName,
87
+ "module": record.module,
88
+ "func": record.funcName,
89
+ "instance_id": getattr(record, "instance_id", None),
90
+ }
91
+
92
+ for key, value in record.__dict__.items():
93
+ if key in payload or key.startswith(("_", "msecs", "relativeCreated")):
94
+ continue
95
+ try:
96
+ json.dumps(value)
97
+ except Exception:
98
+ continue
99
+ payload[key] = value
100
+
101
+ return json.dumps(payload, ensure_ascii=False)
102
+
103
+
104
+ def _gzip_rotator(src: str, dst: str) -> None:
105
+ """Rotate the log file by gzipping it and removing the original."""
106
+ with open(src, "rb") as source, gzip.open(f"{dst}.gz", "wb") as target:
107
+ shutil.copyfileobj(source, target)
108
+ os.remove(src)
109
+
110
+
111
+ def _namer(default_name: str) -> str:
112
+ """Custom name for rotated logs: node-YYYY-MM-DD.log."""
113
+ path = pathlib.Path(default_name)
114
+ parent = path.parent
115
+ name = path.name
116
+ fragments = name.split(".log.")
117
+ if len(fragments) != 2:
118
+ return default_name
119
+ stem, date_part = fragments
120
+ return str(parent / f"{stem}-{date_part}.log")
121
+
122
+
123
+ def _human_line(record: logging.LogRecord) -> str:
124
+ """Format a record as a concise human-readable line."""
125
+ dt = datetime.fromtimestamp(record.created, tz=timezone.utc)
126
+ stamp = f"{dt:%Y-%m-%d}-{dt:%S}-{dt:%M}"
127
+ return f"[{stamp}] [{record.levelname.lower()}] {record.getMessage()}"
128
+
129
+
130
+ class HumanFormatter(logging.Formatter):
131
+ """Simple formatter for optional verbose console output."""
132
+
133
+ def format(self, record: logging.LogRecord) -> str: # type: ignore[override]
134
+ return _human_line(record)
135
+
136
+
137
+ def _shutdown_listener(listener: logging.handlers.QueueListener, handlers: list[logging.Handler]) -> None:
138
+ """Stop the queue listener and close handlers on interpreter exit."""
139
+ try:
140
+ listener.stop()
141
+ except Exception:
142
+ pass
143
+ finally:
144
+ for handler in handlers:
145
+ try:
146
+ handler.close()
147
+ except Exception:
148
+ pass
149
+
150
+
151
+ def logging_setup(config: dict) -> logging.LoggerAdapter:
152
+ """Configure logging according to the runtime config and return an adapter."""
153
+ if config is None:
154
+ config = {}
155
+ elif not isinstance(config, dict):
156
+ config = dict(config)
157
+
158
+ org = _ORG_NAME
159
+ product = _PRODUCT_NAME
160
+ instance_id = _derive_instance_id()
161
+
162
+ retention_value = config.get("retention_days")
163
+ retention_days = int(retention_value) if retention_value is not None else 90
164
+
165
+ verbose = bool(config.get("verbose", False))
166
+
167
+ log_dir = _log_root(org, product, instance_id)
168
+ log_dir.mkdir(parents=True, exist_ok=True)
169
+
170
+ base_file = log_dir / "node.log"
171
+ file_handler = logging.handlers.TimedRotatingFileHandler(
172
+ filename=str(base_file),
173
+ when="midnight",
174
+ interval=1,
175
+ backupCount=max(retention_days, 0),
176
+ utc=True,
177
+ encoding="utf-8",
178
+ delay=True,
179
+ )
180
+ file_handler.setFormatter(JSONFormatter())
181
+ file_handler.rotator = _gzip_rotator
182
+ file_handler.namer = _namer
183
+
184
+ handler_list: list[logging.Handler] = [file_handler]
185
+
186
+ if verbose:
187
+ console_handler = logging.StreamHandler()
188
+ console_handler.setLevel(logging.INFO)
189
+ console_handler.setFormatter(HumanFormatter())
190
+ handler_list.append(console_handler)
191
+
192
+ log_queue: queue.Queue[logging.LogRecord] = queue.Queue(-1)
193
+ queue_handler = logging.handlers.QueueHandler(log_queue)
194
+
195
+ base_logger = logging.getLogger(f"{product}.{instance_id}")
196
+ base_logger.setLevel(logging.INFO)
197
+ base_logger.handlers.clear()
198
+ base_logger.propagate = False
199
+ base_logger.addHandler(queue_handler)
200
+
201
+ listener = logging.handlers.QueueListener(
202
+ log_queue, *handler_list, respect_handler_level=True
203
+ )
204
+ listener.daemon = True
205
+ listener.start()
206
+ atexit.register(_shutdown_listener, listener, handler_list)
207
+
208
+ adapter = logging.LoggerAdapter(base_logger, {"instance_id": instance_id})
209
+ setattr(adapter, "_queue_listener", listener)
210
+ setattr(adapter, "_handlers", handler_list)
211
+
212
+ return adapter
213
+
214
+
215
+ __all__ = [
216
+ "HumanFormatter",
217
+ "JSONFormatter",
218
+ "logging_setup",
219
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: astreum
3
- Version: 0.2.29
3
+ Version: 0.2.61
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
@@ -35,6 +35,8 @@ When initializing an `astreum.Node`, pass a dictionary with any of the options b
35
35
  | `validation_secret_key` | hex string | `None` | X25519 private key that lets the node participate in the validation route. Leave unset for a non‑validator node. |
36
36
  | `storage_path` | string | `None` | Directory where objects are persisted. If *None*, the node uses an in‑memory store. |
37
37
  | `storage_get_relay_timeout` | float | `5` | Seconds to wait for an object requested from peers before timing‑out. |
38
+ | `logging_retention` | int | `90` | Number of days to keep rotated log files (daily gzip). |
39
+ | `verbose` | bool | `False` | When **True**, also mirror JSON logs to stdout with a human-readable format. |
38
40
 
39
41
  ### Networking
40
42
 
@@ -76,24 +78,45 @@ node = Node(config)
76
78
  The Lispeum virtual machine (VM) is embedded inside `astreum.Node`. You feed it Lispeum source text, and the node tokenizes, parses, and **evaluates** the resulting AST inside an isolated environment.
77
79
 
78
80
  ```python
79
- from astreum.node import Node
80
- from astreum.machine.tokenizer import tokenize
81
- from astreum.machine.parser import parse
81
+ # Define a named function int.add (stack body) and call it with bytes 1 and 2
82
+
83
+ import uuid
84
+ from astreum import Node, Env, Expr
85
+
86
+ # 1) Spin‑up a stand‑alone VM
87
+ node = Node()
88
+
89
+ # 2) Create an environment (simple manual setup)
90
+ env_id = uuid.uuid4()
91
+ node.environments[env_id] = Env()
92
+
93
+ # 3) Build a function value using a low‑level stack body via `sk`.
94
+ # Body does: $0 $1 add (i.e., a + b)
95
+ low_body = Expr.ListExpr([
96
+ Expr.Symbol("$0"), # a (first arg)
97
+ Expr.Symbol("$1"), # b (second arg)
98
+ Expr.Symbol("add"),
99
+ ])
82
100
 
83
- # 1. Spin‑up a stand‑alone VM (machine‑only node).
84
- node = Node({"machine-only": True})
101
+ fn_body = Expr.ListExpr([
102
+ Expr.Symbol("a"),
103
+ Expr.Symbol("b"),
104
+ Expr.ListExpr([low_body, Expr.Symbol("sk")]),
105
+ ])
85
106
 
86
- # 2. Create an environment.
87
- env_id = node.machine_create_environment()
107
+ params = Expr.ListExpr([Expr.Symbol("a"), Expr.Symbol("b")])
108
+ int_add_fn = Expr.ListExpr([fn_body, params, Expr.Symbol("fn")])
88
109
 
89
- # 3. Convert Lispeum source Expr AST.
90
- source = '(+ 1 (* 2 3))'
91
- expr, _ = parse(tokenize(source))
110
+ # 4) Store under the name "int.add"
111
+ node.env_set(env_id, b"int.add", int_add_fn)
92
112
 
93
- # 4. Evaluate
94
- result = node.machine_expr_eval(env_id=env_id, expr=expr) # -> Expr.Integer(7)
113
+ # 5) Retrieve the function and call it with bytes 1 and 2
114
+ bound = node.env_get(env_id, b"int.add")
115
+ call = Expr.ListExpr([Expr.Byte(1), Expr.Byte(2), bound])
116
+ res = node.high_eval(env_id, call)
95
117
 
96
- print(result.value) # 7
118
+ # sk returns a list of bytes; for 1+2 expect a single byte with value 3
119
+ print([b.value for b in res.elements]) # [3]
97
120
  ```
98
121
 
99
122
  ### Handling errors
@@ -115,9 +138,22 @@ except ParseError as e:
115
138
 
116
139
  ---
117
140
 
141
+
142
+ ## Logging
143
+
144
+ Every `Node` instance wires up structured logging automatically:
145
+
146
+ - Logs land in per-instance files named `node.log` under `%LOCALAPPDATA%\Astreum\lib-py\logs/<instance_id>` on Windows and `$XDG_STATE_HOME` (or `~/.local/state`)/`Astreum/lib-py/logs/<instance_id>` on other platforms. The `<instance_id>` is the first 16 hex characters of a BLAKE3 hash of the caller's file path, so running the node from different entry points keeps their logs isolated.
147
+ - Files rotate at midnight UTC with gzip compression (`node-YYYY-MM-DD.log.gz`) and retain 90 days by default. Override via `config["logging_retention"]`.
148
+ - Each event is a single JSON line containing timestamp, level, logger, message, process/thread info, module/function, and the derived `instance_id`.
149
+ - Set `config["verbose"] = True` to mirror logs to stdout in a human-friendly format like `[2025-04-13-42-59] [info] Starting Astreum Node`.
150
+ - The very first entry emitted is the banner `Starting Astreum Node`, signalling that the logging pipeline is live before other subsystems spin up.
151
+
118
152
  ## Testing
119
153
 
120
154
  ```bash
155
+ python3 -m venv venv
121
156
  source venv/bin/activate
157
+ pip install -e .
122
158
  python3 -m unittest discover -s tests
123
159
  ```
@@ -0,0 +1,57 @@
1
+ astreum/__init__.py,sha256=9tzA27B_eG5wRF1SAWJIV7xTmCcR1QFc123b_cvFOa4,345
2
+ astreum/_node.py,sha256=S-_NsRVuMpcFxxG6ZD_C4sZU9ms6JueD7ZJKHpBSMiw,6997
3
+ astreum/format.py,sha256=X4tG5GGPweNCE54bHYkLFiuLTbmpy5upO_s1Cef-MGA,2711
4
+ astreum/node.py,sha256=MmlK3jaANTMB3ZAxR8IaSc82OS9meJmVawYIVURADbg,39689
5
+ astreum/_communication/__init__.py,sha256=XJui0yOcfAur4HKt-8sSRlwB-MSU1rchkuOAY-nKDOE,207
6
+ astreum/_communication/message.py,sha256=Wl1IITj7eY9_q0IOT4J7c5gsjS1bF51CH7GcSSuu5OM,3327
7
+ astreum/_communication/peer.py,sha256=CbqkyCwhFCiC2spd1-KjNdeVGNjjt2ECVs8uHot-ETI,875
8
+ astreum/_communication/ping.py,sha256=u_DQTZJsbMdYiDDqjdZDsLaN5na2m9WZjVeEM3zq9_Y,955
9
+ astreum/_communication/route.py,sha256=Dqw8utefBeesZ311Nyizb7PO-g5dl26AoIUavttojHA,4037
10
+ astreum/_communication/setup.py,sha256=EN2GyVfGlVtkSHMhZWwJN33y-Ycfdvq9tjr38Myrmqg,11739
11
+ astreum/_communication/util.py,sha256=bJ3td3naDzmCelAJQpLwiDMoRBkijQl9YLROjsWyOrI,1256
12
+ astreum/_consensus/__init__.py,sha256=gOCpvnIeO17CGjUGr0odaKNvGEggmDRXfT5IuyrYtcM,376
13
+ astreum/_consensus/account.py,sha256=ClMB1e07ky5Wf7BwR4yhEjTSaWK2wiCmpvzio9AsZAY,3295
14
+ astreum/_consensus/accounts.py,sha256=zGq2BCMJPtD_lzcj4jqMzB2Foc3ptxXSPhc9zypB1T0,1106
15
+ astreum/_consensus/block.py,sha256=UfnB0JdzyDpC6BlNRRnj6yfWWzK3640BZflCRrwoqM4,12088
16
+ astreum/_consensus/chain.py,sha256=WwUeLrdg7uj--ZsUxse6xFlzO2QeQQggyKSL49KhfU0,2768
17
+ astreum/_consensus/fork.py,sha256=dK5DtT9hWCj_rQs6MS1c1bcGBbkgVTBPIOudYbqS9vw,3825
18
+ astreum/_consensus/genesis.py,sha256=YOzEEcpH5lQUdRNz1P8cfeddh-IJzjTbOiVKrEPHXPg,2506
19
+ astreum/_consensus/receipt.py,sha256=woUZmfqZIj6cEOFgM2wnh_tTb7u5IBqJcXdEIaIIAAM,4537
20
+ astreum/_consensus/setup.py,sha256=KHW2KzHRgCMLWaJNV2utosKdk7nxjI5aF9ie7jctgcU,4257
21
+ astreum/_consensus/transaction.py,sha256=KE3xcs0KkZQKTh-yr7DDsTMDIS82mpTQBJ_SZDZcy0g,7844
22
+ astreum/_consensus/workers/__init__.py,sha256=bS5FjbevbIR5FHbVGnT4Jli17VIld_5auemRw4CaHFU,278
23
+ astreum/_consensus/workers/discovery.py,sha256=X1yjKGjLSApMJ9mgWbnc7N21ALD09khDf-in-M45Mis,1683
24
+ astreum/_consensus/workers/validation.py,sha256=ftlwTDocwrvCk_BAicrOJgm3deZHgwjN_gn5r-gWE_0,4900
25
+ astreum/_consensus/workers/verify.py,sha256=wNkfh5qMARew79B-yyGL2UiExaSoGpOIq0fXUkDzpdw,1925
26
+ astreum/_lispeum/__init__.py,sha256=LAy2Z-gBBQlByBHRGUKaaQrOB7QzFEEyGRtInmwTpfU,304
27
+ astreum/_lispeum/environment.py,sha256=pJ0rjp9GoQxHhDiPIVei0jP7dZ_Pznso2O_tpp94-Ik,328
28
+ astreum/_lispeum/expression.py,sha256=AJRSax65fKUb1v-fDrgJnKQOFbQVzO_XTSQ-qgECkcI,6797
29
+ astreum/_lispeum/high_evaluation.py,sha256=Z0iSlx4IduhXwWghyflVpM1alKWOLAelqWvthe9rnhY,9554
30
+ astreum/_lispeum/low_evaluation.py,sha256=j4QaUhBUCSdZVcVngQ__gUJL3FxvzyzEIcIox2J50B0,4538
31
+ astreum/_lispeum/meter.py,sha256=5q2PFW7_jmgKVM1-vwE4RRjMfPEthUA4iu1CwR-Axws,505
32
+ astreum/_lispeum/parser.py,sha256=Z_Y0Sax0rPh8JcIo19-iNDQoc5GTdGQkmfFyLpCB4bw,1757
33
+ astreum/_lispeum/tokenizer.py,sha256=P68uIj4aPKzjuCJ85jfzRi67QztpuXIOC1vvLQueBI4,552
34
+ astreum/_storage/__init__.py,sha256=14C37U3MC52oGWIp99tCC6mfWZPYBK9moapl0sXbTHI,104
35
+ astreum/_storage/atom.py,sha256=14LKD-lYsHB0wjfCYrWzXEs-doalJuEwdMb9zuIZDw0,3106
36
+ astreum/_storage/patricia.py,sha256=VPrTAE8aOvHCrY1dzrJ8IObbpEvL8bOefkp1YfLjeMM,16385
37
+ astreum/_storage/setup.py,sha256=sgBrYUn2ljs-ntsliDyKa2KugZrLJwlqOc2Ikg_3WPs,1238
38
+ astreum/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
+ astreum/crypto/ed25519.py,sha256=FRnvlN0kZlxn4j-sJKl-C9tqiz_0z4LZyXLj3KIj1TQ,1760
40
+ astreum/crypto/quadratic_form.py,sha256=pJgbORey2NTWbQNhdyvrjy_6yjORudQ67jBz2ScHptg,4037
41
+ astreum/crypto/wesolowski.py,sha256=SUgGXW3Id07dJtWzDcs4dluIhjqbRWQ8YWjn_mK78AQ,4092
42
+ astreum/crypto/x25519.py,sha256=i29v4BmwKRcbz9E7NKqFDQyxzFtJUqN0St9jd7GS1uA,1137
43
+ astreum/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
+ astreum/models/block.py,sha256=_dgas0zW9OLFV7C1Bo-laxa102klqpJrCAG3DnwQ51U,18092
45
+ astreum/models/merkle.py,sha256=lvWJa9nmrBL0n_2h_uNqpB_9a5s5Hn1FceRLx0IZIVQ,6778
46
+ astreum/models/patricia.py,sha256=ohmXrcaz7Ae561tyC4u4iPOkQPkKr8N0IWJek4upFIg,13392
47
+ astreum/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
+ astreum/storage/object.py,sha256=knFlvw_tpcC4twSu1DGNpHX31wlANN8E5dgEqIfU--Q,2041
49
+ astreum/storage/setup.py,sha256=1-9ztEFI_BvRDvAA0lAn4mFya8iq65THTArlj--M3Hg,626
50
+ astreum/utils/bytes.py,sha256=9QTWC2JCdwWLB5R2mPtmjPro0IUzE58DL3uEul4AheE,846
51
+ astreum/utils/integer.py,sha256=iQt-klWOYVghu_NOT341MmHbOle4FDT3by4PNKNXscg,736
52
+ astreum/utils/logging.py,sha256=mRDtWSCj8vKt58WGKLNSkK9Oa0graNVSoS8URby4Q9g,6684
53
+ astreum-0.2.61.dist-info/licenses/LICENSE,sha256=gYBvRDP-cPLmTyJhvZ346QkrYW_eleke4Z2Yyyu43eQ,1089
54
+ astreum-0.2.61.dist-info/METADATA,sha256=lZdmBEvNYJxK5iWuYpIXo5PUX8zOqfqMx8n7YRHhvvY,7726
55
+ astreum-0.2.61.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
+ astreum-0.2.61.dist-info/top_level.txt,sha256=1EG1GmkOk3NPmUA98FZNdKouhRyget-KiFiMk0i2Uz0,8
57
+ astreum-0.2.61.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- from .parser import parse
2
- from .tokenizer import tokenize
@@ -1,40 +0,0 @@
1
- from typing import Dict, Optional
2
- import uuid
3
-
4
- from astreum.lispeum.expression import Expr
5
-
6
-
7
- class Env:
8
- def __init__(
9
- self,
10
- data: Optional[Dict[str, Expr]] = None,
11
- parent_id: Optional[uuid.UUID] = None,
12
- max_exprs: Optional[int] = 8,
13
- ):
14
- self.data: Dict[str, Expr] = data if data is not None else {}
15
- self.parent_id: Optional[uuid.UUID] = parent_id
16
- self.max_exprs: Optional[int] = max_exprs
17
-
18
- def put(self, name: str, value: Expr) -> None:
19
- if (
20
- self.max_exprs is not None
21
- and name not in self.data
22
- and len(self.data) >= self.max_exprs
23
- ):
24
- raise RuntimeError(
25
- f"environment full: {len(self.data)} ≥ max_exprs={self.max_exprs}"
26
- )
27
- self.data[name] = value
28
-
29
- def get(self, name: str) -> Optional[Expr]:
30
- return self.data.get(name)
31
-
32
- def pop(self, name: str) -> Optional[Expr]:
33
- return self.data.pop(name, None)
34
-
35
- def __repr__(self) -> str:
36
- return (
37
- f"Env(size={len(self.data)}, "
38
- f"max_exprs={self.max_exprs}, "
39
- f"parent_id={self.parent_id})"
40
- )
@@ -1,86 +0,0 @@
1
-
2
- from typing import List, Optional, Union
3
-
4
-
5
- class Expr:
6
- class ListExpr:
7
- def __init__(self, elements: List['Expr']):
8
- self.elements = elements
9
-
10
- def __eq__(self, other):
11
- if not isinstance(other, Expr.ListExpr):
12
- return NotImplemented
13
- return self.elements == other.elements
14
-
15
- def __ne__(self, other):
16
- return not self.__eq__(other)
17
-
18
- @property
19
- def value(self):
20
- inner = " ".join(str(e) for e in self.elements)
21
- return f"({inner})"
22
-
23
-
24
- def __repr__(self):
25
- if not self.elements:
26
- return "()"
27
-
28
- inner = " ".join(str(e) for e in self.elements)
29
- return f"({inner})"
30
-
31
- def __iter__(self):
32
- return iter(self.elements)
33
-
34
- def __getitem__(self, index: Union[int, slice]):
35
- return self.elements[index]
36
-
37
- def __len__(self):
38
- return len(self.elements)
39
-
40
- class Symbol:
41
- def __init__(self, value: str):
42
- self.value = value
43
-
44
- def __repr__(self):
45
- return self.value
46
-
47
- class Integer:
48
- def __init__(self, value: int):
49
- self.value = value
50
-
51
- def __repr__(self):
52
- return str(self.value)
53
-
54
- class String:
55
- def __init__(self, value: str):
56
- self.value = value
57
-
58
- def __repr__(self):
59
- return f'"{self.value}"'
60
-
61
- class Boolean:
62
- def __init__(self, value: bool):
63
- self.value = value
64
-
65
- def __repr__(self):
66
- return "true" if self.value else "false"
67
-
68
- class Function:
69
- def __init__(self, params: List[str], body: 'Expr'):
70
- self.params = params
71
- self.body = body
72
-
73
- def __repr__(self):
74
- params_str = " ".join(self.params)
75
- body_str = str(self.body)
76
- return f"(fn ({params_str}) {body_str})"
77
-
78
- class Error:
79
- def __init__(self, message: str, origin: Optional['Expr'] = None):
80
- self.message = message
81
- self.origin = origin
82
-
83
- def __repr__(self):
84
- if self.origin is None:
85
- return f'(error "{self.message}")'
86
- return f'(error "{self.message}" in {self.origin})'
astreum/lispeum/parser.py DELETED
@@ -1,41 +0,0 @@
1
- from typing import List, Tuple
2
- from ..node import Expr
3
-
4
- class ParseError(Exception):
5
- pass
6
-
7
- def parse(tokens: List[str]) -> Tuple[Expr, List[str]]:
8
- if not tokens:
9
- raise ParseError("Unexpected end of input")
10
-
11
- first_token, *rest = tokens
12
-
13
- if first_token == '(':
14
- if not rest:
15
- raise ParseError("Expected token after '('")
16
-
17
- list_items = []
18
- inner_tokens = rest
19
-
20
- while inner_tokens:
21
- if inner_tokens[0] == ')':
22
- return Expr.ListExpr(list_items), inner_tokens[1:]
23
-
24
- expr, inner_tokens = parse(inner_tokens)
25
- list_items.append(expr)
26
-
27
- raise ParseError("Expected closing ')'")
28
-
29
- elif first_token == ')':
30
- raise ParseError("Unexpected closing parenthesis")
31
-
32
- elif first_token.startswith('"') and first_token.endswith('"'):
33
- string_content = first_token[1:-1]
34
- return Expr.String(string_content), rest
35
-
36
- else:
37
- try:
38
- number = int(first_token)
39
- return Expr.Integer(number), rest
40
- except ValueError:
41
- return Expr.Symbol(first_token), rest
@@ -1,52 +0,0 @@
1
-
2
-
3
- # Tokenizer function
4
- from typing import List
5
-
6
- class ParseError(Exception):
7
- pass
8
-
9
- def tokenize(input: str) -> List[str]:
10
- tokens = []
11
- current_token = ""
12
- in_string = False
13
- escape = False
14
-
15
- for char in input:
16
- if in_string:
17
- if escape:
18
- current_token += char
19
- escape = False
20
- elif char == '\\':
21
- escape = True
22
- elif char == '"':
23
- in_string = False
24
- tokens.append(f'"{current_token}"')
25
- current_token = ""
26
- else:
27
- current_token += char
28
- else:
29
- if char.isspace():
30
- if current_token:
31
- tokens.append(current_token)
32
- current_token = ""
33
- elif char == '(' or char == ')':
34
- if current_token:
35
- tokens.append(current_token)
36
- current_token = ""
37
- tokens.append(char)
38
- elif char == '"':
39
- if current_token:
40
- tokens.append(current_token)
41
- current_token = ""
42
- in_string = True
43
- else:
44
- current_token += char
45
-
46
- if in_string:
47
- raise ParseError("Unterminated string literal")
48
-
49
- if current_token:
50
- tokens.append(current_token)
51
-
52
- return tokens