koi-net 1.0.0b6__py3-none-any.whl → 1.0.0b8__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.
koi_net/core.py CHANGED
@@ -11,25 +11,30 @@ from .protocol.event import Event, EventType
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
+
14
15
  class NodeInterface:
15
16
  cache: Cache
16
17
  identity: NodeIdentity
17
18
  network: NetworkInterface
18
19
  processor: ProcessorInterface
19
20
  first_contact: str
21
+ use_kobj_processor_thread: bool
20
22
 
21
23
  def __init__(
22
24
  self,
23
25
  name: str,
24
26
  profile: NodeProfile,
25
27
  identity_file_path: str = "identity.json",
28
+ event_queues_file_path: str = "event_queues.json",
29
+ cache_directory_path: str = "rid_cache",
30
+ use_kobj_processor_thread: bool = False,
26
31
  first_contact: str | None = None,
27
32
  handlers: list[KnowledgeHandler] | None = None,
28
33
  cache: Cache | None = None,
29
34
  network: NetworkInterface | None = None,
30
35
  processor: ProcessorInterface | None = None
31
36
  ):
32
- self.cache = cache or Cache(directory_path=f".cache")
37
+ self.cache = cache or Cache(cache_directory_path)
33
38
  self.identity = NodeIdentity(
34
39
  name=name,
35
40
  profile=profile,
@@ -38,7 +43,7 @@ class NodeInterface:
38
43
  )
39
44
  self.first_contact = first_contact
40
45
  self.network = network or NetworkInterface(
41
- file_path="event_queues.json",
46
+ file_path=event_queues_file_path,
42
47
  first_contact=self.first_contact,
43
48
  cache=self.cache,
44
49
  identity=self.identity
@@ -50,30 +55,41 @@ class NodeInterface:
50
55
  obj for obj in vars(default_handlers).values()
51
56
  if isinstance(obj, KnowledgeHandler)
52
57
  ]
53
-
58
+
59
+ self.use_kobj_processor_thread = use_kobj_processor_thread
54
60
  self.processor = processor or ProcessorInterface(
55
61
  cache=self.cache,
56
62
  network=self.network,
57
63
  identity=self.identity,
64
+ use_kobj_processor_thread=self.use_kobj_processor_thread,
58
65
  default_handlers=handlers
59
66
  )
60
67
 
61
- def initialize(self) -> None:
62
- """Initializes node, call on startup.
68
+ def start(self) -> None:
69
+ """Starts a node, call this method first.
63
70
 
64
- Loads event queues into memory. Generates network graph from nodes and edges in cache. Processes any state changes of node bundle. Initiates handshake with first contact (if provided) if node doesn't have any neighbors.
71
+ Starts the processor thread (if enabled). Loads event queues into memory. Generates network graph from nodes and edges in cache. Processes any state changes of node bundle. Initiates handshake with first contact (if provided) if node doesn't have any neighbors.
65
72
  """
66
- self.network._load_event_queues()
73
+ if self.use_kobj_processor_thread:
74
+ logger.info("Starting processor worker thread")
75
+ self.processor.worker_thread.start()
67
76
 
77
+ self.network._load_event_queues()
68
78
  self.network.graph.generate()
69
79
 
70
80
  self.processor.handle(
71
81
  bundle=Bundle.generate(
72
82
  rid=self.identity.rid,
73
83
  contents=self.identity.profile.model_dump()
74
- ),
75
- flush=True
84
+ )
76
85
  )
86
+
87
+ logger.info("Waiting for kobj queue to empty")
88
+ if self.use_kobj_processor_thread:
89
+ self.processor.kobj_queue.join()
90
+ else:
91
+ self.processor.flush_kobj_queue()
92
+ logger.info("Done")
77
93
 
78
94
  if not self.network.graph.get_neighbors() and self.first_contact:
79
95
  logger.info(f"I don't have any neighbors, reaching out to first contact {self.first_contact}")
@@ -94,9 +110,17 @@ class NodeInterface:
94
110
  return
95
111
 
96
112
 
97
- def finalize(self):
98
- """Finalizes node, call on shutdown.
113
+ def stop(self):
114
+ """Stops a node, call this method last.
99
115
 
100
- Saves event queues to storage.
116
+ Finishes processing knowledge object queue. Saves event queues to storage.
101
117
  """
118
+ logger.info("Stopping node...")
119
+
120
+ if self.use_kobj_processor_thread:
121
+ logger.info("Waiting for kobj queue to empty")
122
+ self.processor.kobj_queue.join()
123
+ else:
124
+ self.processor.flush_kobj_queue()
125
+
102
126
  self.network._save_event_queues()
