astreum 0.1.8__tar.gz → 0.1.9__tar.gz

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.

Files changed (46) hide show
  1. {astreum-0.1.8/src/astreum.egg-info → astreum-0.1.9}/PKG-INFO +11 -7
  2. {astreum-0.1.8 → astreum-0.1.9}/README.md +8 -6
  3. {astreum-0.1.8 → astreum-0.1.9}/pyproject.toml +21 -17
  4. astreum-0.1.9/src/astreum/lispeum/storage.py +410 -0
  5. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/node/__init__.py +143 -54
  6. astreum-0.1.9/src/astreum/node/models.py +284 -0
  7. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/node/relay/__init__.py +100 -4
  8. astreum-0.1.9/src/astreum/node/relay/bucket.py +90 -0
  9. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/node/relay/message.py +17 -12
  10. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/node/relay/peer.py +3 -0
  11. astreum-0.1.9/src/astreum/node/relay/route.py +161 -0
  12. {astreum-0.1.8 → astreum-0.1.9/src/astreum.egg-info}/PKG-INFO +11 -7
  13. {astreum-0.1.8 → astreum-0.1.9}/src/astreum.egg-info/SOURCES.txt +1 -0
  14. astreum-0.1.9/src/astreum.egg-info/requires.txt +2 -0
  15. astreum-0.1.8/src/astreum/lispeum/storage.py +0 -457
  16. astreum-0.1.8/src/astreum/node/models.py +0 -96
  17. astreum-0.1.8/src/astreum/node/relay/bucket.py +0 -80
  18. astreum-0.1.8/src/astreum/node/relay/route.py +0 -125
  19. {astreum-0.1.8 → astreum-0.1.9}/LICENSE +0 -0
  20. {astreum-0.1.8 → astreum-0.1.9}/setup.cfg +0 -0
  21. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/__init__.py +0 -0
  22. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/__init__.py +0 -0
  23. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/expression.py +0 -0
  24. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/parser.py +0 -0
  25. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/__init__.py +0 -0
  26. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/definition.py +0 -0
  27. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/list/__init__.py +0 -0
  28. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/list/all.py +0 -0
  29. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/list/any.py +0 -0
  30. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/list/fold.py +0 -0
  31. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/list/get.py +0 -0
  32. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/list/insert.py +0 -0
  33. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/list/map.py +0 -0
  34. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/list/position.py +0 -0
  35. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/list/remove.py +0 -0
  36. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/number/__init__.py +0 -0
  37. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/special/number/addition.py +0 -0
  38. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/lispeum/tokenizer.py +0 -0
  39. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/machine/__init__.py +0 -0
  40. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/machine/environment.py +0 -0
  41. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/machine/error.py +0 -0
  42. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/node/relay/envelope.py +0 -0
  43. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/utils/__init__.py +0 -0
  44. {astreum-0.1.8 → astreum-0.1.9}/src/astreum/utils/bytes_format.py +0 -0
  45. {astreum-0.1.8 → astreum-0.1.9}/src/astreum.egg-info/dependency_links.txt +0 -0
  46. {astreum-0.1.8 → astreum-0.1.9}/src/astreum.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: astreum
3
- Version: 0.1.8
3
+ Version: 0.1.9
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
@@ -11,6 +11,8 @@ Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.8
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
+ Requires-Dist: pycryptodomex<4.0.0,>=3.14.1
15
+ Requires-Dist: cryptography<40.0.0,>=39.0.0
14
16
 
15
17
  # lib
16
18
 
@@ -26,9 +28,10 @@ When initializing an Astreum Node, you need to provide a configuration dictionar
26
28
 
27
29
  | Parameter | Type | Default | Description |
28
30
  |-----------|------|---------|-------------|
29
- | `node_id` | bytes | Random 32 bytes | Unique identifier for the node |
30
- | `followed_chain_id` | bytes | None | ID of the blockchain that this node follows |
31
- | `storage_path` | str | "./storage" | Directory path where node data will be stored |
31
+ | `private_key` | string | Auto-generated | Hex string of Ed25519 private key. If not provided, a new keypair will be generated automatically |
32
+ | `storage_path` | string | "storage" | Path to store data |
33
+ | `max_storage_space` | int | 1073741824 (1GB) | Maximum storage space in bytes |
34
+ | `max_object_recursion` | int | 50 | Maximum recursion depth for resolving nested objects |
32
35
 
