koi-net 1.0.0b19__py3-none-any.whl → 1.1.0b1__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.

@@ -0,0 +1,150 @@
1
+ import logging
2
+ import httpx
3
+ from rid_lib import RID
4
+ from rid_lib.core import RIDType
5
+ from rid_lib.ext import Cache, Bundle
6
+ from rid_lib.types import KoiNetNode
7
+
8
+ from .graph import NetworkGraph
9
+ from .request_handler import RequestHandler
10
+ from ..protocol.node import NodeProfile, NodeType
11
+ from ..protocol.event import Event
12
+ from ..protocol.api_models import ErrorResponse
13
+ from ..identity import NodeIdentity
14
+ from ..config import NodeConfig
15
+ from ..effector import Effector
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class NetworkResolver:
21
+ """A collection of functions and classes to interact with the KOI network."""
22
+
23
+ config: NodeConfig
24
+ identity: NodeIdentity
25
+ effector: Effector
26
+ cache: Cache
27
+ graph: NetworkGraph
28
+ request_handler: RequestHandler
29
+
30
+ def __init__(
31
+ self,
32
+ config: NodeConfig,
33
+ cache: Cache,
34
+ identity: NodeIdentity,
35
+ effector: Effector,
36
+ graph: NetworkGraph,
37
+ request_handler: RequestHandler,
38
+ ):
39
+ self.config = config
40
+ self.identity = identity
41
+ self.cache = cache
42
+ self.graph = graph
43
+ self.request_handler = request_handler
44
+ self.effector = effector
45
+
46
+ self.poll_event_queue = dict()
47
+ self.webhook_event_queue = dict()
48
+
49
+ def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
50
+ """Returns list of node RIDs which provide state for the specified RID type."""
51
+
52
+ logger.debug(f"Looking for state providers of '{rid_type}'")
53
+ provider_nodes = []
54
+ for node_rid in self.cache.list_rids(rid_types=[KoiNetNode]):
55
+ if node_rid == self.identity.rid:
56
+ continue
57
+
58
+ node_bundle = self.cache.read(node_rid)
59
+
60
+ node_profile = node_bundle.validate_contents(NodeProfile)
61
+
62
+ if (node_profile.node_type == NodeType.FULL) and (rid_type in node_profile.provides.state):
63
+ logger.debug(f"Found provider {node_rid!r}")
64
+ provider_nodes.append(node_rid)
65
+
66
+ if not provider_nodes:
67
+ logger.debug("Failed to find providers")
68
+ return provider_nodes
69
+
70
+ def fetch_remote_bundle(self, rid: RID) -> tuple[Bundle | None, KoiNetNode | None]:
71
+ """Attempts to fetch a bundle by RID from known peer nodes."""
72
+
73
+ logger.debug(f"Fetching remote bundle {rid!r}")
74
+ remote_bundle, node_rid = None, None
75
+ for node_rid in self.get_state_providers(type(rid)):
76
+ payload = self.request_handler.fetch_bundles(
77
+ node=node_rid, rids=[rid])
78
+
79
+ if payload.bundles:
80
+ remote_bundle = payload.bundles[0]
81
+ logger.debug(f"Got bundle from {node_rid!r}")
82
+ break
83
+
84
+ if not remote_bundle:
85
+ logger.warning("Failed to fetch remote bundle")
86
+
87
+ return remote_bundle, node_rid
88
+
89
+ def fetch_remote_manifest(self, rid: RID) -> tuple[Bundle | None, KoiNetNode | None]:
90
+ """Attempts to fetch a manifest by RID from known peer nodes."""
91
+
92
+ logger.debug(f"Fetching remote manifest {rid!r}")
93
+ remote_manifest, node_rid = None, None
94
+ for node_rid in self.get_state_providers(type(rid)):
95
+ payload = self.request_handler.fetch_manifests(
96
+ node=node_rid, rids=[rid])
97
+
98
+ if payload.manifests:
99
+ remote_manifest = payload.manifests[0]
100
+ logger.debug(f"Got bundle from {node_rid!r}")
101
+ break
102
+
103
+ if not remote_manifest:
104
+ logger.warning("Failed to fetch remote bundle")
105
+
106
+ return remote_manifest, node_rid
107
+
108
+ def poll_neighbors(self) -> dict[KoiNetNode, list[Event]]:
109
+ """Polls all neighboring nodes and returns compiled list of events.
110
+
111
+ If this node has no neighbors, it will instead attempt to poll the provided first contact URL.
112
+ """
113
+
114
+ graph_neighbors = self.graph.get_neighbors()
115
+ neighbors = []
116
+
117
+ if graph_neighbors:
118
+ for node_rid in graph_neighbors:
119
+ node_bundle = self.cache.read(node_rid)
120
+ if not node_bundle:
121
+ continue
122
+ node_profile = node_bundle.validate_contents(NodeProfile)
123
+ if node_profile.node_type != NodeType.FULL:
124
+ continue
125
+ neighbors.append(node_rid)
126
+
127
+ elif self.config.koi_net.first_contact.rid:
128
+ neighbors.append(self.config.koi_net.first_contact.rid)
129
+
130
+ event_dict = dict()
131
+ for node_rid in neighbors:
132
+ try:
133
+ payload = self.request_handler.poll_events(
134
+ node=node_rid,
135
+ rid=self.identity.rid
136
+ )
137
+
138
+ if type(payload) == ErrorResponse:
139
+ continue
140
+
141
+ if payload.events:
142
+ logger.debug(f"Received {len(payload.events)} events from {node_rid!r}")
143
+
144
+ event_dict[node_rid] = payload.events
145
+
146
+ except httpx.ConnectError:
147
+ logger.debug(f"Failed to reach node {node_rid!r}")
148
+ continue
149
+
150
+ return event_dict
@@ -2,6 +2,7 @@ import logging
2
2
  from rid_lib import RID
