koi-net 1.2.4__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.

Files changed (59) hide show
  1. koi_net/__init__.py +1 -0
  2. koi_net/behaviors/handshaker.py +68 -0
  3. koi_net/behaviors/profile_monitor.py +23 -0
  4. koi_net/behaviors/sync_manager.py +68 -0
  5. koi_net/build/artifact.py +209 -0
  6. koi_net/build/assembler.py +60 -0
  7. koi_net/build/comp_order.py +6 -0
  8. koi_net/build/comp_type.py +7 -0
  9. koi_net/build/consts.py +18 -0
  10. koi_net/build/container.py +46 -0
  11. koi_net/cache.py +81 -0
  12. koi_net/config/core.py +113 -0
  13. koi_net/config/full_node.py +45 -0
  14. koi_net/config/loader.py +60 -0
  15. koi_net/config/partial_node.py +26 -0
  16. koi_net/config/proxy.py +20 -0
  17. koi_net/core.py +78 -0
  18. koi_net/effector.py +147 -0
  19. koi_net/entrypoints/__init__.py +2 -0
  20. koi_net/entrypoints/base.py +8 -0
  21. koi_net/entrypoints/poller.py +43 -0
  22. koi_net/entrypoints/server.py +85 -0
  23. koi_net/exceptions.py +107 -0
  24. koi_net/identity.py +20 -0
  25. koi_net/log_system.py +133 -0
  26. koi_net/network/__init__.py +0 -0
  27. koi_net/network/error_handler.py +63 -0
  28. koi_net/network/event_buffer.py +91 -0
  29. koi_net/network/event_queue.py +31 -0
  30. koi_net/network/graph.py +123 -0
  31. koi_net/network/request_handler.py +244 -0
  32. koi_net/network/resolver.py +152 -0
  33. koi_net/network/response_handler.py +130 -0
  34. koi_net/processor/__init__.py +0 -0
  35. koi_net/processor/context.py +36 -0
  36. koi_net/processor/handler.py +61 -0
  37. koi_net/processor/knowledge_handlers.py +302 -0
  38. koi_net/processor/knowledge_object.py +135 -0
  39. koi_net/processor/kobj_queue.py +51 -0
  40. koi_net/processor/pipeline.py +222 -0
  41. koi_net/protocol/__init__.py +0 -0
  42. koi_net/protocol/api_models.py +67 -0
  43. koi_net/protocol/consts.py +7 -0
  44. koi_net/protocol/edge.py +50 -0
  45. koi_net/protocol/envelope.py +65 -0
  46. koi_net/protocol/errors.py +24 -0
  47. koi_net/protocol/event.py +51 -0
  48. koi_net/protocol/model_map.py +62 -0
  49. koi_net/protocol/node.py +18 -0
  50. koi_net/protocol/secure.py +167 -0
  51. koi_net/secure_manager.py +115 -0
  52. koi_net/workers/__init__.py +2 -0
  53. koi_net/workers/base.py +26 -0
  54. koi_net/workers/event_worker.py +111 -0
  55. koi_net/workers/kobj_worker.py +51 -0
  56. koi_net-1.2.4.dist-info/METADATA +485 -0
  57. koi_net-1.2.4.dist-info/RECORD +59 -0
  58. koi_net-1.2.4.dist-info/WHEEL +4 -0
  59. koi_net-1.2.4.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,302 @@