koi_net/network/graph.py CHANGED
@@ -74,13 +74,13 @@ class NetworkGraph:
74
74
  """Returns edges this node belongs to.
75
75
 
76
76
  All edges returned by default, specify `direction` to restrict to incoming or outgoing edges only."""
77
-
77
+
78
78
  edges = []
79
- if direction != "in":
79
+ if direction != "in" and self.dg.out_edges:
80
80
  out_edges = self.dg.out_edges(self.identity.rid)
81
81
  edges.extend([e for e in out_edges])
82
82
 
83
- if direction != "out":
83
+ if direction != "out" and self.dg.in_edges:
84
84
  in_edges = self.dg.in_edges(self.identity.rid)
85
85
  edges.extend([e for e in in_edges])
86
86
 
@@ -165,6 +165,8 @@ class NetworkInterface:
165
165
  return
166
166
 
167
167
  events = self._flush_queue(self.webhook_event_queue, node)
168
+ if not events: return
169
+
168
170
  logger.info(f"Broadcasting {len(events)} events")
169
171
 
170
172
  try:
@@ -173,11 +175,6 @@ class NetworkInterface:
173
175
  logger.warning("Broadcast failed, requeuing events")
174
176
  for event in events:
175
177
  self.push_event_to(event, node)
176
-
177
- def flush_all_webhook_queues(self):
178
- """Flushes all nodes' webhook queues and broadcasts events."""
179
- for node in self.webhook_event_queue.keys():
180
- self.flush_webhook_queue(node)
181
178
 
182
179
  def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
183
180
  """Returns list of node RIDs which provide state for the specified RID type."""
@@ -204,8 +201,8 @@ class NetworkInterface:
204
201
  payload = self.request_handler.fetch_bundles(
205
202
  node=node_rid, rids=[rid])
206
203
 
207
- if payload.manifests:
208
- remote_bundle = payload.manifests[0]
204
+ if payload.bundles:
205
+ remote_bundle = payload.bundles[0]
209
206
  logger.info(f"Got bundle from '{node_rid}'")
210
207
  break
211
208
 
@@ -56,4 +56,4 @@ class ResponseHandler:
56
56
  else:
57
57
  not_found.append(rid)
58
58
 
59
- return BundlesPayload(manifests=bundles, not_found=not_found)
59
+ return BundlesPayload(bundles=bundles, not_found=not_found)
@@ -1,5 +1,6 @@
1
1
  import logging
2
- from queue import Queue
2
+ import queue
3
+ import threading
3
4
  from typing import Callable
4
5
  from rid_lib.core import RID, RIDType
5
6
  from rid_lib.ext import Bundle, Cache, Manifest
@@ -30,20 +31,30 @@ class ProcessorInterface:
30
31
  network: NetworkInterface
31
32
  identity: NodeIdentity
32
33
  handlers: list[KnowledgeHandler]
33
- kobj_queue: Queue[KnowledgeObject]
34
+ kobj_queue: queue.Queue[KnowledgeObject]
35
+ use_kobj_processor_thread: bool
36
+ worker_thread: threading.Thread | None = None
34
37
 
35
38
  def __init__(
36
39
  self,
37
40
  cache: Cache,
38
41
  network: NetworkInterface,
39
42
  identity: NodeIdentity,
43
+ use_kobj_processor_thread: bool,
40
44
  default_handlers: list[KnowledgeHandler] = []
41
45
  ):
42
46
  self.cache = cache
43
47
  self.network = network
44
48
  self.identity = identity
49
+ self.use_kobj_processor_thread = use_kobj_processor_thread
45
50
  self.handlers: list[KnowledgeHandler] = default_handlers
46
- self.kobj_queue = Queue()
51
+ self.kobj_queue = queue.Queue()
52
+
53
+ if self.use_kobj_processor_thread:
54
+ self.worker_thread = threading.Thread(
55
+ target=self.kobj_processor_worker,
56
+ daemon=True
57
+ )
47
58
 
48
59
  def add_handler(self, handler: KnowledgeHandler):
49
60
  self.handlers.append(handler)
@@ -195,18 +206,38 @@ class ProcessorInterface:
195
206
  logger.info("No network targets set")
196
207
 
197
208
  for node in kobj.network_targets:
198
- self.network.push_event_to(kobj.normalized_event, node)
199
- self.network.flush_all_webhook_queues()
209
+ self.network.push_event_to(kobj.normalized_event, node, flush=True)
200
210
 
