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

@@ -1 +0,0 @@
1
- from .interface import NetworkInterface
@@ -0,0 +1,42 @@
1
+ from logging import getLogger
2
+ from rid_lib.types import KoiNetNode
3
+ from ..protocol.event import Event, EventType
4
+ from ..identity import NodeIdentity
5
+ from ..effector import Effector
6
+ from ..network.event_queue import NetworkEventQueue
7
+
8
+ logger = getLogger(__name__)
9
+
10
+
11
+ class Actor:
12
+ identity: NodeIdentity
13
+ effector: Effector
14
+ event_queue: NetworkEventQueue
15
+
16
+ def __init__(
17
+ self,
18
+ identity: NodeIdentity,
19
+ effector: Effector,
20
+ event_queue: NetworkEventQueue
21
+ ):
22
+ self.identity = identity
23
+ self.effector = effector
24
+ self.event_queue = event_queue
25
+
26
+ def handshake_with(self, target: KoiNetNode):
27
+ logger.debug(f"Initiating handshake with {target}")
28
+ self.event_queue.push_event_to(
29
+ Event.from_rid(
30
+ event_type=EventType.FORGET,
31
+ rid=self.identity.rid),
32
+ node=target
33
+ )
34
+
35
+ self.event_queue.push_event_to(
36
+ event=Event.from_bundle(
37
+ event_type=EventType.NEW,
38
+ bundle=self.effector.deref(self.identity.rid)),
39
+ node=target
40
+ )
41
+
42
+ self.event_queue.flush_webhook_queue(target)
@@ -0,0 +1,50 @@
1
+ from logging import getLogger
2
+ from koi_net.protocol.errors import ErrorTypes
3
+ from koi_net.protocol.event import EventType
4
+ from rid_lib.types import KoiNetNode
5
+ from ..processor.interface import ProcessorInterface
6
+ from ..network.behavior import Actor
7
+
8
+ logger = getLogger(__name__)
9
+
10
+
11
+ class ErrorHandler:
12
+ timeout_counter: dict[KoiNetNode, int]
13
+ processor: ProcessorInterface
14
+ actor: Actor
15
+
16
+ def __init__(
17
+ self,
18
+ processor: ProcessorInterface,
19
+ actor: Actor
20
+ ):
21
+ self.processor = processor
22
+ self.actor = actor
23
+ self.timeout_counter = {}
24
+
25
+ def handle_connection_error(self, node: KoiNetNode):
26
+ self.timeout_counter.setdefault(node, 0)
27
+ self.timeout_counter[node] += 1
28
+
29
+ logger.debug(f"{node} has timed out {self.timeout_counter[node]} time(s)")
30
+
31
+ if self.timeout_counter[node] > 3:
32
+ logger.debug(f"Exceeded time out limit, forgetting node")
33
+ self.processor.handle(rid=node, event_type=EventType.FORGET)
34
+ # do something
35
+
36
+
37
+ def handle_protocol_error(
38
+ self,
39
+ error_type: ErrorTypes,
40
+ node: KoiNetNode
41
+ ):
42
+ logger.info(f"Handling protocol error {error_type} for node {node!r}")
43
+ match error_type:
44
+ case ErrorTypes.UnknownNode:
45
+ logger.info("Peer doesn't know me, attempting handshake...")
46
+ self.actor.handshake_with(node)
47
+
48
+ case ErrorTypes.InvalidKey: ...
49
+ case ErrorTypes.InvalidSignature: ...
50
+ case ErrorTypes.InvalidTarget: ...
@@ -1,21 +1,19 @@
1
1
  import logging
2
2
  from queue import Queue
3
- from typing import Generic
4
3
  import httpx
5
4
  from pydantic import BaseModel
6
5
  from rid_lib import RID
7
- from rid_lib.core import RIDType
8
6
  from rid_lib.ext import Cache
9
7
  from rid_lib.types import KoiNetNode
10
8
 
11
9
  from .graph import NetworkGraph
12
10
  from .request_handler import RequestHandler
13
- from .response_handler import ResponseHandler
14
- from ..protocol.node import NodeType
15
- from ..protocol.edge import EdgeType
11
+ from ..protocol.node import NodeProfile, NodeType
12
+ from ..protocol.edge import EdgeProfile, EdgeType
16
13
  from ..protocol.event import Event
17
14
  from ..identity import NodeIdentity
18
- from ..config import ConfigType
15
+ from ..config import NodeConfig
16
+ from ..effector import Effector
19
17
 
20
18
  logger = logging.getLogger(__name__)
21
19
 