1
+ """Implementation of default knowledge handlers."""
2
+
3
+ import structlog
4
+ from rid_lib.ext import Bundle
5
+ from rid_lib.ext.utils import sha256_hash
6
+ from rid_lib.types import KoiNetNode, KoiNetEdge
7
+
8
+ from ..exceptions import RequestError
9
+ from ..protocol.node import NodeType
10
+ from ..protocol.event import Event, EventType
11
+ from ..protocol.edge import EdgeProfile, EdgeStatus, EdgeType, generate_edge_bundle
12
+ from ..protocol.node import NodeProfile
13
+ from .handler import KnowledgeHandler, HandlerType, STOP_CHAIN
14
+ from .knowledge_object import KnowledgeObject
15
+ from .context import HandlerContext
16
+
17
+ log = structlog.stdlib.get_logger()
18
+
19
+
20
+ # RID handlers
21
+
22
+ @KnowledgeHandler.create(HandlerType.RID)
23
+ def basic_rid_handler(ctx: HandlerContext, kobj: KnowledgeObject):
24
+ """Default RID handler.
25
+
26
+ Blocks external events about this node. Sets normalized event type
27
+ for `FORGET` events.
28
+ """
29
+
30
+ if (kobj.rid == ctx.identity.rid and kobj.source is not None):
31
+ log.debug("Externally sourced events about this node not allowed.")
32
+ return STOP_CHAIN
33
+
34
+ if kobj.event_type == EventType.FORGET:
35
+ kobj.normalized_event_type = EventType.FORGET
36
+ return kobj
37
+
38
+
39
+ # Manifest handlers
40
+
41
+ @KnowledgeHandler.create(HandlerType.Manifest)
42
+ def basic_manifest_handler(ctx: HandlerContext, kobj: KnowledgeObject):
43
+ """Normalized event decider based on manifest and cache state.
44
+
45
+ Stops processing for manifests which have the same hash, or aren't
46
+ newer than the cached version. Sets the normalized event type to
47
+ `NEW` or `UPDATE` depending on whether the RID was previously known.
48
+ """
49
+
50
+ prev_bundle = ctx.cache.read(kobj.rid)
51
+
52
+ if prev_bundle:
53
+ if kobj.manifest.sha256_hash == prev_bundle.manifest.sha256_hash:
54
+ log.debug("Hash of incoming manifest is same as existing knowledge, ignoring")
55
+ return STOP_CHAIN
56
+ if kobj.manifest.timestamp <= prev_bundle.manifest.timestamp:
57
+ log.debug("Timestamp of incoming manifest is the same or older than existing knowledge, ignoring")
58
+ return STOP_CHAIN
59
+
60
+ log.debug("RID previously known to me, labeling as 'UPDATE'")
61
+ kobj.normalized_event_type = EventType.UPDATE
62
+
63
+ else:
64
+ log.debug("RID previously unknown to me, labeling as 'NEW'")
65
+ kobj.normalized_event_type = EventType.NEW
66
+
67
+ return kobj
68
+
69
+
70
+ # Bundle handlers
71
+
72
+ @KnowledgeHandler.create(
73
+ handler_type=HandlerType.Bundle,
74
+ rid_types=(KoiNetNode,),
75
+ event_types=(EventType.NEW, EventType.UPDATE))
76
+ def secure_profile_handler(ctx: HandlerContext, kobj: KnowledgeObject):
77
+ """Maintains security of cached node profiles.
78
+
79
+ Blocks bundles with a mismatching public keys in their node profile
80
+ and RID from being written to cache.
81
+ """
82
+
83
+ node_profile = kobj.bundle.validate_contents(NodeProfile)
84
+ node_rid: KoiNetNode = kobj.rid
85
+
86
+ if sha256_hash(node_profile.public_key) != node_rid.hash:
87
+ log.warning(f"Public key hash mismatch for {node_rid!r}!")
88
+ return STOP_CHAIN
89
+
90
+ @KnowledgeHandler.create(
91
+ handler_type=HandlerType.Bundle,
92
+ rid_types=(KoiNetEdge,),
93
+ event_types=(EventType.NEW, EventType.UPDATE))
94
+ def edge_negotiation_handler(ctx: HandlerContext, kobj: KnowledgeObject):
95
+ """Handles edge negotiation process.
96
+
97
+ Automatically approves proposed edges if they request RID types this
98
+ node can provide (or KOI node, edge RIDs). Validates the edge type
99
+ is allowed for the node type (partial nodes cannot use webhooks). If
100
+ edge is invalid, a `FORGET` event is sent to the other node.
101
+ """
102
+
103
+ # only handle incoming events (ignore internal edge knowledge objects)
104
+ if kobj.source is None:
105
+ return
106
+
107
+ edge_profile = kobj.bundle.validate_contents(EdgeProfile)
108
+
109
+ # indicates peer subscribing to this node
110
+ if edge_profile.source == ctx.identity.rid:
111
+ if edge_profile.status != EdgeStatus.PROPOSED:
112
+ return
113
+
114
+ log.debug("Handling edge negotiation")
115
+
116
+ peer_rid = edge_profile.target
117
+ peer_bundle = ctx.cache.read(peer_rid)
118
+
119
+ if not peer_bundle:
120
+ log.warning(f"Peer {peer_rid!r} unknown to me")
121
+ return STOP_CHAIN
122
+
123
+ peer_profile = peer_bundle.validate_contents(NodeProfile)
124
+
125
+ # explicitly provided event RID types and (self) node + edge objects
126
+ provided_events = (
127
+ *ctx.identity.profile.provides.event,
128
+ KoiNetNode, KoiNetEdge
129
+ )
130
+
131
+ abort = False
132
+ if (edge_profile.edge_type == EdgeType.WEBHOOK and
133
+ peer_profile.node_type == NodeType.PARTIAL):
134
+ log.debug("Partial nodes cannot use webhooks")
135
+ abort = True
136
+
137
+ if not set(edge_profile.rid_types).issubset(provided_events):
138
+ log.debug("Requested RID types not provided by this node")
139
+ abort = True
140
+
141
+ if abort:
142
+ event = Event.from_rid(EventType.FORGET, kobj.rid)
143
+ ctx.event_queue.push(event, peer_rid, flush=True)
144
+ return STOP_CHAIN
145
+
146
+ else:
147
+ log.debug("Approving proposed edge")
148
+ edge_profile.status = EdgeStatus.APPROVED
149
+ updated_bundle = Bundle.generate(kobj.rid, edge_profile.model_dump())
150
+
151
+ ctx.kobj_queue.push(bundle=updated_bundle, event_type=EventType.UPDATE)
152
+ return
153
+
154
+ elif edge_profile.target == ctx.identity.rid:
155
+ if edge_profile.status == EdgeStatus.APPROVED:
156
+ log.debug("Edge approved by other node!")
157
+
158
+
159
+ # Network handlers
160
+
161
+ @KnowledgeHandler.create(HandlerType.Network, rid_types=(KoiNetNode,))
162
+ def node_contact_handler(ctx: HandlerContext, kobj: KnowledgeObject):
163
+ """Makes contact with providers of RID types of interest.
164
+
165
+ When an incoming node knowledge object is identified as a provider
166
+ of an RID type of interest, this handler will propose a new edge
167
+ subscribing to future node events, and fetch existing nodes to catch
168
+ up to the current state.
169
+ """
170
+ # prevents nodes from attempting to form a self loop
171
+ if kobj.rid == ctx.identity.rid:
172
+ return
173
+
174
+ node_profile = kobj.bundle.validate_contents(NodeProfile)
175
+
176
+ available_rid_types = list(
177
+ set(ctx.config.koi_net.rid_types_of_interest) &
178
+ set(node_profile.provides.event)
179
+ )
180
+
181
+ if not available_rid_types:
182
+ return
183
+
184
+ edge_rid = ctx.graph.get_edge(
185
+ source=kobj.rid,
186
+ target=ctx.identity.rid,
187
+ )
188
+
189
+ # already have an edge established
190
+ if edge_rid:
191
+ prev_edge_bundle = ctx.cache.read(edge_rid)
192
+ edge_profile = prev_edge_bundle.validate_contents(EdgeProfile)
193
+
194
+ if set(edge_profile.rid_types) == set(available_rid_types):
195
+ # no change in rid types
196
+ return
197
+
198
+ log.info(f"Proposing updated edge with node provider {available_rid_types}")
199
+
200
+ edge_profile.rid_types = available_rid_types
201
+ edge_profile.status = EdgeStatus.PROPOSED
202
+ edge_bundle = Bundle.generate(edge_rid, edge_profile.model_dump())
203
+
204
+ # no existing edge
205
+ else:
206
+ log.info(f"Proposing new edge with node provider {available_rid_types}")
207
+ edge_bundle = generate_edge_bundle(
208
+ source=kobj.rid,
209
+ target=ctx.identity.rid,
210
+ rid_types=available_rid_types,
211
+ edge_type=(
212
+ EdgeType.WEBHOOK
213
+ if ctx.identity.profile.node_type == NodeType.FULL
214
+ else EdgeType.POLL
215
+ )
216
+ )
217
+
218
+ # queued for processing
219
+ ctx.kobj_queue.push(bundle=edge_bundle)
220
+
221
+ log.info("Catching up on network state")
222
+ try:
223
+ payload = ctx.request_handler.fetch_rids(
224
+ node=kobj.rid,
225
+ rid_types=available_rid_types
226
+ )
227
+ except RequestError:
228
+ log.info("Failed to reach node")
229
+ return
230
+
231
+ for rid in payload.rids:
232
+ if rid == ctx.identity.rid:
233
+ log.info("Skipping myself")
234
+ continue
235
+ if ctx.cache.exists(rid):
236
+ log.info(f"Skipping known RID {rid!r}")
237
+ continue
238
+
239
+ # marked as external since we are handling RIDs from another node
240
+ # will fetch remotely instead of checking local cache
241
+ ctx.kobj_queue.push(rid=rid, source=kobj.rid)
242
+ log.info("Done")
243
+
244
+ @KnowledgeHandler.create(HandlerType.Network)
245
+ def basic_network_output_filter(ctx: HandlerContext, kobj: KnowledgeObject):
246
+ """Sets network targets of outgoing event for knowledge object.
247
+
248
+ Allows broadcasting of all RID types this node is an event provider
249
+ for (set in node profile), and other nodes have subscribed to. All
250
+ nodes will also broadcast events about their own (internally sourced)
251
+ KOI node, and KOI edges that they are part of, regardless of their
252
+ node profile configuration. Finally, nodes will also broadcast about
253
+ edges to the other node involved (regardless of if they are subscribed).
254
+ """
255
+
256
+ involves_this_node = False
257
+ # internally source knowledge objects
258
+ if kobj.source is None:
259
+ if type(kobj.rid) is KoiNetNode:
260
+ if (kobj.rid == ctx.identity.rid):
261
+ involves_this_node = True
262
+
263
+ elif type(kobj.rid) is KoiNetEdge:
264
+ edge_profile = kobj.bundle.validate_contents(EdgeProfile)
265
+
266
+ if edge_profile.source == ctx.identity.rid:
267
+ log.debug(f"Adding edge target '{edge_profile.target!r}' to network targets")
268
+ kobj.network_targets.add(edge_profile.target)
269
+ involves_this_node = True
270
+
271
+ elif edge_profile.target == ctx.identity.rid:
272
+ log.debug(f"Adding edge source '{edge_profile.source!r}' to network targets")
273
+ kobj.network_targets.add(edge_profile.source)
274
+ involves_this_node = True
275
+
276
+ if (type(kobj.rid) in ctx.identity.profile.provides.event or involves_this_node):
277
+ subscribers = ctx.graph.get_neighbors(
278
+ direction="out",
279
+ allowed_type=type(kobj.rid)
280
+ )
281
+
282
+ log.debug(f"Updating network targets with '{type(kobj.rid)}' subscribers: {subscribers}")
283
+ kobj.network_targets.update(subscribers)
284
+
285
+ return kobj
286
+
287
+ @KnowledgeHandler.create(HandlerType.Final, rid_types=[KoiNetNode])
288
+ def forget_edge_on_node_deletion(ctx: HandlerContext, kobj: KnowledgeObject):
289
+ """Removes edges to forgotten nodes."""
290
+
291
+ if kobj.normalized_event_type != EventType.FORGET:
292
+ return
293
+
294
+ for edge_rid in ctx.graph.get_edges():
295
+ edge_bundle = ctx.cache.read(edge_rid)
296
+ if not edge_bundle:
297
+ continue
298
+ edge_profile = edge_bundle.validate_contents(EdgeProfile)
299
+
300
+ if kobj.rid in (edge_profile.source, edge_profile.target):
301
+ log.debug("Identified edge with forgotten node")
302
+ ctx.kobj_queue.push(rid=edge_rid, event_type=EventType.FORGET)
@@ -0,0 +1,135 @@
1
+ from pydantic import BaseModel
2
+ from rid_lib import RID
3
+ from rid_lib.ext import Manifest, Bundle
4
+ from rid_lib.types import KoiNetNode
5
+ from ..protocol.event import Event, EventType
6
+
7
+
8
+ class KnowledgeObject(BaseModel):
9
+ """A normalized knowledge representation for internal processing.
10
+
11
+ Represents an RID, manifest, bundle, or event. Contains three fields
12
+ (`normalized_event_type`, `source`, `network_targets`) used for
13
+ decision making in the knowledge processing pipeline.
14
+
15
+ The source indicates which node this object originated from, or
16
+ `None` if it was generated by this node.
17
+
18
+ The normalized event type indicates how the knowledge object is
19
+ viewed from the perspective of this node, and what cache actions
20
+ will take place. (`NEW`, `UPDATE`) -> cache write, `FORGET` ->
21
+ cache delete, `None` -> no cache action, and end of pipeline.
22
+
23
+ The network targets indicate other nodes in the network this
24
+ knowledge object will be sent to. The event sent to them will be
25
+ constructed from this knowledge object's RID, manifest, contents,
26
+ and normalized event type.
27
+ """
28
+
29
+ rid: RID
30
+ manifest: Manifest | None = None
31
+ contents: dict | None = None
32
+ event_type: EventType | None = None
33
+ normalized_event_type: EventType | None = None
34
+ source: KoiNetNode | None = None
35
+ network_targets: set[KoiNetNode] = set()
36
+
37
+ def __repr__(self):
38
+ return f"<KObj '{self.rid}' event type: '{self.event_type}' -> '{self.normalized_event_type}', source: '{self.source}'>"
39
+
40
+ @classmethod
41
+ def from_rid(
42
+ cls,
43
+ rid: RID,
44
+ event_type: EventType | None = None,
45
+ source: KoiNetNode | None = None
46
+ ) -> "KnowledgeObject":
47
+ """Creates a `KnowledgeObject` from an `RID`."""
48
+
49
+ return cls(
50
+ rid=rid,
51
+ event_type=event_type,
52
+ source=source
53
+ )
54
+
55
+ @classmethod
56
+ def from_manifest(
57
+ cls,
58
+ manifest: Manifest,
59
+ event_type: EventType | None = None,
60
+ source: KoiNetNode | None = None
61
+ ) -> "KnowledgeObject":
62
+ """Creates a `KnowledgeObject` from a `Manifest`."""
63
+
64
+ return cls(
65
+ rid=manifest.rid,
66
+ manifest=manifest,
67
+ event_type=event_type,
68
+ source=source
69
+ )
70
+
71
+ @classmethod
72
+ def from_bundle(
73
+ cls,
74
+ bundle: Bundle,
75
+ event_type: EventType | None = None,
76
+ source: KoiNetNode | None = None
77
+ ) -> "KnowledgeObject":
78
+ """Creates a `KnowledgeObject` from a `Bundle`."""
79
+
80
+ return cls(
81
+ rid=bundle.rid,
82
+ manifest=bundle.manifest,
83
+ contents=bundle.contents,
84
+ event_type=event_type,
85
+ source=source
86
+ )
87
+
88
+ @classmethod
89
+ def from_event(
90
+ cls,
91
+ event: Event,
92
+ source: KoiNetNode | None = None
93
+ ) -> "KnowledgeObject":
94
+ """Creates a `KnowledgeObject` from an `Event`."""
95
+
96
+ return cls(
97
+ rid=event.rid,
98
+ manifest=event.manifest,
99
+ contents=event.contents,
100
+ event_type=event.event_type,
101
+ source=source
102
+ )
103
+
104
+ @property
105
+ def bundle(self) -> Bundle:
106
+ """Bundle representation of knowledge object."""
107
+
108
+ if self.manifest is None or self.contents is None:
109
+ raise ValueError("Knowledge object missing manifest or contents, cannot convert to `Bundle`.")
110
+
111
+ return Bundle(
112
+ manifest=self.manifest,
113
+ contents=self.contents
114
+ )
115
+
116
+ @property
117
+ def normalized_event(self) -> Event:
118
+ """Event representation of knowledge object."""
119
+
120
+ if self.normalized_event_type is None:
121
+ raise ValueError("Internal event's normalized event type is None, cannot convert to Event")
122
+
123
+ elif self.normalized_event_type == EventType.FORGET:
124
+ return Event(
125
+ rid=self.rid,
126
+ event_type=EventType.FORGET
127
+ )
128
+
129
+ else:
130
+ return Event(
131
+ rid=self.rid,
132
+ event_type=self.normalized_event_type,
133
+ manifest=self.manifest,
134
+ contents=self.contents
135
+ )
@@ -0,0 +1,51 @@
1
+ import structlog
2
+ from queue import Queue
3
+ from rid_lib.core import RID
4
+ from rid_lib.ext import Bundle, Manifest
5
+ from rid_lib.types import KoiNetNode
6
+ from ..protocol.event import Event, EventType
7
+ from .knowledge_object import KnowledgeObject
8
+
9
+ log = structlog.stdlib.get_logger()
10
+
11
+
12
+ class KobjQueue:
13
+ """Queue for knowledge objects entering the processing pipeline."""
14
+ q: Queue[KnowledgeObject]
15
+
16
+ def __init__(self):
17
+ self.q = Queue()
18
+
19
+ def push(
20
+ self,
21
+ rid: RID | None = None,
22
+ manifest: Manifest | None = None,
23
+ bundle: Bundle | None = None,
24
+ event: Event | None = None,
25
+ kobj: KnowledgeObject | None = None,
26
+ event_type: EventType | None = None,
27
+ source: KoiNetNode | None = None
28
+ ):
29
+ """Pushes knowledge object to queue.
30
+
31
+ Input may take the form of an RID, manifest, bundle, event,
32
+ or knowledge object (with an optional event type for RID,
33
+ manifest, or bundle objects). All objects will be normalized
34
+ to knowledge objects and queued.
35
+ """
36
+
37
+ if rid:
38
+ _kobj = KnowledgeObject.from_rid(rid, event_type, source)
39
+ elif manifest:
40
+ _kobj = KnowledgeObject.from_manifest(manifest, event_type, source)
41
+ elif bundle:
42
+ _kobj = KnowledgeObject.from_bundle(bundle, event_type, source)
43
+ elif event:
44
+ _kobj = KnowledgeObject.from_event(event, source)
45
+ elif kobj:
46
+ _kobj = kobj
47
+ else:
48
+ raise ValueError("One of 'rid', 'manifest', 'bundle', 'event', or 'kobj' must be provided")
49
+
50
+ self.q.put(_kobj)
51
+ log.debug(f"Queued {_kobj!r}")