201
211
  kobj = self.call_handler_chain(HandlerType.Final, kobj)
202
-
212
+
203
213
  def flush_kobj_queue(self):
204
- """Flushes all knowledge objects from queue and processes them."""
214
+ """Flushes all knowledge objects from queue and processes them.
215
+
216
+ NOTE: ONLY CALL THIS METHOD IN SINGLE THREADED NODES, OTHERWISE THIS WILL CAUSE RACE CONDITIONS.
217
+ """
218
+ if self.use_kobj_processor_thread:
219
+ logger.warning("You are using a worker thread, calling this method can cause race conditions!")
220
+
205
221
  while not self.kobj_queue.empty():
206
222
  kobj = self.kobj_queue.get()
207
223
  logger.info(f"Dequeued {kobj!r}")
208
224
  self.process_kobj(kobj)
225
+ self.kobj_queue.task_done()
209
226
  logger.info("Done handling")
227
+
228
+ def kobj_processor_worker(self, timeout=0.1):
229
+ while True:
230
+ try:
231
+ kobj = self.kobj_queue.get(timeout=timeout)
232
+ logger.info(f"Dequeued {kobj!r}")
233
+ self.process_kobj(kobj)
234
+ self.kobj_queue.task_done()
235
+
236
+ except queue.Empty:
237
+ pass
238
+
239
+ except Exception as e:
240
+ logger.warning(f"Error processing kobj: {e}")
210
241
 
211
242
  def handle(
212
243
  self,
@@ -216,8 +247,7 @@ class ProcessorInterface:
216
247
  event: Event | None = None,
217
248
  kobj: KnowledgeObject | None = None,
218
249
  event_type: KnowledgeEventType = None,
219
- source: KnowledgeSource = KnowledgeSource.Internal,
220
- flush: bool = False
250
+ source: KnowledgeSource = KnowledgeSource.Internal
221
251
  ):
