lattica 1.0.13__cp38-abi3-macosx_11_0_arm64.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.
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,268 @@
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, timeout_secs: int = 10) -> bytes:
215
+ try:
216
+ return self._lattica_instance.get_block(cid, timeout_secs=timeout_secs)
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 close(self):
245
+ try:
246
+ self._lattica_instance.close()
247
+ except Exception as e:
248
+ raise RuntimeError(f"Failed to close client: {e}")
249
+
250
+ def is_symmetric_nat(self):
251
+ try:
252
+ return self._lattica_instance.is_symmetric_nat()
253
+ except Exception as e:
254
+ raise RuntimeError(f"Failed to check is_symmetric_nat error: {e}")
255
+
256
+ def __enter__(self):
257
+ self._ensure_initialized()
258
+ return self
259
+
260
+ def __exit__(self, exc_type, exc_val, exc_tb):
261
+ pass
262
+
263
+ def __del__(self):
264
+ if self._lattica_instance is not None:
265
+ try:
266
+ self._lattica_instance.close()
267
+ except Exception as e:
268
+ print(f"Warning: Failed to shutdown Lattica: {e}")
@@ -0,0 +1,408 @@
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
+
219
+ @staticmethod
220
+ def _create_stream_iter_handler(method_name: str):
221
+ def handler(self, data: bytes) ->bytes:
222
+ try:
223
+ # Get method and type information
224
+ method = getattr(self, method_name)
225
+ param_types, _ = get_method_type_hints(method)
226
+
227
+ # Smart deserialization of request data
228
+ if data:
229
+ sig = inspect.signature(method)
230
+ params = [p for name, p in sig.parameters.items() if name != 'self']
231
+
232
+ if len(params) == 1 and params[0].name in param_types:
233
+ # Single protobuf parameter
234
+ proto_type = param_types[params[0].name]
235
+ request_data = smart_deserialize(data, proto_type)
236
+ else:
237
+ # Other cases
238
+ request_data = smart_deserialize(data)
239
+ else:
240
+ request_data = None
241
+
242
+ # Call method
243
+ return ConnectionHandlerMeta._call_method(method, request_data)
244
+ except Exception as e:
245
+ raise RuntimeError(f"Stream iter method {method_name} failed: {e}")
246
+ return handler
247
+
248
+ @staticmethod
249
+ def _call_method(method, request_data):
250
+ if request_data is not None:
251
+ sig = inspect.signature(method)
252
+ params = list(sig.parameters.values())
253
+ if params and params[0].name == 'self':
254
+ params = params[1:]
255
+
256
+ if len(params) == 0:
257
+ return method()
258
+ elif len(params) == 1:
259
+ return method(request_data)
260
+ else:
261
+ if isinstance(request_data, dict):
262
+ return method(**request_data)
263
+ elif isinstance(request_data, (list, tuple)):
264
+ return method(*request_data)
265
+ else:
266
+ return method(request_data)
267
+ else:
268
+ return method()
269
+
270
+ class MethodStub:
271
+ def __init__(self, stub: 'ServiceStub', method_name: str, is_stream: bool = False, is_stream_iter: bool = False):
272
+ self.stub = stub
273
+ self.method_name = method_name
274
+ self.is_stream = is_stream
275
+ self.is_stream_iter = is_stream_iter
276
+
277
+ def __call__(self, *args, **kwargs):
278
+ # Handle parameters - support protobuf auto-serialization
279
+ if len(args) == 0 and len(kwargs) == 0:
280
+ data = None
281
+ elif len(args) == 1 and len(kwargs) == 0:
282
+ data = args[0]
283
+ elif len(args) > 0 and len(kwargs) == 0:
284
+ data = list(args)
285
+ elif len(kwargs) > 0 and len(args) == 0:
286
+ data = kwargs
287
+ else:
288
+ data = kwargs
289
+ for i, arg in enumerate(args):
290
+ data[f'arg{i}'] = arg
291
+
292
+ full_method_name = f"{self.stub.service_name}.{self.method_name}"
293
+
294
+ # Smart serialization of data
295
+ serialized_data = smart_serialize(data)
296
+
297
+ # Get expected return type
298
+ expected_return_type = None
299
+ if hasattr(self.stub.connection_handler, '_method_type_info'):
300
+ method_info = self.stub.connection_handler._method_type_info.get(self.method_name)
301
+ if method_info:
302
+ param_types, return_type = method_info
303
+ expected_return_type = return_type
304
+
305
+ if self.is_stream_iter:
306
+ return self.stub.connection_handler._call_stream_iter_method(
307
+ self.stub.peer_id, full_method_name, serialized_data
308
+ )
309
+ elif self.is_stream:
310
+ # Stream call
311
+ future = self.stub.connection_handler._call_stream_method(
312
+ self.stub.peer_id, full_method_name, serialized_data
313
+ )
314
+
315
+ return FutureWrapper(future, expected_return_type, is_stream=True)
316
+ else:
317
+ # Regular RPC call
318
+ future = self.stub.connection_handler._call_method(
319
+ self.stub.peer_id, full_method_name, serialized_data
320
+ )
321
+ # Smart deserialization of response using expected return type
322
+ return FutureWrapper(future, expected_return_type, is_stream=False)
323
+
324
+ class ServiceStub:
325
+ def __init__(self, connection_handler: 'ConnectionHandler', peer_id: str, service_name: str):
326
+ self.connection_handler = connection_handler
327
+ self.peer_id = peer_id
328
+ self.service_name = service_name
329
+ self._method_cache = {}
330
+
331
+ def __getattr__(self, name: str):
332
+ if name in self._method_cache:
333
+ return self._method_cache[name]
334
+
335
+ is_stream = name in getattr(self.connection_handler, '_stream_methods', [])
336
+ is_stream_iter = name in getattr(self.connection_handler, '_stream_iter_methods', [])
337
+
338
+ method_stub = MethodStub(self, name, is_stream, is_stream_iter)
339
+ self._method_cache[name] = method_stub
340
+ return method_stub
341
+
342
+ class ConnectionHandler(metaclass=ConnectionHandlerMeta):
343
+ def __init__(self, lattica_instance: Lattica):
344
+ self.lattica_instance = lattica_instance
345
+ self._service_name = self.__class__.__name__
346
+ self._register_service()
347
+
348
+ def _register_service(self):
349
+ try:
350
+ self.lattica_instance.register_service(self)
351
+ except Exception as e:
352
+ raise
353
+
354
+ def get_service_name(self) -> str:
355
+ return self._service_name
356
+
357
+ def get_methods(self) -> List[str]:
358
+ return getattr(self, '_rpc_methods', [])
359
+
360
+ def get_stream_methods(self) -> List[str]:
361
+ return getattr(self, '_stream_methods', [])
362
+
363
+ def get_stub(self, peer_id: str) -> ServiceStub:
364
+ return ServiceStub(self, peer_id, self._service_name)
365
+
366
+ def _call_method(self, peer_id: str, method_name: str, data: bytes) -> bytes:
367
+ try:
368
+ client = self.lattica_instance.get_client(peer_id)
369
+ return client.call(method_name, data)
370
+ except Exception as e:
371
+ raise
372
+
373
+ def _call_stream_method(self, peer_id: str, method_name: str, data: bytes):
374
+ try:
375
+ client = self.lattica_instance.get_client(peer_id)
376
+ return client.call_stream(method_name, data)
377
+ except Exception as e:
378
+ raise e
379
+
380
+ def _call_stream_iter_method(self, peer_id: str, method_name: str, data: bytes):
381
+ try:
382
+ client = self.lattica_instance.get_client(peer_id)
383
+ return client.call_stream_iter(method_name, data)
384
+ except Exception as e:
385
+ raise
386
+
387
+ class FutureWrapper:
388
+ def __init__(self, future, expected_return_type, is_stream=False):
389
+ self.future = future
390
+ self.expected_return_type = expected_return_type
391
+ self.is_stream = is_stream
392
+
393
+ def result(self, timeout=180):
394
+ raw_result = self.future.result(timeout=timeout)
395
+ return self._process_result(raw_result)
396
+
397
+ def __await__(self):
398
+ return self._async_result().__await__()
399
+
400
+ async def _async_result(self):
401
+ raw_result = await self.future
402
+ return self._process_result(raw_result)
403
+
404
+ def _process_result(self, raw_result):
405
+ if self.is_stream:
406
+ return smart_deserialize(raw_result, self.expected_return_type)
407
+ else:
408
+ return smart_deserialize(raw_result, self.expected_return_type)
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: lattica
3
+ Version: 1.0.13
4
+ Requires-Dist: asyncio ; python_full_version >= '3.8'
5
+ Requires-Dist: typing-extensions ; python_full_version < '3.8'
6
+ Requires-Dist: nest-asyncio ; python_full_version >= '3.8'
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.8
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://github.com/GradientHQ/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_dht_db_path`: DHT Persistent db path
191
+ - `with_key_path`: Set Keypair path
192
+
193
+ ## Development
194
+
195
+ ### Building from Source
196
+
197
+ ```bash
198
+ # Install maturin
199
+ pip install maturin
200
+
201
+ # Build the package
202
+ cd bindings/python
203
+ pip install .
204
+ ```
@@ -0,0 +1,8 @@
1
+ lattica-1.0.13.dist-info/METADATA,sha256=rrfkJa-oASU9Ul2bEH49JDsASq11-TXxKv69a4IQRxs,5919
2
+ lattica-1.0.13.dist-info/WHEEL,sha256=tsXOb_QjXfrIzsTjWbzE9EA8s59Oy2CLZwoCmtd6s2o,103
3
+ lattica/__init__.py,sha256=8YITlLnp2HwFscCFPXoydAQlZfWQbWMIhESS26GovPI,295
4
+ lattica/client.py,sha256=QYCTkaHa6UwdcPU9uVHgGCUjlPB1SF6Dl9MfmyoyULk,9098
5
+ lattica/connection_handler.py,sha256=5i15iskTblL3YxJi2-IdnMC-04Yw24B7bBR9-FyNPek,15751
6
+ lattica_python_core/__init__.py,sha256=uXPn47KzWKWEomcCmXdktHbovVOR38Cbsv8Vu8IrTPQ,159
7
+ lattica_python_core/lattica_python_core.abi3.so,sha256=9-EWqefTuOJYFnLX4Ae9GNqYB-wuexLnUu6bjV08VzU,18812064
8
+ lattica-1.0.13.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.10.2)
3
+ Root-Is-Purelib: false
4
+ Tag: cp38-abi3-macosx_11_0_arm64
@@ -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__