@@ -26,34 +24,36 @@ class EventQueueModel(BaseModel):
26
24
 
27
25
  type EventQueue = dict[RID, Queue[Event]]
28
26
 
29
- class NetworkInterface(Generic[ConfigType]):
27
+ class NetworkEventQueue:
30
28
  """A collection of functions and classes to interact with the KOI network."""
31
29
 
32
- config: ConfigType
30
+ config: NodeConfig
33
31
  identity: NodeIdentity
32
+ effector: Effector
34
33
  cache: Cache
35
34
  graph: NetworkGraph
36
35
  request_handler: RequestHandler
37
- response_handler: ResponseHandler
38
36
  poll_event_queue: EventQueue
39
37
  webhook_event_queue: EventQueue
40
38
 
41
39
  def __init__(
42
40
  self,
43
- config: ConfigType,
41
+ config: NodeConfig,
44
42
  cache: Cache,
45
- identity: NodeIdentity
43
+ identity: NodeIdentity,
44
+ effector: Effector,
45
+ graph: NetworkGraph,
46
+ request_handler: RequestHandler,
46
47
  ):
47
48
  self.config = config
48
49
  self.identity = identity
49
50
  self.cache = cache
50
- self.graph = NetworkGraph(cache, identity)
51
- self.request_handler = RequestHandler(cache, self.graph)
52
- self.response_handler = ResponseHandler(cache)
51
+ self.graph = graph
52
+ self.request_handler = request_handler
53
+ self.effector = effector
53
54
 
54
55
  self.poll_event_queue = dict()
55
56
  self.webhook_event_queue = dict()
56
- self._load_event_queues()
57
57
 
58
58
  def _load_event_queues(self):
59
59
  """Loads event queues from storage."""
@@ -100,29 +100,43 @@ class NetworkInterface(Generic[ConfigType]):
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.debug(f"Pushing event {event.event_type} {event.rid} to {node}")
104
-
105
- node_profile = self.graph.get_node_profile(node)
106
- if not node_profile:
107
- logger.warning(f"Node {node!r} unknown to me")
103
+ logger.debug(f"Pushing event {event.event_type} {event.rid!r} to {node}")
104
+
105
+ node_bundle = self.effector.deref(node)
108
106
 
109
107
  # if there's an edge from me to the target node, override broadcast type