222
252
  """Queues provided knowledge to be handled by processing pipeline.
223
253
 
@@ -238,6 +268,3 @@ class ProcessorInterface:
238
268
 
239
269
  self.kobj_queue.put(_kobj)
240
270
  logger.info(f"Queued {_kobj!r}")
241
-
242
- if flush:
243
- self.flush_kobj_queue()
@@ -33,7 +33,7 @@ class ManifestsPayload(BaseModel):
33
33
  not_found: list[RID] = []
34
34
 
35
35
  class BundlesPayload(BaseModel):
36
- manifests: list[Bundle]
36
+ bundles: list[Bundle]
37
37
  not_found: list[RID] = []
38
38
  deferred: list[RID] = []
39
39
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: koi-net
3
- Version: 1.0.0b6
3
+ Version: 1.0.0b8
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>
@@ -144,13 +144,14 @@ node = NodeInterface(
144
144
  event=[],
145
145
  state=[]
146
146
  )
147
- )
147
+ ),
148
+ use_kobj_processor_thread=True
148
149
  )
149
150
  ```
150
151
 
151
152
  ## Knowledge Processing
152
153
 
153
- Next we'll set up the knowledge processing flow for our node. This is where most of the node's logic and behavior will come into play. For partial nodes this will be an event loop, and for full nodes we will use webhooks. Make sure to call `node.initialize()` and `node.finalize()` at the beginning and end of your node's life cycle.
154
+ Next we'll set up the knowledge processing flow for our node. This is where most of the node's logic and behavior will come into play. For partial nodes this will be an event loop, and for full nodes we will use webhooks. Make sure to call `node.start()` and `node.stop()` at the beginning and end of your node's life cycle.
154
155
 
155
156
  ### Partial Node
156
157
  Make sure to set `source=KnowledgeSource.External`, this indicates to the knowledge processing pipeline that the incoming knowledge was received from an external source. Where the knowledge is sourced from will impact decisions in the node's knowledge handlers.
@@ -159,7 +160,7 @@ import time
159
160
  from koi_net.processor.knowledge_object import KnowledgeSource
160
161
 
161
162
  if __name__ == "__main__":
162
- node.initialize()
163
+ node.start()
163
164
 
164
165
  try:
165
166
  while True:
@@ -170,20 +171,20 @@ if __name__ == "__main__":
170
171
  time.sleep(5)
171
172
 
172
173
  finally:
173
- node.finalize()
174
+ node.stop()
174
175
  ```
175
176
 
176
177
  ### Full Node
177
- Setting up a full node is slightly more complex as we'll need a webserver. For this example, we'll use FastAPI and uvicorn. First we need to setup the "lifespan" of the server, to initialize and finalize the node before and after execution, as well as the FastAPI app which will be our web server.
178
+ Setting up a full node is slightly more complex as we'll need a webserver. For this example, we'll use FastAPI and uvicorn. First we need to setup the "lifespan" of the server, to start and stop the node before and after execution, as well as the FastAPI app which will be our web server.
178
179
  ```python
179
180
  from contextlib import asynccontextmanager
180
181
  from fastapi import FastAPI
181
182
 
182
183
  @asynccontextmanager
183
184
  async def lifespan(app: FastAPI):
184
- node.initialize()
185
+ node.start()
185
186
  yield
186
- node.finalize()
187
+ node.stop()
187
188
 
188
189
 
189
190
  app = FastAPI(lifespan=lifespan, root_path="/koi-net")
@@ -196,11 +197,9 @@ from koi_net.protocol.api_models import *
196
197
  from koi_net.protocol.consts import *
197
198
 
198
199
  @app.post(BROADCAST_EVENTS_PATH)
199
- def broadcast_events(req: EventsPayload, background: BackgroundTasks):
200
+ def broadcast_events(req: EventsPayload):
200
201
  for event in req.events:
201
202
  node.processor.handle(event=event, source=KnowledgeSource.External)
202
-
203
- background.add_task(node.processor.flush_kobj_queue)
204
203
  ```
205
204
 
206
205
  Next we can add the event polling endpoint, this allows partial nodes to receive events from us.
@@ -265,12 +264,16 @@ class NodeInterface:
265
264
  network: NetworkInterface
266
265
  processor: ProcessorInterface
267
266
  first_contact: str
267
+ use_kobj_processor_thread: bool
268
268
 
269
269
  def __init__(
270
270
  self,
271
271
  name: str,
272
272
  profile: NodeProfile,
273
273
  identity_file_path: str = "identity.json",
274
+ event_queues_file_path: str = "event_queues.json",
275
+ cache_directory_path: str = "rid_cache",
276
+ use_kobj_processor_thread: bool = False,
274
277
  first_contact: str | None = None,
275
278
  handlers: list[KnowledgeHandler] | None = None,
276
279
  cache: Cache | None = None,
@@ -278,8 +281,8 @@ class NodeInterface:
278
281
  processor: ProcessorInterface | None = None
279
282
  ): ...
280
283
 
281
- def initialize(self): ...
282
- def finalize(self): ...
284
+ def start(self): ...
285
+ def stop(self): ...
283
286
  ```
284
287
  As you can see, only a name and profile are required. The other fields allow for additional customization if needed.
285
288
 
@@ -313,7 +316,6 @@ class NetworkInterface:
313
316
 
314
317
  def flush_poll_queue(self, node: KoiNetNode) -> list[Event]: ...
315
318
  def flush_webhook_queue(self, node: RID): ...
316
- def flush_all_webhook_queues(self): ...
317
319
 
318
320
  def fetch_remote_bundle(self, rid: RID): ...
319
321
  def fetch_remote_manifest(self, rid: RID): ...
@@ -432,11 +434,14 @@ def poll_events(req: PollEvents) -> EventsPayload:
432
434
  The `ProcessorInterface` class provides access to a node's internal knowledge processing pipeline.
433
435
  ```python
434
436
  class ProcessorInterface:
437
+ worker_thread: threading.Thread | None = None
438
+
435
439
  def __init__(
436
440
  self,
437
441
  cache: Cache,
438
442
  network: NetworkInterface,
439
443
  identity: NodeIdentity,
444
+ use_kobj_processor_thread: bool,
440
445
  default_handlers: list[KnowledgeHandler] = []
441
446
  ): ...
442
447
 
@@ -1,24 +1,24 @@
1
1
  koi_net/__init__.py,sha256=b0Ze0pZmJAuygpWUFHM6Kvqo3DkU_uzmkptv1EpAArw,31
2
- koi_net/core.py,sha256=BzD1QWNaC0M13NHjBW-t-1qazksKfwAxSqliRowMNac,3550
2
+ koi_net/core.py,sha256=dE4sE2qsoIRUU1zsnrjx7aqYtYdHyCx-Dv4cwbkRjy4,4613
3
3
  koi_net/identity.py,sha256=PBgmAx5f3zzQmHASB1TJW2g19n9TLfmSJMXg2eQFg0A,2386
4
4
  koi_net/network/__init__.py,sha256=r_RN-q_mDYC-2RAkN-lJoMUX76TXyfEUc_MVKW87z0g,39
5
- koi_net/network/graph.py,sha256=SbA0ATIYaiBnq6PCEN2Mu7dgmfyZW3p5tRhZu23UR4E,4782
6
- koi_net/network/interface.py,sha256=paBJjQFJC8UkUz-BWeRwvuWD2cv8WFt7PyhoV7VOhWI,10823
5
+ koi_net/network/graph.py,sha256=KMUCU3AweRvivwy7GuWgX2zX74FPgHeVMO5ydvhVyvA,4833
6
+ koi_net/network/interface.py,sha256=4JTeg8Eah0z5YKhcVKJbCVZw_Ghl_6xfG8aa1I5PCWI,10643
7
7
  koi_net/network/request_handler.py,sha256=fhuCDsxI8fZ4p5TntcTZR4mnLrLQ61zDy7Oca3ooFCE,4402
8
- koi_net/network/response_handler.py,sha256=mA3FtrN3aTZATcLaHQhJUWrJdIKNv6d24fhvOl-nDKY,1890
8
+ koi_net/network/response_handler.py,sha256=HaP8Fl0bp_lfMmevhdVY8s9o0Uf8CR1ZaW5g3jsX8gw,1888
9
9
  koi_net/processor/__init__.py,sha256=x4fAY0hvQEDcpfdTB3POIzxBQjYAtn0qQazPo1Xm0m4,41
10
10
  koi_net/processor/default_handlers.py,sha256=Yc7a9n5sAOYMHzzY59VMXYOxQL-6O9zbMQzd61XbIEs,7184
11
11
  koi_net/processor/handler.py,sha256=APCECwU7MFcgP7Vu6UTngs0XIjaXSQ_f8rqy8cH5_rM,2242
12
- koi_net/processor/interface.py,sha256=qd4hBzNkNuNoDflzJSV7tuZOx0iRp6dG6tnafh1AWt8,11093
12
+ koi_net/processor/interface.py,sha256=szLLeDfMgeqU35F2na-LvzytJ0irpCtR9g0empo4JoI,12169
13
13
  koi_net/processor/knowledge_object.py,sha256=cGv33fwNZQMylkhlTaQTbk96FVIVbdOUaBsG06u0m4k,4187
14
14
  koi_net/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- koi_net/protocol/api_models.py,sha256=79B5IWQ7gsJ_QIsSRv9424F1frF_DMGkhBbYWkXgtOI,1118
15
+ koi_net/protocol/api_models.py,sha256=RDwVHAahiWzwzUnlj5MIm9et5WVpQOaG-Uscv1B9coU,1116
16
16
  koi_net/protocol/consts.py,sha256=zeWJvRpqcERrqJq39heyNHb6f_9QrvoBZJHd70yE914,249
17
17
  koi_net/protocol/edge.py,sha256=G3D9Ie0vbTSMJdoTw9g_oBmFCqzJ1gO7U1PVrw7p3j8,447
18
18
  koi_net/protocol/event.py,sha256=dzJmcHbimo7p5NwH2drccF0vMcAj9oQRj3iZ9Bjf7kg,1275
19
19
  koi_net/protocol/helpers.py,sha256=9E9PaoIuSNrTBATGCLJ_kSBMZ2z-KIMnLJzGOTqQDC0,719
20
20
  koi_net/protocol/node.py,sha256=Ntrx01dbm39ViKGtr4gLmztcMwKpTIweS6rRL-zoU_Y,391
21
- koi_net-1.0.0b6.dist-info/METADATA,sha256=2fUM02c7-EieTdAA-WNI_6WgRPFofhURpNRJuAwlB_k,21211
22
- koi_net-1.0.0b6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- koi_net-1.0.0b6.dist-info/licenses/LICENSE,sha256=XBcvl8yjCAezfuqN1jadQykrX7H2g4nr2WRDmHLW6ik,1090
24
- koi_net-1.0.0b6.dist-info/RECORD,,
21
+ koi_net-1.0.0b8.dist-info/METADATA,sha256=-oGUkUtRG4biV7yo7WIl2XiDv4k8wVTV_DbyebOiuoI,21352
22
+ koi_net-1.0.0b8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ koi_net-1.0.0b8.dist-info/licenses/LICENSE,sha256=XBcvl8yjCAezfuqN1jadQykrX7H2g4nr2WRDmHLW6ik,1090
24
+ koi_net-1.0.0b8.dist-info/RECORD,,