distributed-state-network 0.0.2__tar.gz → 0.0.4__tar.gz

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.
Files changed (31) hide show
  1. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/PKG-INFO +11 -22
  2. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/README.md +10 -21
  3. distributed_state_network-0.0.4/documentation/ds-node-config.md +47 -0
  4. distributed_state_network-0.0.4/documentation/ds-node-server.md +58 -0
  5. distributed_state_network-0.0.4/documentation/ds-node.md +65 -0
  6. distributed_state_network-0.0.4/documentation/protocol.md +37 -0
  7. distributed_state_network-0.0.4/documentation/usage.md +84 -0
  8. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/pyproject.toml +1 -1
  9. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/dsnode.py +10 -1
  10. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/handler.py +8 -8
  11. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/tests/connections.py +32 -2
  12. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/.github/workflows/publish.yml +0 -0
  13. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/.gitignore +0 -0
  14. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/LICENSE +0 -0
  15. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/coverage.sh +0 -0
  16. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/__init__.py +0 -0
  17. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/create_key.py +0 -0
  18. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/objects/config.py +0 -0
  19. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/objects/endpoint.py +0 -0
  20. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/objects/hello_packet.py +0 -0
  21. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/objects/peers_packet.py +0 -0
  22. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/objects/signed_packet.py +0 -0
  23. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/objects/state_packet.py +0 -0
  24. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/util/__init__.py +0 -0
  25. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/util/aes.py +0 -0
  26. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/util/byte_helper.py +0 -0
  27. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/util/ecdsa.py +0 -0
  28. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/util/https.py +0 -0
  29. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/src/distributed_state_network/util/key_manager.py +0 -0
  30. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/technical.md +0 -0
  31. {distributed_state_network-0.0.2 → distributed_state_network-0.0.4}/test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: distributed-state-network
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: A tool to distribute the state of a network device to other devices on the network
5
5
  Project-URL: Homepage, https://github.com/erinclemmer/distributed_state_network
6
6
  Project-URL: Issues, https://github.com/erinclemmer/distributed_state_network/issues
@@ -79,26 +79,6 @@ DSN creates a peer-to-peer network where each node maintains its own state datab
79
79
  - Any node can read any other node's state instantly
80
80
  - All communication is encrypted with AES + ECDSA + HTTPS
81
81
 
82
- ## API Reference
83
-
84
- ### Node Methods
85
-
86
- **update_data(key, value)** - Update a key in this node's state
87
- ```python
88
- node.update_data("sensor_reading", "42.0")
89
- ```
90
-
91
- **read_data(node_id, key)** - Read a value from any node's state
92
-
93
- ```python
94
- temperature = node.read_data("sensor_node", "temperature")
95
- ```
96
-
97
- **peers()** - List all connected nodes
98
- ```python
99
- connected_nodes = node.peers()
100
- ```
101
-
102
82
  ## Example: Distributed Temperature Monitoring
103
83
 
104
84
  Create a network of temperature sensors that share readings:
@@ -126,4 +106,13 @@ for node_id in monitor.node.peers():
126
106
  if node_id.startswith("sensor_"):
127
107
  temp = monitor.node.read_data(node_id, "temperature")
128
108
  print(f"{node_id}: {temp}°F")
129
- ```
109
+ ```
110
+
111
+ ### Documentation
112
+ * [Usage Examples](./documentation/usage.md)
113
+ * [Protocol](./documentation/protocol.md)
114
+
115
+ #### API Reference
116
+ * [DSNodeServer](./documentation/ds-node-server.md)
117
+ * [DSNode](./documentation/ds-node.md)
118
+ * [DSNodeConfig](./documentation/ds-node-config.md)
@@ -54,26 +54,6 @@ DSN creates a peer-to-peer network where each node maintains its own state datab
54
54
  - Any node can read any other node's state instantly
55
55
  - All communication is encrypted with AES + ECDSA + HTTPS
