colibri-stateless 0.7.2__cp38-cp38-win_amd64.whl → 0.7.4__cp38-cp38-win_amd64.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.
- colibri/__init__.py +15 -13
- colibri/_native.cp38-win_amd64.pyd +0 -0
- colibri/client.py +6 -21
- colibri/testing.py +339 -14
- colibri/types.py +2 -2
- {colibri_stateless-0.7.2.dist-info → colibri_stateless-0.7.4.dist-info}/METADATA +32 -4
- colibri_stateless-0.7.4.dist-info/RECORD +11 -0
- colibri_stateless-0.7.2.dist-info/RECORD +0 -11
- {colibri_stateless-0.7.2.dist-info → colibri_stateless-0.7.4.dist-info}/WHEEL +0 -0
- {colibri_stateless-0.7.2.dist-info → colibri_stateless-0.7.4.dist-info}/top_level.txt +0 -0
colibri/__init__.py
CHANGED
|
@@ -6,8 +6,8 @@ Python bindings for the Colibri stateless Ethereum proof library.
|
|
|
6
6
|
|
|
7
7
|
import atexit
|
|
8
8
|
from .storage import ColibriStorage, DefaultStorage, MemoryStorage
|
|
9
|
-
from .types import MethodType, ColibriError
|
|
10
|
-
from .testing import MockStorage, MockRequestHandler
|
|
9
|
+
from .types import MethodType, ColibriError, RPCError
|
|
10
|
+
from .testing import MockStorage, MockRequestHandler, MockProofData, TestHelper
|
|
11
11
|
|
|
12
12
|
# Try to import native module
|
|
13
13
|
try:
|
|
@@ -24,21 +24,20 @@ _global_storage = None
|
|
|
24
24
|
_storage_registered = False
|
|
25
25
|
|
|
26
26
|
def _register_global_storage(storage: ColibriStorage = None):
|
|
27
|
-
"""Register global storage
|
|
27
|
+
"""Register global storage (can be changed, but affects all Colibri instances)"""
|
|
28
28
|
global _global_storage, _storage_registered
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
f"ignoring {type(storage).__name__}. C storage is global across all instances.",
|
|
37
|
-
RuntimeWarning
|
|
38
|
-
)
|
|
39
|
-
return _global_storage
|
|
30
|
+
# Allow storage re-registration for testing
|
|
31
|
+
# Note: This affects ALL Colibri instances as C storage is global
|
|
32
|
+
if _storage_registered and storage is not None:
|
|
33
|
+
if type(storage) != type(_global_storage):
|
|
34
|
+
# Storage type is changing - re-register
|
|
35
|
+
_storage_registered = False
|
|
40
36
|
|
|
41
37
|
if storage is None:
|
|
38
|
+
if _storage_registered:
|
|
39
|
+
# Return existing storage
|
|
40
|
+
return _global_storage
|
|
42
41
|
storage = DefaultStorage()
|
|
43
42
|
|
|
44
43
|
_global_storage = storage
|
|
@@ -79,6 +78,9 @@ __all__ = [
|
|
|
79
78
|
"MemoryStorage",
|
|
80
79
|
"MethodType",
|
|
81
80
|
"ColibriError",
|
|
81
|
+
"RPCError",
|
|
82
82
|
"MockStorage",
|
|
83
83
|
"MockRequestHandler",
|
|
84
|
+
"MockProofData",
|
|
85
|
+
"TestHelper",
|
|
84
86
|
]
|
|
Binary file
|
colibri/client.py
CHANGED
|
@@ -154,7 +154,7 @@ class Colibri:
|
|
|
154
154
|
type_int = native.get_method_support(self.chain_id, method)
|
|
155
155
|
return MethodType(type_int)
|
|
156
156
|
except (ValueError, TypeError):
|
|
157
|
-
return MethodType.
|
|
157
|
+
return MethodType.UNDEFINED
|
|
158
158
|
|
|
159
159
|
# Fallback implementation for testing
|
|
160
160
|
proofable_methods = {
|
|
@@ -173,7 +173,7 @@ class Colibri:
|
|
|
173
173
|
elif method.startswith("eth_"):
|
|
174
174
|
return MethodType.UNPROOFABLE
|
|
175
175
|
else:
|
|
176
|
-
return MethodType.
|
|
176
|
+
return MethodType.UNDEFINED
|
|
177
177
|
|
|
178
178
|
async def create_proof(self, method: str, params: List[Any]) -> bytes:
|
|
179
179
|
"""
|
|
@@ -281,27 +281,12 @@ class Colibri:
|
|
|
281
281
|
if not status_json:
|
|
282
282
|
raise VerificationError("Verification execution returned null")
|
|
283
283
|
|
|
284
|
-
#
|
|
285
|
-
# DEBUG: Raw C JSON response available for debugging
|
|
286
|
-
|
|
284
|
+
# Parse JSON response from C library
|
|
287
285
|
try:
|
|
288
286
|
status = json.loads(status_json)
|
|
289
287
|
except json.JSONDecodeError as e:
|
|
290
|
-
#
|
|
291
|
-
|
|
292
|
-
# Workaround: Fix trailing comma in C JSON
|
|
293
|
-
fixed_json = status_json.replace(",}", "}")
|
|
294
|
-
print(f" Fixed JSON: {repr(fixed_json)}")
|
|
295
|
-
try:
|
|
296
|
-
status = json.loads(fixed_json)
|
|
297
|
-
# JSON parsing successful after fix
|
|
298
|
-
except json.JSONDecodeError as e2:
|
|
299
|
-
print(f"Still invalid after fix: {e2}")
|
|
300
|
-
raise VerificationError(f"Invalid JSON in verification response: {e2}")
|
|
301
|
-
else:
|
|
302
|
-
# JSON parse error occurred
|
|
303
|
-
print(f" Raw response: {status_json}")
|
|
304
|
-
raise VerificationError(f"Invalid JSON in verification response: {e}")
|
|
288
|
+
# JSON parsing failed - this indicates a bug in the C library
|
|
289
|
+
raise VerificationError(f"Invalid JSON from C library: {e}") from e
|
|
305
290
|
|
|
306
291
|
if status["status"] == "success":
|
|
307
292
|
return status.get("result")
|
|
@@ -358,7 +343,7 @@ class Colibri:
|
|
|
358
343
|
# Local methods use empty proof
|
|
359
344
|
return await self.verify_proof(b"", method, params)
|
|
360
345
|
|
|
361
|
-
elif method_type == MethodType.NOT_SUPPORTED:
|
|
346
|
+
elif method_type == MethodType.NOT_SUPPORTED or method_type == MethodType.UNDEFINED:
|
|
362
347
|
raise ColibriError(f"Method {method} is not supported")
|
|
363
348
|
|
|
364
349
|
else:
|
colibri/testing.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Testing utilities and mock implementations for Colibri Python bindings
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
5
7
|
import json
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
from typing import Any, Dict, List, Optional, Union
|
|
@@ -11,6 +13,115 @@ from .storage import ColibriStorage
|
|
|
11
13
|
from .types import DataRequest, ColibriError
|
|
12
14
|
|
|
13
15
|
|
|
16
|
+
class MockProofData:
|
|
17
|
+
"""Utility class for creating mock proof data for testing"""
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def create_proof(method: str, params: List[Any], result: Any) -> bytes:
|
|
21
|
+
"""
|
|
22
|
+
Create mock proof data for a given method, params, and result.
|
|
23
|
+
|
|
24
|
+
@param method The RPC method name
|
|
25
|
+
@param params The RPC method parameters
|
|
26
|
+
@param result The expected result
|
|
27
|
+
@return Mock proof data as bytes
|
|
28
|
+
"""
|
|
29
|
+
proof_dict = {
|
|
30
|
+
"method": method,
|
|
31
|
+
"params": params,
|
|
32
|
+
"result": result,
|
|
33
|
+
"mock": True
|
|
34
|
+
}
|
|
35
|
+
return json.dumps(proof_dict).encode('utf-8')
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def create_empty_proof() -> bytes:
|
|
39
|
+
"""
|
|
40
|
+
Create an empty proof (used for LOCAL methods).
|
|
41
|
+
|
|
42
|
+
@return Empty bytes
|
|
43
|
+
"""
|
|
44
|
+
return b""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestHelper:
|
|
48
|
+
"""Helper utilities for setting up test scenarios"""
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def setup_eth_get_balance_mock(
|
|
52
|
+
handler: 'MockRequestHandler',
|
|
53
|
+
address: str,
|
|
54
|
+
block: str,
|
|
55
|
+
balance: str
|
|
56
|
+
) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Setup a mock response for eth_getBalance.
|
|
59
|
+
|
|
60
|
+
@param handler The mock request handler
|
|
61
|
+
@param address The Ethereum address
|
|
62
|
+
@param block The block number or tag
|
|
63
|
+
@param balance The balance to return (hex string)
|
|
64
|
+
"""
|
|
65
|
+
response = {
|
|
66
|
+
"jsonrpc": "2.0",
|
|
67
|
+
"id": 1,
|
|
68
|
+
"result": balance
|
|
69
|
+
}
|
|
70
|
+
handler.add_response("eth_getBalance", [address, block], response)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def setup_eth_get_block_mock(
|
|
74
|
+
handler: 'MockRequestHandler',
|
|
75
|
+
block_hash: str,
|
|
76
|
+
block_data: Dict[str, Any]
|
|
77
|
+
) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Setup a mock response for eth_getBlockByHash.
|
|
80
|
+
|
|
81
|
+
@param handler The mock request handler
|
|
82
|
+
@param block_hash The block hash
|
|
83
|
+
@param block_data The block data to return
|
|
84
|
+
"""
|
|
85
|
+
response = {
|
|
86
|
+
"jsonrpc": "2.0",
|
|
87
|
+
"id": 1,
|
|
88
|
+
"result": block_data
|
|
89
|
+
}
|
|
90
|
+
handler.add_response("eth_getBlockByHash", [block_hash, False], response)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def setup_proof_mock(
|
|
94
|
+
handler: 'MockRequestHandler',
|
|
95
|
+
method: str,
|
|
96
|
+
params: List[Any],
|
|
97
|
+
proof_data: Optional[bytes] = None
|
|
98
|
+
) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Setup a mock proof response for a method.
|
|
101
|
+
|
|
102
|
+
@param handler The mock request handler
|
|
103
|
+
@param method The RPC method name
|
|
104
|
+
@param params The RPC method parameters
|
|
105
|
+
@param proof_data Optional custom proof data (generates default if None)
|
|
106
|
+
"""
|
|
107
|
+
if proof_data is None:
|
|
108
|
+
proof_data = MockProofData.create_proof(method, params, "mock_result")
|
|
109
|
+
handler.add_response(method, params, proof_data)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def create_mock_storage_with_data(preset_data: Dict[str, bytes]) -> MockStorage:
|
|
113
|
+
"""
|
|
114
|
+
Create a MockStorage instance with preset data.
|
|
115
|
+
|
|
116
|
+
@param preset_data Dictionary of key-value pairs to preset in storage
|
|
117
|
+
@return MockStorage instance with preset data
|
|
118
|
+
"""
|
|
119
|
+
storage = MockStorage()
|
|
120
|
+
for key, value in preset_data.items():
|
|
121
|
+
storage.set(key, value)
|
|
122
|
+
return storage
|
|
123
|
+
|
|
124
|
+
|
|
14
125
|
class MockStorage(ColibriStorage):
|
|
15
126
|
"""Mock storage implementation for testing"""
|
|
16
127
|
|
|
@@ -32,18 +143,139 @@ class MockStorage(ColibriStorage):
|
|
|
32
143
|
self.delete_calls.append(key)
|
|
33
144
|
self._data.pop(key, None)
|
|
34
145
|
|
|
146
|
+
def size(self) -> int:
|
|
147
|
+
"""Return the number of items in storage"""
|
|
148
|
+
return len(self._data)
|
|
149
|
+
|
|
150
|
+
def preset_data(self, data: Dict[str, bytes]) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Preset storage with initial data without tracking calls.
|
|
153
|
+
|
|
154
|
+
@param data Dictionary of key-value pairs to preset
|
|
155
|
+
"""
|
|
156
|
+
self._data.update(data)
|
|
157
|
+
|
|
158
|
+
def clear_data(self) -> None:
|
|
159
|
+
"""Clear all data from storage without tracking calls"""
|
|
160
|
+
self._data.clear()
|
|
161
|
+
|
|
162
|
+
def clear_calls(self) -> None:
|
|
163
|
+
"""Clear the history of tracked calls"""
|
|
164
|
+
self.get_calls.clear()
|
|
165
|
+
self.set_calls.clear()
|
|
166
|
+
self.delete_calls.clear()
|
|
167
|
+
|
|
35
168
|
|
|
36
169
|
class MockRequestHandler:
|
|
37
170
|
"""Mock HTTP request handler for testing"""
|
|
38
171
|
|
|
39
172
|
def __init__(self):
|
|
40
173
|
self._responses: Dict[str, Union[bytes, Dict[str, Any]]] = {}
|
|
174
|
+
self._method_responses: Dict[str, Union[bytes, Dict[str, Any]]] = {}
|
|
175
|
+
self._default_response: Optional[bytes] = None
|
|
41
176
|
self.request_calls: List[DataRequest] = []
|
|
42
177
|
|
|
178
|
+
def _make_key(self, method: str, params: List[Any]) -> str:
|
|
179
|
+
"""Create a unique key for method + params combination"""
|
|
180
|
+
return f"{method}:{json.dumps(params, sort_keys=True)}"
|
|
181
|
+
|
|
182
|
+
def add_response(
|
|
183
|
+
self,
|
|
184
|
+
method: str,
|
|
185
|
+
params: List[Any],
|
|
186
|
+
response: Union[bytes, Dict[str, Any], str]
|
|
187
|
+
) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Add a mock response for a specific method and params combination.
|
|
190
|
+
|
|
191
|
+
@param method The RPC method name
|
|
192
|
+
@param params The RPC method parameters
|
|
193
|
+
@param response The response to return (can be bytes, dict, or string)
|
|
194
|
+
"""
|
|
195
|
+
key = self._make_key(method, params)
|
|
196
|
+
self._responses[key] = response
|
|
197
|
+
|
|
198
|
+
def add_method_response(
|
|
199
|
+
self,
|
|
200
|
+
method: str,
|
|
201
|
+
response: Union[bytes, Dict[str, Any], str]
|
|
202
|
+
) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Add a mock response for any call to a method (ignoring params).
|
|
205
|
+
|
|
206
|
+
@param method The RPC method name
|
|
207
|
+
@param response The response to return
|
|
208
|
+
"""
|
|
209
|
+
self._method_responses[method] = response
|
|
210
|
+
|
|
211
|
+
def set_default_response(self, response: bytes) -> None:
|
|
212
|
+
"""
|
|
213
|
+
Set a default response for any unmatched requests.
|
|
214
|
+
|
|
215
|
+
@param response The default response to return
|
|
216
|
+
"""
|
|
217
|
+
self._default_response = response
|
|
218
|
+
|
|
219
|
+
def clear_responses(self) -> None:
|
|
220
|
+
"""Clear all configured responses"""
|
|
221
|
+
self._responses.clear()
|
|
222
|
+
self._method_responses.clear()
|
|
223
|
+
self._default_response = None
|
|
224
|
+
|
|
225
|
+
def clear_calls(self) -> None:
|
|
226
|
+
"""Clear the history of request calls"""
|
|
227
|
+
self.request_calls.clear()
|
|
228
|
+
|
|
229
|
+
def get_calls_for_method(self, method: str) -> List[DataRequest]:
|
|
230
|
+
"""
|
|
231
|
+
Get all request calls for a specific method.
|
|
232
|
+
|
|
233
|
+
@param method The RPC method name
|
|
234
|
+
@return List of matching requests
|
|
235
|
+
"""
|
|
236
|
+
return [
|
|
237
|
+
req for req in self.request_calls
|
|
238
|
+
if req.payload and req.payload.get("method") == method
|
|
239
|
+
]
|
|
240
|
+
|
|
43
241
|
async def handle_request(self, request: DataRequest) -> bytes:
|
|
44
|
-
"""
|
|
242
|
+
"""
|
|
243
|
+
Handle a mock HTTP request.
|
|
244
|
+
|
|
245
|
+
@param request The data request to handle
|
|
246
|
+
@return Mock response data
|
|
247
|
+
"""
|
|
45
248
|
self.request_calls.append(request)
|
|
46
|
-
|
|
249
|
+
|
|
250
|
+
# Try to find a matching response
|
|
251
|
+
if request.payload and "method" in request.payload:
|
|
252
|
+
method = request.payload["method"]
|
|
253
|
+
params = request.payload.get("params", [])
|
|
254
|
+
|
|
255
|
+
# First try exact match (method + params)
|
|
256
|
+
key = self._make_key(method, params)
|
|
257
|
+
if key in self._responses:
|
|
258
|
+
response = self._responses[key]
|
|
259
|
+
if isinstance(response, bytes):
|
|
260
|
+
return response
|
|
261
|
+
return json.dumps(response).encode('utf-8')
|
|
262
|
+
|
|
263
|
+
# Then try method-only match
|
|
264
|
+
if method in self._method_responses:
|
|
265
|
+
response = self._method_responses[method]
|
|
266
|
+
if isinstance(response, bytes):
|
|
267
|
+
return response
|
|
268
|
+
return json.dumps(response).encode('utf-8')
|
|
269
|
+
|
|
270
|
+
# Use default response if available
|
|
271
|
+
if self._default_response is not None:
|
|
272
|
+
return self._default_response
|
|
273
|
+
|
|
274
|
+
# No response configured
|
|
275
|
+
raise ColibriError(
|
|
276
|
+
f"No mock response configured for request: "
|
|
277
|
+
f"method={request.payload.get('method') if request.payload else 'unknown'}"
|
|
278
|
+
)
|
|
47
279
|
|
|
48
280
|
|
|
49
281
|
class FileBasedMockStorage(ColibriStorage):
|
|
@@ -54,6 +286,46 @@ class FileBasedMockStorage(ColibriStorage):
|
|
|
54
286
|
self._cache: Dict[str, Optional[bytes]] = {}
|
|
55
287
|
self._access_count: Dict[str, int] = {}
|
|
56
288
|
self._max_access_per_key = 5 # Prevent infinite loops
|
|
289
|
+
|
|
290
|
+
def _find_file_with_truncation(self, filename: str) -> Optional[Path]:
|
|
291
|
+
"""
|
|
292
|
+
Find a file, handling filesystem truncation (macOS has 255 char limit).
|
|
293
|
+
|
|
294
|
+
@param filename The full filename to search for
|
|
295
|
+
@return The path to the file if found, None otherwise
|
|
296
|
+
"""
|
|
297
|
+
file_path = self.test_data_dir / filename
|
|
298
|
+
|
|
299
|
+
# Try exact match first
|
|
300
|
+
if file_path.exists():
|
|
301
|
+
return file_path
|
|
302
|
+
|
|
303
|
+
# If not found and filename is long, try to find truncated versions
|
|
304
|
+
if len(filename) > 200:
|
|
305
|
+
# Get the extension
|
|
306
|
+
parts = filename.rsplit('.', 1)
|
|
307
|
+
if len(parts) == 2:
|
|
308
|
+
base_name, extension = parts
|
|
309
|
+
|
|
310
|
+
# Search for files that start with the same prefix and have same extension
|
|
311
|
+
for prefix_len in [250, 240, 230, 220, 200, 150, 100]:
|
|
312
|
+
if len(base_name) > prefix_len:
|
|
313
|
+
prefix = base_name[:prefix_len]
|
|
314
|
+
pattern = f"{prefix}*.{extension}"
|
|
315
|
+
matching_files = list(self.test_data_dir.glob(pattern))
|
|
316
|
+
if matching_files:
|
|
317
|
+
return matching_files[0]
|
|
318
|
+
else:
|
|
319
|
+
# No extension, just search by prefix
|
|
320
|
+
for prefix_len in [250, 240, 230, 220, 200, 150, 100]:
|
|
321
|
+
if len(filename) > prefix_len:
|
|
322
|
+
prefix = filename[:prefix_len]
|
|
323
|
+
pattern = f"{prefix}*"
|
|
324
|
+
matching_files = list(self.test_data_dir.glob(pattern))
|
|
325
|
+
if matching_files:
|
|
326
|
+
return matching_files[0]
|
|
327
|
+
|
|
328
|
+
return None
|
|
57
329
|
|
|
58
330
|
def get(self, key: str) -> Optional[bytes]:
|
|
59
331
|
# CRITICAL: Return None immediately to break infinite loops
|
|
@@ -70,9 +342,9 @@ class FileBasedMockStorage(ColibriStorage):
|
|
|
70
342
|
# Return cached value
|
|
71
343
|
return self._cache[key]
|
|
72
344
|
|
|
73
|
-
# Load from file
|
|
74
|
-
file_path = self.
|
|
75
|
-
if file_path
|
|
345
|
+
# Load from file (with truncation handling)
|
|
346
|
+
file_path = self._find_file_with_truncation(key)
|
|
347
|
+
if file_path:
|
|
76
348
|
data = file_path.read_bytes()
|
|
77
349
|
# Load file from storage
|
|
78
350
|
self._cache[key] = data
|
|
@@ -99,6 +371,40 @@ class FileBasedMockRequestHandler:
|
|
|
99
371
|
self._request_count = 0
|
|
100
372
|
self._max_requests = 50 # Prevent infinite request loops
|
|
101
373
|
|
|
374
|
+
def _find_file_with_truncation(self, filename: str) -> Optional[Path]:
|
|
375
|
+
"""
|
|
376
|
+
Find a file, handling filesystem truncation (macOS has 255 char limit).
|
|
377
|
+
|
|
378
|
+
@param filename The full filename to search for
|
|
379
|
+
@return The path to the file if found, None otherwise
|
|
380
|
+
"""
|
|
381
|
+
file_path = self.test_data_dir / filename
|
|
382
|
+
|
|
383
|
+
# Try exact match first
|
|
384
|
+
if file_path.exists():
|
|
385
|
+
return file_path
|
|
386
|
+
|
|
387
|
+
# If not found and filename is long, try to find truncated versions
|
|
388
|
+
# macOS typically truncates at 255 characters
|
|
389
|
+
if len(filename) > 200: # If it's potentially truncated
|
|
390
|
+
# Get the extension
|
|
391
|
+
parts = filename.rsplit('.', 1)
|
|
392
|
+
if len(parts) == 2:
|
|
393
|
+
base_name, extension = parts
|
|
394
|
+
|
|
395
|
+
# Search for files that start with the same prefix and have same extension
|
|
396
|
+
# Use progressively shorter prefixes to find the truncated file
|
|
397
|
+
for prefix_len in [250, 240, 230, 220, 200, 150, 100]:
|
|
398
|
+
if len(base_name) > prefix_len:
|
|
399
|
+
prefix = base_name[:prefix_len]
|
|
400
|
+
pattern = f"{prefix}*.{extension}"
|
|
401
|
+
matching_files = list(self.test_data_dir.glob(pattern))
|
|
402
|
+
if matching_files:
|
|
403
|
+
# Return the first match
|
|
404
|
+
return matching_files[0]
|
|
405
|
+
|
|
406
|
+
return None
|
|
407
|
+
|
|
102
408
|
async def handle_request(self, request: DataRequest) -> bytes:
|
|
103
409
|
"""Handle mock HTTP request by loading from file"""
|
|
104
410
|
|
|
@@ -106,20 +412,39 @@ class FileBasedMockRequestHandler:
|
|
|
106
412
|
if self._request_count > self._max_requests:
|
|
107
413
|
raise Exception(f"Too many requests ({self._request_count}) - possible infinite loop")
|
|
108
414
|
|
|
109
|
-
# Convert request to filename (
|
|
415
|
+
# Convert request to filename base (without extension) - matching C implementation
|
|
110
416
|
if request.url:
|
|
111
|
-
|
|
112
|
-
|
|
417
|
+
# Sanitize URL to create filename base
|
|
418
|
+
base_name = request.url
|
|
419
|
+
# Replace problematic characters with underscore (matching C implementation)
|
|
420
|
+
for char in ['/', '.', ',', ' ', ':', '=', '?', '"', '&', '[', ']', '{', '}']:
|
|
421
|
+
base_name = base_name.replace(char, '_')
|
|
113
422
|
elif request.payload and 'method' in request.payload:
|
|
114
|
-
method
|
|
115
|
-
|
|
423
|
+
# For RPC requests, use method name and parameters
|
|
424
|
+
import json
|
|
425
|
+
method = request.payload.get('method', '')
|
|
426
|
+
params = request.payload.get('params', [])
|
|
427
|
+
base_name = method
|
|
428
|
+
for param in params:
|
|
429
|
+
param_str = param if isinstance(param, str) else json.dumps(param)
|
|
430
|
+
base_name += '_' + param_str
|
|
431
|
+
# Sanitize the base name
|
|
432
|
+
for char in ['/', '.', ',', ' ', ':', '=', '?', '"', '&', '[', ']', '{', '}']:
|
|
433
|
+
base_name = base_name.replace(char, '_')
|
|
116
434
|
else:
|
|
117
|
-
|
|
435
|
+
base_name = 'unknown'
|
|
118
436
|
|
|
119
|
-
#
|
|
437
|
+
# CRITICAL: Truncate to maximum length BEFORE adding extension (matching C: C4_MAX_MOCKNAME_LEN = 100)
|
|
438
|
+
MAX_MOCKNAME_LEN = 100
|
|
439
|
+
if len(base_name) > MAX_MOCKNAME_LEN:
|
|
440
|
+
base_name = base_name[:MAX_MOCKNAME_LEN]
|
|
120
441
|
|
|
121
|
-
|
|
122
|
-
|
|
442
|
+
# Add file extension based on encoding type
|
|
443
|
+
filename = base_name + '.' + request.encoding
|
|
444
|
+
|
|
445
|
+
# Look for mock response file (with truncation handling)
|
|
446
|
+
file_path = self._find_file_with_truncation(filename)
|
|
447
|
+
if file_path:
|
|
123
448
|
data = file_path.read_bytes()
|
|
124
449
|
# Found mock response file
|
|
125
450
|
return data
|
colibri/types.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
|
|
|
8
8
|
|
|
9
9
|
class MethodType(IntEnum):
|
|
10
10
|
"""Enum for RPC method support types"""
|
|
11
|
-
|
|
11
|
+
UNDEFINED = 0 # Method is not defined/recognized
|
|
12
12
|
PROOFABLE = 1
|
|
13
13
|
UNPROOFABLE = 2
|
|
14
14
|
NOT_SUPPORTED = 3
|
|
@@ -21,7 +21,7 @@ class MethodType(IntEnum):
|
|
|
21
21
|
def description(self) -> str:
|
|
22
22
|
"""Human-readable description of the method type"""
|
|
23
23
|
descriptions = {
|
|
24
|
-
MethodType.
|
|
24
|
+
MethodType.UNDEFINED: "Method not defined/recognized",
|
|
25
25
|
MethodType.PROOFABLE: "Method supports proof generation",
|
|
26
26
|
MethodType.UNPROOFABLE: "Method doesn't support proofs, direct RPC call",
|
|
27
27
|
MethodType.NOT_SUPPORTED: "Method not supported",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: colibri-stateless
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
4
4
|
Summary: Python bindings for Colibri stateless Ethereum proof library
|
|
5
5
|
Home-page: https://github.com/corpus-core/colibri-stateless
|
|
6
6
|
Author: corpus.core
|
|
@@ -54,7 +54,7 @@ The colibri client is a stateless and trustless ethereum client, which is optimi
|
|
|
54
54
|
### Installation
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
-
pip install colibri-stateless
|
|
57
|
+
python3 -m pip install colibri-stateless
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
### Basic Usage
|
|
@@ -113,11 +113,39 @@ cd colibri-stateless/bindings/python
|
|
|
113
113
|
# Build native extension
|
|
114
114
|
./build.sh
|
|
115
115
|
|
|
116
|
-
#
|
|
116
|
+
# Option 1: Use virtual environment (recommended)
|
|
117
|
+
python3 -m venv venv
|
|
118
|
+
source venv/bin/activate
|
|
117
119
|
pip install -e .
|
|
120
|
+
pip install -r requirements-dev.txt
|
|
118
121
|
|
|
119
122
|
# Run tests
|
|
120
|
-
|
|
123
|
+
pytest tests/ -v
|
|
124
|
+
|
|
125
|
+
# Deactivate when done
|
|
126
|
+
deactivate
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Alternative without virtual environment:**
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Install test dependencies with --user flag
|
|
133
|
+
python3 -m pip install --user pytest pytest-asyncio aiohttp
|
|
134
|
+
|
|
135
|
+
# Run tests directly with PYTHONPATH
|
|
136
|
+
PYTHONPATH=src python3 -m pytest tests/ -v
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Quick Debug Build
|
|
140
|
+
|
|
141
|
+
For faster iteration during development:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Build in debug mode
|
|
145
|
+
./build_debug.sh
|
|
146
|
+
|
|
147
|
+
# Run tests without installation
|
|
148
|
+
PYTHONPATH=src python3 -m pytest tests/ -v
|
|
121
149
|
```
|
|
122
150
|
|
|
123
151
|
### Integration Tests
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
colibri/__init__.py,sha256=99QBzNVvVAO8P3Bd40ud_R8WJ82dkSPEMyTKhPSzKWo,2506
|
|
2
|
+
colibri/_native.cp38-win_amd64.pyd,sha256=k1AUyslZSsuhT6VAp0jvosUrux-eLbAOmAe9XUw0vKQ,887808
|
|
3
|
+
colibri/client.py,sha256=kgwSwY9kbqxln8yinXsYMRZnc2HPwldB02AQjbZE2wo,20955
|
|
4
|
+
colibri/py.typed,sha256=fDUY3AqEcQHC01aGMGdCqP2qwZXb9ILFNzjSSBBwh1U,61
|
|
5
|
+
colibri/storage.py,sha256=evl2zA4kbZd7sQFpvr2pfI4dNH0c94AvFvQw1VEy5do,4579
|
|
6
|
+
colibri/testing.py,sha256=ti2FwuMGQ6TxKrSAB_rZ2ralpmLAxQX8r8OB5vzJtAg,22148
|
|
7
|
+
colibri/types.py,sha256=QNwehmdN9xdJi3R_4QMwBklw4l1rPQ6LxpheNnGw8lQ,4049
|
|
8
|
+
colibri_stateless-0.7.4.dist-info/METADATA,sha256=p_1BRsXeBCqEW0bHAjOrbIUGh79hOO0_eAEqMiPtrnE,6217
|
|
9
|
+
colibri_stateless-0.7.4.dist-info/WHEEL,sha256=2M046GvC9RLU1f1TWyM-2sB7cRKLhAC7ucAFK8l8f24,99
|
|
10
|
+
colibri_stateless-0.7.4.dist-info/top_level.txt,sha256=7YrQNaIHaKEkVXyESwWV8heheMQY94YJfVoepEYjWRk,8
|
|
11
|
+
colibri_stateless-0.7.4.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
colibri/__init__.py,sha256=5r_j9C9M-XUEojWhTSSmn0A85UZN5ztazAdc2MSM6Nk,2442
|
|
2
|
-
colibri/_native.cp38-win_amd64.pyd,sha256=vdvW7CRqZUDBzz-gV8nNYgtH-AME3usu8jF5GwaAL_o,887808
|
|
3
|
-
colibri/client.py,sha256=8ERKG2-QM-ZImqFUvpcnQxlmiqUmNX61auQPACNuOco,21893
|
|
4
|
-
colibri/py.typed,sha256=fDUY3AqEcQHC01aGMGdCqP2qwZXb9ILFNzjSSBBwh1U,61
|
|
5
|
-
colibri/storage.py,sha256=evl2zA4kbZd7sQFpvr2pfI4dNH0c94AvFvQw1VEy5do,4579
|
|
6
|
-
colibri/testing.py,sha256=nCvlOSD1ydzv-lv7bLd8UJNtvnlRCPhKKF7KHtZrf5o,10016
|
|
7
|
-
colibri/types.py,sha256=9gwXeUkcCPUwKaMFRyCO0vEtoJLyw8ZKNvouCnrtpTQ,3995
|
|
8
|
-
colibri_stateless-0.7.2.dist-info/METADATA,sha256=3p_KPydAmdUZkDWzMbf6_bvQfnqqa5MU7uYdgxcevWI,5609
|
|
9
|
-
colibri_stateless-0.7.2.dist-info/WHEEL,sha256=2M046GvC9RLU1f1TWyM-2sB7cRKLhAC7ucAFK8l8f24,99
|
|
10
|
-
colibri_stateless-0.7.2.dist-info/top_level.txt,sha256=7YrQNaIHaKEkVXyESwWV8heheMQY94YJfVoepEYjWRk,8
|
|
11
|
-
colibri_stateless-0.7.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|