lattica 1.0.0__cp312-cp312-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.

Potentially problematic release.


This version of lattica might be problematic. Click here for more details.

lattica/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from .client import Lattica
2
+ from .connection_handler import (
3
+ ConnectionHandler
4
+ )
5
+
6
+ from lattica_python_core import rpc_method, rpc_stream
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = [
11
+ "Lattica",
12
+ "rpc_method",
13
+ "rpc_stream",
14
+ "ConnectionHandler",
15
+ ]
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,356 @@
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
+
107
+ # Inherit methods from base classes
108
+ for base in bases:
109
+ if hasattr(base, '_rpc_methods'):
110
+ rpc_methods.update(base._rpc_methods)
111
+ if hasattr(base, '_stream_methods'):
112
+ stream_methods.update(base._stream_methods)
113
+
114
+ # Scan current class methods
115
+ for attr_name, attr_value in attrs.items():
116
+ if hasattr(attr_value, '_is_rpc_method'):
117
+ if hasattr(attr_value, '_is_stream_method') and attr_value._is_stream_method:
118
+ stream_methods.add(attr_name)
119
+ else:
120
+ rpc_methods.add(attr_name)
121
+
122
+ attrs['_rpc_methods'] = list(rpc_methods)
123
+ attrs['_stream_methods'] = list(stream_methods)
124
+
125
+ # Create handlers for RPC methods
126
+ for method_name in rpc_methods:
127
+ if f'_handle_{method_name}' not in attrs:
128
+ attrs[f'_handle_{method_name}'] = mcs._create_rpc_handler(method_name)
129
+
130
+ # Create handlers for Stream methods
131
+ for method_name in stream_methods:
132
+ if f'_handle_stream_{method_name}' not in attrs:
133
+ attrs[f'_handle_stream_{method_name}'] = mcs._create_stream_handler(method_name)
134
+
135
+ # Save method type information for client use
136
+ attrs['_method_type_info'] = {}
137
+ for method_name in rpc_methods | stream_methods:
138
+ if method_name in attrs:
139
+ method = attrs[method_name]
140
+ attrs['_method_type_info'][method_name] = get_method_type_hints(method)
141
+
142
+ return super().__new__(mcs, name, bases, attrs)
143
+
144
+ @staticmethod
145
+ def _create_rpc_handler(method_name: str):
146
+ def handler(self, data: bytes) -> bytes:
147
+ try:
148
+ # Get method and type information
149
+ method = getattr(self, method_name)
150
+ param_types, return_type = get_method_type_hints(method)
151
+
152
+ # Smart deserialization of request data
153
+ if data:
154
+ # Check if there's a single protobuf parameter
155
+ sig = inspect.signature(method)
156
+ params = [p for name, p in sig.parameters.items() if name != 'self']
157
+
158
+ if len(params) == 1 and params[0].name in param_types:
159
+ # Single protobuf parameter
160
+ proto_type = param_types[params[0].name]
161
+ request_data = smart_deserialize(data, proto_type)
162
+ else:
163
+ # Other cases
164
+ request_data = smart_deserialize(data)
165
+ else:
166
+ request_data = None
167
+
168
+ # Call method
169
+ result = ConnectionHandlerMeta._call_method(method, request_data)
170
+
171
+ # Smart serialization of return data
172
+ return smart_serialize(result)
173
+
174
+ except Exception as e:
175
+ raise RuntimeError(f"RPC method {method_name} failed: {e}")
176
+ return handler
177
+
178
+ @staticmethod
179
+ def _create_stream_handler(method_name: str):
180
+ def handler(self, data: bytes) ->bytes:
181
+ try:
182
+ # Get method and type information
183
+ method = getattr(self, method_name)
184
+ param_types, return_type = get_method_type_hints(method)
185
+
186
+ # Smart deserialization of request data
187
+ if data:
188
+ sig = inspect.signature(method)
189
+ params = [p for name, p in sig.parameters.items() if name != 'self']
190
+
191
+ if len(params) == 1 and params[0].name in param_types:
192
+ # Single protobuf parameter
193
+ proto_type = param_types[params[0].name]
194
+ request_data = smart_deserialize(data, proto_type)
195
+ else:
196
+ # Other cases
197
+ request_data = smart_deserialize(data)
198
+ else:
199
+ request_data = None
200
+
201
+ # Call method
202
+ result = ConnectionHandlerMeta._call_method(method, request_data)
203
+ return smart_serialize(result)
204
+
205
+ except Exception as e:
206
+ raise RuntimeError(f"Stream method {method_name} failed: {e}")
207
+ return handler
208
+
209
+ @staticmethod
210
+ def _call_method(method, request_data):
211
+ if request_data is not None:
212
+ sig = inspect.signature(method)
213
+ params = list(sig.parameters.values())
214
+ if params and params[0].name == 'self':
215
+ params = params[1:]
216
+
217
+ if len(params) == 0:
218
+ return method()
219
+ elif len(params) == 1:
220
+ return method(request_data)
221
+ else:
222
+ if isinstance(request_data, dict):
223
+ return method(**request_data)
224
+ elif isinstance(request_data, (list, tuple)):
225
+ return method(*request_data)
226
+ else:
227
+ return method(request_data)
228
+ else:
229
+ return method()
230
+
231
+ class MethodStub:
232
+ def __init__(self, stub: 'ServiceStub', method_name: str, is_stream: bool = False):
233
+ self.stub = stub
234
+ self.method_name = method_name
235
+ self.is_stream = is_stream
236
+
237
+ def __call__(self, *args, **kwargs):
238
+ # Handle parameters - support protobuf auto-serialization
239
+ if len(args) == 0 and len(kwargs) == 0:
240
+ data = None
241
+ elif len(args) == 1 and len(kwargs) == 0:
242
+ data = args[0]
243
+ elif len(args) > 0 and len(kwargs) == 0:
244
+ data = list(args)
245
+ elif len(kwargs) > 0 and len(args) == 0:
246
+ data = kwargs
247
+ else:
248
+ data = kwargs
249
+ for i, arg in enumerate(args):
250
+ data[f'arg{i}'] = arg
251
+
252
+ full_method_name = f"{self.stub.service_name}.{self.method_name}"
253
+
254
+ # Smart serialization of data
255
+ serialized_data = smart_serialize(data)
256
+
257
+ # Get expected return type
258
+ expected_return_type = None
259
+ if hasattr(self.stub.connection_handler, '_method_type_info'):
260
+ method_info = self.stub.connection_handler._method_type_info.get(self.method_name)
261
+ if method_info:
262
+ param_types, return_type = method_info
263
+ expected_return_type = return_type
264
+
265
+ if self.is_stream:
266
+ # Stream call
267
+ future = self.stub.connection_handler._call_stream_method(
268
+ self.stub.peer_id, full_method_name, serialized_data
269
+ )
270
+
271
+ return FutureWrapper(future, expected_return_type, is_stream=True)
272
+ else:
273
+ # Regular RPC call
274
+ future = self.stub.connection_handler._call_method(
275
+ self.stub.peer_id, full_method_name, serialized_data
276
+ )
277
+ # Smart deserialization of response using expected return type
278
+ return FutureWrapper(future, expected_return_type, is_stream=False)
279
+
280
+ class ServiceStub:
281
+ def __init__(self, connection_handler: 'ConnectionHandler', peer_id: str, service_name: str):
282
+ self.connection_handler = connection_handler
283
+ self.peer_id = peer_id
284
+ self.service_name = service_name
285
+ self._method_cache = {}
286
+
287
+ def __getattr__(self, name: str):
288
+ if name in self._method_cache:
289
+ return self._method_cache[name]
290
+
291
+ is_stream = name in getattr(self.connection_handler, '_stream_methods', [])
292
+
293
+ method_stub = MethodStub(self, name, is_stream)
294
+ self._method_cache[name] = method_stub
295
+ return method_stub
296
+
297
+ class ConnectionHandler(metaclass=ConnectionHandlerMeta):
298
+ def __init__(self, lattica_instance: Lattica):
299
+ self.lattica_instance = lattica_instance
300
+ self._service_name = self.__class__.__name__
301
+ self._register_service()
302
+
303
+ def _register_service(self):
304
+ try:
305
+ self.lattica_instance.register_service(self)
306
+ except Exception as e:
307
+ raise
308
+
309
+ def get_service_name(self) -> str:
310
+ return self._service_name
311
+
312
+ def get_methods(self) -> List[str]:
313
+ return getattr(self, '_rpc_methods', [])
314
+
315
+ def get_stream_methods(self) -> List[str]:
316
+ return getattr(self, '_stream_methods', [])
317
+
318
+ def get_stub(self, peer_id: str) -> ServiceStub:
319
+ return ServiceStub(self, peer_id, self._service_name)
320
+
321
+ def _call_method(self, peer_id: str, method_name: str, data: bytes) -> bytes:
322
+ try:
323
+ client = self.lattica_instance.get_client(peer_id)
324
+ return client.call(method_name, data)
325
+ except Exception as e:
326
+ raise
327
+
328
+ def _call_stream_method(self, peer_id: str, method_name: str, data: bytes):
329
+ try:
330
+ client = self.lattica_instance.get_client(peer_id)
331
+ return client.call_stream(method_name, data)
332
+ except Exception as e:
333
+ raise e
334
+
335
+ class FutureWrapper:
336
+ def __init__(self, future, expected_return_type, is_stream=False):
337
+ self.future = future
338
+ self.expected_return_type = expected_return_type
339
+ self.is_stream = is_stream
340
+
341
+ def result(self, timeout=180):
342
+ raw_result = self.future.result(timeout=timeout)
343
+ return self._process_result(raw_result)
344
+
345
+ def __await__(self):
346
+ return self._async_result().__await__()
347
+
348
+ async def _async_result(self):
349
+ raw_result = await self.future
350
+ return self._process_result(raw_result)
351
+
352
+ def _process_result(self, raw_result):
353
+ if self.is_stream:
354
+ return smart_deserialize(raw_result, self.expected_return_type)
355
+ else:
356
+ return smart_deserialize(raw_result, self.expected_return_type)
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: lattica
3
+ Version: 1.0.0
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, 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
+ # Create client1 as RPC server and bootstrap node
131
+ lattica = Lattica.builder()
132
+ .build()
133
+ service = MyService(lattica)
134
+
135
+ # Create client2 with bootstrap nodes
136
+ lattica = Lattica.builder() \
137
+ .with_bootstraps(["/ip4/127.0.0.1/tcp/54282/p2p/QmServerPeerId"]) \
138
+ .build()
139
+ client_service = MyService(client_lattica)
140
+
141
+ # Make RPC calls
142
+ stub = client_service.get_stub(server_peer_id)
143
+ result = stub.add(10, 20) # Returns 30
144
+
145
+ # Handle complex data types
146
+ num_floats = int(2 * 1024 * 1024 * 1024) // 8 #2GB
147
+ test_data = [random.random() for _ in range(num_floats)]
148
+ result = stub.process_data(test_data)
149
+
150
+ ```
151
+
152
+ ## Configuration
153
+
154
+ ### Builder Pattern
155
+
156
+ ```python
157
+ lattica = Lattica.builder() \
158
+ .with_bootstraps([
159
+ "/ip4/127.0.0.1/tcp/8080/p2p/QmBootstrap1",
160
+ "/ip4/127.0.0.1/tcp/8081/p2p/QmBootstrap2"
161
+ ]) \
162
+ .with_listen_addrs(["/ip4/0.0.0.0/tcp/0", "/ip4/0.0.0.0/udp/0/quic-v1"])
163
+ .with_external_addrs(["/ip4/0.0.0.0/tcp/0"])
164
+ .with_mdns(True) \
165
+ .with_upnp(True) \
166
+ .build()
167
+ ```
168
+
169
+ ### Configuration Options
170
+
171
+ - `with_bootstraps(nodes)`: Set bootstrap nodes for network discovery
172
+ - `with_listen_addrs(addrs)`: Set listening address
173
+ - `with_mdns(enabled)`: Enable/disable mDNS peer discovery
174
+ - `with_upnp(enabled)`: Enable/disable UPnP NAT traversal
175
+ - `with_relay_servers(servers)`: Set relay servers for network relay
176
+ - `with_autonat(enabled)`: Enable/disable AutoNAT detect[need relay servers]
177
+ - `with_dcutr(enabled)`: Enable/disable TCP/QUIC NAT travelsal[need relay servers]
178
+ - `with_external_addrs(addrs)`: Set external address
179
+ - `with_storage_path`: Persistent storage path
180
+ - `with_key_path`: Set Keypair path
181
+
182
+ ## Development
183
+
184
+ ### Building from Source
185
+
186
+ ```bash
187
+ # Install maturin
188
+ pip install maturin
189
+
190
+ # Build the package
191
+ cd bindings/python
192
+ pip install .
193
+ ```
@@ -0,0 +1,8 @@
1
+ lattica-1.0.0.dist-info/METADATA,sha256=XXpO4idPo6eAFQumRwJrwwE2dEMEvAS6AIjbUI35Djc,5734
2
+ lattica-1.0.0.dist-info/WHEEL,sha256=qPmdr8fB5VyzHuEe_BZYkwViBC6_YLDjr40IyuSix7k,104
3
+ lattica/__init__.py,sha256=BLGWIZILK6lm9ewbDaAtU1WNuq4qJ7JNmMu_-3AAEA4,255
4
+ lattica/client.py,sha256=-wxEgMW-c_MA8Vc-I5PB34878xVvlP2aNYZIHrdRKy8,8437
5
+ lattica/connection_handler.py,sha256=oNFxBNO5-UJe9YWPvaojn1ayADjy4CC7ZDozU_jlqT4,13296
6
+ lattica_python_core/__init__.py,sha256=uXPn47KzWKWEomcCmXdktHbovVOR38Cbsv8Vu8IrTPQ,159
7
+ lattica_python_core/lattica_python_core.cpython-312-darwin.so,sha256=NeczriWXytb0BR5925vs5RyNlZdgmkebtZZJX0QAgCM,22537056
8
+ lattica-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.9.4)
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-cp312-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__