lattica 1.0.0__cp312-cp312-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 +15 -0
- lattica/client.py +249 -0
- lattica/connection_handler.py +356 -0
- lattica-1.0.0.dist-info/METADATA +193 -0
- lattica-1.0.0.dist-info/RECORD +8 -0
- lattica-1.0.0.dist-info/WHEEL +4 -0
- lattica_python_core/__init__.py +5 -0
- lattica_python_core/lattica_python_core.cpython-312-x86_64-linux-gnu.so +0 -0
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://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, 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=Nm2ECjbhKYHLe637AlfdmqgeZJN0CXx1bnhKYSY0i7Q,5636
|
|
2
|
+
lattica-1.0.0.dist-info/WHEEL,sha256=NOMs1MdVcEWyaeT2j95Z65pmYXrQCtsVAd0_H3LkRg0,108
|
|
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-x86_64-linux-gnu.so,sha256=4Fkt5D185FvcNEQUATA5rYglXwK8HEbb7tViTGywSpA,29066040
|
|
8
|
+
lattica-1.0.0.dist-info/RECORD,,
|
|
Binary file
|