simpledht 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.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.
simpledht/__init__.py CHANGED
@@ -20,7 +20,7 @@ Example usage:
20
20
  node.bootstrap('other_node_ip:5000')
21
21
  """
22
22
 
23
- __version__ = "0.1.1"
23
+ __version__ = "0.1.2"
24
24
 
25
25
  from .dht_node import DHTNode
26
26
  from .cli import main as cli_main
simpledht/cli.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import click
2
- from dht_node import DHTNode
2
+ from .dht_node import DHTNode
3
3
  import json
4
4
  import time
5
5
  import socket
@@ -12,19 +12,35 @@ def cli():
12
12
 
13
13
  @cli.command()
14
14
  @click.option('--host', default='0.0.0.0', help='Host to bind to')
15
- @click.option('--port', default=5000, help='Port to bind to')
15
+ @click.option('--port', default=5000, type=int, help='Port to bind to')
16
16
  @click.option('--bootstrap', help='Comma-separated list of bootstrap nodes (host:port)')
17
17
  def start(host, port, bootstrap):
18
18
  """Start a new DHT node."""
19
19
  bootstrap_nodes = bootstrap.split(',') if bootstrap else []
20
- node = DHTNode(host, port, bootstrap_nodes)
21
- node.start()
22
20
 
23
21
  try:
24
- while True:
25
- pass
26
- except KeyboardInterrupt:
27
- node.stop()
22
+ node = DHTNode(host, port, bootstrap_nodes)
23
+ node.start()
24
+
25
+ click.echo(f"Node started successfully on {host}:{port}")
26
+ click.echo(f"Public IP: {node.public_ip}")
27
+ click.echo(f"Node ID: {node.id}")
28
+
29
+ if bootstrap_nodes:
30
+ click.echo(f"Connected to bootstrap nodes: {', '.join(bootstrap_nodes)}")
31
+
32
+ click.echo("Press Ctrl+C to stop the node...")
33
+
34
+ try:
35
+ while True:
36
+ time.sleep(1)
37
+ except KeyboardInterrupt:
38
+ click.echo("Stopping node...")
39
+ node.stop()
40
+ click.echo("Node stopped.")
41
+ except Exception as e:
42
+ click.echo(f"Error starting node: {e}")
43
+ sys.exit(1)
28
44
 
29
45
  def _send_message(host: str, port: int, message: dict, timeout: int = 5) -> dict:
30
46
  """Send a message to a DHT node and wait for response."""
@@ -34,7 +50,7 @@ def _send_message(host: str, port: int, message: dict, timeout: int = 5) -> dict
34
50
  sock.settimeout(timeout)
35
51
 
36
52
  # Send message
37
- click.echo(f"Sending message to {host}:{port}...")
53
+ click.echo(f"Connecting to node at {host}:{port}...")
38
54
  sock.sendto(json.dumps(message).encode(), (host, port))
39
55
 
40
56
  # Wait for response
@@ -70,8 +86,10 @@ def put(host, port, timeout, key, value):
70
86
 
71
87
  if response.get('type') == 'store_ack':
72
88
  click.echo(f"Successfully stored {key}={value}")
89
+ click.echo("Value has been replicated to all nodes in the network")
73
90
  else:
74
91
  click.echo(f"Failed to store {key}={value}")
92
+ click.echo(f"Response: {response}")
75
93
 
76
94
  @cli.command()
77
95
  @click.option('--host', required=True, help='Host of the DHT node')
@@ -93,6 +111,32 @@ def get(host, port, timeout, key):
93
111
  click.echo(f"Value for {key}: {value}")
94
112
  else:
95
113
  click.echo(f"Failed to retrieve value for key: {key}")
114
+ click.echo(f"Response: {response}")
115
+
116
+ @cli.command()
117
+ @click.option('--host', required=True, help='Host of the DHT node')
118
+ @click.option('--port', required=True, type=int, help='Port of the DHT node')
119
+ @click.option('--timeout', default=5, help='Timeout in seconds')
120
+ def info(host, port, timeout):
121
+ """Get information about a DHT node."""
122
+ response = _send_message(host, port, {
123
+ 'type': 'info_request'
124
+ }, timeout)
125
+
126
+ if response.get('type') == 'info_response':
127
+ node_id = response.get('node_id', 'Unknown')
128
+ peer_count = response.get('peer_count', 0)
129
+ data_count = response.get('data_count', 0)
130
+
131
+ click.echo(f"Node ID: {node_id}")
132
+ click.echo(f"Connected peers: {peer_count}")
133
+ click.echo(f"Stored key-value pairs: {data_count}")
134
+ else:
135
+ click.echo("Failed to get node information")
136
+
137
+ def main():
138
+ """Entry point for the CLI."""
139
+ cli()
96
140
 
97
141
  if __name__ == '__main__':
98
- cli()
142
+ main()
simpledht/dht_node.py CHANGED
@@ -128,6 +128,24 @@ class DHTNode:
128
128
  # Update our routing table with the received nodes
129
129
  received_table = message.get('routing_table', {})
130
130
  self.routing_table.update(received_table)
131
+ elif msg_type == 'sync_request':
132
+ # Send our data to the requesting node
133
+ self._send_response(addr, {
134
+ 'type': 'sync_response',
135
+ 'data': self.data
136
+ })
137
+ elif msg_type == 'sync_response':
138
+ # Update our data with the received data
139
+ received_data = message.get('data', {})
140
+ self.data.update(received_data)
141
+ elif msg_type == 'info_request':
142
+ # Send information about this node
143
+ self._send_response(addr, {
144
+ 'type': 'info_response',
145
+ 'node_id': self.id,
146
+ 'peer_count': len(self.routing_table) - 1, # Exclude self
147
+ 'data_count': len(self.data)
148
+ })
131
149
 
132
150
  def _replicate_data(self, key: str, value: str):
133
151
  """Replicate data to other nodes in the network."""
@@ -164,11 +182,31 @@ class DHTNode:
164
182
  'value': None
165
183
  })
166
184
 
167
- def _send_message_with_response(self, addr: Tuple[str, int], message: dict) -> dict:
168
- """Send a message and wait for response."""
185
+ def _send_message_with_response(self, addr: Tuple[str, int], message: dict, timeout: int = 5) -> dict:
186
+ """Send a message and wait for response.
187
+
188
+ Args:
189
+ addr: The address to send the message to
190
+ message: The message to send
191
+ timeout: The timeout in seconds
192
+
193
+ Returns:
194
+ The response message
195
+ """
169
196
  self._send_message(addr, message)
170
- data, _ = self.socket.recvfrom(4096)
171
- return json.loads(data.decode())
197
+
198
+ # Set timeout for receiving response
199
+ self.socket.settimeout(timeout)
200
+ try:
201
+ data, _ = self.socket.recvfrom(4096)
202
+ # Reset timeout to None (blocking mode)
203
+ self.socket.settimeout(None)
204
+ return json.loads(data.decode())
205
+ except socket.timeout:
206
+ raise
207
+ finally:
208
+ # Make sure we reset the timeout even if an exception occurs
209
+ self.socket.settimeout(None)
172
210
 
173
211
  def _send_response(self, addr: Tuple[str, int], response: dict):
174
212
  """Send a response to a node."""
@@ -208,4 +246,131 @@ class DHTNode:
208
246
  def stop(self):
209
247
  """Stop the DHT node."""
210
248
  self.running = False
211
- self.socket.close()
249
+ self.socket.close()
250
+
251
+ def put(self, key: str, value: str) -> bool:
252
+ """Store a key-value pair in the DHT.
253
+
254
+ Args:
255
+ key: The key to store
256
+ value: The value to store
257
+
258
+ Returns:
259
+ bool: True if successful, False otherwise
260
+ """
261
+ # Store locally
262
+ self.data[key] = value
263
+
264
+ # Replicate to other nodes
265
+ self._replicate_data(key, value)
266
+
267
+ return True
268
+
269
+ def get(self, key: str) -> Optional[str]:
270
+ """Retrieve a value from the DHT.
271
+
272
+ Args:
273
+ key: The key to retrieve
274
+
275
+ Returns:
276
+ The value if found, None otherwise
277
+ """
278
+ # Check if we have it locally
279
+ if key in self.data:
280
+ return self.data[key]
281
+
282
+ # If not, ask other nodes
283
+ for node_id, (host, port) in self.routing_table.items():
284
+ if node_id != self.id: # Don't ask self
285
+ try:
286
+ response = self._send_message_with_response((host, port), {
287
+ 'type': 'get',
288
+ 'key': key
289
+ })
290
+
291
+ if response.get('type') == 'get_response' and response.get('value') is not None:
292
+ return response.get('value')
293
+ except Exception as e:
294
+ print(f"Failed to get value from {host}:{port}: {e}")
295
+
296
+ # Not found anywhere
297
+ return None
298
+
299
+ def bootstrap(self, node_address: str):
300
+ """Connect to another node to join the network.
301
+
302
+ Args:
303
+ node_address: The address of the node to connect to in the format 'host:port'
304
+
305
+ Returns:
306
+ bool: True if successfully connected, False otherwise
307
+ """
308
+ try:
309
+ parts = node_address.split(':')
310
+ if len(parts) != 2:
311
+ print(f"Invalid bootstrap node format: {node_address}. Expected format: IP:PORT")
312
+ return False
313
+
314
+ host, port = parts
315
+ try:
316
+ port = int(port)
317
+ except ValueError:
318
+ print(f"Invalid port number in bootstrap node: {node_address}")
319
+ return False
320
+
321
+ print(f"Bootstrapping with node {host}:{port}")
322
+
323
+ # Try multiple times in case of network issues
324
+ max_attempts = 3
325
+ for attempt in range(max_attempts):
326
+ try:
327
+ response = self._send_message_with_response((host, port), {
328
+ 'type': 'join',
329
+ 'node_id': self.id,
330
+ 'host': self.public_ip,
331
+ 'port': self.port
332
+ }, timeout=5)
333
+
334
+ if response.get('type') == 'join_ack':
335
+ received_table = response.get('routing_table', {})
336
+ self.routing_table.update(received_table)
337
+ print(f"Successfully connected to {host}:{port}")
338
+
339
+ # Sync data with the network
340
+ self._sync_data_with_network()
341
+ return True
342
+ break
343
+ except socket.timeout:
344
+ print(f"Connection attempt {attempt+1}/{max_attempts} timed out, retrying...")
345
+ if attempt == max_attempts - 1:
346
+ print(f"Failed to connect to {host}:{port} after {max_attempts} attempts")
347
+ return False
348
+ except Exception as e:
349
+ print(f"Error during connection attempt {attempt+1}: {e}")
350
+ break
351
+ except Exception as e:
352
+ print(f"Failed to bootstrap with {node_address}: {e}")
353
+
354
+ return False
355
+
356
+ def _sync_data_with_network(self):
357
+ """Sync data with other nodes in the network after joining."""
358
+ if not self.routing_table:
359
+ return
360
+
361
+ # Request data from a random node in the routing table
362
+ for node_id, (host, port) in self.routing_table.items():
363
+ if node_id != self.id: # Don't ask self
364
+ try:
365
+ response = self._send_message_with_response((host, port), {
366
+ 'type': 'sync_request'
367
+ })
368
+
369
+ if response.get('type') == 'sync_response':
370
+ # Update our data with the received data
371
+ received_data = response.get('data', {})
372
+ self.data.update(received_data)
373
+ print(f"Synced {len(received_data)} key-value pairs from the network")
374
+ return
375
+ except Exception as e:
376
+ print(f"Failed to sync data from {host}:{port}: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simpledht
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A simple distributed hash table implementation
5
5
  Home-page: https://github.com/dhruvldrp9/simpledht
6
6
  Author: Dhruvkumar Patel
@@ -0,0 +1,10 @@
1
+ simpledht/__init__.py,sha256=UMGuX6JHuDsQG_JuvBv4Q5JT2Yq9zzPwTuqgRdHJVow,546
2
+ simpledht/cli.py,sha256=9QgsyZv1-SKwblj2hYLsGKRjiCyBFjlGalafdqhoXrk,4877
3
+ simpledht/dht_node.py,sha256=58fd7Yq8gPLVbHaaJMW-9gnrC2YjPMF0EtD3I81EI6Q,14864
4
+ simpledht/test_dht.py,sha256=tsWGhtLkTArFJIdkaWManekhFx2KCUO0Gp93EGBUAlw,2346
5
+ simpledht-0.1.2.dist-info/licenses/LICENSE,sha256=MrSAzCY_7J3C4Hp8PaXaaFJP2ygfmuAWoLTVowEE3B0,1073
6
+ simpledht-0.1.2.dist-info/METADATA,sha256=n9kVjGyjUno8VTeN5IgM2isJO2UVBxfsm_p51ckxy98,779
7
+ simpledht-0.1.2.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
8
+ simpledht-0.1.2.dist-info/entry_points.txt,sha256=Hm3BYz1MykAhg94m3JSYsMcdEQEia3bdRocokc-0wxs,49
9
+ simpledht-0.1.2.dist-info/top_level.txt,sha256=VjRxeoXfQJeoPoFhr5JGGMAI5NSLkYJl0WbySmpZ4bs,10
10
+ simpledht-0.1.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.1.0)
2
+ Generator: setuptools (80.4.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,10 +0,0 @@
1
- simpledht/__init__.py,sha256=g6zuNY67roLXDgvxl7JXEXbY23tVWDvWL4FQE3v1vdk,546
2
- simpledht/cli.py,sha256=5KrXFqxHAHuU9RleB1FPRyI2YZwKwaJijXH-DAmZV5k,3225
3
- simpledht/dht_node.py,sha256=FZHLigu7NLkJ9smWyJyLK2FBFu2kKy9kxr9Mig6yh0M,8481
4
- simpledht/test_dht.py,sha256=tsWGhtLkTArFJIdkaWManekhFx2KCUO0Gp93EGBUAlw,2346
5
- simpledht-0.1.1.dist-info/licenses/LICENSE,sha256=MrSAzCY_7J3C4Hp8PaXaaFJP2ygfmuAWoLTVowEE3B0,1073
6
- simpledht-0.1.1.dist-info/METADATA,sha256=iTVFk2v6EjXM0RhR1Mis2-6iIT9ZFl1aCmkTLkKD4jM,779
7
- simpledht-0.1.1.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
8
- simpledht-0.1.1.dist-info/entry_points.txt,sha256=Hm3BYz1MykAhg94m3JSYsMcdEQEia3bdRocokc-0wxs,49
9
- simpledht-0.1.1.dist-info/top_level.txt,sha256=VjRxeoXfQJeoPoFhr5JGGMAI5NSLkYJl0WbySmpZ4bs,10
10
- simpledht-0.1.1.dist-info/RECORD,,