56
56
 
57
- ## API Reference
58
-
59
- ### Node Methods
60
-
61
- **update_data(key, value)** - Update a key in this node's state
62
- ```python
63
- node.update_data("sensor_reading", "42.0")
64
- ```
65
-
66
- **read_data(node_id, key)** - Read a value from any node's state
67
-
68
- ```python
69
- temperature = node.read_data("sensor_node", "temperature")
70
- ```
71
-
72
- **peers()** - List all connected nodes
73
- ```python
74
- connected_nodes = node.peers()
75
- ```
76
-
77
57
  ## Example: Distributed Temperature Monitoring
78
58
 
79
59
  Create a network of temperature sensors that share readings:
@@ -101,4 +81,13 @@ for node_id in monitor.node.peers():
101
81
  if node_id.startswith("sensor_"):
102
82
  temp = monitor.node.read_data(node_id, "temperature")
103
83
  print(f"{node_id}: {temp}°F")
104
- ```
84
+ ```
85
+
86
+ ### Documentation
87
+ * [Usage Examples](./documentation/usage.md)
88
+ * [Protocol](./documentation/protocol.md)
89
+
90
+ #### API Reference
91
+ * [DSNodeServer](./documentation/ds-node-server.md)
92
+ * [DSNode](./documentation/ds-node.md)
93
+ * [DSNodeConfig](./documentation/ds-node-config.md)
@@ -0,0 +1,47 @@
1
+ ## DSNodeConfig
2
+
3
+ Configuration object for initializing a DSNode instance.
4
+
5
+ ```python
6
+ from distributed_state_network import DSNodeConfig
7
+ ```
8
+
9
+ ### Class Definition
10
+ ```python
11
+ @dataclass(frozen=True)
12
+ class DSNodeConfig:
13
+ node_id: str
14
+ port: int
15
+ aes_key_file: str
16
+ bootstrap_nodes: List[Endpoint]
17
+ ```
18
+
19
+ ### Attributes
20
+ - **node_id** (`str`): Unique identifier for the node
21
+ - **port** (`int`): Port number for the node to listen on
22
+ - **aes_key_file** (`str`): Path to the AES key file for encryption/decryption
23
+ - **bootstrap_nodes** (`List[Endpoint]`): List of initial nodes to connect to when joining the network
24
+
25
+ ### Methods
26
+
27
+ ### `from_dict(data: Dict) -> DSNodeConfig`
28
+ Creates a DSNodeConfig instance from a dictionary.
29
+
30
+ **Parameters:**
31
+ - `data` (`Dict`): Dictionary containing configuration parameters
32
+
33
+ **Returns:**
34
+ - `DSNodeConfig`: Configuration instance
35
+
36
+ **Example:**
37
+ ```python
38
+ config_dict = {
39
+ "node_id": "node1",
40
+ "port": 8000,
41
+ "aes_key_file": "/path/to/key.aes",
42
+ "bootstrap_nodes": [
43
+ {"address": "127.0.0.1", "port": 8001}
44
+ ]
45
+ }
46
+ config = DSNodeConfig.from_dict(config_dict)
47
+ ```
@@ -0,0 +1,58 @@
1
+ ## DSNodeServer
2
+
3
+ HTTPS server wrapper for DSNode that handles incoming network requests.
4
+
5
+ ```python
6
+ from distributed_state_network import DSNodeServer
7
+ ```
8
+
9
+ ### Class Definition
10
+ ```python
11
+ class DSNodeServer(HTTPServer):
12
+ config: DSNodeConfig
13
+ node: DSNode
14
+ ```
15
+
16
+ ### Constructor
17
+
18
+ **Parameters:**
19
+ - `config` (`DSNodeConfig`): Node configuration
20
+ - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events
21
+ - `update_callback` (`Optional[Callable]`): Callback for state update events
22
+
23
+ ### Static Methods
24
+
25
+ ### `start(config: DSNodeConfig, disconnect_callback: Optional[Callable] = None, update_callback: Optional[Callable] = None) -> DSNodeServer`
26
+ Creates and starts a new DSNodeServer instance with SSL configuration.
27
+ ```python
28
+ server = DSNodeServer.start(config)
29
+ ```
30
+
31
+ **Parameters:**
32
+ - `config` (`DSNodeConfig`): Node configuration
33
+ - `disconnect_callback` (`Optional[Callable]`): Callback for disconnect events
34
+ - `update_callback` (`Optional[Callable]`): Callback for state update events
35
+
36
+ **Returns:**
37
+ - `DSNodeServer`: Running server instance
38
+
39
+ ### `generate_key(out_file_path: str) -> None`
40
+ Generates a new AES key file for network encryption.
41
+
42
+ **Parameters:**
43
+ - `out_file_path` (`str`): Path where the key file will be saved
44
+
45
+ **Example:**
46
+ ```python
47
+ DSNodeServer.generate_key("/path/to/network.key")
48
+ ```
49
+
50
+ ### Instance Methods
51
+
52
+ ### `stop() -> None`
53
+ Gracefully shuts down the server and cleans up resources.
54
+
55
+ **Example:**
56
+ ```python
57
+ server.stop()
58
+ ```
@@ -0,0 +1,65 @@
1
+ ## DSNode
2
+
3
+ Main node class that handles all distributed state network operations including peer management, state synchronization, and encrypted communication.
4
+
5
+ ```python
6
+ from distributed_state_network import DSNode
7
+ ```
8
+
9
+ ### Class Definition
10
+ ```python
11
+ class DSNode:
12
+ config: DSNodeConfig
13
+ address_book: Dict[str, Endpoint]
14
+ node_states: Dict[str, StatePacket]
15
+ shutting_down: bool = False
16
+ ```
17
+
18
+ ### Constructor
19
+
20
+ ```python
21
+ node = DSNode(config, "0.0.3", disconnect_callback=on_disconnect, update_callback=on_update)
22
+ ```
23
+
24
+ **Parameters:**
25
+ - `config` (`DSNodeConfig`): Node configuration
26
+ - `version` (`str`): Protocol version string for compatibility checking
27
+ - `disconnect_callback` (`Optional[Callable]`): Callback function invoked when a peer disconnects
28
+ - `update_callback` (`Optional[Callable]`): Callback function invoked when the state of node updates
29
+
30
+ ### Key Methods
31
+
32
+ ### `bootstrap(con: Endpoint) -> None`
33
+ Connects to a bootstrap node to join the network.
34
+
35
+ **Parameters:**
36
+ - `con` (`Endpoint`): Connection endpoint of the bootstrap node
37
+
38
+ **Raises:**
39
+ - `Exception`: If connection to bootstrap node fails
40
+
41
+ ### `update_data(key: str, val: str) -> None`
42
+ Updates a key-value pair in the node's state and broadcasts the update to all peers.
43
+ ```python
44
+ node.update_data("status", "active")
45
+ ```
46
+
47
+ **Parameters:**
48
+ - `key` (`str`): State key to update
49
+ - `val` (`str`): New value for the key
50
+
51
+ ### `read_data(node_id: str, key: str) -> Optional[str]`
52
+ Reads a value from a specific node's state.
53
+
54
+ **Parameters:**
55
+ - `node_id` (`str`): ID of the node to read from
56
+ - `key` (`str`): Key to retrieve
57
+
58
+ **Returns:**
59
+ - `Optional[str]`: Value if exists, None otherwise
60
+
61
+ ### `peers() -> List[str]`
62
+ Returns a list of all connected peer node IDs.
63
+
64
+ **Returns:**
65
+ - `List[str]`: List of node IDs
@@ -0,0 +1,37 @@
1
+ ## Network Protocol
2
+
3
+ ### Endpoints
4
+ The server exposes the following HTTPS endpoints:
5
+
6
+ - **POST /hello**: Exchange node information and certificates
7
+ - **POST /peers**: Request/share peer list
8
+ - **POST /update**: Send/receive state updates
9
+ - **POST /ping**: Connectivity check
10
+
11
+ ### Security
12
+ - All communication is encrypted using AES with a shared key
13
+ - Messages are signed using ECDSA for authentication
14
+ - HTTPS with self-signed certificates for transport security
15
+
16
+ ### State Synchronization
17
+ - Nodes maintain a copy of all peers' states
18
+ - Updates are broadcast to all connected peers
19
+ - Timestamps prevent older updates from overwriting newer ones
20
+
21
+ ## Important Notes
22
+
23
+ 1. **Shared AES Key**: All nodes in the network must use the same AES key file
24
+ 2. **Unique Node IDs**: Each node must have a unique node_id
25
+ 3. **Port Availability**: Ensure the specified port is available before starting
26
+ 4. **Bootstrap Nodes**: At least one bootstrap node is required to join an existing network
27
+ 5. **Network Tick**: The network performs maintenance checks every 3 seconds
28
+ 6. **Certificate Management**: Certificates and keys are automatically generated and stored in `certs/` and `credentials/` directories
29
+
30
+ ## Error Handling
31
+
32
+ Common HTTP status codes returned:
33
+ - **200**: Success
34
+ - **401**: Not Authorized (signature verification failed)
35
+ - **404**: Endpoint not found
36
+ - **406**: Not Acceptable (invalid data or version mismatch)
37
+ - **505**: Version not supported
@@ -0,0 +1,84 @@
1
+ ## Usage Examples
2
+
3
+ ### Basic Setup
4
+
5
+ ```python
6
+ from distributed_state_network import DSNodeServer, DSNodeConfig, Endpoint
7
+
8
+ # Create configuration
9
+ config = DSNodeConfig(
10
+ node_id="node1",
11
+ port=8000,
12
+ aes_key_file="/path/to/shared.key",
13
+ bootstrap_nodes=[] # Empty for first node
14
+ )
15
+
16
+ # Start server
17
+ server = DSNodeServer.start(config)
18
+
19
+ # Update state
20
+ server.node.update_data("status", "online")
21
+ server.node.update_data("version", "1.0.0")
22
+
23
+ # Read own state
24
+ my_status = server.node.read_data("node1", "status")
25
+
26
+ # Get connected peers
27
+ peers = server.node.peers()
28
+ print(f"Connected peers: {peers}")
29
+
30
+ # Shutdown
31
+ server.stop()
32
+ ```
33
+
34
+ ### Joining Existing Network
35
+
36
+ ```python
37
+ # Node 2 configuration with bootstrap
38
+ config2 = DSNodeConfig(
39
+ node_id="node2",
40
+ port=8001,
41
+ aes_key_file="/path/to/shared.key", # Same key file as network
42
+ bootstrap_nodes=[
43
+ Endpoint("127.0.0.1", 8000) # Node 1's endpoint
44
+ ]
45
+ )
46
+
47
+ server2 = DSNodeServer.start(config2)
48
+
49
+ # Read state from peer
50
+ peer_status = server2.node.read_data("node1", "status")
51
+ ```
52
+
53
+ ### With Disconnect Callback
54
+
55
+ ```python
56
+ def handle_disconnect():
57
+ print("A peer has disconnected!")
58
+ # Handle reconnection logic
59
+
60
+ config = DSNodeConfig(
61
+ node_id="node3",
62
+ port=8002,
63
+ aes_key_file="/path/to/shared.key",
64
+ bootstrap_nodes=[Endpoint("127.0.0.1", 8000)]
65
+ )
66
+
67
+ server = DSNodeServer.start(config, disconnect_callback=handle_disconnect)
68
+ ```
69
+
70
+ ### With Update Callback
71
+
72
+ ```python
73
+ def handle_update():
74
+ print("A peer has updated!")
75
+
76
+ config = DSNodeConfig(
77
+ node_id="node3",
78
+ port=8002,
79
+ aes_key_file="/path/to/shared.key",
80
+ bootstrap_nodes=[Endpoint("127.0.0.1", 8000)]
81
+ )
82
+
83
+ server = DSNodeServer.start(config, update_callback=handle_update)
84
+ ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "distributed-state-network"
7
- version = "0.0.2"
7
+ version = "0.0.4"
8
8
  authors = [{name="Erin Clemmer", email="erin.c.clemmer@gmail.com"}]
9
9
  description = "A tool to distribute the state of a network device to other devices on the network"
10
10
  keywords = ["distributed", "networking"]
@@ -5,7 +5,7 @@ import logging
5
5
  import requests
6
6
  import threading
7
7
  from requests import RequestException
8
- from typing import Dict, Tuple, List, Optional
8
+ from typing import Dict, Tuple, List, Optional, Callable
9
9
 
10
10
  from distributed_state_network.objects.endpoint import Endpoint
11
11
  from distributed_state_network.objects.hello_packet import HelloPacket
@@ -29,6 +29,8 @@ class DSNode:
29
29
  self,
30
30
  config: DSNodeConfig,
31
31
  version: str,
32
+ disconnect_callback: Optional[Callable] = None,
33
+ update_callback: Optional[Callable] = None
32
34
  ):
33
35
  self.config = config
34
36
  self.version = version
@@ -48,6 +50,8 @@ class DSNode:
48
50
  }
49
51
 
50
52
  self.logger = logging.getLogger("DSN: " + config.node_id)
53
+ self.disconnect_cb = disconnect_callback
54
+ self.update_cb = update_callback
51
55
  if not os.path.exists(config.aes_key_file):
52
56
  raise Exception(f"Could not find aes key file in {config.aes_key_file}")
53
57
  threading.Thread(target=self.network_tick).start()
@@ -80,6 +84,8 @@ class DSNode:
80
84
  except RequestException:
81
85
  if node_id in self.node_states: # double check if something has changed since the ping request started
82
86
  remove(node_id)
87
+ if self.disconnect_cb is not None:
88
+ self.disconnect_cb()
83
89
 
84
90
  def send_request_to_node(self, node_id: str, path: str, payload: bytes, verify) -> Tuple[requests.Response, bytes]:
85
91
  con = self.connection_from_node(node_id)
@@ -227,6 +233,9 @@ class DSNode:
227
233
  if get_dict_hash(self.node_states[pkt.node_id].state_data) != get_dict_hash(pkt.state_data):
228
234
  self.node_states[pkt.node_id] = pkt
229
235
 
236
+ if self.update_cb is not None:
237
+ self.update_cb()
238
+
230
239
  return self.my_state().to_bytes()
231
240
 
232
241
  def my_state(self):
@@ -1,9 +1,7 @@
1
1
  import ssl
2
2
  import threading
3
- import json
4
3
  import logging
5
- from typing import Tuple, Callable, Dict
6
- from threading import Thread
4
+ from typing import Callable, Optional
7
5
  from http.server import HTTPServer, BaseHTTPRequestHandler
8
6
 
9
7
  from distributed_state_network.dsnode import DSNode
@@ -11,7 +9,7 @@ from distributed_state_network.objects.config import DSNodeConfig
11
9
  from distributed_state_network.util.aes import generate_aes_key
12
10
  from distributed_state_network.util import stop_thread
13
11
 
14
- VERSION = "0.0.1"
12
+ VERSION = "0.0.3"
15
13
  logging.basicConfig(level=logging.INFO)
16
14
 
17
15
  def respond_bytes(handler: BaseHTTPRequestHandler, data: bytes):
@@ -71,11 +69,13 @@ def serve(httpd):
71
69
  class DSNodeServer(HTTPServer):
72
70
  def __init__(
73
71
  self,
74
- config: DSNodeConfig
72
+ config: DSNodeConfig,
73
+ disconnect_callback: Optional[Callable] = None,
74
+ update_callback: Optional[Callable] = None
75
75
  ):
76
76
  super().__init__(("127.0.0.1", config.port), DSNodeHandler)
77
77
  self.config = config
78
- self.node = DSNode(config, VERSION)
78
+ self.node = DSNode(config, VERSION, disconnect_callback, update_callback)
79
79
  self.node.logger.info(f'Started DSNode on port {config.port}')
80
80
 
81
81
  def stop(self):
@@ -91,8 +91,8 @@ class DSNodeServer(HTTPServer):
91
91
  f.write(key)
92
92
 
93
93
  @staticmethod
94
- def start(config: DSNodeConfig) -> 'NodeServer':
95
- n = DSNodeServer(config)
94
+ def start(config: DSNodeConfig, disconnect_callback: Optional[Callable] = None, update_callback: Optional[Callable] = None) -> 'NodeServer':
95
+ n = DSNodeServer(config, disconnect_callback, update_callback)
96
96
  ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
97
97
  public_path = n.node.cert_manager.public_path(n.config.node_id)
98
98
  ssl_context.load_cert_chain(
@@ -2,6 +2,7 @@ import os
2
2
  import sys
3
3
  import time
4
4
  import unittest
5
+ from typing import Optional, Callable
5
6
 
6
7
  sys.path.append(os.path.join(os.path.dirname(__file__), '../src'))
7
8
 
@@ -12,9 +13,11 @@ KEY_FILE = "network.key"
12
13
  if not os.path.exists(KEY_FILE):
13
14
  DSNodeServer.generate_key("network.key")
14
15
 
16
+ cb_test = 0
17
+
15
18
  current_port = 5000
16
19
 
17
- def start_node(node_id: str, bootstrap_port: int = None):
20
+ def start_node(node_id: str, bootstrap_port: int = None, disconnect_cb: Optional[Callable] = None, update_cb: Optional[Callable] = None):
18
21
  global current_port
19
22
  args = {
20
23
  "node_id": node_id,
@@ -30,7 +33,7 @@ def start_node(node_id: str, bootstrap_port: int = None):
30
33
  else:
31
34
  args["bootstrap_nodes"] = []
32
35
 
33
- return DSNodeServer.start(DSNodeConfig(**args))
36
+ return DSNodeServer.start(DSNodeConfig(**args), disconnect_cb, update_cb)
34
37
 
35
38
  class ConnectionsTest(unittest.TestCase):
36
39
  def test_one(self):
@@ -71,5 +74,32 @@ class ConnectionsTest(unittest.TestCase):
71
74
  self.assertEqual(["node-1", "node-3", "node-4"], sorted(node3.node.peers()))
72
75
  self.assertEqual(["node-1", "node-3", "node-4"], sorted(node4.node.peers()))
73
76
 
77
+ def test_disconnect_cb(self):
78
+ global cb_test
79
+ cb_test = 0
80
+ def inc():
81
+ global cb_test
82
+ cb_test = 1
83
+ node1 = start_node("node-1", None, inc)
84
+ node2 = start_node("node-2", node1.config.port)
85
+ time.sleep(1)
86
+ node2.stop()
87
+ time.sleep(5)
88
+ self.assertEqual(cb_test, 1)
89
+
90
+ def test_update_cb(self):
91
+ global cb_test
92
+ cb_test = 0
93
+ def inc():
94
+ global cb_test
95
+ cb_test = 1
96
+ node1 = start_node("node-1", None, None, inc)
97
+ node2 = start_node("node-2", node1.config.port)
98
+
99
+ node2.node.update_data("key", "value")
100
+ time.sleep(1)
101
+
102
+ self.assertEqual(cb_test, 1)
103
+
74
104
  if __name__ == '__main__':
75
105
  unittest.main()