lattica 1.0.1__cp311-abi3-manylinux_2_34_x86_64.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 lattica might be problematic. Click here for more details.

lattica/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ from .client import Lattica
2
+ from .connection_handler import (
3
+ ConnectionHandler
4
+ )
5
+
6
+ from lattica_python_core import rpc_method, rpc_stream, rpc_stream_iter
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = [
11
+ "Lattica",
12
+ "rpc_method",
13
+ "rpc_stream",
14
+ "rpc_stream_iter",
15
+ "ConnectionHandler",
16
+ ]
lattica/client.py ADDED
@@ -0,0 +1,249 @@
1
+ from typing import Optional, Any, List, Union
2
+ import pickle
3
+ import time
4
+ from dataclasses import dataclass
5
+ from lattica_python_core import LatticaSDK, RpcClient, PeerInfo
6
+
7
+ @dataclass
8
+ class ValueWithExpiration:
9
+ value: Any
10
+ expiration_time: float
11
+
12
+ def __iter__(self):
13
+ return iter((self.value, self.expiration_time))
14
+
15
+ def __getitem__(self, item):
16
+ if item == 0:
17
+ return self.value
18
+ elif item == 1:
19
+ return self.expiration_time
20
+ else:
21
+ return getattr(self, item)
22
+
23
+ def __eq__(self, item):
24
+ if isinstance(item, ValueWithExpiration):
25
+ return self.value == item.value and self.expiration_time == item.expiration_time
26
+ elif isinstance(item, tuple):
27
+ return tuple.__eq__((self.value, self.expiration_time), item)
28
+ else:
29
+ return False
30
+
31
+ def get_dht_time():
32
+ return time.time()
33
+
34
+
35
+ class Lattica:
36
+ def __init__(self):
37
+ self.config = {}
38
+ self._lattica_instance = None
39
+ self._initialized = False
40
+
41
+ @classmethod
42
+ def builder(cls) -> 'Lattica':
43
+ return cls()
44
+
45
+ def with_bootstraps(self, bootstrap_nodes: List[str]) -> 'Lattica':
46
+ self.config['bootstrap_nodes'] = bootstrap_nodes
47
+ return self
48
+ def with_listen_addrs(self, listen_addrs: List[str]) -> 'Lattica':
49
+ self.config['listen_addrs'] = listen_addrs
50
+ return self
51
+
52
+ def with_idle_timeout(self, timeout_seconds: int) -> 'Lattica':
53
+ self.config['idle_timeout'] = timeout_seconds
54
+ return self
55
+
56
+ def with_mdns(self, with_mdns: bool) -> 'Lattica':
57
+ self.config['with_mdns'] = with_mdns
58
+ return self
59
+
60
+ def with_upnp(self, with_upnp: bool) -> 'Lattica':
61
+ self.config['with_upnp'] = with_upnp
62
+ return self
63
+
64
+ def with_relay_servers(self, relay_servers: List[str]) -> 'Lattica':
65
+ self.config['relay_servers'] = relay_servers
66
+ return self
67
+
68
+ def with_autonat(self, with_autonat: bool) -> 'Lattica':
69
+ self.config['with_autonat'] = with_autonat
70
+ return self
71
+
72
+ def with_dcutr(self, with_dcutr: bool) -> 'Lattica':
73
+ self.config['with_dcutr'] = with_dcutr
74
+ return self
75
+
76
+ def with_external_addrs(self, external_addrs: List[str]) -> 'Lattica':
77
+ self.config['external_addrs'] = external_addrs
78
+ return self
79
+
80
+ def with_storage_path(self, storage_path: str) -> 'Lattica':
81
+ self.config['storage_path'] = storage_path
82
+ return self
83
+
84
+ def with_dht_db_path(self, db_path: str) -> 'Lattica':
85
+ self.config['dht_db_path'] = db_path
86
+ return self
87
+
88
+ def with_key_path(self, key_path: str) -> 'Lattica':
89
+ self.config['key_path'] = key_path
90
+ return self
91
+
92
+ def with_protocol(self, protocol: str) -> 'Lattica':
93
+ self.config['protocol'] = protocol
94
+ return self
95
+
96
+ def build(self) -> 'Lattica':
97
+ self._initialize_client()
98
+ return self
99
+
100
+ def _initialize_client(self):
101
+ if self._initialized:
102
+ return
103
+
104
+ try:
105
+ self._initialize_lattica()
106
+ self._initialized = True
107
+ except Exception as e:
108
+ raise RuntimeError(f"Failed to initialize Lattica: {e}")
109
+
110
+ def _initialize_lattica(self):
111
+ try:
112
+ if self.config:
113
+ self._lattica_instance = LatticaSDK(self.config)
114
+ else:
115
+ self._lattica_instance = LatticaSDK()
116
+ except Exception as e:
117
+ raise RuntimeError(f"Failed to initialize the lattica1 instance: {e}")
118
+
119
+ def _ensure_initialized(self):
120
+ if not self._initialized:
121
+ self._initialize_client()
122
+
123
+ def store(self, key: str, value: Any, expiration_time: Optional[float] = None, subkey: Optional[str] = None) -> Union[bool, None]:
124
+ try:
125
+ # default expiration 10min
126
+ if expiration_time is None:
127
+ expiration_time = get_dht_time() + 600
128
+
129
+ serialized_value = pickle.dumps(value)
130
+ self._lattica_instance.store_with_subkey(key, serialized_value, expiration_time, subkey)
131
+ return True
132
+ except Exception as e:
133
+ print(f"Failed to store value: {e}")
134
+ return False
135
+
136
+ def get(self, key: str) -> Union[ValueWithExpiration, None]:
137
+ try:
138
+ result = self._lattica_instance.get_with_subkey(key)
139
+ if result is None:
140
+ return None
141
+
142
+ if isinstance(result, dict):
143
+ parsed_value = {}
144
+
145
+ for subkey, (serialized_value, expiration) in result.items():
146
+ value = pickle.loads(serialized_value)
147
+ parsed_value[subkey] = ValueWithExpiration(value=value, expiration_time=expiration)
148
+
149
+ first_expiration = next(iter(result.values()))[1]
150
+ return ValueWithExpiration(value=parsed_value, expiration_time=first_expiration)
151
+ else:
152
+ serialized_value, expiration = result
153
+ value = pickle.loads(serialized_value)
154
+ return ValueWithExpiration(value=value, expiration_time=expiration)
155
+
156
+ except Exception as e:
157
+ print(f"Error getting value: {e}")
158
+ return None
159
+
160
+ def get_visible_maddrs(self) -> List[str]:
161
+ try:
162
+ return self._lattica_instance.get_visible_maddrs()
163
+ except Exception as e:
164
+ raise RuntimeError(f"Failed to get visible addresses: {e}")
165
+
166
+ def get_client(self, peer_id: str) -> RpcClient:
167
+ try:
168
+ return self._lattica_instance.get_client(peer_id)
169
+ except Exception as e:
170
+ raise RuntimeError(f"Failed to get client: {e}")
171
+
172
+ def register_service(self, service_instance) -> None:
173
+ try:
174
+ self._lattica_instance.register_service(service_instance)
175
+ except Exception as e:
176
+ raise RuntimeError(f"Failed to register service: {e}")
177
+
178
+ def peer_id(self) -> str:
179
+ try:
180
+ return self._lattica_instance.peer_id()
181
+ except Exception as e:
182
+ raise RuntimeError(f"Failed to get peer ID: {e}")
183
+
184
+ def get_peer_info(self, peer_id: str) -> Optional[PeerInfo]:
185
+ try:
186
+ return self._lattica_instance.get_peer_info(peer_id)
187
+ except Exception as e:
188
+ raise RuntimeError(f"Failed to get peer info: {e}")
189
+
190
+ def get_all_peers(self) -> List[str]:
191
+ try:
192
+ return self._lattica_instance.get_all_peers()
193
+ except Exception as e:
194
+ raise RuntimeError(f"Failed to get all peers: {e}")
195
+
196
+ def get_peer_addresses(self, peer_id: str) -> List[str]:
197
+ try:
198
+ return self._lattica_instance.get_peer_addresses(peer_id)
199
+ except Exception as e:
200
+ raise RuntimeError(f"Failed to get peer addresses: {e}")
201
+
202
+ def get_peer_rtt(self, peer_id: str) -> float:
203
+ try:
204
+ return self._lattica_instance.get_peer_rtt(peer_id)
205
+ except Exception as e:
206
+ raise RuntimeError(f"Failed to get peer RTT: {e}")
207
+
208
+ def put_block(self, data: bytes) -> str:
209
+ try:
210
+ return self._lattica_instance.put_block(data)
211
+ except Exception as e:
212
+ raise RuntimeError(f"Failed to put block: {e}")
213
+
214
+ def get_block(self, cid: str) -> bytes:
215
+ try:
216
+ return self._lattica_instance.get_block(cid)
217
+ except Exception as e:
218
+ raise RuntimeError(f"Failed to get block: {e}")
219
+
220
+ def remove_block(self, cid: str):
221
+ try:
222
+ return self._lattica_instance.remove_block(cid)
223
+ except Exception as e:
224
+ raise RuntimeError(f"Failed to remove block: {e}")
225
+
226
+ def start_providing(self, key: str):
227
+ try:
228
+ return self._lattica_instance.start_providing(key)
229
+ except Exception as e:
230
+ raise RuntimeError(f"Failed to start providing: {e}")
231
+
232
+ def get_providers(self, key: str) -> List[str]:
233
+ try:
234
+ return self._lattica_instance.get_providers(key)
235
+ except Exception as e:
236
+ raise RuntimeError(f"Failed to get providers: {e}")
237
+
238
+ def stop_providing(self, key: str):
239
+ try:
240
+ return self._lattica_instance.stop_providing(key)
241
+ except Exception as e:
242
+ raise RuntimeError(f"Failed to stop providing: {e}")
243
+
244
+ def __enter__(self):
245
+ self._ensure_initialized()
246
+ return self
247
+
248
+ def __exit__(self, exc_type, exc_val, exc_tb):
249
+ pass
@@ -0,0 +1,406 @@
1
+ import inspect
2
+ from typing import List, get_type_hints
3
+ from .client import Lattica
4
+
5
+ def is_protobuf_message(obj):
6
+ return hasattr(obj, 'SerializeToString') and hasattr(obj, 'ParseFromString')
7
+
8
+ def is_protobuf_class(cls):
9
+ # Check if it has protobuf key methods
10
+ has_serialize = hasattr(cls, 'SerializeToString')
11
+ has_parse = hasattr(cls, 'ParseFromString')
12
+
13
+ # Real protobuf classes or mock protobuf classes should have both methods
14
+ if has_serialize and has_parse:
15
+ return True
16
+
17
+ # Fallback check: module name contains _pb2 (real protobuf generated files)
18
+ if hasattr(cls, '__module__') and '_pb2' in cls.__module__:
19
+ return True
20
+
21
+ return False
22
+
23
+ def smart_serialize(data):
24
+ if data is None:
25
+ return b''
26
+ elif is_protobuf_message(data):
27
+ return data.SerializeToString()
28
+ else:
29
+ import pickle
30
+ return pickle.dumps(data)
31
+
32
+ def smart_deserialize(data, expected_type=None):
33
+ if not data:
34
+ return None
35
+
36
+ # If protobuf type is specified, try protobuf deserialization
37
+ if expected_type and is_protobuf_class(expected_type):
38
+ try:
39
+ proto_obj = expected_type()
40
+ proto_obj.ParseFromString(data)
41
+ return proto_obj
42
+ except Exception:
43
+ pass # Try fallback methods
44
+
45
+ # Try auto-detection - check if it's pickle serialized dict with protobuf characteristics
46
+ try:
47
+ import pickle
48
+ result = pickle.loads(data)
49
+
50
+ # If result is dict, try to convert to matching protobuf type
51
+ if isinstance(result, dict) and expected_type and is_protobuf_class(expected_type):
52
+ # Try to create protobuf object and set attributes
53
+ try:
54
+ proto_obj = expected_type()
55
+ for key, value in result.items():
56
+ if hasattr(proto_obj, key):
57
+ setattr(proto_obj, key, value)
58
+ return proto_obj
59
+ except Exception:
60
+ pass
61
+
62
+ return result
63
+ except Exception as e:
64
+ raise RuntimeError(f"Failed to deserialize data: {e}")
65
+
66
+ def get_method_type_hints(method):
67
+ try:
68
+ type_hints = get_type_hints(method)
69
+ sig = inspect.signature(method)
70
+
71
+ # Analyze parameter types
72
+ param_types = {}
73
+ for param_name, param in sig.parameters.items():
74
+ if param_name in type_hints and param_name != 'self':
75
+ annotation = type_hints[param_name]
76
+ if is_protobuf_class(annotation):
77
+ param_types[param_name] = annotation
78
+ # Handle generics like Optional[ProtoType]
79
+ elif hasattr(annotation, '__origin__'):
80
+ for arg in getattr(annotation, '__args__', []):
81
+ if is_protobuf_class(arg):
82
+ param_types[param_name] = arg
83
+ break
84
+
85
+ # Analyze return type
86
+ return_type = None
87
+ if 'return' in type_hints:
88
+ annotation = type_hints['return']
89
+ if is_protobuf_class(annotation):
90
+ return_type = annotation
91
+ elif hasattr(annotation, '__origin__'):
92
+ for arg in getattr(annotation, '__args__', []):
93
+ if is_protobuf_class(arg):
94
+ return_type = arg
95
+ break
96
+
97
+ return param_types, return_type
98
+ except Exception:
99
+ return {}, None
100
+
101
+ class ConnectionHandlerMeta(type):
102
+ def __new__(mcs, name, bases, attrs):
103
+ # Collect RPC methods and stream methods
104
+ rpc_methods = set()
105
+ stream_methods = set()
106
+ stream_iter_methods = set()
107
+
108
+ # Inherit methods from base classes
109
+ for base in bases:
110
+ if hasattr(base, '_rpc_methods'):
111
+ rpc_methods.update(base._rpc_methods)
112
+ if hasattr(base, '_stream_methods'):
113
+ stream_methods.update(base._stream_methods)
114
+ if hasattr(base, '_stream_iter_methods'):
115
+ stream_iter_methods.update(base._stream_iter_methods)
116
+
117
+ # Scan current class methods
118
+ for attr_name, attr_value in attrs.items():
119
+ if hasattr(attr_value, '_is_rpc_method'):
120
+ if getattr(attr_value, '_is_stream_iter_method', False):
121
+ stream_iter_methods.add(attr_name)
122
+ elif getattr(attr_value, '_is_stream_method', False):
123
+ stream_methods.add(attr_name)
124
+ else:
125
+ rpc_methods.add(attr_name)
126
+
127
+ attrs['_rpc_methods'] = list(rpc_methods)
128
+ attrs['_stream_methods'] = list(stream_methods)
129
+ attrs['_stream_iter_methods'] = list(stream_iter_methods)
130
+
131
+ # Create handlers for RPC methods
132
+ for method_name in rpc_methods:
133
+ if f'_handle_{method_name}' not in attrs:
134
+ attrs[f'_handle_{method_name}'] = mcs._create_rpc_handler(method_name)
135
+
136
+ # Create handlers for Stream methods
137
+ for method_name in stream_methods:
138
+ if f'_handle_stream_{method_name}' not in attrs:
139
+ attrs[f'_handle_stream_{method_name}'] = mcs._create_stream_handler(method_name)
140
+
141
+ for method_name in stream_iter_methods:
142
+ if f'_handle_stream_{method_name}' not in attrs:
143
+ attrs[f'_handle_stream_iter_{method_name}'] = mcs._create_stream_iter_handler(method_name)
144
+
145
+ # Save method type information for client use
146
+ attrs['_method_type_info'] = {}
147
+ for method_name in rpc_methods | stream_methods | stream_iter_methods:
148
+ if method_name in attrs:
149
+ method = attrs[method_name]
150
+ attrs['_method_type_info'][method_name] = get_method_type_hints(method)
151
+
152
+ return super().__new__(mcs, name, bases, attrs)
153
+
154
+ @staticmethod
155
+ def _create_rpc_handler(method_name: str):
156
+ def handler(self, data: bytes) -> bytes:
157
+ try:
158
+ # Get method and type information
159
+ method = getattr(self, method_name)
160
+ param_types, return_type = get_method_type_hints(method)
161
+
162
+ # Smart deserialization of request data
163
+ if data:
164
+ # Check if there's a single protobuf parameter
165
+ sig = inspect.signature(method)
166
+ params = [p for name, p in sig.parameters.items() if name != 'self']
167
+
168
+ if len(params) == 1 and params[0].name in param_types:
169
+ # Single protobuf parameter
170
+ proto_type = param_types[params[0].name]
171
+ request_data = smart_deserialize(data, proto_type)
172
+ else:
173
+ # Other cases
174
+ request_data = smart_deserialize(data)
175
+ else:
176
+ request_data = None
177
+
178
+ # Call method
179
+ result = ConnectionHandlerMeta._call_method(method, request_data)
180
+
181
+ # Smart serialization of return data
182
+ return smart_serialize(result)
183
+
184
+ except Exception as e:
185
+ raise RuntimeError(f"RPC method {method_name} failed: {e}")
186
+ return handler
187
+
188
+ @staticmethod
189
+ def _create_stream_handler(method_name: str):
190
+ def handler(self, data: bytes) ->bytes:
191
+ try:
192
+ # Get method and type information
193
+ method = getattr(self, method_name)
194
+ param_types, return_type = get_method_type_hints(method)
195
+
196
+ # Smart deserialization of request data
197
+ if data:
198
+ sig = inspect.signature(method)
199
+ params = [p for name, p in sig.parameters.items() if name != 'self']
200
+
201
+ if len(params) == 1 and params[0].name in param_types:
202
+ # Single protobuf parameter
203
+ proto_type = param_types[params[0].name]
204
+ request_data = smart_deserialize(data, proto_type)
205
+ else:
206
+ # Other cases
207
+ request_data = smart_deserialize(data)
208
+ else:
209
+ request_data = None
210
+
211
+ # Call method
212
+ result = ConnectionHandlerMeta._call_method(method, request_data)
213
+ return smart_serialize(result)
214
+
215
+ except Exception as e:
216
+ raise RuntimeError(f"Stream method {method_name} failed: {e}")
217
+ return handler
218
+ @staticmethod
219
+ def _create_stream_iter_handler(method_name: str):
220
+ def handler(self, data: bytes) ->bytes:
221
+ try:
222
+ # Get method and type information
223
+ method = getattr(self, method_name)
224
+ param_types, _ = get_method_type_hints(method)
225
+
226
+ # Smart deserialization of request data
227
+ if data:
228
+ sig = inspect.signature(method)
229
+ params = [p for name, p in sig.parameters.items() if name != 'self']
230
+
231
+ if len(params) == 1 and params[0].name in param_types:
232
+ # Single protobuf parameter
233
+ proto_type = param_types[params[0].name]
234
+ request_data = smart_deserialize(data, proto_type)
235
+ else:
236
+ # Other cases
237
+ request_data = smart_deserialize(data)
238
+ else:
239
+ request_data = None
240
+
241
+ # Call method
242
+ return ConnectionHandlerMeta._call_method(method, request_data)
243
+ except Exception as e:
244
+ raise RuntimeError(f"Stream iter method {method_name} failed: {e}")
245
+ return handler
246
+ @staticmethod
247
+ def _call_method(method, request_data):
248
+ if request_data is not None:
249
+ sig = inspect.signature(method)
250
+ params = list(sig.parameters.values())
251
+ if params and params[0].name == 'self':
252
+ params = params[1:]
253
+
254
+ if len(params) == 0:
255
+ return method()
256
+ elif len(params) == 1:
257
+ return method(request_data)
258
+ else:
259
+ if isinstance(request_data, dict):
260
+ return method(**request_data)
261
+ elif isinstance(request_data, (list, tuple)):
262
+ return method(*request_data)
263
+ else:
264
+ return method(request_data)
265
+ else:
266
+ return method()
267
+
268
+ class MethodStub:
269
+ def __init__(self, stub: 'ServiceStub', method_name: str, is_stream: bool = False, is_stream_iter: bool = False):
270
+ self.stub = stub
271
+ self.method_name = method_name
272
+ self.is_stream = is_stream
273
+ self.is_stream_iter = is_stream_iter
274
+
275
+ def __call__(self, *args, **kwargs):
276
+ # Handle parameters - support protobuf auto-serialization
277
+ if len(args) == 0 and len(kwargs) == 0:
278
+ data = None
279
+ elif len(args) == 1 and len(kwargs) == 0:
280
+ data = args[0]
281
+ elif len(args) > 0 and len(kwargs) == 0:
282
+ data = list(args)
283
+ elif len(kwargs) > 0 and len(args) == 0:
284
+ data = kwargs
285
+ else:
286
+ data = kwargs
287
+ for i, arg in enumerate(args):
288
+ data[f'arg{i}'] = arg
289
+
290
+ full_method_name = f"{self.stub.service_name}.{self.method_name}"
291
+
292
+ # Smart serialization of data
293
+ serialized_data = smart_serialize(data)
294
+
295
+ # Get expected return type
296
+ expected_return_type = None
297
+ if hasattr(self.stub.connection_handler, '_method_type_info'):
298
+ method_info = self.stub.connection_handler._method_type_info.get(self.method_name)
299
+ if method_info:
300
+ param_types, return_type = method_info
301
+ expected_return_type = return_type
302
+
303
+ if self.is_stream_iter:
304
+ return self.stub.connection_handler._call_stream_iter_method(
305
+ self.stub.peer_id, full_method_name, serialized_data
306
+ )
307
+ elif self.is_stream:
308
+ # Stream call
309
+ future = self.stub.connection_handler._call_stream_method(
310
+ self.stub.peer_id, full_method_name, serialized_data
311
+ )
312
+
313
+ return FutureWrapper(future, expected_return_type, is_stream=True)
314
+ else:
315
+ # Regular RPC call
316
+ future = self.stub.connection_handler._call_method(
317
+ self.stub.peer_id, full_method_name, serialized_data
318
+ )
319
+ # Smart deserialization of response using expected return type
320
+ return FutureWrapper(future, expected_return_type, is_stream=False)
321
+
322
+ class ServiceStub:
323
+ def __init__(self, connection_handler: 'ConnectionHandler', peer_id: str, service_name: str):
324
+ self.connection_handler = connection_handler
325
+ self.peer_id = peer_id
326
+ self.service_name = service_name
327
+ self._method_cache = {}
328
+
329
+ def __getattr__(self, name: str):
330
+ if name in self._method_cache:
331
+ return self._method_cache[name]
332
+
333
+ is_stream = name in getattr(self.connection_handler, '_stream_methods', [])
334
+ is_stream_iter = name in getattr(self.connection_handler, '_stream_iter_methods', [])
335
+
336
+ method_stub = MethodStub(self, name, is_stream, is_stream_iter)
337
+ self._method_cache[name] = method_stub
338
+ return method_stub
339
+
340
+ class ConnectionHandler(metaclass=ConnectionHandlerMeta):
341
+ def __init__(self, lattica_instance: Lattica):
342
+ self.lattica_instance = lattica_instance
343
+ self._service_name = self.__class__.__name__
344
+ self._register_service()
345
+
346
+ def _register_service(self):
347
+ try:
348
+ self.lattica_instance.register_service(self)
349
+ except Exception as e:
350
+ raise
351
+
352
+ def get_service_name(self) -> str:
353
+ return self._service_name
354
+
355
+ def get_methods(self) -> List[str]:
356
+ return getattr(self, '_rpc_methods', [])
357
+
358
+ def get_stream_methods(self) -> List[str]:
359
+ return getattr(self, '_stream_methods', [])
360
+
361
+ def get_stub(self, peer_id: str) -> ServiceStub:
362
+ return ServiceStub(self, peer_id, self._service_name)
363
+
364
+ def _call_method(self, peer_id: str, method_name: str, data: bytes) -> bytes:
365
+ try:
366
+ client = self.lattica_instance.get_client(peer_id)
367
+ return client.call(method_name, data)
368
+ except Exception as e:
369
+ raise
370
+
371
+ def _call_stream_method(self, peer_id: str, method_name: str, data: bytes):
372
+ try:
373
+ client = self.lattica_instance.get_client(peer_id)
374
+ return client.call_stream(method_name, data)
375
+ except Exception as e:
376
+ raise e
377
+
378
+ def _call_stream_iter_method(self, peer_id: str, method_name: str, data: bytes):
379
+ try:
380
+ client = self.lattica_instance.get_client(peer_id)
381
+ return client.call_stream_iter(method_name, data)
382
+ except Exception as e:
383
+ raise
384
+
385
+ class FutureWrapper:
386
+ def __init__(self, future, expected_return_type, is_stream=False):
387
+ self.future = future
388
+ self.expected_return_type = expected_return_type
389
+ self.is_stream = is_stream
390
+
391
+ def result(self, timeout=180):
392
+ raw_result = self.future.result(timeout=timeout)
393
+ return self._process_result(raw_result)
394
+
395
+ def __await__(self):
396
+ return self._async_result().__await__()
397
+
398
+ async def _async_result(self):
399
+ raw_result = await self.future
400
+ return self._process_result(raw_result)
401
+
402
+ def _process_result(self, raw_result):
403
+ if self.is_stream:
404
+ return smart_deserialize(raw_result, self.expected_return_type)
405
+ else:
406
+ return smart_deserialize(raw_result, self.expected_return_type)
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: lattica
3
+ Version: 1.0.1
4
+ Requires-Dist: asyncio ; python_full_version >= '3.11'
5
+ Requires-Dist: typing-extensions ; python_full_version < '3.12'
6
+ Requires-Dist: nest-asyncio ; python_full_version >= '3.11'
7
+ Requires-Dist: pytest>=7.0.0 ; extra == 'dev'
8
+ Requires-Dist: pytest-asyncio>=0.21.0 ; extra == 'dev'
9
+ Requires-Dist: pytest-cov>=4.0.0 ; extra == 'dev'
10
+ Requires-Dist: black>=23.0.0 ; extra == 'dev'
11
+ Requires-Dist: isort>=5.12.0 ; extra == 'dev'
12
+ Requires-Dist: flake8>=6.0.0 ; extra == 'dev'
13
+ Requires-Dist: mypy>=1.0.0 ; extra == 'dev'
14
+ Requires-Dist: psutil>=5.9.0 ; extra == 'dev'
15
+ Requires-Dist: pytest>=7.0.0 ; extra == 'test'
16
+ Requires-Dist: pytest-asyncio>=0.21.0 ; extra == 'test'
17
+ Requires-Dist: pytest-cov>=4.0.0 ; extra == 'test'
18
+ Requires-Dist: psutil>=5.9.0 ; extra == 'bench'
19
+ Requires-Dist: matplotlib>=3.7.0 ; extra == 'bench'
20
+ Requires-Dist: pandas>=2.0.0 ; extra == 'bench'
21
+ Provides-Extra: dev
22
+ Provides-Extra: test
23
+ Provides-Extra: bench
24
+ Summary: A unified Python SDK for P2P networking with integrated DHT and NAT capabilities
25
+ Keywords: p2p,networking,dht,nat,libp2p,distributed
26
+ License: MIT
27
+ Requires-Python: >=3.11
28
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
29
+
30
+ # Lattica Python SDK
31
+
32
+ A unified Python SDK for P2P networking with integrated DHT and NAT capabilities, built on top of libp2p.
33
+
34
+ ## Features
35
+
36
+ - **Distributed Hash Table (DHT)**: Store and retrieve key-value pairs across the network
37
+ - **Remote Procedure Call (RPC)**: Execute remote functions with support for complex data types
38
+ - **Streaming RPC**: Handle large data transfers with streaming capabilities
39
+ - **NAT Traversal**: Automatic NAT traversal with UPnP support
40
+ - **Peer Discovery**: mDNS and rendezvous-based peer discovery
41
+ - **High Performance**: Built with Rust for optimal performance
42
+
43
+ ## Installation
44
+ You can install the released versions
45
+ ```bash
46
+ pip install lattica
47
+ ```
48
+ Or you can install from source using:
49
+ ```bash
50
+ pip install git+https://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@github.com/OpenEdgeHQ/p2p-lattica.git#subdirectory=bindings/python
51
+ ```
52
+
53
+ ## Quick Start
54
+ ### Basic Usage
55
+
56
+ ```python
57
+ from lattica import Lattica
58
+
59
+ # Create a Lattica instance
60
+ lattica = Lattica.builder().build()
61
+
62
+ # Get your peer ID
63
+ peer_id = lattica.peer_id()
64
+ print(f"My peer ID: {peer_id}")
65
+ ```
66
+
67
+ ## Examples
68
+
69
+ ### 1. DHT Operations
70
+
71
+ The DHT example demonstrates basic key-value storage and retrieval with subkey support.
72
+
73
+ ```python
74
+ from lattica import Lattica
75
+
76
+ # Create client1 as bootstrap node
77
+ lattica = Lattica.builder()
78
+ .build()
79
+
80
+ # Create client2 with bootstrap nodes
81
+ lattica = Lattica.builder() \
82
+ .with_bootstraps(["/ip4/127.0.0.1/tcp/54282/p2p/QmServerPeerId"]) \
83
+ .build()
84
+
85
+ # Store a simple key-value pair with default expiration 10 minute
86
+ lattica.store("name", "alice")
87
+
88
+ # get the value
89
+ result = lattica.get("name")
90
+ if result:
91
+ print(f"Value: {result.value}")
92
+ print(f"Expires: {result.expiration_time}")
93
+
94
+
95
+ # Store values with subkeys (useful for voting, user data, etc.)
96
+ key = "peer_list"
97
+ peers = ["alice", "bob", "carol"]
98
+
99
+ # Each peer stores their vote with a subkey
100
+ lattica.store(key, "yes", expiration_time, subkey="alice")
101
+ lattica.store(key, "no", expiration_time, subkey="bob")
102
+ lattica.store(key, "maybe", expiration_time, subkey="carol")
103
+
104
+ # get all votes
105
+ votes_result = lattica.get(key)
106
+ if votes_result:
107
+ for peer, vote_info in votes_result.value.items():
108
+ print(f"{peer}: {vote_info.value}")
109
+ ```
110
+
111
+ ### 2. RPC Operations
112
+
113
+ The RPC example demonstrates remote procedure calls with support for complex data types and streaming.
114
+
115
+ ```python
116
+ from lattica import Lattica, rpc_method, rpc_stream, rpc_stream_iter, ConnectionHandler
117
+
118
+
119
+
120
+ class MyService(ConnectionHandler):
121
+ @rpc_method
122
+ def add(self, a: int, b: int) -> int:
123
+ """Simple addition"""
124
+ return a + b
125
+
126
+ @rpc_stream
127
+ def process_data(self, data: list) -> list:
128
+ return data
129
+
130
+ @rpc_stream_iter
131
+ def stream_rpc_iter(self):
132
+ while True:
133
+ text = "hello world"
134
+ yield text
135
+
136
+ # Create client1 as RPC server and bootstrap node
137
+ lattica = Lattica.builder()
138
+ .build()
139
+ service = MyService(lattica)
140
+
141
+ # Create client2 with bootstrap nodes
142
+ lattica = Lattica.builder() \
143
+ .with_bootstraps(["/ip4/127.0.0.1/tcp/54282/p2p/QmServerPeerId"]) \
144
+ .build()
145
+ client_service = MyService(client_lattica)
146
+
147
+ # Make RPC calls
148
+ stub = client_service.get_stub(server_peer_id)
149
+ result = stub.add(10, 20) # Returns 30
150
+
151
+ # Handle complex data types
152
+ num_floats = int(2 * 1024 * 1024 * 1024) // 8 #2GB
153
+ test_data = [random.random() for _ in range(num_floats)]
154
+ result = stub.process_data(test_data)
155
+
156
+ # stream iter call
157
+ for text in stub.stream_rpc_iter():
158
+ print(f"recv: {text}")
159
+
160
+ ```
161
+
162
+ ## Configuration
163
+
164
+ ### Builder Pattern
165
+
166
+ ```python
167
+ lattica = Lattica.builder() \
168
+ .with_bootstraps([
169
+ "/ip4/127.0.0.1/tcp/8080/p2p/QmBootstrap1",
170
+ "/ip4/127.0.0.1/tcp/8081/p2p/QmBootstrap2"
171
+ ]) \
172
+ .with_listen_addrs(["/ip4/0.0.0.0/tcp/0", "/ip4/0.0.0.0/udp/0/quic-v1"])
173
+ .with_external_addrs(["/ip4/0.0.0.0/tcp/0"])
174
+ .with_mdns(True) \
175
+ .with_upnp(True) \
176
+ .build()
177
+ ```
178
+
179
+ ### Configuration Options
180
+
181
+ - `with_bootstraps(nodes)`: Set bootstrap nodes for network discovery
182
+ - `with_listen_addrs(addrs)`: Set listening address
183
+ - `with_mdns(enabled)`: Enable/disable mDNS peer discovery
184
+ - `with_upnp(enabled)`: Enable/disable UPnP NAT traversal
185
+ - `with_relay_servers(servers)`: Set relay servers for network relay
186
+ - `with_autonat(enabled)`: Enable/disable AutoNAT detect[need relay servers]
187
+ - `with_dcutr(enabled)`: Enable/disable TCP/QUIC NAT travelsal[need relay servers]
188
+ - `with_external_addrs(addrs)`: Set external address
189
+ - `with_storage_path`: Persistent storage path
190
+ - `with_key_path`: Set Keypair path
191
+
192
+ ## Development
193
+
194
+ ### Building from Source
195
+
196
+ ```bash
197
+ # Install maturin
198
+ pip install maturin
199
+
200
+ # Build the package
201
+ cd bindings/python
202
+ pip install .
203
+ ```
@@ -0,0 +1,8 @@
1
+ lattica-1.0.1.dist-info/METADATA,sha256=-utMtRx16rwZqRy41RWRyPssUVc0B2eePw6XDd8m1h8,5979
2
+ lattica-1.0.1.dist-info/WHEEL,sha256=LWHqq2IM4QLepK4wz-qn725zui-nG-9ig3PtbmCZl-g,107
3
+ lattica/__init__.py,sha256=8YITlLnp2HwFscCFPXoydAQlZfWQbWMIhESS26GovPI,295
4
+ lattica/client.py,sha256=-wxEgMW-c_MA8Vc-I5PB34878xVvlP2aNYZIHrdRKy8,8437
5
+ lattica/connection_handler.py,sha256=w8D4hG7KsXfbDdzSmZjn9cOfl8n-HCDEf91b6rElP_4,15749
6
+ lattica_python_core/__init__.py,sha256=uXPn47KzWKWEomcCmXdktHbovVOR38Cbsv8Vu8IrTPQ,159
7
+ lattica_python_core/lattica_python_core.abi3.so,sha256=YSitKEkmeRFHB1k4ti81Th1cVOy06EYhui-9oXZ1ybU,29424760
8
+ lattica-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.9.6)
3
+ Root-Is-Purelib: false
4
+ Tag: cp311-abi3-manylinux_2_34_x86_64
@@ -0,0 +1,5 @@
1
+ from .lattica_python_core import *
2
+
3
+ __doc__ = lattica_python_core.__doc__
4
+ if hasattr(lattica_python_core, "__all__"):
5
+ __all__ = lattica_python_core.__all__