110
- edge_profile = self.graph.get_edge_profile(
108
+ edge_rid = self.graph.get_edge(
111
109
  source=self.identity.rid,
112
110
  target=node
113
111
  )
114
112
 
115
- if edge_profile:
113
+ edge_bundle = self.effector.deref(edge_rid) if edge_rid else None
114
+
115
+ if edge_bundle:
116
+ logger.debug(f"Found edge from me to {node!r}")
117
+ edge_profile = edge_bundle.validate_contents(EdgeProfile)
116
118
  if edge_profile.edge_type == EdgeType.WEBHOOK:
117
119
  event_queue = self.webhook_event_queue
118
120
  elif edge_profile.edge_type == EdgeType.POLL:
119
121
  event_queue = self.poll_event_queue
120
- else:
122
+
123
+ elif node_bundle:
124
+ logger.debug(f"Found bundle for {node!r}")
125
+ node_profile = node_bundle.validate_contents(NodeProfile)
121
126
  if node_profile.node_type == NodeType.FULL:
122
127
  event_queue = self.webhook_event_queue
123
128
  elif node_profile.node_type == NodeType.PARTIAL:
124
129
  event_queue = self.poll_event_queue
125
130
 
131
+ elif node == self.config.koi_net.first_contact.rid:
132
+ logger.debug(f"Node {node!r} is my first contact")
133
+ # first contact node is always a webhook node
134
+ event_queue = self.webhook_event_queue
135
+
136
+ else:
137
+ logger.warning(f"Node {node!r} unknown to me")
138
+ return
139
+
126
140
  queue = event_queue.setdefault(node, Queue())
127
141
  queue.put(event)
128
142
 
@@ -136,7 +150,7 @@ class NetworkInterface(Generic[ConfigType]):
136
150
  if queue:
137
151
  while not queue.empty():
138
152
  event = queue.get()
139
- logger.debug(f"Dequeued {event.event_type} '{event.rid}'")
153
+ logger.debug(f"Dequeued {event.event_type} {event.rid!r}")
140
154
  events.append(event)
141
155
 
142
156
  return events
@@ -146,7 +160,7 @@ class NetworkInterface(Generic[ConfigType]):
146
160
  logger.debug(f"Flushing poll queue for {node}")
147
161
  return self._flush_queue(self.poll_event_queue, node)
148
162
 
149
- def flush_webhook_queue(self, node: KoiNetNode):
163
+ def flush_webhook_queue(self, node: KoiNetNode, requeue_on_fail: bool = True):
150
164
  """Flushes a node's webhook queue, and broadcasts events.
151
165
 
152
166
  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.
@@ -154,15 +168,17 @@ class NetworkInterface(Generic[ConfigType]):
154
168
 
155
169
  logger.debug(f"Flushing webhook queue for {node}")
156
170
 
157
- node_profile = self.graph.get_node_profile(node)
171
+ # node_bundle = self.effector.deref(node)
158
172
 
159
- if not node_profile:
160
- logger.warning(f"{node!r} not found")
161
- return
173
+ # if not node_bundle:
174
+ # logger.warning(f"{node!r} not found")
175
+ # return
162
176
 
163
- if node_profile.node_type != NodeType.FULL:
164
- logger.warning(f"{node!r} is a partial node!")
165
- return
177
+ # node_profile = node_bundle.validate_contents(NodeProfile)
178
+
179
+ # if node_profile.node_type != NodeType.FULL:
180
+ # logger.warning(f"{node!r} is a partial node!")
181
+ # return
166
182
 
167
183
  events = self._flush_queue(self.webhook_event_queue, node)
168
184
  if not events: return
@@ -174,103 +190,8 @@ class NetworkInterface(Generic[ConfigType]):
174
190
  return True
175
191
  except httpx.ConnectError:
176
192
  logger.warning("Broadcast failed")
177
- for event in events:
178
- self.push_event_to(event, node)
179
- return False
180
-
181
- def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
182
- """Returns list of node RIDs which provide state for the specified RID type."""
183
-
184
- logger.debug(f"Looking for state providers of '{rid_type}'")
185
- provider_nodes = []
186
- for node_rid in self.cache.list_rids(rid_types=[KoiNetNode]):
187
- node = self.graph.get_node_profile(node_rid)
188
-
189
- if node.node_type == NodeType.FULL and rid_type in node.provides.state:
190
- logger.debug(f"Found provider '{node_rid}'")
191
- provider_nodes.append(node_rid)
192
-
193
- if not provider_nodes:
194
- logger.debug("Failed to find providers")
195
- return provider_nodes
196
193
 
197
- def fetch_remote_bundle(self, rid: RID):
198
- """Attempts to fetch a bundle by RID from known peer nodes."""
199
-
200
- logger.debug(f"Fetching remote bundle '{rid}'")
201
- remote_bundle = None
202
- for node_rid in self.get_state_providers(type(rid)):
203
- payload = self.request_handler.fetch_bundles(
204
- node=node_rid, rids=[rid])
205
-
206
- if payload.bundles:
207
- remote_bundle = payload.bundles[0]
208
- logger.debug(f"Got bundle from '{node_rid}'")
209
- break
210
-
211
- if not remote_bundle:
212
- logger.warning("Failed to fetch remote bundle")
213
-
214
- return remote_bundle
215
-
216
- def fetch_remote_manifest(self, rid: RID):
217
- """Attempts to fetch a manifest by RID from known peer nodes."""
218
-
219
- logger.debug(f"Fetching remote manifest '{rid}'")
220
- remote_manifest = None
221
- for node_rid in self.get_state_providers(type(rid)):
222
- payload = self.request_handler.fetch_manifests(
223
- node=node_rid, rids=[rid])
224
-
225
- if payload.manifests:
226
- remote_manifest = payload.manifests[0]
227
- logger.debug(f"Got bundle from '{node_rid}'")
228
- break
229
-
230
- if not remote_manifest:
231
- logger.warning("Failed to fetch remote bundle")
232
-
233
- return remote_manifest
234
-
235
- def poll_neighbors(self) -> list[Event]:
236
- """Polls all neighboring nodes and returns compiled list of events.
237
-
238
- If this node has no neighbors, it will instead attempt to poll the provided first contact URL.
239
- """
240
-
241
- neighbors = self.graph.get_neighbors()
242
-
243
- if not neighbors and self.config.koi_net.first_contact:
244
- logger.debug("No neighbors found, polling first contact")
245
- try:
246
- payload = self.request_handler.poll_events(
247
- url=self.config.koi_net.first_contact,
248
- rid=self.identity.rid
249
- )
250
- if payload.events:
251
- logger.debug(f"Received {len(payload.events)} events from '{self.config.koi_net.first_contact}'")
252
- return payload.events
253
- except httpx.ConnectError:
254
- logger.debug(f"Failed to reach first contact '{self.config.koi_net.first_contact}'")
255
-
256
- events = []
257
- for node_rid in neighbors:
258
- node = self.graph.get_node_profile(node_rid)
259
- if not node: continue
260
- if node.node_type != NodeType.FULL: continue
261
-
262
- try:
263
- payload = self.request_handler.poll_events(
264
- node=node_rid,
265
- rid=self.identity.rid
266
- )
267
- if payload.events:
268
- logger.debug(f"Received {len(payload.events)} events from {node_rid!r}")
269
- events.extend(payload.events)
270
- except httpx.ConnectError:
271
- logger.debug(f"Failed to reach node '{node_rid}'")
272
- continue
273
-
274
- return events
275
-
276
-
194
+ if requeue_on_fail:
195
+ for event in events:
196
+ self.push_event_to(event, node)
197
+ return False
koi_net/network/graph.py CHANGED
@@ -6,7 +6,6 @@ from rid_lib.ext import Cache
6
6
  from rid_lib.types import KoiNetEdge, KoiNetNode
7
7
  from ..identity import NodeIdentity
8
8
  from ..protocol.edge import EdgeProfile, EdgeStatus
9
- from ..protocol.node import NodeProfile
10
9
 
11
10
  logger = logging.getLogger(__name__)
12
11
 
@@ -30,43 +29,27 @@ class NetworkGraph:
30
29
  for rid in self.cache.list_rids():
31
30
  if type(rid) == KoiNetNode:
32
31
  self.dg.add_node(rid)
33
- logger.debug(f"Added node {rid}")
32
+ logger.debug(f"Added node {rid!r}")
34
33
 
35
34
  elif type(rid) == KoiNetEdge:
36
- edge_profile = self.get_edge_profile(rid)
37
- if not edge_profile:
35
+ edge_bundle = self.cache.read(rid)
36
+ if not edge_bundle:
38
37
  logger.warning(f"Failed to load {rid!r}")
39
38
  continue
39
+ edge_profile = edge_bundle.validate_contents(EdgeProfile)
40
40
  self.dg.add_edge(edge_profile.source, edge_profile.target, rid=rid)
41
- logger.debug(f"Added edge {rid} ({edge_profile.source} -> {edge_profile.target})")
41
+ logger.debug(f"Added edge {rid!r} ({edge_profile.source} -> {edge_profile.target})")
42
42
  logger.debug("Done")
43
43
 
44
- def get_node_profile(self, rid: KoiNetNode) -> NodeProfile | None:
45
- """Returns node profile given its RID."""
46
- bundle = self.cache.read(rid)
47
- if bundle:
48
- return bundle.validate_contents(NodeProfile)
49
-
50
- def get_edge_profile(
51
- self,
52
- rid: KoiNetEdge | None = None,
53
- source: KoiNetNode | None = None,
54
- target: KoiNetNode | None = None,
55
- ) -> EdgeProfile | None:
56
- """Returns edge profile given its RID, or source and target node RIDs."""
57
- if source and target:
58
- if (source, target) not in self.dg.edges: return
44
+ def get_edge(self, source: KoiNetNode, target: KoiNetNode,) -> EdgeProfile | None:
45
+ """Returns edge RID given the RIDs of a source and target node."""
46
+ if (source, target) in self.dg.edges:
59
47
  edge_data = self.dg.get_edge_data(source, target)
60
- if not edge_data: return
61
- rid = edge_data.get("rid")
62
- if not rid: return
63
- elif not rid:
64
- raise ValueError("Either 'rid' or 'source' and 'target' must be provided")
65
-
66
- bundle = self.cache.read(rid)
67
- if bundle:
68
- return bundle.validate_contents(EdgeProfile)
69
-
48
+ if edge_data:
49
+ return edge_data.get("rid")
50
+
51
+ return None
52
+
70
53
  def get_edges(
71
54
  self,
72
55
  direction: Literal["in", "out"] | None = None,
@@ -106,12 +89,14 @@ class NetworkGraph:
106
89
 
107
90
  neighbors = []
108
91
  for edge_rid in self.get_edges(direction):
109
- edge_profile = self.get_edge_profile(edge_rid)
92
+ edge_bundle = self.cache.read(edge_rid)
110
93
 
111
- if not edge_profile:
94
+ if not edge_bundle:
112
95
  logger.warning(f"Failed to find edge {edge_rid!r} in cache")
113
96
  continue
114
-
97
+
98
+ edge_profile = edge_bundle.validate_contents(EdgeProfile)
99
+
115
100
  if status and edge_profile.status != status:
116
101
  continue
117
102