koi-net 1.0.0b9__py3-none-any.whl → 1.0.0b11__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.
Potentially problematic release.
This version of koi-net might be problematic. Click here for more details.
- koi_net/core.py +6 -6
- koi_net/network/graph.py +4 -4
- koi_net/network/interface.py +20 -18
- koi_net/network/request_handler.py +27 -19
- koi_net/processor/default_handlers.py +14 -14
- koi_net/processor/interface.py +50 -30
- koi_net/processor/knowledge_object.py +1 -1
- koi_net/protocol/event.py +3 -0
- {koi_net-1.0.0b9.dist-info → koi_net-1.0.0b11.dist-info}/METADATA +36 -3
- koi_net-1.0.0b11.dist-info/RECORD +24 -0
- koi_net-1.0.0b9.dist-info/RECORD +0 -24
- {koi_net-1.0.0b9.dist-info → koi_net-1.0.0b11.dist-info}/WHEEL +0 -0
- {koi_net-1.0.0b9.dist-info → koi_net-1.0.0b11.dist-info}/licenses/LICENSE +0 -0
koi_net/core.py
CHANGED
|
@@ -50,7 +50,7 @@ class NodeInterface:
|
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
# pull all handlers defined in default_handlers module
|
|
53
|
-
if
|
|
53
|
+
if handlers is None:
|
|
54
54
|
handlers = [
|
|
55
55
|
obj for obj in vars(default_handlers).values()
|
|
56
56
|
if isinstance(obj, KnowledgeHandler)
|
|
@@ -84,15 +84,15 @@ class NodeInterface:
|
|
|
84
84
|
)
|
|
85
85
|
)
|
|
86
86
|
|
|
87
|
-
logger.
|
|
87
|
+
logger.debug("Waiting for kobj queue to empty")
|
|
88
88
|
if self.use_kobj_processor_thread:
|
|
89
89
|
self.processor.kobj_queue.join()
|
|
90
90
|
else:
|
|
91
91
|
self.processor.flush_kobj_queue()
|
|
92
|
-
logger.
|
|
92
|
+
logger.debug("Done")
|
|
93
93
|
|
|
94
94
|
if not self.network.graph.get_neighbors() and self.first_contact:
|
|
95
|
-
logger.
|
|
95
|
+
logger.debug(f"I don't have any neighbors, reaching out to first contact {self.first_contact}")
|
|
96
96
|
|
|
97
97
|
events = [
|
|
98
98
|
Event.from_rid(EventType.FORGET, self.identity.rid),
|
|
@@ -106,7 +106,7 @@ class NodeInterface:
|
|
|
106
106
|
)
|
|
107
107
|
|
|
108
108
|
except httpx.ConnectError:
|
|
109
|
-
logger.
|
|
109
|
+
logger.warning("Failed to reach first contact")
|
|
110
110
|
return
|
|
111
111
|
|
|
112
112
|
|
|
@@ -118,7 +118,7 @@ class NodeInterface:
|
|
|
118
118
|
logger.info("Stopping node...")
|
|
119
119
|
|
|
120
120
|
if self.use_kobj_processor_thread:
|
|
121
|
-
logger.info("Waiting for kobj queue to empty")
|
|
121
|
+
logger.info(f"Waiting for kobj queue to empty ({self.processor.kobj_queue.unfinished_tasks} tasks remaining)")
|
|
122
122
|
self.processor.kobj_queue.join()
|
|
123
123
|
else:
|
|
124
124
|
self.processor.flush_kobj_queue()
|
koi_net/network/graph.py
CHANGED
|
@@ -25,12 +25,12 @@ class NetworkGraph:
|
|
|
25
25
|
|
|
26
26
|
def generate(self):
|
|
27
27
|
"""Generates directed graph from cached KOI nodes and edges."""
|
|
28
|
-
logger.
|
|
28
|
+
logger.debug("Generating network graph")
|
|
29
29
|
self.dg.clear()
|
|
30
30
|
for rid in self.cache.list_rids():
|
|
31
31
|
if type(rid) == KoiNetNode:
|
|
32
32
|
self.dg.add_node(rid)
|
|
33
|
-
logger.
|
|
33
|
+
logger.debug(f"Added node {rid}")
|
|
34
34
|
|
|
35
35
|
elif type(rid) == KoiNetEdge:
|
|
36
36
|
edge_profile = self.get_edge_profile(rid)
|
|
@@ -38,8 +38,8 @@ class NetworkGraph:
|
|
|
38
38
|
logger.warning(f"Failed to load {rid!r}")
|
|
39
39
|
continue
|
|
40
40
|
self.dg.add_edge(edge_profile.source, edge_profile.target, rid=rid)
|
|
41
|
-
logger.
|
|
42
|
-
logger.
|
|
41
|
+
logger.debug(f"Added edge {rid} ({edge_profile.source} -> {edge_profile.target})")
|
|
42
|
+
logger.debug("Done")
|
|
43
43
|
|
|
44
44
|
def get_node_profile(self, rid: KoiNetNode) -> NodeProfile | None:
|
|
45
45
|
"""Returns node profile given its RID."""
|
koi_net/network/interface.py
CHANGED
|
@@ -100,7 +100,7 @@ class NetworkInterface:
|
|
|
100
100
|
|
|
101
101
|
Event will be sent to webhook or poll queue depending on the node type and edge type of the specified node. If `flush` is set to `True`, the webhook queued will be flushed after pushing the event.
|
|
102
102
|
"""
|
|
103
|
-
logger.
|
|
103
|
+
logger.debug(f"Pushing event {event.event_type} {event.rid} to {node}")
|
|
104
104
|
|
|
105
105
|
node_profile = self.graph.get_node_profile(node)
|
|
106
106
|
if not node_profile:
|
|
@@ -136,14 +136,14 @@ class NetworkInterface:
|
|
|
136
136
|
if queue:
|
|
137
137
|
while not queue.empty():
|
|
138
138
|
event = queue.get()
|
|
139
|
-
logger.
|
|
139
|
+
logger.debug(f"Dequeued {event.event_type} '{event.rid}'")
|
|
140
140
|
events.append(event)
|
|
141
141
|
|
|
142
142
|
return events
|
|
143
143
|
|
|
144
144
|
def flush_poll_queue(self, node: KoiNetNode) -> list[Event]:
|
|
145
145
|
"""Flushes a node's poll queue, returning list of events."""
|
|
146
|
-
logger.
|
|
146
|
+
logger.debug(f"Flushing poll queue for {node}")
|
|
147
147
|
return self._flush_queue(self.poll_event_queue, node)
|
|
148
148
|
|
|
149
149
|
def flush_webhook_queue(self, node: KoiNetNode):
|
|
@@ -152,7 +152,7 @@ class NetworkInterface:
|
|
|
152
152
|
If node profile is unknown, or node type is not `FULL`, this operation will fail silently. If the remote node cannot be reached, all events will be requeued.
|
|
153
153
|
"""
|
|
154
154
|
|
|
155
|
-
logger.
|
|
155
|
+
logger.debug(f"Flushing webhook queue for {node}")
|
|
156
156
|
|
|
157
157
|
node_profile = self.graph.get_node_profile(node)
|
|
158
158
|
|
|
@@ -167,35 +167,37 @@ class NetworkInterface:
|
|
|
167
167
|
events = self._flush_queue(self.webhook_event_queue, node)
|
|
168
168
|
if not events: return
|
|
169
169
|
|
|
170
|
-
logger.
|
|
170
|
+
logger.debug(f"Broadcasting {len(events)} events")
|
|
171
171
|
|
|
172
172
|
try:
|
|
173
173
|
self.request_handler.broadcast_events(node, events=events)
|
|
174
|
+
return True
|
|
174
175
|
except httpx.ConnectError:
|
|
175
|
-
logger.warning("Broadcast failed,
|
|
176
|
+
logger.warning("Broadcast failed, dropping node")
|
|
176
177
|
for event in events:
|
|
177
178
|
self.push_event_to(event, node)
|
|
179
|
+
return False
|
|
178
180
|
|
|
179
181
|
def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
|
|
180
182
|
"""Returns list of node RIDs which provide state for the specified RID type."""
|
|
181
183
|
|
|
182
|
-
logger.
|
|
184
|
+
logger.debug(f"Looking for state providers of '{rid_type}'")
|
|
183
185
|
provider_nodes = []
|
|
184
186
|
for node_rid in self.cache.list_rids(rid_types=[KoiNetNode]):
|
|
185
187
|
node = self.graph.get_node_profile(node_rid)
|
|
186
188
|
|
|
187
189
|
if node.node_type == NodeType.FULL and rid_type in node.provides.state:
|
|
188
|
-
logger.
|
|
190
|
+
logger.debug(f"Found provider '{node_rid}'")
|
|
189
191
|
provider_nodes.append(node_rid)
|
|
190
192
|
|
|
191
193
|
if not provider_nodes:
|
|
192
|
-
logger.
|
|
194
|
+
logger.debug("Failed to find providers")
|
|
193
195
|
return provider_nodes
|
|
194
196
|
|
|
195
197
|
def fetch_remote_bundle(self, rid: RID):
|
|
196
198
|
"""Attempts to fetch a bundle by RID from known peer nodes."""
|
|
197
199
|
|
|
198
|
-
logger.
|
|
200
|
+
logger.debug(f"Fetching remote bundle '{rid}'")
|
|
199
201
|
remote_bundle = None
|
|
200
202
|
for node_rid in self.get_state_providers(type(rid)):
|
|
201
203
|
payload = self.request_handler.fetch_bundles(
|
|
@@ -203,7 +205,7 @@ class NetworkInterface:
|
|
|
203
205
|
|
|
204
206
|
if payload.bundles:
|
|
205
207
|
remote_bundle = payload.bundles[0]
|
|
206
|
-
logger.
|
|
208
|
+
logger.debug(f"Got bundle from '{node_rid}'")
|
|
207
209
|
break
|
|
208
210
|
|
|
209
211
|
if not remote_bundle:
|
|
@@ -214,7 +216,7 @@ class NetworkInterface:
|
|
|
214
216
|
def fetch_remote_manifest(self, rid: RID):
|
|
215
217
|
"""Attempts to fetch a manifest by RID from known peer nodes."""
|
|
216
218
|
|
|
217
|
-
logger.
|
|
219
|
+
logger.debug(f"Fetching remote manifest '{rid}'")
|
|
218
220
|
remote_manifest = None
|
|
219
221
|
for node_rid in self.get_state_providers(type(rid)):
|
|
220
222
|
payload = self.request_handler.fetch_manifests(
|
|
@@ -222,7 +224,7 @@ class NetworkInterface:
|
|
|
222
224
|
|
|
223
225
|
if payload.manifests:
|
|
224
226
|
remote_manifest = payload.manifests[0]
|
|
225
|
-
logger.
|
|
227
|
+
logger.debug(f"Got bundle from '{node_rid}'")
|
|
226
228
|
break
|
|
227
229
|
|
|
228
230
|
if not remote_manifest:
|
|
@@ -239,17 +241,17 @@ class NetworkInterface:
|
|
|
239
241
|
neighbors = self.graph.get_neighbors()
|
|
240
242
|
|
|
241
243
|
if not neighbors and self.first_contact:
|
|
242
|
-
logger.
|
|
244
|
+
logger.debug("No neighbors found, polling first contact")
|
|
243
245
|
try:
|
|
244
246
|
payload = self.request_handler.poll_events(
|
|
245
247
|
url=self.first_contact,
|
|
246
248
|
rid=self.identity.rid
|
|
247
249
|
)
|
|
248
250
|
if payload.events:
|
|
249
|
-
logger.
|
|
251
|
+
logger.debug(f"Received {len(payload.events)} events from '{self.first_contact}'")
|
|
250
252
|
return payload.events
|
|
251
253
|
except httpx.ConnectError:
|
|
252
|
-
logger.
|
|
254
|
+
logger.debug(f"Failed to reach first contact '{self.first_contact}'")
|
|
253
255
|
|
|
254
256
|
events = []
|
|
255
257
|
for node_rid in neighbors:
|
|
@@ -263,10 +265,10 @@ class NetworkInterface:
|
|
|
263
265
|
rid=self.identity.rid
|
|
264
266
|
)
|
|
265
267
|
if payload.events:
|
|
266
|
-
logger.
|
|
268
|
+
logger.debug(f"Received {len(payload.events)} events from {node_rid!r}")
|
|
267
269
|
events.extend(payload.events)
|
|
268
270
|
except httpx.ConnectError:
|
|
269
|
-
logger.
|
|
271
|
+
logger.debug(f"Failed to reach node '{node_rid}'")
|
|
270
272
|
continue
|
|
271
273
|
|
|
272
274
|
return events
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import httpx
|
|
3
|
-
from pydantic import BaseModel
|
|
4
3
|
from rid_lib import RID
|
|
5
4
|
from rid_lib.ext import Cache
|
|
6
5
|
from rid_lib.types.koi_net_node import KoiNetNode
|
|
@@ -46,7 +45,7 @@ class RequestHandler:
|
|
|
46
45
|
request: RequestModels,
|
|
47
46
|
response_model: type[ResponseModels] | None = None
|
|
48
47
|
) -> ResponseModels | None:
|
|
49
|
-
logger.
|
|
48
|
+
logger.debug(f"Making request to {url}")
|
|
50
49
|
resp = httpx.post(
|
|
51
50
|
url=url,
|
|
52
51
|
data=request.model_dump_json()
|
|
@@ -66,7 +65,7 @@ class RequestHandler:
|
|
|
66
65
|
raise Exception("Node not found")
|
|
67
66
|
if node_profile.node_type != NodeType.FULL:
|
|
68
67
|
raise Exception("Can't query partial node")
|
|
69
|
-
logger.
|
|
68
|
+
logger.debug(f"Resolved {node_rid!r} to {node_profile.base_url}")
|
|
70
69
|
return node_profile.base_url
|
|
71
70
|
else:
|
|
72
71
|
return url
|
|
@@ -79,10 +78,11 @@ class RequestHandler:
|
|
|
79
78
|
**kwargs
|
|
80
79
|
) -> None:
|
|
81
80
|
"""See protocol.api_models.EventsPayload for available kwargs."""
|
|
81
|
+
request = req or EventsPayload.model_validate(kwargs)
|
|
82
82
|
self.make_request(
|
|
83
|
-
self.get_url(node, url) + BROADCAST_EVENTS_PATH,
|
|
84
|
-
req or EventsPayload.model_validate(kwargs)
|
|
83
|
+
self.get_url(node, url) + BROADCAST_EVENTS_PATH, request
|
|
85
84
|
)
|
|
85
|
+
logger.info(f"Broadcasted {len(request.events)} event(s) to {node or url!r}")
|
|
86
86
|
|
|
87
87
|
def poll_events(
|
|
88
88
|
self,
|
|
@@ -92,12 +92,14 @@ class RequestHandler:
|
|
|
92
92
|
**kwargs
|
|
93
93
|
) -> EventsPayload:
|
|
94
94
|
"""See protocol.api_models.PollEvents for available kwargs."""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
request = req or PollEvents.model_validate(kwargs)
|
|
96
|
+
resp = self.make_request(
|
|
97
|
+
self.get_url(node, url) + POLL_EVENTS_PATH, request,
|
|
98
98
|
response_model=EventsPayload
|
|
99
99
|
)
|
|
100
|
-
|
|
100
|
+
logger.info(f"Polled {len(resp.events)} events from {node or url!r}")
|
|
101
|
+
return resp
|
|
102
|
+
|
|
101
103
|
def fetch_rids(
|
|
102
104
|
self,
|
|
103
105
|
node: RID = None,
|
|
@@ -106,11 +108,13 @@ class RequestHandler:
|
|
|
106
108
|
**kwargs
|
|
107
109
|
) -> RidsPayload:
|
|
108
110
|
"""See protocol.api_models.FetchRids for available kwargs."""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
request = req or FetchRids.model_validate(kwargs)
|
|
112
|
+
resp = self.make_request(
|
|
113
|
+
self.get_url(node, url) + FETCH_RIDS_PATH, request,
|
|
112
114
|
response_model=RidsPayload
|
|
113
115
|
)
|
|
116
|
+
logger.info(f"Fetched {len(resp.rids)} RID(s) from {node or url!r}")
|
|
117
|
+
return resp
|
|
114
118
|
|
|
115
119
|
def fetch_manifests(
|
|
116
120
|
self,
|
|
@@ -120,11 +124,13 @@ class RequestHandler:
|
|
|
120
124
|
**kwargs
|
|
121
125
|
) -> ManifestsPayload:
|
|
122
126
|
"""See protocol.api_models.FetchManifests for available kwargs."""
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
request = req or FetchManifests.model_validate(kwargs)
|
|
128
|
+
resp = self.make_request(
|
|
129
|
+
self.get_url(node, url) + FETCH_MANIFESTS_PATH, request,
|
|
126
130
|
response_model=ManifestsPayload
|
|
127
131
|
)
|
|
132
|
+
logger.info(f"Fetched {len(resp.manifests)} manifest(s) from {node or url!r}")
|
|
133
|
+
return resp
|
|
128
134
|
|
|
129
135
|
def fetch_bundles(
|
|
130
136
|
self,
|
|
@@ -134,8 +140,10 @@ class RequestHandler:
|
|
|
134
140
|
**kwargs
|
|
135
141
|
) -> BundlesPayload:
|
|
136
142
|
"""See protocol.api_models.FetchBundles for available kwargs."""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
request = req or FetchBundles.model_validate(kwargs)
|
|
144
|
+
resp = self.make_request(
|
|
145
|
+
self.get_url(node, url) + FETCH_BUNDLES_PATH, request,
|
|
140
146
|
response_model=BundlesPayload
|
|
141
|
-
)
|
|
147
|
+
)
|
|
148
|
+
logger.info(f"Fetched {len(resp.bundles)} bundle(s) from {node or url!r}")
|
|
149
|
+
return resp
|
|
@@ -22,7 +22,7 @@ def basic_rid_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
|
|
|
22
22
|
"""
|
|
23
23
|
if (kobj.rid == processor.identity.rid and
|
|
24
24
|
kobj.source == KnowledgeSource.External):
|
|
25
|
-
logger.
|
|
25
|
+
logger.debug("Don't let anyone else tell me who I am!")
|
|
26
26
|
return STOP_CHAIN
|
|
27
27
|
|
|
28
28
|
if kobj.event_type == EventType.FORGET:
|
|
@@ -41,17 +41,17 @@ def basic_manifest_handler(processor: ProcessorInterface, kobj: KnowledgeObject)
|
|
|
41
41
|
|
|
42
42
|
if prev_bundle:
|
|
43
43
|
if kobj.manifest.sha256_hash == prev_bundle.manifest.sha256_hash:
|
|
44
|
-
logger.
|
|
44
|
+
logger.debug("Hash of incoming manifest is same as existing knowledge, ignoring")
|
|
45
45
|
return STOP_CHAIN
|
|
46
46
|
if kobj.manifest.timestamp <= prev_bundle.manifest.timestamp:
|
|
47
|
-
logger.
|
|
47
|
+
logger.debug("Timestamp of incoming manifest is the same or older than existing knowledge, ignoring")
|
|
48
48
|
return STOP_CHAIN
|
|
49
49
|
|
|
50
|
-
logger.
|
|
50
|
+
logger.debug("RID previously known to me, labeling as 'UPDATE'")
|
|
51
51
|
kobj.normalized_event_type = EventType.UPDATE
|
|
52
52
|
|
|
53
53
|
else:
|
|
54
|
-
logger.
|
|
54
|
+
logger.debug("RID previously unknown to me, labeling as 'NEW'")
|
|
55
55
|
kobj.normalized_event_type = EventType.NEW
|
|
56
56
|
|
|
57
57
|
return kobj
|
|
@@ -77,7 +77,7 @@ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObjec
|
|
|
77
77
|
if edge_profile.status != EdgeStatus.PROPOSED:
|
|
78
78
|
return
|
|
79
79
|
|
|
80
|
-
logger.
|
|
80
|
+
logger.debug("Handling edge negotiation")
|
|
81
81
|
|
|
82
82
|
peer_rid = edge_profile.target
|
|
83
83
|
peer_profile = processor.network.graph.get_node_profile(peer_rid)
|
|
@@ -96,11 +96,11 @@ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObjec
|
|
|
96
96
|
abort = False
|
|
97
97
|
if (edge_profile.edge_type == EdgeType.WEBHOOK and
|
|
98
98
|
peer_profile.node_type == NodeType.PARTIAL):
|
|
99
|
-
logger.
|
|
99
|
+
logger.debug("Partial nodes cannot use webhooks")
|
|
100
100
|
abort = True
|
|
101
101
|
|
|
102
102
|
if not set(edge_profile.rid_types).issubset(provided_events):
|
|
103
|
-
logger.
|
|
103
|
+
logger.debug("Requested RID types not provided by this node")
|
|
104
104
|
abort = True
|
|
105
105
|
|
|
106
106
|
if abort:
|
|
@@ -110,16 +110,16 @@ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObjec
|
|
|
110
110
|
|
|
111
111
|
else:
|
|
112
112
|
# approve edge profile
|
|
113
|
-
logger.
|
|
113
|
+
logger.debug("Approving proposed edge")
|
|
114
114
|
edge_profile.status = EdgeStatus.APPROVED
|
|
115
115
|
updated_bundle = Bundle.generate(kobj.rid, edge_profile.model_dump())
|
|
116
116
|
|
|
117
|
-
processor.handle(bundle=updated_bundle)
|
|
117
|
+
processor.handle(bundle=updated_bundle, event_type=EventType.UPDATE)
|
|
118
118
|
return
|
|
119
119
|
|
|
120
120
|
elif edge_profile.target == processor.identity.rid:
|
|
121
121
|
if edge_profile.status == EdgeStatus.APPROVED:
|
|
122
|
-
logger.
|
|
122
|
+
logger.debug("Edge approved by other node!")
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
# Network handlers
|
|
@@ -140,12 +140,12 @@ def basic_network_output_filter(processor: ProcessorInterface, kobj: KnowledgeOb
|
|
|
140
140
|
edge_profile = kobj.bundle.validate_contents(EdgeProfile)
|
|
141
141
|
|
|
142
142
|
if edge_profile.source == processor.identity.rid:
|
|
143
|
-
logger.
|
|
143
|
+
logger.debug(f"Adding edge target '{edge_profile.target!r}' to network targets")
|
|
144
144
|
kobj.network_targets.update([edge_profile.target])
|
|
145
145
|
involves_me = True
|
|
146
146
|
|
|
147
147
|
elif edge_profile.target == processor.identity.rid:
|
|
148
|
-
logger.
|
|
148
|
+
logger.debug(f"Adding edge source '{edge_profile.source!r}' to network targets")
|
|
149
149
|
kobj.network_targets.update([edge_profile.source])
|
|
150
150
|
involves_me = True
|
|
151
151
|
|
|
@@ -156,7 +156,7 @@ def basic_network_output_filter(processor: ProcessorInterface, kobj: KnowledgeOb
|
|
|
156
156
|
allowed_type=type(kobj.rid)
|
|
157
157
|
)
|
|
158
158
|
|
|
159
|
-
logger.
|
|
159
|
+
logger.debug(f"Updating network targets with '{type(kobj.rid)}' subscribers: {subscribers}")
|
|
160
160
|
kobj.network_targets.update(subscribers)
|
|
161
161
|
|
|
162
162
|
return kobj
|
koi_net/processor/interface.py
CHANGED
|
@@ -101,12 +101,12 @@ class ProcessorInterface:
|
|
|
101
101
|
if handler.event_types and kobj.event_type not in handler.event_types:
|
|
102
102
|
continue
|
|
103
103
|
|
|
104
|
-
logger.
|
|
104
|
+
logger.debug(f"Calling {handler_type} handler '{handler.func.__name__}'")
|
|
105
105
|
resp = handler.func(self, kobj.model_copy())
|
|
106
106
|
|
|
107
107
|
# stops handler chain execution
|
|
108
108
|
if resp is STOP_CHAIN:
|
|
109
|
-
logger.
|
|
109
|
+
logger.debug(f"Handler chain stopped by {handler.func.__name__}")
|
|
110
110
|
return STOP_CHAIN
|
|
111
111
|
# kobj unmodified
|
|
112
112
|
elif resp is None:
|
|
@@ -114,7 +114,7 @@ class ProcessorInterface:
|
|
|
114
114
|
# kobj modified by handler
|
|
115
115
|
elif isinstance(resp, KnowledgeObject):
|
|
116
116
|
kobj = resp
|
|
117
|
-
logger.
|
|
117
|
+
logger.debug(f"Knowledge object modified by {handler.func.__name__}")
|
|
118
118
|
else:
|
|
119
119
|
raise ValueError(f"Handler {handler.func.__name__} returned invalid response '{resp}'")
|
|
120
120
|
|
|
@@ -134,34 +134,41 @@ class ProcessorInterface:
|
|
|
134
134
|
The pipeline may be stopped by any point by a single handler returning the `STOP_CHAIN` sentinel. In that case, the process will exit immediately. Further handlers of that type and later handler chains will not be called.
|
|
135
135
|
"""
|
|
136
136
|
|
|
137
|
-
logger.
|
|
137
|
+
logger.debug(f"Handling {kobj!r}")
|
|
138
138
|
kobj = self.call_handler_chain(HandlerType.RID, kobj)
|
|
139
139
|
if kobj is STOP_CHAIN: return
|
|
140
140
|
|
|
141
141
|
if kobj.event_type == EventType.FORGET:
|
|
142
142
|
bundle = self.cache.read(kobj.rid)
|
|
143
143
|
if not bundle:
|
|
144
|
-
logger.
|
|
144
|
+
logger.debug("Local bundle not found")
|
|
145
145
|
return
|
|
146
146
|
|
|
147
147
|
# the bundle (to be deleted) attached to kobj for downstream analysis
|
|
148
|
-
logger.
|
|
148
|
+
logger.debug("Adding local bundle (to be deleted) to knowledge object")
|
|
149
149
|
kobj.manifest = bundle.manifest
|
|
150
150
|
kobj.contents = bundle.contents
|
|
151
151
|
|
|
152
152
|
else:
|
|
153
153
|
# attempt to retrieve manifest
|
|
154
154
|
if not kobj.manifest:
|
|
155
|
+
logger.debug("Manifest not found")
|
|
155
156
|
if kobj.source == KnowledgeSource.External:
|
|
156
|
-
logger.
|
|
157
|
+
logger.debug("Attempting to fetch remote manifest")
|
|
157
158
|
manifest = self.network.fetch_remote_manifest(kobj.rid)
|
|
158
|
-
if not manifest: return
|
|
159
159
|
|
|
160
160
|
elif kobj.source == KnowledgeSource.Internal:
|
|
161
|
-
logger.
|
|
161
|
+
logger.debug("Attempting to read manifest from cache")
|
|
162
162
|
bundle = self.cache.read(kobj.rid)
|
|
163
|
-
if
|
|
164
|
-
|
|
163
|
+
if bundle:
|
|
164
|
+
manifest = bundle.manifest
|
|
165
|
+
else:
|
|
166
|
+
manifest = None
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
if not manifest:
|
|
170
|
+
logger.debug("Failed to find manifest")
|
|
171
|
+
return
|
|
165
172
|
|
|
166
173
|
kobj.manifest = manifest
|
|
167
174
|
|
|
@@ -170,19 +177,22 @@ class ProcessorInterface:
|
|
|
170
177
|
|
|
171
178
|
# attempt to retrieve bundle
|
|
172
179
|
if not kobj.bundle:
|
|
180
|
+
logger.debug("Bundle not found")
|
|
173
181
|
if kobj.source == KnowledgeSource.External:
|
|
174
|
-
logger.
|
|
182
|
+
logger.debug("Attempting to fetch remote bundle")
|
|
175
183
|
bundle = self.network.fetch_remote_bundle(kobj.rid)
|
|
176
|
-
# TODO: WARNING MANIFEST MAY BE DIFFERENT
|
|
177
184
|
|
|
178
185
|
elif kobj.source == KnowledgeSource.Internal:
|
|
179
|
-
logger.
|
|
186
|
+
logger.debug("Attempting to read bundle from cache")
|
|
180
187
|
bundle = self.cache.read(kobj.rid)
|
|
181
188
|
|
|
189
|
+
if not bundle:
|
|
190
|
+
logger.debug("Failed to find bundle")
|
|
191
|
+
return
|
|
192
|
+
|
|
182
193
|
if kobj.manifest != bundle.manifest:
|
|
183
194
|
logger.warning("Retrieved bundle contains a different manifest")
|
|
184
195
|
|
|
185
|
-
if not bundle: return
|
|
186
196
|
kobj.manifest = bundle.manifest
|
|
187
197
|
kobj.contents = bundle.contents
|
|
188
198
|
|
|
@@ -190,31 +200,34 @@ class ProcessorInterface:
|
|
|
190
200
|
if kobj is STOP_CHAIN: return
|
|
191
201
|
|
|
192
202
|
if kobj.normalized_event_type in (EventType.UPDATE, EventType.NEW):
|
|
193
|
-
logger.info(f"Writing {kobj!r}
|
|
203
|
+
logger.info(f"Writing to cache: {kobj!r}")
|
|
194
204
|
self.cache.write(kobj.bundle)
|
|
195
205
|
|
|
196
206
|
elif kobj.normalized_event_type == EventType.FORGET:
|
|
197
|
-
logger.info(f"Deleting {kobj!r}
|
|
207
|
+
logger.info(f"Deleting from cache: {kobj!r}")
|
|
198
208
|
self.cache.delete(kobj.rid)
|
|
199
209
|
|
|
200
210
|
else:
|
|
201
|
-
logger.
|
|
211
|
+
logger.debug("Normalized event type was never set, no cache or network operations will occur")
|
|
202
212
|
return
|
|
203
213
|
|
|
204
214
|
if type(kobj.rid) in (KoiNetNode, KoiNetEdge):
|
|
205
|
-
logger.
|
|
215
|
+
logger.debug("Change to node or edge, regenerating network graph")
|
|
206
216
|
self.network.graph.generate()
|
|
207
217
|
|
|
208
218
|
kobj = self.call_handler_chain(HandlerType.Network, kobj)
|
|
209
219
|
if kobj is STOP_CHAIN: return
|
|
210
220
|
|
|
211
221
|
if kobj.network_targets:
|
|
212
|
-
logger.
|
|
222
|
+
logger.debug(f"Broadcasting event to {len(kobj.network_targets)} network target(s)")
|
|
213
223
|
else:
|
|
214
|
-
logger.
|
|
224
|
+
logger.debug("No network targets set")
|
|
215
225
|
|
|
216
226
|
for node in kobj.network_targets:
|
|
217
|
-
self.network.push_event_to(kobj.normalized_event, node
|
|
227
|
+
self.network.push_event_to(kobj.normalized_event, node)
|
|
228
|
+
if not self.network.flush_webhook_queue(node):
|
|
229
|
+
logger.warning("Dropping unresponsive node")
|
|
230
|
+
self.handle(rid=node, event_type=EventType.FORGET)
|
|
218
231
|
|
|
219
232
|
kobj = self.call_handler_chain(HandlerType.Final, kobj)
|
|
220
233
|
|
|
@@ -228,18 +241,25 @@ class ProcessorInterface:
|
|
|
228
241
|
|
|
229
242
|
while not self.kobj_queue.empty():
|
|
230
243
|
kobj = self.kobj_queue.get()
|
|
231
|
-
logger.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
244
|
+
logger.debug(f"Dequeued {kobj!r}")
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
self.process_kobj(kobj)
|
|
248
|
+
finally:
|
|
249
|
+
self.kobj_queue.task_done()
|
|
250
|
+
logger.debug("Done")
|
|
235
251
|
|
|
236
252
|
def kobj_processor_worker(self, timeout=0.1):
|
|
237
253
|
while True:
|
|
238
254
|
try:
|
|
239
255
|
kobj = self.kobj_queue.get(timeout=timeout)
|
|
240
|
-
logger.
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
logger.debug(f"Dequeued {kobj!r}")
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
self.process_kobj(kobj)
|
|
260
|
+
finally:
|
|
261
|
+
self.kobj_queue.task_done()
|
|
262
|
+
logger.debug("Done")
|
|
243
263
|
|
|
244
264
|
except queue.Empty:
|
|
245
265
|
pass
|
|
@@ -275,4 +295,4 @@ class ProcessorInterface:
|
|
|
275
295
|
raise ValueError("One of 'rid', 'manifest', 'bundle', 'event', or 'kobj' must be provided")
|
|
276
296
|
|
|
277
297
|
self.kobj_queue.put(_kobj)
|
|
278
|
-
logger.
|
|
298
|
+
logger.debug(f"Queued {_kobj!r}")
|
|
@@ -35,7 +35,7 @@ class KnowledgeObject(BaseModel):
|
|
|
35
35
|
network_targets: set[KoiNetNode] = set()
|
|
36
36
|
|
|
37
37
|
def __repr__(self):
|
|
38
|
-
return f"<
|
|
38
|
+
return f"<KObj '{self.rid}' event type: '{self.event_type}' -> '{self.normalized_event_type}', source: '{self.source}'>"
|
|
39
39
|
|
|
40
40
|
@classmethod
|
|
41
41
|
def from_rid(
|
koi_net/protocol/event.py
CHANGED
|
@@ -15,6 +15,9 @@ class Event(BaseModel):
|
|
|
15
15
|
manifest: Manifest | None = None
|
|
16
16
|
contents: dict | None = None
|
|
17
17
|
|
|
18
|
+
def __repr__(self):
|
|
19
|
+
return f"<Event '{self.rid}' event type: '{self.event_type}'>"
|
|
20
|
+
|
|
18
21
|
@classmethod
|
|
19
22
|
def from_bundle(cls, event_type: EventType, bundle: Bundle):
|
|
20
23
|
return cls(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: koi-net
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0b11
|
|
4
4
|
Summary: Implementation of KOI-net protocol in Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/BlockScience/koi-net/
|
|
6
6
|
Author-email: Luke Miller <luke@block.science>
|
|
@@ -44,6 +44,37 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
|
|
45
45
|
*This specification is the result of several iterations of KOI research, [read more here](https://github.com/BlockScience/koi).*
|
|
46
46
|
|
|
47
|
+
### Jump to Sections:
|
|
48
|
+
- [Protocol](#protocol)
|
|
49
|
+
- [Introduction](#introduction)
|
|
50
|
+
- [Communication Methods](#communication-methods)
|
|
51
|
+
- [Quickstart](#quickstart)
|
|
52
|
+
- [Setup](#setup)
|
|
53
|
+
- [Creating a Node](#creating-a-node)
|
|
54
|
+
- [Knowledge Processing](#knowledge-processing)
|
|
55
|
+
- [Try It Out!](#try-it-out)
|
|
56
|
+
- [Advanced](#advanced)
|
|
57
|
+
- [Knowledge Processing Pipeline](#knowledge-processing-pipeline)
|
|
58
|
+
- [Knowledge Handlers](#knowledge-handlers)
|
|
59
|
+
- [RID Handler](#rid-handler)
|
|
60
|
+
- [Manifest Handler](#manifest-handler)
|
|
61
|
+
- [Bundle Handler](#bundle-handler)
|
|
62
|
+
- [Network Handler](#network-handler)
|
|
63
|
+
- [Final Handler](#final-handler)
|
|
64
|
+
- [Registering Handlers](#registering-handlers)
|
|
65
|
+
- [Default Behavior](#default-behavior)
|
|
66
|
+
- [Implementation Reference](#implementation-reference)
|
|
67
|
+
- [Node Interface](#node-interface)
|
|
68
|
+
- [Node Identity](#node-identity)
|
|
69
|
+
- [Network Interface](#network-interface)
|
|
70
|
+
- [Network Graph](#network-graph)
|
|
71
|
+
- [Request Handler](#request-handler)
|
|
72
|
+
- [Response Handler](#response-handler)
|
|
73
|
+
- [Processor Interface](#processor-interface)
|
|
74
|
+
- [Development](#development)
|
|
75
|
+
- [Setup](#setup-1)
|
|
76
|
+
- [Distribution](#distribution)
|
|
77
|
+
|
|
47
78
|
# Protocol
|
|
48
79
|
## Introduction
|
|
49
80
|
|
|
@@ -87,7 +118,9 @@ The request and payload JSON objects are composed of the fundamental "knowledge
|
|
|
87
118
|
}
|
|
88
119
|
```
|
|
89
120
|
|
|
90
|
-
|
|
121
|
+
An event is a signalling construct that conveys information about RID objects between networked nodes. Events are composed of an RID, manifest, or bundle with an event type attached. Event types can be one of `"FORGET"`, `"UPDATE"`, or `"NEW"` forming the "FUN" acronym.
|
|
122
|
+
|
|
123
|
+
As opposed to CRUD (create, read, update, delete), events are a series of messages, not operations. Each node has its own autonomy in deciding how to react based on the message it receives. For example, a processor node may receive a `"NEW"` event for an RID object its not interested in, and ignore it. Or it may decide that an `"UPDATE"` event should trigger fetching a bundle from another node. A node emits an event to indicate that its internal state has changed:
|
|
91
124
|
- `"NEW"` - indicates an previously unknown RID was cached
|
|
92
125
|
- `"UPDATE"` - indicates a previously known RID was cached
|
|
93
126
|
- `"FORGET"` - indicates a previously known RID was deleted
|
|
@@ -653,5 +686,5 @@ python -m build
|
|
|
653
686
|
```
|
|
654
687
|
Push new package build to PyPI:
|
|
655
688
|
```shell
|
|
656
|
-
python -m twine upload dist/*
|
|
689
|
+
python -m twine upload --skip-existing dist/*
|
|
657
690
|
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
koi_net/__init__.py,sha256=b0Ze0pZmJAuygpWUFHM6Kvqo3DkU_uzmkptv1EpAArw,31
|
|
2
|
+
koi_net/core.py,sha256=975g-IcEQ72wZCulg34Orh7vJNRWMCh5iC5kppz_18Q,4687
|
|
3
|
+
koi_net/identity.py,sha256=PBgmAx5f3zzQmHASB1TJW2g19n9TLfmSJMXg2eQFg0A,2386
|
|
4
|
+
koi_net/network/__init__.py,sha256=r_RN-q_mDYC-2RAkN-lJoMUX76TXyfEUc_MVKW87z0g,39
|
|
5
|
+
koi_net/network/graph.py,sha256=KyS4Vv94DhHQk12u6rmE8nOIuTxeoJjrnoYEwVMNM74,4837
|
|
6
|
+
koi_net/network/interface.py,sha256=ogVnVufGPv_x83KnGf3AwhCBTF8xETBH3QId6oqpyNs,10708
|
|
7
|
+
koi_net/network/request_handler.py,sha256=dufyJRwKD5w578UM-wzFVT3EU78NMvrh_yNSiI7vgfw,4938
|
|
8
|
+
koi_net/network/response_handler.py,sha256=HaP8Fl0bp_lfMmevhdVY8s9o0Uf8CR1ZaW5g3jsX8gw,1888
|
|
9
|
+
koi_net/processor/__init__.py,sha256=x4fAY0hvQEDcpfdTB3POIzxBQjYAtn0qQazPo1Xm0m4,41
|
|
10
|
+
koi_net/processor/default_handlers.py,sha256=ebFJ_MqvGBAb0SxmreZ9Pvyi0tD7ygDvJKECglw8uAQ,6972
|
|
11
|
+
koi_net/processor/handler.py,sha256=a2wxG3kgux-07YP13HSj_PH5VH_PjbnTS4uZEHjxMw0,2582
|
|
12
|
+
koi_net/processor/interface.py,sha256=IH3rh2lAliqNSKsuH8PwY9N098SnVUBy3Ccel8YgiRM,13188
|
|
13
|
+
koi_net/processor/knowledge_object.py,sha256=xI1K4lIGJrWbb7yStjpU4eqZGI7t4wFklv72V7TRfXY,4212
|
|
14
|
+
koi_net/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
koi_net/protocol/api_models.py,sha256=RDwVHAahiWzwzUnlj5MIm9et5WVpQOaG-Uscv1B9coU,1116
|
|
16
|
+
koi_net/protocol/consts.py,sha256=zeWJvRpqcERrqJq39heyNHb6f_9QrvoBZJHd70yE914,249
|
|
17
|
+
koi_net/protocol/edge.py,sha256=G3D9Ie0vbTSMJdoTw9g_oBmFCqzJ1gO7U1PVrw7p3j8,447
|
|
18
|
+
koi_net/protocol/event.py,sha256=YHsLD9HPwib-HiCWv9ep0sPqL2VHQqx_egDmbriChXM,1378
|
|
19
|
+
koi_net/protocol/helpers.py,sha256=9E9PaoIuSNrTBATGCLJ_kSBMZ2z-KIMnLJzGOTqQDC0,719
|
|
20
|
+
koi_net/protocol/node.py,sha256=Ntrx01dbm39ViKGtr4gLmztcMwKpTIweS6rRL-zoU_Y,391
|
|
21
|
+
koi_net-1.0.0b11.dist-info/METADATA,sha256=CAQb9AH_yDb2FO1XT9FzmDrrfUsCYTDUUo3mCPq8VQ4,34131
|
|
22
|
+
koi_net-1.0.0b11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
+
koi_net-1.0.0b11.dist-info/licenses/LICENSE,sha256=XBcvl8yjCAezfuqN1jadQykrX7H2g4nr2WRDmHLW6ik,1090
|
|
24
|
+
koi_net-1.0.0b11.dist-info/RECORD,,
|
koi_net-1.0.0b9.dist-info/RECORD
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
koi_net/__init__.py,sha256=b0Ze0pZmJAuygpWUFHM6Kvqo3DkU_uzmkptv1EpAArw,31
|
|
2
|
-
koi_net/core.py,sha256=dE4sE2qsoIRUU1zsnrjx7aqYtYdHyCx-Dv4cwbkRjy4,4613
|
|
3
|
-
koi_net/identity.py,sha256=PBgmAx5f3zzQmHASB1TJW2g19n9TLfmSJMXg2eQFg0A,2386
|
|
4
|
-
koi_net/network/__init__.py,sha256=r_RN-q_mDYC-2RAkN-lJoMUX76TXyfEUc_MVKW87z0g,39
|
|
5
|
-
koi_net/network/graph.py,sha256=KMUCU3AweRvivwy7GuWgX2zX74FPgHeVMO5ydvhVyvA,4833
|
|
6
|
-
koi_net/network/interface.py,sha256=4JTeg8Eah0z5YKhcVKJbCVZw_Ghl_6xfG8aa1I5PCWI,10643
|
|
7
|
-
koi_net/network/request_handler.py,sha256=fhuCDsxI8fZ4p5TntcTZR4mnLrLQ61zDy7Oca3ooFCE,4402
|
|
8
|
-
koi_net/network/response_handler.py,sha256=HaP8Fl0bp_lfMmevhdVY8s9o0Uf8CR1ZaW5g3jsX8gw,1888
|
|
9
|
-
koi_net/processor/__init__.py,sha256=x4fAY0hvQEDcpfdTB3POIzxBQjYAtn0qQazPo1Xm0m4,41
|
|
10
|
-
koi_net/processor/default_handlers.py,sha256=cdGDFb4z1vnDmAXQO5XeDjhbUHbB8TndC-E6AFJzp2M,6930
|
|
11
|
-
koi_net/processor/handler.py,sha256=a2wxG3kgux-07YP13HSj_PH5VH_PjbnTS4uZEHjxMw0,2582
|
|
12
|
-
koi_net/processor/interface.py,sha256=5zYxaD_ATjjp1xFunRomCrrzjyspi8zZzeY2Rca7bVU,12522
|
|
13
|
-
koi_net/processor/knowledge_object.py,sha256=cGv33fwNZQMylkhlTaQTbk96FVIVbdOUaBsG06u0m4k,4187
|
|
14
|
-
koi_net/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
koi_net/protocol/api_models.py,sha256=RDwVHAahiWzwzUnlj5MIm9et5WVpQOaG-Uscv1B9coU,1116
|
|
16
|
-
koi_net/protocol/consts.py,sha256=zeWJvRpqcERrqJq39heyNHb6f_9QrvoBZJHd70yE914,249
|
|
17
|
-
koi_net/protocol/edge.py,sha256=G3D9Ie0vbTSMJdoTw9g_oBmFCqzJ1gO7U1PVrw7p3j8,447
|
|
18
|
-
koi_net/protocol/event.py,sha256=dzJmcHbimo7p5NwH2drccF0vMcAj9oQRj3iZ9Bjf7kg,1275
|
|
19
|
-
koi_net/protocol/helpers.py,sha256=9E9PaoIuSNrTBATGCLJ_kSBMZ2z-KIMnLJzGOTqQDC0,719
|
|
20
|
-
koi_net/protocol/node.py,sha256=Ntrx01dbm39ViKGtr4gLmztcMwKpTIweS6rRL-zoU_Y,391
|
|
21
|
-
koi_net-1.0.0b9.dist-info/METADATA,sha256=GZbTzNd_k5uVlvaik7_YXdYd17Ya6XtVTf6NXrxbRes,32539
|
|
22
|
-
koi_net-1.0.0b9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
-
koi_net-1.0.0b9.dist-info/licenses/LICENSE,sha256=XBcvl8yjCAezfuqN1jadQykrX7H2g4nr2WRDmHLW6ik,1090
|
|
24
|
-
koi_net-1.0.0b9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|