koi-net 1.0.0b11__py3-none-any.whl → 1.0.0b12__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,276 +1,274 @@
1
- import logging
2
- from queue import Queue
3
- import httpx
4
- from pydantic import BaseModel
5
- from rid_lib import RID
6
- from rid_lib.core import RIDType
7
- from rid_lib.ext import Cache
8
- from rid_lib.types import KoiNetNode
9
- from .graph import NetworkGraph
10
- from .request_handler import RequestHandler
11
- from .response_handler import ResponseHandler
12
- from ..protocol.node import NodeType
13
- from ..protocol.edge import EdgeType
14
- from ..protocol.event import Event
15
- from ..identity import NodeIdentity
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class EventQueueModel(BaseModel):
21
- webhook: dict[KoiNetNode, list[Event]]
22
- poll: dict[KoiNetNode, list[Event]]
23
-
24
- type EventQueue = dict[RID, Queue[Event]]
25
-
26
- class NetworkInterface:
27
- """A collection of functions and classes to interact with the KOI network."""
28
-
29
- identity: NodeIdentity
30
- cache: Cache
31
- first_contact: str | None
32
- graph: NetworkGraph
33
- request_handler: RequestHandler
34
- response_handler: ResponseHandler
35
- poll_event_queue: EventQueue
36
- webhook_event_queue: EventQueue
37
- event_queues_file_path: str
38
-
39
- def __init__(
40
- self,
41
- file_path: str,
42
- first_contact: str | None,
43
- cache: Cache,
44
- identity: NodeIdentity
45
- ):
46
- self.identity = identity
47
- self.cache = cache
48
- self.first_contact = first_contact
49
- self.graph = NetworkGraph(cache, identity)
50
- self.request_handler = RequestHandler(cache, self.graph)
51
- self.response_handler = ResponseHandler(cache)
52
- self.event_queues_file_path = file_path
53
-
54
- self.poll_event_queue = dict()
55
- self.webhook_event_queue = dict()
56
- self._load_event_queues()
57
-
58
- def _load_event_queues(self):
59
- """Loads event queues from storage."""
60
- try:
61
- with open(self.event_queues_file_path, "r") as f:
62
- queues = EventQueueModel.model_validate_json(f.read())
63
-
64
- for node in queues.poll.keys():
65
- for event in queues.poll[node]:
66
- queue = self.poll_event_queue.setdefault(node, Queue())
67
- queue.put(event)
68
-
69
- for node in queues.webhook.keys():
70
- for event in queues.webhook[node]:
71
- queue = self.webhook_event_queue.setdefault(node, Queue())
72
- queue.put(event)
73
-
74
- except FileNotFoundError:
75
- return
76
-
77
- def _save_event_queues(self):
78
- """Writes event queues to storage."""
79
- events_model = EventQueueModel(
80
- poll={
81
- node: list(queue.queue)
82
- for node, queue in self.poll_event_queue.items()
83
- if not queue.empty()
84
- },
85
- webhook={
86
- node: list(queue.queue)
87
- for node, queue in self.webhook_event_queue.items()
88
- if not queue.empty()
89
- }
90
- )
91
-
92
- if len(events_model.poll) == 0 and len(events_model.webhook) == 0:
93
- return
94
-
95
- with open(self.event_queues_file_path, "w") as f:
96
- f.write(events_model.model_dump_json(indent=2))
97
-
98
- def push_event_to(self, event: Event, node: KoiNetNode, flush=False):
99
- """Pushes event to queue of specified node.
100
-
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
- """
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")
108
-
109
- # if there's an edge from me to the target node, override broadcast type
110
- edge_profile = self.graph.get_edge_profile(
111
- source=self.identity.rid,
112
- target=node
113
- )
114
-
115
- if edge_profile:
116
- if edge_profile.edge_type == EdgeType.WEBHOOK:
117
- event_queue = self.webhook_event_queue
118
- elif edge_profile.edge_type == EdgeType.POLL:
119
- event_queue = self.poll_event_queue
120
- else:
121
- if node_profile.node_type == NodeType.FULL:
122
- event_queue = self.webhook_event_queue
123
- elif node_profile.node_type == NodeType.PARTIAL:
124
- event_queue = self.poll_event_queue
125
-
126
- queue = event_queue.setdefault(node, Queue())
127
- queue.put(event)
128
-
129
- if flush and event_queue is self.webhook_event_queue:
130
- self.flush_webhook_queue(node)
131
-
132
- def _flush_queue(self, event_queue: EventQueue, node: KoiNetNode) -> list[Event]:
133
- """Flushes a node's queue, returning list of events."""
134
- queue = event_queue.get(node)
135
- events = list()
136
- if queue:
137
- while not queue.empty():
138
- event = queue.get()
139
- logger.debug(f"Dequeued {event.event_type} '{event.rid}'")
140
- events.append(event)
141
-
142
- return events
143
-
144
- def flush_poll_queue(self, node: KoiNetNode) -> list[Event]:
145
- """Flushes a node's poll queue, returning list of events."""
146
- logger.debug(f"Flushing poll queue for {node}")
147
- return self._flush_queue(self.poll_event_queue, node)
148
-
149
- def flush_webhook_queue(self, node: KoiNetNode):
150
- """Flushes a node's webhook queue, and broadcasts events.
151
-
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
- """
154
-
155
- logger.debug(f"Flushing webhook queue for {node}")
156
-
157
- node_profile = self.graph.get_node_profile(node)
158
-
159
- if not node_profile:
160
- logger.warning(f"{node!r} not found")
161
- return
162
-
163
- if node_profile.node_type != NodeType.FULL:
164
- logger.warning(f"{node!r} is a partial node!")
165
- return
166
-
167
- events = self._flush_queue(self.webhook_event_queue, node)
168
- if not events: return
169
-
170
- logger.debug(f"Broadcasting {len(events)} events")
171
-
172
- try:
173
- self.request_handler.broadcast_events(node, events=events)
174
- return True
175
- except httpx.ConnectError:
176
- logger.warning("Broadcast failed, dropping node")
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
-
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.first_contact:
244
- logger.debug("No neighbors found, polling first contact")
245
- try:
246
- payload = self.request_handler.poll_events(
247
- url=self.first_contact,
248
- rid=self.identity.rid
249
- )
250
- if payload.events:
251
- logger.debug(f"Received {len(payload.events)} events from '{self.first_contact}'")
252
- return payload.events
253
- except httpx.ConnectError:
254
- logger.debug(f"Failed to reach first contact '{self.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
-
1
+ import logging
2
+ from queue import Queue
3
+ import httpx
4
+ from pydantic import BaseModel
5
+ from rid_lib import RID
6
+ from rid_lib.core import RIDType
7
+ from rid_lib.ext import Cache
8
+ from rid_lib.types import KoiNetNode
9
+ from .graph import NetworkGraph
10
+ from .request_handler import RequestHandler
11
+ from .response_handler import ResponseHandler
12
+ from ..protocol.node import NodeType
13
+ from ..protocol.edge import EdgeType
14
+ from ..protocol.event import Event
15
+ from ..identity import NodeIdentity
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class EventQueueModel(BaseModel):
21
+ webhook: dict[KoiNetNode, list[Event]]
22
+ poll: dict[KoiNetNode, list[Event]]
23
+
24
+ type EventQueue = dict[RID, Queue[Event]]
25
+
26
+ class NetworkInterface:
27
+ """A collection of functions and classes to interact with the KOI network."""
28
+
29
+ identity: NodeIdentity
30
+ cache: Cache
31
+ first_contact: str | None
32
+ graph: NetworkGraph
33
+ request_handler: RequestHandler
34
+ response_handler: ResponseHandler
35
+ poll_event_queue: EventQueue
36
+ webhook_event_queue: EventQueue
37
+ event_queues_file_path: str
38
+
39
+ def __init__(
40
+ self,
41
+ file_path: str,
42
+ first_contact: str | None,
43
+ cache: Cache,
44
+ identity: NodeIdentity
45
+ ):
46
+ self.identity = identity
47
+ self.cache = cache
48
+ self.first_contact = first_contact
49
+ self.graph = NetworkGraph(cache, identity)
50
+ self.request_handler = RequestHandler(cache, self.graph)
51
+ self.response_handler = ResponseHandler(cache)
52
+ self.event_queues_file_path = file_path
53
+
54
+ self.poll_event_queue = dict()
55
+ self.webhook_event_queue = dict()
56
+ self._load_event_queues()
57
+
58
+ def _load_event_queues(self):
59
+ """Loads event queues from storage."""
60
+ try:
61
+ with open(self.event_queues_file_path, "r") as f:
62
+ queues = EventQueueModel.model_validate_json(f.read())
63
+
64
+ for node in queues.poll.keys():
65
+ for event in queues.poll[node]:
66
+ queue = self.poll_event_queue.setdefault(node, Queue())
67
+ queue.put(event)
68
+
69
+ for node in queues.webhook.keys():
70
+ for event in queues.webhook[node]:
71
+ queue = self.webhook_event_queue.setdefault(node, Queue())
72
+ queue.put(event)
73
+
74
+ except FileNotFoundError:
75
+ return
76
+
77
+ def _save_event_queues(self):
78
+ """Writes event queues to storage."""
79
+ events_model = EventQueueModel(
80
+ poll={
81
+ node: list(queue.queue)
82
+ for node, queue in self.poll_event_queue.items()
83
+ if not queue.empty()
84
+ },
85
+ webhook={
86
+ node: list(queue.queue)
87
+ for node, queue in self.webhook_event_queue.items()
88
+ if not queue.empty()
89
+ }
90
+ )
91
+
92
+ if len(events_model.poll) == 0 and len(events_model.webhook) == 0:
93
+ return
94
+
95
+ with open(self.event_queues_file_path, "w") as f:
96
+ f.write(events_model.model_dump_json(indent=2))
97
+
98
+ def push_event_to(self, event: Event, node: KoiNetNode, flush=False):
99
+ """Pushes event to queue of specified node.
100
+
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
+ """
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")
108
+
109
+ # if there's an edge from me to the target node, override broadcast type
110
+ edge_profile = self.graph.get_edge_profile(
111
+ source=self.identity.rid,
112
+ target=node
113
+ )
114
+
115
+ if edge_profile:
116
+ if edge_profile.edge_type == EdgeType.WEBHOOK:
117
+ event_queue = self.webhook_event_queue
118
+ elif edge_profile.edge_type == EdgeType.POLL:
119
+ event_queue = self.poll_event_queue
120
+ else:
121
+ if node_profile.node_type == NodeType.FULL:
122
+ event_queue = self.webhook_event_queue
123
+ elif node_profile.node_type == NodeType.PARTIAL:
124
+ event_queue = self.poll_event_queue
125
+
126
+ queue = event_queue.setdefault(node, Queue())
127
+ queue.put(event)
128
+
129
+ if flush and event_queue is self.webhook_event_queue:
130
+ self.flush_webhook_queue(node)
131
+
132
+ def _flush_queue(self, event_queue: EventQueue, node: KoiNetNode) -> list[Event]:
133
+ """Flushes a node's queue, returning list of events."""
134
+ queue = event_queue.get(node)
135
+ events = list()
136
+ if queue:
137
+ while not queue.empty():
138
+ event = queue.get()
139
+ logger.debug(f"Dequeued {event.event_type} '{event.rid}'")
140
+ events.append(event)
141
+
142
+ return events
143
+
144
+ def flush_poll_queue(self, node: KoiNetNode) -> list[Event]:
145
+ """Flushes a node's poll queue, returning list of events."""
146
+ logger.debug(f"Flushing poll queue for {node}")
147
+ return self._flush_queue(self.poll_event_queue, node)
148
+
149
+ def flush_webhook_queue(self, node: KoiNetNode):
150
+ """Flushes a node's webhook queue, and broadcasts events.
151
+
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
+ """
154
+
155
+ logger.debug(f"Flushing webhook queue for {node}")
156
+
157
+ node_profile = self.graph.get_node_profile(node)
158
+
159
+ if not node_profile:
160
+ logger.warning(f"{node!r} not found")
161
+ return
162
+
163
+ if node_profile.node_type != NodeType.FULL:
164
+ logger.warning(f"{node!r} is a partial node!")
165
+ return
166
+
167
+ events = self._flush_queue(self.webhook_event_queue, node)
168
+ if not events: return
169
+
170
+ logger.debug(f"Broadcasting {len(events)} events")
171
+
172
+ try:
173
+ self.request_handler.broadcast_events(node, events=events)
174
+ except httpx.ConnectError:
175
+ logger.warning("Broadcast failed, requeuing events")
176
+ for event in events:
177
+ self.push_event_to(event, node)
178
+
179
+ def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
180
+ """Returns list of node RIDs which provide state for the specified RID type."""
181
+
182
+ logger.debug(f"Looking for state providers of '{rid_type}'")
183
+ provider_nodes = []
184
+ for node_rid in self.cache.list_rids(rid_types=[KoiNetNode]):
185
+ node = self.graph.get_node_profile(node_rid)
186
+
187
+ if node.node_type == NodeType.FULL and rid_type in node.provides.state:
188
+ logger.debug(f"Found provider '{node_rid}'")
189
+ provider_nodes.append(node_rid)
190
+
191
+ if not provider_nodes:
192
+ logger.debug("Failed to find providers")
193
+ return provider_nodes
194
+
195
+ def fetch_remote_bundle(self, rid: RID):
196
+ """Attempts to fetch a bundle by RID from known peer nodes."""
197
+
198
+ logger.debug(f"Fetching remote bundle '{rid}'")
199
+ remote_bundle = None
200
+ for node_rid in self.get_state_providers(type(rid)):
201
+ payload = self.request_handler.fetch_bundles(
202
+ node=node_rid, rids=[rid])
203
+
204
+ if payload.bundles:
205
+ remote_bundle = payload.bundles[0]
206
+ logger.debug(f"Got bundle from '{node_rid}'")
207
+ break
208
+
209
+ if not remote_bundle:
210
+ logger.warning("Failed to fetch remote bundle")
211
+
212
+ return remote_bundle
213
+
214
+ def fetch_remote_manifest(self, rid: RID):
215
+ """Attempts to fetch a manifest by RID from known peer nodes."""
216
+
217
+ logger.debug(f"Fetching remote manifest '{rid}'")
218
+ remote_manifest = None
219
+ for node_rid in self.get_state_providers(type(rid)):
220
+ payload = self.request_handler.fetch_manifests(
221
+ node=node_rid, rids=[rid])
222
+
223
+ if payload.manifests:
224
+ remote_manifest = payload.manifests[0]
225
+ logger.debug(f"Got bundle from '{node_rid}'")
226
+ break
227
+
228
+ if not remote_manifest:
229
+ logger.warning("Failed to fetch remote bundle")
230
+
231
+ return remote_manifest
232
+
233
+ def poll_neighbors(self) -> list[Event]:
234
+ """Polls all neighboring nodes and returns compiled list of events.
235
+
236
+ If this node has no neighbors, it will instead attempt to poll the provided first contact URL.
237
+ """
238
+
239
+ neighbors = self.graph.get_neighbors()
240
+
241
+ if not neighbors and self.first_contact:
242
+ logger.debug("No neighbors found, polling first contact")
243
+ try:
244
+ payload = self.request_handler.poll_events(
245
+ url=self.first_contact,
246
+ rid=self.identity.rid
247
+ )
248
+ if payload.events:
249
+ logger.debug(f"Received {len(payload.events)} events from '{self.first_contact}'")
250
+ return payload.events
251
+ except httpx.ConnectError:
252
+ logger.debug(f"Failed to reach first contact '{self.first_contact}'")
253
+
254
+ events = []
255
+ for node_rid in neighbors:
256
+ node = self.graph.get_node_profile(node_rid)
257
+ if not node: continue
258
+ if node.node_type != NodeType.FULL: continue
259
+
260
+ try:
261
+ payload = self.request_handler.poll_events(
262
+ node=node_rid,
263
+ rid=self.identity.rid
264
+ )
265
+ if payload.events:
266
+ logger.debug(f"Received {len(payload.events)} events from {node_rid!r}")
267
+ events.extend(payload.events)
268
+ except httpx.ConnectError:
269
+ logger.debug(f"Failed to reach node '{node_rid}'")
270
+ continue
271
+
272
+ return events
273
+
276
274