33
36
  ### Network Configuration
34
37
 
@@ -38,15 +41,17 @@ When initializing an Astreum Node, you need to provide a configuration dictionar
38
41
  | `incoming_port` | int | 7373 | Port to listen for incoming messages |
39
42
  | `max_message_size` | int | 65536 | Maximum size of UDP datagrams in bytes |
40
43
  | `num_workers` | int | 4 | Number of worker threads for message processing |
44
+ | `network_request_timeout` | float | 5.0 | Maximum time (in seconds) to wait for network object requests |
41
45
 
42
46
  ### Route Configuration
43
47
 
44
48
  | Parameter | Type | Default | Description |
45
49
  |-----------|------|---------|-------------|
46
- | `peer_route` | bool | False | Whether to participate in the peer discovery route |
47
50
  | `validation_route` | bool | False | Whether to participate in the block validation route |
48
51
  | `bootstrap_peers` | list | [] | List of bootstrap peers in the format `[("hostname", port), ...]` |
49
52
 
53
+ > **Note:** The peer route is always enabled as it's necessary for object discovery and retrieval.
54
+
50
55
  ### Example Usage
51
56
 
52
57
  ```python
@@ -54,8 +59,7 @@ from astreum.node import Node
54
59
 
55
60
  # Configuration dictionary
56
61
  config = {
57
- "node_id": b"my-unique-node-id-goes-here-exactly-32", # 32 bytes
58
- "followed_chain_id": b"main-chain-id-goes-here",
62
+ "private_key": "my-private-key-goes-here",
59
63
  "storage_path": "./data/node1",
60
64
  "incoming_port": 7373,
61
65
  "use_ipv6": False,
@@ -12,9 +12,10 @@ When initializing an Astreum Node, you need to provide a configuration dictionar
12
12
 
13
13
  | Parameter | Type | Default | Description |
14
14
  |-----------|------|---------|-------------|
15
- | `node_id` | bytes | Random 32 bytes | Unique identifier for the node |
16
- | `followed_chain_id` | bytes | None | ID of the blockchain that this node follows |
17
- | `storage_path` | str | "./storage" | Directory path where node data will be stored |
15
+ | `private_key` | string | Auto-generated | Hex string of Ed25519 private key. If not provided, a new keypair will be generated automatically |
16
+ | `storage_path` | string | "storage" | Path to store data |
17
+ | `max_storage_space` | int | 1073741824 (1GB) | Maximum storage space in bytes |
18
+ | `max_object_recursion` | int | 50 | Maximum recursion depth for resolving nested objects |
18
19
 
19
20
  ### Network Configuration
20
21
 
@@ -24,15 +25,17 @@ When initializing an Astreum Node, you need to provide a configuration dictionar
24
25
  | `incoming_port` | int | 7373 | Port to listen for incoming messages |
25
26
  | `max_message_size` | int | 65536 | Maximum size of UDP datagrams in bytes |
26
27
  | `num_workers` | int | 4 | Number of worker threads for message processing |
28
+ | `network_request_timeout` | float | 5.0 | Maximum time (in seconds) to wait for network object requests |
27
29
 
28
30
  ### Route Configuration
29
31
 
30
32
  | Parameter | Type | Default | Description |
31
33
  |-----------|------|---------|-------------|
32
- | `peer_route` | bool | False | Whether to participate in the peer discovery route |
33
34
  | `validation_route` | bool | False | Whether to participate in the block validation route |
34
35
  | `bootstrap_peers` | list | [] | List of bootstrap peers in the format `[("hostname", port), ...]` |
35
36
 
37
+ > **Note:** The peer route is always enabled as it's necessary for object discovery and retrieval.
38
+
36
39
  ### Example Usage
37
40
 
38
41
  ```python
@@ -40,8 +43,7 @@ from astreum.node import Node
40
43
 
41
44
  # Configuration dictionary