3
3
  from rid_lib.ext import Manifest, Cache
4
4
  from rid_lib.ext.bundle import Bundle
5
+
5
6
  from ..protocol.api_models import (
6
7
  RidsPayload,
7
8
  ManifestsPayload,
@@ -10,6 +11,7 @@ from ..protocol.api_models import (
10
11
  FetchManifests,
11
12
  FetchBundles,
12
13
  )
14
+ from ..effector import Effector
13
15
 
14
16
  logger = logging.getLogger(__name__)
15
17
 
@@ -18,9 +20,15 @@ class ResponseHandler:
18
20
  """Handles generating responses to requests from other KOI nodes."""
19
21
 
20
22
  cache: Cache
23
+ effector: Effector
21
24
 
22
- def __init__(self, cache: Cache):
25
+ def __init__(
26
+ self,
27
+ cache: Cache,
28
+ effector: Effector,
29
+ ):
23
30
  self.cache = cache
31
+ self.effector = effector
24
32
 
25
33
  def fetch_rids(self, req: FetchRids) -> RidsPayload:
26
34
  logger.info(f"Request to fetch rids, allowed types {req.rid_types}")
@@ -35,7 +43,7 @@ class ResponseHandler:
35
43
  not_found: list[RID] = []
36
44
 
37
45
  for rid in (req.rids or self.cache.list_rids(req.rid_types)):
38
- bundle = self.cache.read(rid)
46
+ bundle = self.effector.deref(rid)
39
47
  if bundle:
40
48
  manifests.append(bundle.manifest)
41
49
  else:
@@ -50,7 +58,7 @@ class ResponseHandler:
50
58
  not_found: list[RID] = []
51
59
 
52
60
  for rid in req.rids:
53
- bundle = self.cache.read(rid)
61
+ bundle = self.effector.deref(rid)
54
62
  if bundle:
55
63
  bundles.append(bundle)
56
64
  else:
@@ -1 +0,0 @@
1
- from .interface import ProcessorInterface
@@ -1,29 +1,28 @@
1
1
  """Provides implementations of default knowledge handlers."""
2
2
 
3
3
  import logging
4
- from rid_lib.ext.bundle import Bundle
4
+ from rid_lib.ext import Bundle
5
+ from rid_lib.ext.utils import sha256_hash
5
6
  from rid_lib.types import KoiNetNode, KoiNetEdge
6
7
  from koi_net.protocol.node import NodeType
7
- from .interface import ProcessorInterface
8
8
  from .handler import KnowledgeHandler, HandlerType, STOP_CHAIN
9
- from .knowledge_object import KnowledgeObject, KnowledgeSource
9
+ from .knowledge_object import KnowledgeObject
10
+ from ..context import HandlerContext
10
11
  from ..protocol.event import Event, EventType
11
- from ..protocol.edge import EdgeProfile, EdgeStatus, EdgeType
12
+ from ..protocol.edge import EdgeProfile, EdgeStatus, EdgeType, generate_edge_bundle
12
13
  from ..protocol.node import NodeProfile
13
- from ..protocol.helpers import generate_edge_bundle
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
  # RID handlers
18
18
 
19
19
  @KnowledgeHandler.create(HandlerType.RID)
20
- def basic_rid_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
20
+ def basic_rid_handler(ctx: HandlerContext, kobj: KnowledgeObject):
21
21
  """Default RID handler.
22
22
 
23
23
  Blocks external events about this node. Allows `FORGET` events if RID is known to this node.
24
24
  """
25
- if (kobj.rid == processor.identity.rid and
26
- kobj.source == KnowledgeSource.External):
25
+ if (kobj.rid == ctx.identity.rid and kobj.source):
27
26
  logger.debug("Don't let anyone else tell me who I am!")
28
27
  return STOP_CHAIN
29
28
 
@@ -34,12 +33,12 @@ def basic_rid_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
34
33
  # Manifest handlers
35
34
 
36
35
  @KnowledgeHandler.create(HandlerType.Manifest)
37
- def basic_manifest_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
36
+ def basic_manifest_handler(ctx: HandlerContext, kobj: KnowledgeObject):
38
37
  """Default manifest handler.
39
38
 
40
39
  Blocks manifests with the same hash, or aren't newer than the cached version. Sets the normalized event type to `NEW` or `UPDATE` depending on whether the RID was previously known to this node.
41
40
  """
42
- prev_bundle = processor.cache.read(kobj.rid)
41
+ prev_bundle = ctx.cache.read(kobj.rid)
43
42
 
44
43
  if prev_bundle:
45
44
  if kobj.manifest.sha256_hash == prev_bundle.manifest.sha256_hash:
@@ -61,36 +60,53 @@ def basic_manifest_handler(processor: ProcessorInterface, kobj: KnowledgeObject)
61
60
 
62
61
  # Bundle handlers
63
62
 
63
+ @KnowledgeHandler.create(
64
+ handler_type=HandlerType.Bundle,
65
+ rid_types=[KoiNetNode],
66
+ event_types=[EventType.NEW, EventType.UPDATE]
67
+ )
68
+ def secure_profile_handler(ctx: HandlerContext, kobj: KnowledgeObject):
69
+ node_profile = kobj.bundle.validate_contents(NodeProfile)
70
+ node_rid: KoiNetNode = kobj.rid
71
+
72
+ if sha256_hash(node_profile.public_key) != node_rid.hash:
73
+ logger.warning(f"Public key hash mismatch for {node_rid!r}!")
74
+ return STOP_CHAIN
75
+
64
76
  @KnowledgeHandler.create(
65
77
  handler_type=HandlerType.Bundle,
66
78
  rid_types=[KoiNetEdge],
67
- source=KnowledgeSource.External,
68
79
  event_types=[EventType.NEW, EventType.UPDATE])
69
- def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
80
+ def edge_negotiation_handler(ctx: HandlerContext, kobj: KnowledgeObject):
70
81
  """Handles basic edge negotiation process.
71
82
 
72
83
  Automatically approves proposed edges if they request RID types this node can provide (or KOI nodes/edges). Validates the edge type is allowed for the node type (partial nodes cannot use webhooks). If edge is invalid, a `FORGET` event is sent to the other node.
73
84
  """
85
+
86
+ # only respond when source is another node
87
+ if kobj.source is None: return
74
88
 
75
- edge_profile = EdgeProfile.model_validate(kobj.contents)
89
+ edge_profile = kobj.bundle.validate_contents(EdgeProfile)
76
90
 
77
91
  # indicates peer subscribing to me
78
- if edge_profile.source == processor.identity.rid:
92
+ if edge_profile.source == ctx.identity.rid:
79
93
  if edge_profile.status != EdgeStatus.PROPOSED:
80
94
  return
81
95
 
82
96
  logger.debug("Handling edge negotiation")
83
97
 
84
98
  peer_rid = edge_profile.target
85
- peer_profile = processor.network.graph.get_node_profile(peer_rid)
99
+ peer_bundle = ctx.effector.deref(peer_rid)
86
100
 
87
- if not peer_profile:
88
- logger.warning(f"Peer {peer_rid} unknown to me")
101
+ if not peer_bundle:
102
+ logger.warning(f"Peer {peer_rid!r} unknown to me")
89
103
  return STOP_CHAIN
90
104
 
105
+ peer_profile = peer_bundle.validate_contents(NodeProfile)
106
+
91
107
  # explicitly provided event RID types and (self) node + edge objects
92
108
  provided_events = (
93
- *processor.identity.profile.provides.event,
109
+ *ctx.identity.profile.provides.event,
94
110
  KoiNetNode, KoiNetEdge
95
111
  )
96
112
 
@@ -107,7 +123,7 @@ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObjec
107
123
 
108
124
  if abort:
109
125
  event = Event.from_rid(EventType.FORGET, kobj.rid)
110
- processor.network.push_event_to(event, peer_rid, flush=True)
126
+ ctx.event_queue.push_event_to(event, peer_rid, flush=True)
111
127
  return STOP_CHAIN
112
128
 
113
129
  else:
@@ -116,10 +132,10 @@ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObjec
116
132
  edge_profile.status = EdgeStatus.APPROVED
117
133
  updated_bundle = Bundle.generate(kobj.rid, edge_profile.model_dump())
118
134
 
119
- processor.handle(bundle=updated_bundle, event_type=EventType.UPDATE)
135
+ ctx.handle(bundle=updated_bundle, event_type=EventType.UPDATE)
120
136
  return
121
137
 
122
- elif edge_profile.target == processor.identity.rid:
138
+ elif edge_profile.target == ctx.identity.rid:
123
139
  if edge_profile.status == EdgeStatus.APPROVED:
124
140
  logger.debug("Edge approved by other node!")
125
141
 
@@ -127,88 +143,88 @@ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObjec
127
143
  # Network handlers
128
144
 
129
145
  @KnowledgeHandler.create(HandlerType.Network, rid_types=[KoiNetNode])
130
- def coordinator_contact(processor: ProcessorInterface, kobj: KnowledgeObject):
146
+ def coordinator_contact(ctx: HandlerContext, kobj: KnowledgeObject):
131
147
  node_profile = kobj.bundle.validate_contents(NodeProfile)
132
-
148
+
133
149
  # looking for event provider of nodes
134
150
  if KoiNetNode not in node_profile.provides.event:
135
151
  return
136
152
 
137
153
  # prevents coordinators from attempting to form a self loop
138
- if kobj.rid == processor.identity.rid:
154
+ if kobj.rid == ctx.identity.rid:
139
155
  return
140
156
 
141
157
  # already have an edge established
142
- if processor.network.graph.get_edge_profile(
158
+ if ctx.graph.get_edge(
143
159
  source=kobj.rid,
144
- target=processor.identity.rid,
160
+ target=ctx.identity.rid,
145
161
  ) is not None:
146
162
  return
147
163
 
148
164
  logger.info("Identified a coordinator!")
149
165
  logger.info("Proposing new edge")
150
166
 
151
- if processor.identity.profile.node_type == NodeType.FULL:
167
+ if ctx.identity.profile.node_type == NodeType.FULL:
152
168
  edge_type = EdgeType.WEBHOOK
153
169
  else:
154
170
  edge_type = EdgeType.POLL
155
171
 
156
172
  # queued for processing
157
- processor.handle(bundle=generate_edge_bundle(
173
+ ctx.handle(bundle=generate_edge_bundle(
158
174
  source=kobj.rid,
159
- target=processor.identity.rid,
175
+ target=ctx.identity.rid,
160
176
  edge_type=edge_type,
161
177
  rid_types=[KoiNetNode]
162
178
  ))
163
179
 
164
180
  logger.info("Catching up on network state")
165
181
 
166
- payload = processor.network.request_handler.fetch_rids(
182
+ payload = ctx.request_handler.fetch_rids(
167
183
  node=kobj.rid,
168
184
  rid_types=[KoiNetNode]
169
185
  )
170
186
  for rid in payload.rids:
171
- if rid == processor.identity.rid:
187
+ if rid == ctx.identity.rid:
172
188
  logger.info("Skipping myself")
173
189
  continue
174
- if processor.cache.exists(rid):
175
- logger.info(f"Skipping known RID '{rid}'")
190
+ if ctx.cache.exists(rid):
191
+ logger.info(f"Skipping known RID {rid!r}")
176
192
  continue
177
193
 
178
194
  # marked as external since we are handling RIDs from another node
179
195
  # will fetch remotely instead of checking local cache
180
- processor.handle(rid=rid, source=KnowledgeSource.External)
196
+ ctx.handle(rid=rid, source=kobj.rid)
181
197
  logger.info("Done")
182
198
 
183
199
 
184
200
  @KnowledgeHandler.create(HandlerType.Network)
185
- def basic_network_output_filter(processor: ProcessorInterface, kobj: KnowledgeObject):
201
+ def basic_network_output_filter(ctx: HandlerContext, kobj: KnowledgeObject):
186
202
  """Default network handler.
187
203
 
188
204
  Allows broadcasting of all RID types this node is an event provider for (set in node profile), and other nodes have subscribed to. All nodes will also broadcast about their own (internally sourced) KOI node, and KOI edges that they are part of, regardless of their node profile configuration. Finally, nodes will also broadcast about edges to the other node involved (regardless of if they are subscribed)."""
189
205
 
190
206
  involves_me = False
191
- if kobj.source == KnowledgeSource.Internal:
207
+ if kobj.source is None:
192
208
  if (type(kobj.rid) == KoiNetNode):
193
- if (kobj.rid == processor.identity.rid):
209
+ if (kobj.rid == ctx.identity.rid):
194
210
  involves_me = True
195
211
 
196
212
  elif type(kobj.rid) == KoiNetEdge:
197
213
  edge_profile = kobj.bundle.validate_contents(EdgeProfile)
198
214
 
199
- if edge_profile.source == processor.identity.rid:
215
+ if edge_profile.source == ctx.identity.rid:
200
216
  logger.debug(f"Adding edge target '{edge_profile.target!r}' to network targets")
201
217
  kobj.network_targets.update([edge_profile.target])
202
218
  involves_me = True
203
219
 
204
- elif edge_profile.target == processor.identity.rid:
220
+ elif edge_profile.target == ctx.identity.rid:
205
221
  logger.debug(f"Adding edge source '{edge_profile.source!r}' to network targets")
206
222
  kobj.network_targets.update([edge_profile.source])
207
223
  involves_me = True
208
224
 
209
- if (type(kobj.rid) in processor.identity.profile.provides.event or involves_me):
225
+ if (type(kobj.rid) in ctx.identity.profile.provides.event or involves_me):
210
226
  # broadcasts to subscribers if I'm an event provider of this RID type OR it involves me
211
- subscribers = processor.network.graph.get_neighbors(
227
+ subscribers = ctx.graph.get_neighbors(
212
228
  direction="out",
213
229
  allowed_type=type(kobj.rid)
214
230
  )
@@ -2,9 +2,7 @@ from dataclasses import dataclass
2
2
  from enum import StrEnum
3
3
  from typing import Callable
4
4
  from rid_lib import RIDType
5
-
6
5
  from ..protocol.event import EventType
7
- from .knowledge_object import KnowledgeSource, KnowledgeEventType
8
6
 
9
7
 
10
8
  class StopChain:
@@ -37,23 +35,21 @@ class KnowledgeHandler:
37
35
  func: Callable
38
36
  handler_type: HandlerType
39
37
  rid_types: list[RIDType] | None
40
- source: KnowledgeSource | None = None
41
- event_types: list[KnowledgeEventType] | None = None
38
+ event_types: list[EventType | None] | None = None
42
39
 
43
40
  @classmethod
44
41
  def create(
45
42
  cls,
46
43
  handler_type: HandlerType,
47
44
  rid_types: list[RIDType] | None = None,
48
- source: KnowledgeSource | None = None,
49
- event_types: list[KnowledgeEventType] | None = None
45
+ event_types: list[EventType | None] | None = None
50
46
  ):
51
47
  """Special decorator that returns a KnowledgeHandler instead of a function.
52
48
 
53
49
  The function symbol will redefined as a `KnowledgeHandler`, which can be passed into the `ProcessorInterface` constructor. This is used to register default handlers.
54
50
  """
55
51
  def decorator(func: Callable) -> KnowledgeHandler:
56
- handler = cls(func, handler_type, rid_types, source, event_types)
52
+ handler = cls(func, handler_type, rid_types, event_types)
57
53
  return handler
58
54
  return decorator
59
55