42
45
  config = {
43
- "node_id": b"my-unique-node-id-goes-here-exactly-32", # 32 bytes
44
- "followed_chain_id": b"main-chain-id-goes-here",
46
+ "private_key": "my-private-key-goes-here",
45
47
  "storage_path": "./data/node1",
46
48
  "incoming_port": 7373,
47
49
  "use_ipv6": False,
@@ -1,18 +1,22 @@
1
- [project]
2
- name = "astreum"
3
- version = "0.1.8"
4
- authors = [
5
- { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
- ]
7
- description = "Python library to interact with the Astreum blockchain and its Lispeum virtual machine."
8
- readme = "README.md"
9
- requires-python = ">=3.8"
10
- classifiers = [
11
- "Programming Language :: Python :: 3",
12
- "License :: OSI Approved :: MIT License",
13
- "Operating System :: OS Independent",
14
- ]
15
-
16
- [project.urls]
17
- Homepage = "https://github.com/astreum/lib"
1
+ [project]
2
+ name = "astreum"
3
+ version = "0.1.9"
4
+ authors = [
5
+ { name="Roy R. O. Okello", email="roy@stelar.xyz" },
6
+ ]
7
+ description = "Python library to interact with the Astreum blockchain and its Lispeum virtual machine."
8
+ readme = "README.md"
9
+ requires-python = ">=3.8"
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: OS Independent",
14
+ ]
15
+ dependencies = [
16
+ "pycryptodomex>=3.14.1,<4.0.0",
17
+ "cryptography>=39.0.0,<40.0.0",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/astreum/lib"
18
22
  Issues = "https://github.com/astreum/lib/issues"
@@ -0,0 +1,410 @@
1
+ """
2
+ Storage utilities for Lispeum expressions.
3
+
4
+ This module provides functions to convert Lispeum expressions to an
5
+ object-based Merkle tree representation for storage and retrieval.
6
+ """
7
+
8
+ import hashlib
9
+ import struct
10
+ from typing import Dict, Tuple, Any, List, Optional
11
+
12
+ from astreum.lispeum.expression import Expr
13
+
14
+
15
+ def expr_to_objects(expr: Any) -> Tuple[bytes, Dict[bytes, bytes]]:
16
+ """
17
+ Convert a Lispeum expression to a collection of objects in a Merkle tree structure.
18
+
19
+ Args:
20
+ expr: The expression to convert
21
+
22
+ Returns:
23
+ A tuple containing (root_hash, objects_dict) where:
24
+ - root_hash is the hash of the root object
25
+ - objects_dict is a dictionary mapping object hashes to their serialized representations
26
+ """
27
+ objects = {}
28
+ root_hash = _serialize_expr(expr, objects)
29
+ return root_hash, objects
30
+
31
+
32
+ def objects_to_expr(root_hash: bytes, objects: Dict[bytes, bytes]) -> Any:
33
+ """
34
+ Convert a collection of objects back to a Lispeum expression.
35
+
36
+ Args:
37
+ root_hash: The hash of the root object
38
+ objects: A dictionary mapping object hashes to their serialized representations
39
+
40
+ Returns:
41
+ The reconstructed Lispeum expression
42
+ """
43
+ return _deserialize_expr(root_hash, objects)
44
+
45
+
46
+ def _serialize_expr(expr: Any, objects: Dict[bytes, bytes]) -> bytes:
47
+ """
48
+ Serialize a Lispeum expression to bytes and add it to the objects dictionary.
49
+
50
+ Args:
51
+ expr: The expression to serialize
52
+ objects: Dictionary to store serialized objects
53
+
54
+ Returns:
55
+ The hash of the serialized expression
56
+ """
57
+ if expr is None:
58
+ # None type
59
+ is_leaf = True
60
+ type_bytes = b'N' # N for None
61
+
62
+ # Create the object with leaf flag and body
63
+ object_bytes = struct.pack("?", is_leaf) + type_bytes
64
+ object_hash = hashlib.sha256(object_bytes).digest()
65
+ objects[object_hash] = object_bytes
66
+
67
+ return object_hash
68
+
69
+ elif isinstance(expr, Expr.ListExpr):
70
+ # Create type object
71
+ is_leaf = False
72
+ type_bytes = b'L' # L for List
73
+
74
+ # Serialize each element and collect their hashes
75
+ element_hashes = []
76
+ for elem in expr.elements:
77
+ elem_hash = _serialize_expr(elem, objects)
78
+ element_hashes.append(elem_hash)
79
+
80
+ # Create value leaf with all element hashes
81
+ value_bytes = b''.join(element_hashes)
82
+
83
+ # Create the object with leaf flag and body
84
+ object_bytes = struct.pack("?", is_leaf) + type_bytes + value_bytes
85
+ object_hash = hashlib.sha256(object_bytes).digest()
86
+ objects[object_hash] = object_bytes
87
+
88
+ return object_hash
89
+
90
+ elif isinstance(expr, Expr.Symbol):
91
+ # Create the object - symbols are leaf nodes
92
+ is_leaf = True
93
+ type_bytes = b'S' # S for Symbol
94
+ value_bytes = expr.value.encode('utf-8')
95
+
96
+ # Create the object with leaf flag and body
97
+ object_bytes = struct.pack("?", is_leaf) + type_bytes + value_bytes
98
+ object_hash = hashlib.sha256(object_bytes).digest()
99
+ objects[object_hash] = object_bytes
100
+
101
+ return object_hash
102
+
103
+ elif isinstance(expr, Expr.Integer):
104
+ # Create the object - integers are leaf nodes
105
+ is_leaf = True
106
+ type_bytes = b'I' # I for Integer
107
+ value_bytes = struct.pack("<q", expr.value) # 8-byte little endian
108
+
109
+ # Create the object with leaf flag and body
110
+ object_bytes = struct.pack("?", is_leaf) + type_bytes + value_bytes
111
+ object_hash = hashlib.sha256(object_bytes).digest()
112
+ objects[object_hash] = object_bytes
113
+
114
+ return object_hash
115
+
116
+ elif isinstance(expr, Expr.String):
117
+ # Create the object - strings are leaf nodes
118
+ is_leaf = True
119
+ type_bytes = b'T' # T for Text/String
120
+ value_bytes = expr.value.encode('utf-8')
121
+
122
+ # Create the object with leaf flag and body
123
+ object_bytes = struct.pack("?", is_leaf) + type_bytes + value_bytes
124
+ object_hash = hashlib.sha256(object_bytes).digest()
125
+ objects[object_hash] = object_bytes
126
+
127
+ return object_hash
128
+
129
+ elif isinstance(expr, Expr.Boolean):
130
+ # Create the object - booleans are leaf nodes
131
+ is_leaf = True
132
+ type_bytes = b'B' # B for Boolean
133
+ value_bytes = b'1' if expr.value else b'0'
134
+
135
+ # Create the object with leaf flag and body
136
+ object_bytes = struct.pack("?", is_leaf) + type_bytes + value_bytes
137
+ object_hash = hashlib.sha256(object_bytes).digest()
138
+ objects[object_hash] = object_bytes
139
+
140
+ return object_hash
141
+
142
+ elif isinstance(expr, Expr.Function):
143
+ # Create the object - functions are not leaf nodes
144
+ is_leaf = False
145
+ type_bytes = b'F' # F for Function
146
+
147
+ # Serialize params
148
+ params_list = []
149
+ for param in expr.params:
150
+ params_list.append(param.encode('utf-8'))
151
+ params_bytes = b','.join(params_list)
152
+
153
+ # Serialize body recursively
154
+ body_hash = _serialize_expr(expr.body, objects)
155
+
156
+ # Create the object with leaf flag and body
157
+ object_bytes = struct.pack("?", is_leaf) + type_bytes + params_bytes + body_hash
158
+ object_hash = hashlib.sha256(object_bytes).digest()
159
+ objects[object_hash] = object_bytes
160
+
161
+ return object_hash
162
+
163
+ elif isinstance(expr, Expr.Error):
164
+ # Create the object - errors are not leaf nodes
165
+ is_leaf = False
166
+ type_bytes = b'E' # E for Error
167
+
168
+ # Serialize error components
169
+ category_bytes = expr.category.encode('utf-8')
170
+ message_bytes = expr.message.encode('utf-8')
171
+ details_bytes = b'' if expr.details is None else expr.details.encode('utf-8')
172
+
173
+ # Create the object with leaf flag and body
174
+ object_bytes = struct.pack("?", is_leaf) + type_bytes + category_bytes + b'\0' + message_bytes + b'\0' + details_bytes
175
+ object_hash = hashlib.sha256(object_bytes).digest()
176
+ objects[object_hash] = object_bytes
177
+
178
+ return object_hash
179
+
180
+ else:
181
+ # Default fallback for unknown types
182
+ is_leaf = True
183
+ type_bytes = b'U' # U for Unknown
184
+ value_bytes = str(expr).encode('utf-8')
185
+
186
+ # Create the object with leaf flag and body
187
+ object_bytes = struct.pack("?", is_leaf) + type_bytes + value_bytes
188
+ object_hash = hashlib.sha256(object_bytes).digest()
189
+ objects[object_hash] = object_bytes
190
+
191
+ return object_hash
192
+
193
+
194
+ def _deserialize_expr(obj_hash: bytes, objects: Dict[bytes, bytes]) -> Any:
195
+ """
196
+ Deserialize a Lispeum expression from its hash.
197
+
198
+ Args:
199
+ obj_hash: The hash of the object to deserialize
200
+ objects: Dictionary containing serialized objects
201
+
202
+ Returns:
203
+ The deserialized Lispeum expression
204
+ """
205
+ if obj_hash not in objects:
206
+ return None
207
+
208
+ obj_bytes = objects[obj_hash]
209
+
210
+ # Extract leaf flag
211
+ is_leaf = struct.unpack("?", obj_bytes[0:1])[0]
212
+
213
+ # Get type indicator
214
+ type_indicator = obj_bytes[1:2]
215
+
216
+ # Deserialize based on type
217
+ if type_indicator == b'N': # None
218
+ return None
219
+
220
+ elif type_indicator == b'L': # List
221
+ if is_leaf:
222
+ # Empty list
223
+ return Expr.ListExpr([])
224
+
225
+ # Non-leaf list has child element hashes
226
+ elements_bytes = obj_bytes[2:]
227
+ element_hashes = [elements_bytes[i:i+32] for i in range(0, len(elements_bytes), 32)]
228
+
229
+ # Deserialize each element
230
+ elements = []
231
+ for elem_hash in element_hashes:
232
+ elem = _deserialize_expr(elem_hash, objects)
233
+ elements.append(elem)
234
+
235
+ return Expr.ListExpr(elements)
236
+
237
+ elif type_indicator == b'S': # Symbol
238
+ value_bytes = obj_bytes[2:]
239
+ return Expr.Symbol(value_bytes.decode('utf-8'))
240
+
241
+ elif type_indicator == b'I': # Integer
242
+ value_bytes = obj_bytes[2:10] # 8 bytes for int64
243
+ value = struct.unpack("<q", value_bytes)[0]
244
+ return Expr.Integer(value)
245
+
246
+ elif type_indicator == b'T': # Text/String
247
+ value_bytes = obj_bytes[2:]
248
+ return Expr.String(value_bytes.decode('utf-8'))
249
+
250
+ elif type_indicator == b'B': # Boolean
251
+ value_bytes = obj_bytes[2:3]
252
+ return Expr.Boolean(value_bytes == b'1')
253
+
254
+ elif type_indicator == b'F': # Function
255
+ # Non-leaf function
256
+ remaining_bytes = obj_bytes[2:]
257
+
258
+ # Find the separator between params and body hash
259
+ params_end = remaining_bytes.find(b',', remaining_bytes.rfind(b','))
260
+ if params_end == -1:
261
+ params_end = 0 # No params
262
+
263
+ params_bytes = remaining_bytes[:params_end+1]
264
+ body_hash = remaining_bytes[params_end+1:]
265
+
266
+ # Parse params
267
+ params = []
268
+ if params_bytes:
269
+ for param_bytes in params_bytes.split(b','):
270
+ if param_bytes: # Skip empty strings
271
+ params.append(param_bytes.decode('utf-8'))
272
+
273
+ # Deserialize body
274
+ body = _deserialize_expr(body_hash, objects)
275
+
276
+ return Expr.Function(params, body)
277
+
278
+ elif type_indicator == b'E': # Error
279
+ remaining_bytes = obj_bytes[2:]
280
+
281
+ # Split by null bytes to get category, message, and details
282
+ parts = remaining_bytes.split(b'\0', 2)
283
+
284
+ category = parts[0].decode('utf-8')
285
+ message = parts[1].decode('utf-8') if len(parts) > 1 else ""
286
+ details = parts[2].decode('utf-8') if len(parts) > 2 else None
287
+
288
+ return Expr.Error(category, message, details)
289
+
290
+ else: # Unknown
291
+ value_bytes = obj_bytes[2:]
292
+ # Return as a string
293
+ return value_bytes.decode('utf-8')
294
+
295
+
296
+ def store_expr(expr: Any, storage) -> bytes:
297
+ """
298
+ Store a Lispeum expression in the provided storage.
299
+
300
+ Args:
301
+ expr: The expression to store
302
+ storage: Storage interface with put(key, value) method
303
+
304
+ Returns:
305
+ The hash of the root object
306
+ """
307
+ # Convert expression to objects
308
+ root_hash, objects = expr_to_objects(expr)
309
+
310
+ # Store each object in the storage
311
+ for obj_hash, obj_data in objects.items():
312
+ storage.put(obj_hash, obj_data)
313
+
314
+ return root_hash
315
+
316
+
317
+ def get_expr_from_storage(root_hash: bytes, storage, max_depth: int = 50) -> Any:
318
+ """
319
+ Load a Lispeum expression from storage. Will recursively resolve
320
+ objects from the storage until a leaf node is reached.
321
+
322
+ Args:
323
+ root_hash: The hash of the root object
324
+ storage: Storage interface with get method and get_recursive method
325
+ max_depth: Maximum recursion depth for resolution
326
+
327
+ Returns:
328
+ The loaded Lispeum expression, or None if not found
329
+ """
330
+ # Check if storage has the get_recursive method (newer storage interface)
331
+ if hasattr(storage, 'get_recursive'):
332
+ # Use the storage's built-in recursive retrieval
333
+ objects = storage.get_recursive(root_hash, max_depth)
334
+ else:
335
+ # Fallback to manual recursive retrieval for older storage interfaces
336
+ objects = {}
337
+ _load_objects_recursive(root_hash, storage, objects, max_depth)
338
+
339
+ # If no objects were retrieved, return None
340
+ if not objects:
341
+ return None
342
+
343
+ # Deserialize the expression
344
+ return objects_to_expr(root_hash, objects)
345
+
346
+
347
+ def _load_objects_recursive(obj_hash: bytes, storage, objects: Dict[bytes, bytes], max_depth: int, current_depth: int = 0) -> bool:
348
+ """
349
+ Recursively load objects from storage.
350
+
351
+ Args:
352
+ obj_hash: The hash of the object to load
353
+ storage: Storage interface with get(key) method
354
+ objects: Dictionary to store loaded objects
355
+ max_depth: Maximum recursion depth
356
+ current_depth: Current recursion depth
357
+
358
+ Returns:
359
+ True if object was loaded, False otherwise
360
+ """
361
+ # Check if we've reached max recursion depth
362
+ if current_depth >= max_depth:
363
+ print(f"Warning: Max recursion depth {max_depth} reached while loading objects")
364
+ return False
365
+
366
+ # Check if we already have this object
367
+ if obj_hash in objects:
368
+ return True
369
+
370
+ # Load the object from storage
371
+ obj_data = storage.get(obj_hash)
372
+ if obj_data is None:
373
+ return False
374
+
375
+ # Store the object
376
+ objects[obj_hash] = obj_data
377
+
378
+ # Check if this is a leaf node
379
+ is_leaf = struct.unpack("?", obj_data[0:1])[0]
380
+ if is_leaf:
381
+ # Leaf node, no need to recurse
382
+ return True
383
+
384
+ # For non-leaf nodes, recursively load child objects
385
+ type_indicator = obj_data[1:2]
386
+
387
+ if type_indicator == b'L': # List
388
+ # Non-leaf list has child element hashes
389
+ elements_bytes = obj_data[2:]
390
+ element_hashes = [elements_bytes[i:i+32] for i in range(0, len(elements_bytes), 32)]
391
+
392
+ # Load each element
393
+ for elem_hash in element_hashes:
394
+ _load_objects_recursive(elem_hash, storage, objects, max_depth, current_depth + 1)
395
+
396
+ elif type_indicator == b'F': # Function
397
+ # Non-leaf function has body hash
398
+ remaining_bytes = obj_data[2:]
399
+
400
+ # Find the separator between params and body hash
401
+ params_end = remaining_bytes.find(b',', remaining_bytes.rfind(b','))
402
+ if params_end == -1:
403
+ params_end = 0 # No params
404
+
405
+ body_hash = remaining_bytes[params_end+1:]
406
+
407
+ # Load body
408
+ _load_objects_recursive(body_hash, storage, objects, max_depth, current_depth + 1)
409
+
410
+ return True