koi-net 1.0.0b8__tar.gz → 1.0.0b10__tar.gz

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 (31) hide show
  1. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/PKG-INFO +183 -10
  2. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/README.md +182 -9
  3. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/pyproject.toml +1 -1
  4. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/core.py +5 -5
  5. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/network/graph.py +4 -4
  6. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/network/interface.py +17 -17
  7. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/network/request_handler.py +27 -19
  8. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/processor/default_handlers.py +22 -29
  9. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/processor/handler.py +9 -2
  10. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/processor/interface.py +58 -33
  11. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/processor/knowledge_object.py +1 -1
  12. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/protocol/event.py +3 -0
  13. koi_net-1.0.0b10/test.py +25 -0
  14. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/.gitignore +0 -0
  15. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/LICENSE +0 -0
  16. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/examples/basic_coordinator_node.py +0 -0
  17. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/examples/basic_partial_node.py +0 -0
  18. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/examples/full_node_template.py +0 -0
  19. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/examples/partial_node_template.py +0 -0
  20. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/requirements.txt +0 -0
  21. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/__init__.py +0 -0
  22. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/identity.py +0 -0
  23. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/network/__init__.py +0 -0
  24. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/network/response_handler.py +0 -0
  25. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/processor/__init__.py +0 -0
  26. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/protocol/__init__.py +0 -0
  27. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/protocol/api_models.py +0 -0
  28. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/protocol/consts.py +0 -0
  29. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/protocol/edge.py +0 -0
  30. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/protocol/helpers.py +0 -0
  31. {koi_net-1.0.0b8 → koi_net-1.0.0b10}/src/koi_net/protocol/node.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: koi-net
3
- Version: 1.0.0b8
3
+ Version: 1.0.0b10
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>
@@ -44,6 +44,37 @@ Description-Content-Type: text/markdown
44
44
 
45
45
  *This specification is the result of several iterations of KOI research, [read more here](https://github.com/BlockScience/koi).*
46
46
 
47
+ ### Jump to Sections:
48
+ - [Protocol](#protocol)
49
+ - [Introduction](#introduction)
50
+ - [Communication Methods](#communication-methods)
51
+ - [Quickstart](#quickstart)
52
+ - [Setup](#setup)
53
+ - [Creating a Node](#creating-a-node)
54
+ - [Knowledge Processing](#knowledge-processing)
55
+ - [Try It Out!](#try-it-out)
56
+ - [Advanced](#advanced)
57
+ - [Knowledge Processing Pipeline](#knowledge-processing-pipeline)
58
+ - [Knowledge Handlers](#knowledge-handlers)
59
+ - [RID Handler](#rid-handler)
60
+ - [Manifest Handler](#manifest-handler)
61
+ - [Bundle Handler](#bundle-handler)
62
+ - [Network Handler](#network-handler)
63
+ - [Final Handler](#final-handler)
64
+ - [Registering Handlers](#registering-handlers)
65
+ - [Default Behavior](#default-behavior)
66
+ - [Implementation Reference](#implementation-reference)
67
+ - [Node Interface](#node-interface)
68
+ - [Node Identity](#node-identity)
69
+ - [Network Interface](#network-interface)
70
+ - [Network Graph](#network-graph)
71
+ - [Request Handler](#request-handler)
72
+ - [Response Handler](#response-handler)
73
+ - [Processor Interface](#processor-interface)
74
+ - [Development](#development)
75
+ - [Setup](#setup-1)
76
+ - [Distribution](#distribution)
77
+
47
78
  # Protocol
48
79
  ## Introduction
49
80
 
@@ -87,10 +118,10 @@ The request and payload JSON objects are composed of the fundamental "knowledge
87
118
  }
88
119
  ```
89
120
 
90
- This means that events are essentially just an RID, manifest, or bundle with an event type attached. Event types can be one of `FORGET`, `UPDATE`, or `NEW` forming the "FUN" acronym. While these types roughly correspond to delete, update, and create from CRUD operations, but they are not commands, they are signals. A node emits an event to indicate that its internal state has changed:
91
- - `NEW` - indicates an previously unknown RID was cached
92
- - `UPDATE` - indicates a previously known RID was cached
93
- - `FORGET` - indicates a previously known RID was deleted
121
+ This means that events are essentially just an RID, manifest, or bundle with an event type attached. Event types can be one of `"FORGET"`, `"UPDATE"`, or `"NEW"` forming the "FUN" acronym. While these types roughly correspond to delete, update, and create from CRUD operations, but they are not commands, they are signals. A node emits an event to indicate that its internal state has changed:
122
+ - `"NEW"` - indicates an previously unknown RID was cached
123
+ - `"UPDATE"` - indicates a previously known RID was cached
124
+ - `"FORGET"` - indicates a previously known RID was deleted
94
125
 
95
126
  Nodes may broadcast events to other nodes to indicate their internal state changed. Conversely, nodes may also listen to events from other nodes and as a result decide to change their internal state, take some other action, or do nothing.
96
127
 
@@ -149,12 +180,14 @@ node = NodeInterface(
149
180
  )
150
181
  ```
151
182
 
183
+ When creating a node, you optionally enable `use_kobj_processor_thread` which will run the knowledge processing pipeline on a separate thread. This thread will automatically dequeue and process knowledge objects as they are added to the `kobj_queue`, which happenes when you call `node.process.handle(...)`. This is required to prevent race conditions in asynchronous applications, like web servers, therefore it is recommended to enable this feature for all full nodes.
184
+
152
185
  ## Knowledge Processing
153
186
 
154
187
  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.
155
188
 
156
189
  ### Partial Node
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.
190
+ Make sure to set `source=KnowledgeSource.External` when calling `handle` on external knowledge, this indicates to the knowledge processing pipeline that the incoming knowledge was received from another node. Where the knowledge is sourced from will impact decisions in the node's knowledge handlers.
158
191
  ```python
159
192
  import time
160
193
  from koi_net.processor.knowledge_object import KnowledgeSource
@@ -252,6 +285,147 @@ python -m examples.basic_coordinator_node
252
285
  python -m examples.basic_partial_node
253
286
  ```
254
287
 
288
+ # Advanced
289
+
290
+ ## Knowledge Processing Pipeline
291
+ Beyond the `NodeInterface` setup and boiler plate for partial/full nodes, node behavior is mostly controlled through the use of knowledge handlers. Effectively creating your own handlers relies on a solid understanding of the knowledge processing pipeline, so we'll start with that. As a developer, you will interface with the pipeline through the `ProcessorInterface` accessed with `node.processor`. The pipeline handles knowledge objects, from the `KnowledgeObject` class, a container for all knowledge types in the RID / KOI-net ecosystem:
292
+ - RIDs
293
+ - Manifests
294
+ - Bundles
295
+ - Events
296
+
297
+ Here is the class definition for a knowledge object:
298
+ ```python
299
+ type KnowledgeEventType = EventType | None
300
+
301
+ class KnowledgeSource(StrEnum):
302
+ Internal = "INTERNAL"
303
+ External = "EXTERNAL"
304
+
305
+ class KnowledgeObject(BaseModel):
306
+ rid: RID
307
+ manifest: Manifest | None = None
308
+ contents: dict | None = None
309
+ event_type: KnowledgeEventType = None
310
+ source: KnowledgeSource
311
+ normalized_event_type: KnowledgeEventType = None
312
+ network_targets: set[KoiNetNode] = set()
313
+ ```
314
+
315
+ In addition to the fields required to represent the knowledge types (`rid`, `manifest`, `contents`, `event_type`), knowledge objects also include a `source` field, indicating whether the knowledge originated from within the node (`KnowledgeSource.Internal`) or from another node (`KnowledgeSource.External`).
316
+
317
+ The final two fields are not inputs, but are set by handlers as the knowledge object moves through the processing pipeline. The normalized event type indicates the event type normalized to the perspective of the node's cache, and the network targets indicate where the resulting event should be broadcasted to.
318
+
319
+ Knowledge objects enter the processing pipeline through the `node.processor.handle(...)` method. Using kwargs you can pass any of the knowledge types listed above, a knowledge source, and an optional `event_type` (for non-event knowledge types). The handle function will simply normalize the provided knowledge type into a knowledge object, and put it in the `kobj_queue`, an internal, thread-safe queue of knowledge objects. If you have enabled `use_kobj_processor_thread` then the queue will be automatically processed on the processor thread, otherwise you will need to regularly call `flush_kobj_queue` to process queued knowledge objects (as in the partial node example). Both methods will process knowledge objects sequentially, in the order that they were queued in (FIFO).
320
+
321
+
322
+ ## Knowledge Handlers
323
+
324
+ Processing happens through five distinct phases, corresponding to the handler types: `RID`, `Manifest`, `Bundle`, `Network`, and `Final`. Each handler type can be understood by describing (1) what knowledge object fields are available to the handler, and (2) what action takes place after this phase, which the handler can influence. As knowledge objects pass through the pipeline, fields may be added or updated.
325
+
326
+ Handlers are registered in a single handler array within the processor. There is no limit to the number of handlers in use, and multiple handlers can be assigned to the same handler type. At each phase of knowledge processing, we will chain together all of the handlers of the corresponding type and run them in their array order. The order handlers are registered in matters!
327
+
328
+ Each handler will be passed a knowledge object. They can choose to return one of three types: `None`, `KnowledgeObject`, or `STOP_CHAIN`. Returning `None` will pass the unmodified knowledge object (the same one the handler received) to the next handler in the chain. If a handler modified their knowledge object, they should return it to pass the new version to the next handler. Finally, a handler can return `STOP_CHAIN` to immediately stop processing the knowledge object. No further handlers will be called and it will not enter the next phase of processing.
329
+
330
+ Summary of processing pipeline:
331
+ ```
332
+ RID -> Manifest -> Bundle -> [cache action] -> Network -> [network action] -> Final
333
+ |
334
+ (skip if event type is "FORGET")
335
+ ```
336
+
337
+ ### RID Handler
338
+ The knowledge object passed to handlers of this type are guaranteed to have an RID and knowledge source field. This handler type acts as a filter, if none of the handlers return `STOP_CHAIN` the pipeline will progress to the next phase. The pipeline diverges slightly after this handler chain, based on the event type of the knowledge object.
339
+
340
+ If the event type is `"NEW"`, `"UPDATE"`, or `None` and the manifest is not already in the knowledge object, the node will attempt to retrieve it from (1) the local cache if the source is internal, or (2) from another node if the source is external. If it fails to retrieves the manifest, the pipeline will end. Next, the manifest handler chain will be called.
341
+
342
+ If the event type is `"FORGET"`, and the bundle (manifest + contents) is not already in the knowledge object, the node will attempt to retrieve it from the local cache, regardless of the source. In this case the knowledge object represents what we will delete from the cache, not new incoming knowledge. If it fails to retrieve the bundle, the pipeline will end. Next, the bundle handler chain will be called.
343
+
344
+ ### Manifest Handler
345
+ The knowledge object passed to handlers of this type are guaranteed to have an RID, manifest, and knowledge source field. This handler type acts as a filter, if none of the handlers return `STOP_CHAIN` the pipeline will progress to the next phase.
346
+
347
+ If the bundle (manifest + contents) is not already in the knowledge object, the node will attempt to retrieve it from (1) the local cache if the source is internal, or (2) from another node if the source is external. If it fails to retrieve the bundle, the pipeline will end. Next, the bundle handler chain will be called.
348
+
349
+ ### Bundle Handler
350
+ The knowledge object passed to handlers of this type are guaranteed to have an RID, manifest, bundle (manifest + contents), and knowledge source field. This handler type acts as a decider. In this phase, the knowledge object's normalized event type must be set to `"NEW"` or `"UPDATE"` to write it to cache, or `"FORGET"` to delete it from the cache. If the normalized event type remains unset (`None`), or a handler returns `STOP_CHAIN`, then the pipeline will end without taking any cache action.
351
+
352
+ The cache action will take place after the handler chain ends, so if multiple handlers set a normalized event type, the final handler will take precedence.
353
+
354
+ ### Network Handler
355
+ The knowledge object passed to handlers of this type are guaranteed to have an RID, manifest, bundle (manifest + contents), normalized event type, and knowledge source field. This handler type acts as a decider. In this phase, handlers decide which nodes to broadcast this knowledge object to by appending KOI-net node RIDs to the knowledge object's `network_targets` field. If a handler returns `STOP_CHAIN`, the pipeline will end without taking any network action.
356
+
357
+ The network action will take place after the handler chain ends. The node will attempt to broadcast a "normalized event", created from the knowledge object's RID, bundle, and normalized event type, to all of the node's in the network targets array.
358
+
359
+ ### Final Handler
360
+ The knowledge object passed to handlers of this type are guaranteed to have an RID, manifest, bundle (manifest + contents), normalized event type, and knowledge source field.
361
+
362
+ This is the final handler chain that is called, it doesn't make any decisions or filter for succesive handler types. Handlers here can be useful if you want to take some action after the network broadcast has ended.
363
+
364
+ ## Registering Handlers
365
+ Knowledge handlers are registered with a node's processor by decorating a handler function. There are two types of decorators, the first way converts the function into a handler object which can be manually added to a processor. This is how the default handlers are defined, and makes them more portable (could be imported from another package). The second automatically registers a handler with your node instance. This is not portable but more convenient. The input of the decorated function will be the processor instance, and a knowledge object.
366
+
367
+ ```python
368
+ from .handler import KnowledgeHandler, HandlerType, STOP_CHAIN
369
+
370
+ @KnowledgeHandler.create(HandlerType.RID)
371
+ def example_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
372
+ ...
373
+
374
+ @node.processor.register_handler(HandlerType.RID)
375
+ def example_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
376
+ ...
377
+ ```
378
+
379
+ While handler's only require specifying the handler type, you can also specify the RID types, knowledge source, or event types you want to handle. If a knowledge object doesn't match all of the specified parameters, it won't be called. By default, handlers will match all RID types, all event types, and both internal and external sourced knowledge.
380
+
381
+ ```python
382
+ @KnowledgeHandler.create(
383
+ handler_type=HandlerType.Bundle,
384
+ rid_types=[KoiNetEdge],
385
+ source=KnowledgeSource.External,
386
+ event_types=[EventType.NEW, EventType.UPDATE])
387
+ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
388
+ ...
389
+ ```
390
+
391
+ The processor instance passed to your function should be used to take any necessary node actions (cache, network, etc.). It is also sometimes useful to add new knowledge objects to the queue while processing a different knowledge object. You can simply call `processor.handle(...)` in the same way as you would outside of a handler. It will put at the end of the queue and processed when it is dequeued like any other knowledge object.
392
+
393
+
394
+ ## Default Behavior
395
+
396
+ The default configuration provides four default handlers which will take precedence over any handlers you add yourself. To override this behavior, you can set the `handlers` field in the `NodeInterface`:
397
+
398
+ ```python
399
+ from koi_net import NodeInterface
400
+ from koi_net.protocol.node import NodeProfile, NodeProvides, NodeType
401
+ from koi_net.processor.default_handlers import (
402
+ basic_rid_handler,
403
+ basic_manifest_handler,
404
+ edge_negotiation_handler,
405
+ basic_network_output_filter
406
+ )
407
+
408
+ node = NodeInterface(
409
+ name="mypartialnode",
410
+ profile=NodeProfile(
411
+ node_type=NodeType.PARTIAL,
412
+ provides=NodeProvides(
413
+ event=[]
414
+ )
415
+ ),
416
+ handlers=[
417
+ basic_rid_handler,
418
+ basic_manifest_handler,
419
+ edge_negotiation_handler,
420
+ basic_network_output_filter
421
+
422
+ # include all or none of the default handlers
423
+ ]
424
+ )
425
+ ```
426
+
427
+ Take a look at `src/koi_net/processor/default_handlers.py` to see some more in depth examples and better understand the default node behavior.
428
+
255
429
  # Implementation Reference
256
430
  This section provides high level explanations of the Python implementation. More detailed explanations of methods can be found in the docstrings within the codebase itself.
257
431
 
@@ -464,14 +638,13 @@ class ProcessorInterface:
464
638
  event: Event | None = None,
465
639
  kobj: KnowledgeObject | None = None,
466
640
  event_type: KnowledgeEventType = None,
467
- source: KnowledgeSource = KnowledgeSource.Internal,
468
- flush: bool = False
641
+ source: KnowledgeSource = KnowledgeSource.Internal
469
642
  ): ...
470
643
  ```
471
644
 
472
645
  The `register_handler` method is a decorator which can wrap a function to create a new `KnowledgeHandler` and add it to the processing pipeline in a single step. The `add_handler` method adds an existing `KnowledgeHandler` to the processining pipeline.
473
646
 
474
- The most commonly used functions in this class are `handle` and `flush_kobj_queue`. The `handle` method can be called on RIDs, manifests, bundles, and events to convert them to normalized to `KnowledgeObject` instances which are then added to the processing queue. The `flush` flag can be set to `True` to immediately start processing, or `flush_kobj_queue` can be called after queueing multiple knowledge objects. When calling the `handle` method, knowledge objects are marked as internally source by default. If you are handling RIDs, manifests, bundles, or events sourced from other nodes, `source` should be set to `KnowledgeSource.External`.
647
+ The most commonly used functions in this class are `handle` and `flush_kobj_queue`. The `handle` method can be called on RIDs, manifests, bundles, and events to convert them to normalized to `KnowledgeObject` instances which are then added to the processing queue. If you have enabled `use_kobj_processor_thread` then the queue will be automatically processed, otherwise you will need to regularly call `flush_kobj_queue` to process queued knolwedge objects. When calling the `handle` method, knowledge objects are marked as internally source by default. If you are handling RIDs, manifests, bundles, or events sourced from other nodes, `source` should be set to `KnowledgeSource.External`.
475
648
 
476
649
  Here is an example of how an event polling loop would be implemented using the knowledge processing pipeline:
477
650
  ```python
@@ -511,5 +684,5 @@ python -m build
511
684
  ```
512
685
  Push new package build to PyPI:
513
686
  ```shell
514
- python -m twine upload dist/*
687
+ python -m twine upload --skip-existing dist/*
515
688
  ```
@@ -2,6 +2,37 @@
2
2
 
3
3
  *This specification is the result of several iterations of KOI research, [read more here](https://github.com/BlockScience/koi).*
4
4
 
5
+ ### Jump to Sections:
6
+ - [Protocol](#protocol)
7
+ - [Introduction](#introduction)
8
+ - [Communication Methods](#communication-methods)
9
+ - [Quickstart](#quickstart)
10
+ - [Setup](#setup)
11
+ - [Creating a Node](#creating-a-node)
12
+ - [Knowledge Processing](#knowledge-processing)
13
+ - [Try It Out!](#try-it-out)
14
+ - [Advanced](#advanced)
15
+ - [Knowledge Processing Pipeline](#knowledge-processing-pipeline)
16
+ - [Knowledge Handlers](#knowledge-handlers)
17
+ - [RID Handler](#rid-handler)
18
+ - [Manifest Handler](#manifest-handler)
19
+ - [Bundle Handler](#bundle-handler)
20
+ - [Network Handler](#network-handler)
21
+ - [Final Handler](#final-handler)
22
+ - [Registering Handlers](#registering-handlers)
23
+ - [Default Behavior](#default-behavior)
24
+ - [Implementation Reference](#implementation-reference)
25
+ - [Node Interface](#node-interface)
26
+ - [Node Identity](#node-identity)
27
+ - [Network Interface](#network-interface)
28
+ - [Network Graph](#network-graph)
29
+ - [Request Handler](#request-handler)
30
+ - [Response Handler](#response-handler)
31
+ - [Processor Interface](#processor-interface)
32
+ - [Development](#development)
33
+ - [Setup](#setup-1)
34
+ - [Distribution](#distribution)
35
+
5
36
  # Protocol
6
37
  ## Introduction
7
38
 
@@ -45,10 +76,10 @@ The request and payload JSON objects are composed of the fundamental "knowledge
45
76
  }
46
77
  ```
47
78
 
48
- This means that events are essentially just an RID, manifest, or bundle with an event type attached. Event types can be one of `FORGET`, `UPDATE`, or `NEW` forming the "FUN" acronym. While these types roughly correspond to delete, update, and create from CRUD operations, but they are not commands, they are signals. A node emits an event to indicate that its internal state has changed:
49
- - `NEW` - indicates an previously unknown RID was cached
50
- - `UPDATE` - indicates a previously known RID was cached
51
- - `FORGET` - indicates a previously known RID was deleted
79
+ This means that events are essentially just an RID, manifest, or bundle with an event type attached. Event types can be one of `"FORGET"`, `"UPDATE"`, or `"NEW"` forming the "FUN" acronym. While these types roughly correspond to delete, update, and create from CRUD operations, but they are not commands, they are signals. A node emits an event to indicate that its internal state has changed:
80
+ - `"NEW"` - indicates an previously unknown RID was cached
81
+ - `"UPDATE"` - indicates a previously known RID was cached
82
+ - `"FORGET"` - indicates a previously known RID was deleted
52
83
 
53
84
  Nodes may broadcast events to other nodes to indicate their internal state changed. Conversely, nodes may also listen to events from other nodes and as a result decide to change their internal state, take some other action, or do nothing.
54
85
 
@@ -107,12 +138,14 @@ node = NodeInterface(
107
138
  )
108
139
  ```
109
140
 
141
+ When creating a node, you optionally enable `use_kobj_processor_thread` which will run the knowledge processing pipeline on a separate thread. This thread will automatically dequeue and process knowledge objects as they are added to the `kobj_queue`, which happenes when you call `node.process.handle(...)`. This is required to prevent race conditions in asynchronous applications, like web servers, therefore it is recommended to enable this feature for all full nodes.
142
+
110
143
  ## Knowledge Processing
111
144
 
112
145
  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.
113
146
 
114
147
  ### Partial Node
115
- 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.
148
+ Make sure to set `source=KnowledgeSource.External` when calling `handle` on external knowledge, this indicates to the knowledge processing pipeline that the incoming knowledge was received from another node. Where the knowledge is sourced from will impact decisions in the node's knowledge handlers.
116
149
  ```python
117
150
  import time
118
151
  from koi_net.processor.knowledge_object import KnowledgeSource
@@ -210,6 +243,147 @@ python -m examples.basic_coordinator_node
210
243
  python -m examples.basic_partial_node
211
244
  ```
212
245
 
246
+ # Advanced
247
+
248
+ ## Knowledge Processing Pipeline
249
+ Beyond the `NodeInterface` setup and boiler plate for partial/full nodes, node behavior is mostly controlled through the use of knowledge handlers. Effectively creating your own handlers relies on a solid understanding of the knowledge processing pipeline, so we'll start with that. As a developer, you will interface with the pipeline through the `ProcessorInterface` accessed with `node.processor`. The pipeline handles knowledge objects, from the `KnowledgeObject` class, a container for all knowledge types in the RID / KOI-net ecosystem:
250
+ - RIDs
251
+ - Manifests
252
+ - Bundles
253
+ - Events
254
+
255
+ Here is the class definition for a knowledge object:
256
+ ```python
257
+ type KnowledgeEventType = EventType | None
258
+
259
+ class KnowledgeSource(StrEnum):
260
+ Internal = "INTERNAL"
261
+ External = "EXTERNAL"
262
+
263
+ class KnowledgeObject(BaseModel):
264
+ rid: RID
265
+ manifest: Manifest | None = None
266
+ contents: dict | None = None
267
+ event_type: KnowledgeEventType = None
268
+ source: KnowledgeSource
269
+ normalized_event_type: KnowledgeEventType = None
270
+ network_targets: set[KoiNetNode] = set()
271
+ ```
272
+
273
+ In addition to the fields required to represent the knowledge types (`rid`, `manifest`, `contents`, `event_type`), knowledge objects also include a `source` field, indicating whether the knowledge originated from within the node (`KnowledgeSource.Internal`) or from another node (`KnowledgeSource.External`).
274
+
275
+ The final two fields are not inputs, but are set by handlers as the knowledge object moves through the processing pipeline. The normalized event type indicates the event type normalized to the perspective of the node's cache, and the network targets indicate where the resulting event should be broadcasted to.
276
+
277
+ Knowledge objects enter the processing pipeline through the `node.processor.handle(...)` method. Using kwargs you can pass any of the knowledge types listed above, a knowledge source, and an optional `event_type` (for non-event knowledge types). The handle function will simply normalize the provided knowledge type into a knowledge object, and put it in the `kobj_queue`, an internal, thread-safe queue of knowledge objects. If you have enabled `use_kobj_processor_thread` then the queue will be automatically processed on the processor thread, otherwise you will need to regularly call `flush_kobj_queue` to process queued knowledge objects (as in the partial node example). Both methods will process knowledge objects sequentially, in the order that they were queued in (FIFO).
278
+
279
+
280
+ ## Knowledge Handlers
281
+
282
+ Processing happens through five distinct phases, corresponding to the handler types: `RID`, `Manifest`, `Bundle`, `Network`, and `Final`. Each handler type can be understood by describing (1) what knowledge object fields are available to the handler, and (2) what action takes place after this phase, which the handler can influence. As knowledge objects pass through the pipeline, fields may be added or updated.
283
+
284
+ Handlers are registered in a single handler array within the processor. There is no limit to the number of handlers in use, and multiple handlers can be assigned to the same handler type. At each phase of knowledge processing, we will chain together all of the handlers of the corresponding type and run them in their array order. The order handlers are registered in matters!
285
+
286
+ Each handler will be passed a knowledge object. They can choose to return one of three types: `None`, `KnowledgeObject`, or `STOP_CHAIN`. Returning `None` will pass the unmodified knowledge object (the same one the handler received) to the next handler in the chain. If a handler modified their knowledge object, they should return it to pass the new version to the next handler. Finally, a handler can return `STOP_CHAIN` to immediately stop processing the knowledge object. No further handlers will be called and it will not enter the next phase of processing.
287
+
288
+ Summary of processing pipeline:
289
+ ```
290
+ RID -> Manifest -> Bundle -> [cache action] -> Network -> [network action] -> Final
291
+ |
292
+ (skip if event type is "FORGET")
293
+ ```
294
+
295
+ ### RID Handler
296
+ The knowledge object passed to handlers of this type are guaranteed to have an RID and knowledge source field. This handler type acts as a filter, if none of the handlers return `STOP_CHAIN` the pipeline will progress to the next phase. The pipeline diverges slightly after this handler chain, based on the event type of the knowledge object.
297
+
298
+ If the event type is `"NEW"`, `"UPDATE"`, or `None` and the manifest is not already in the knowledge object, the node will attempt to retrieve it from (1) the local cache if the source is internal, or (2) from another node if the source is external. If it fails to retrieves the manifest, the pipeline will end. Next, the manifest handler chain will be called.
299
+
300
+ If the event type is `"FORGET"`, and the bundle (manifest + contents) is not already in the knowledge object, the node will attempt to retrieve it from the local cache, regardless of the source. In this case the knowledge object represents what we will delete from the cache, not new incoming knowledge. If it fails to retrieve the bundle, the pipeline will end. Next, the bundle handler chain will be called.
301
+
302
+ ### Manifest Handler
303
+ The knowledge object passed to handlers of this type are guaranteed to have an RID, manifest, and knowledge source field. This handler type acts as a filter, if none of the handlers return `STOP_CHAIN` the pipeline will progress to the next phase.
304
+
305
+ If the bundle (manifest + contents) is not already in the knowledge object, the node will attempt to retrieve it from (1) the local cache if the source is internal, or (2) from another node if the source is external. If it fails to retrieve the bundle, the pipeline will end. Next, the bundle handler chain will be called.
306
+
307
+ ### Bundle Handler
308
+ The knowledge object passed to handlers of this type are guaranteed to have an RID, manifest, bundle (manifest + contents), and knowledge source field. This handler type acts as a decider. In this phase, the knowledge object's normalized event type must be set to `"NEW"` or `"UPDATE"` to write it to cache, or `"FORGET"` to delete it from the cache. If the normalized event type remains unset (`None`), or a handler returns `STOP_CHAIN`, then the pipeline will end without taking any cache action.
309
+
310
+ The cache action will take place after the handler chain ends, so if multiple handlers set a normalized event type, the final handler will take precedence.
311
+
312
+ ### Network Handler
313
+ The knowledge object passed to handlers of this type are guaranteed to have an RID, manifest, bundle (manifest + contents), normalized event type, and knowledge source field. This handler type acts as a decider. In this phase, handlers decide which nodes to broadcast this knowledge object to by appending KOI-net node RIDs to the knowledge object's `network_targets` field. If a handler returns `STOP_CHAIN`, the pipeline will end without taking any network action.
314
+
315
+ The network action will take place after the handler chain ends. The node will attempt to broadcast a "normalized event", created from the knowledge object's RID, bundle, and normalized event type, to all of the node's in the network targets array.
316
+
317
+ ### Final Handler
318
+ The knowledge object passed to handlers of this type are guaranteed to have an RID, manifest, bundle (manifest + contents), normalized event type, and knowledge source field.
319
+
320
+ This is the final handler chain that is called, it doesn't make any decisions or filter for succesive handler types. Handlers here can be useful if you want to take some action after the network broadcast has ended.
321
+
322
+ ## Registering Handlers
323
+ Knowledge handlers are registered with a node's processor by decorating a handler function. There are two types of decorators, the first way converts the function into a handler object which can be manually added to a processor. This is how the default handlers are defined, and makes them more portable (could be imported from another package). The second automatically registers a handler with your node instance. This is not portable but more convenient. The input of the decorated function will be the processor instance, and a knowledge object.
324
+
325
+ ```python
326
+ from .handler import KnowledgeHandler, HandlerType, STOP_CHAIN
327
+
328
+ @KnowledgeHandler.create(HandlerType.RID)
329
+ def example_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
330
+ ...
331
+
332
+ @node.processor.register_handler(HandlerType.RID)
333
+ def example_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
334
+ ...
335
+ ```
336
+
337
+ While handler's only require specifying the handler type, you can also specify the RID types, knowledge source, or event types you want to handle. If a knowledge object doesn't match all of the specified parameters, it won't be called. By default, handlers will match all RID types, all event types, and both internal and external sourced knowledge.
338
+
339
+ ```python
340
+ @KnowledgeHandler.create(
341
+ handler_type=HandlerType.Bundle,
342
+ rid_types=[KoiNetEdge],
343
+ source=KnowledgeSource.External,
344
+ event_types=[EventType.NEW, EventType.UPDATE])
345
+ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
346
+ ...
347
+ ```
348
+
349
+ The processor instance passed to your function should be used to take any necessary node actions (cache, network, etc.). It is also sometimes useful to add new knowledge objects to the queue while processing a different knowledge object. You can simply call `processor.handle(...)` in the same way as you would outside of a handler. It will put at the end of the queue and processed when it is dequeued like any other knowledge object.
350
+
351
+
352
+ ## Default Behavior
353
+
354
+ The default configuration provides four default handlers which will take precedence over any handlers you add yourself. To override this behavior, you can set the `handlers` field in the `NodeInterface`:
355
+
356
+ ```python
357
+ from koi_net import NodeInterface
358
+ from koi_net.protocol.node import NodeProfile, NodeProvides, NodeType
359
+ from koi_net.processor.default_handlers import (
360
+ basic_rid_handler,
361
+ basic_manifest_handler,
362
+ edge_negotiation_handler,
363
+ basic_network_output_filter
364
+ )
365
+
366
+ node = NodeInterface(
367
+ name="mypartialnode",
368
+ profile=NodeProfile(
369
+ node_type=NodeType.PARTIAL,
370
+ provides=NodeProvides(
371
+ event=[]
372
+ )
373
+ ),
374
+ handlers=[
375
+ basic_rid_handler,
376
+ basic_manifest_handler,
377
+ edge_negotiation_handler,
378
+ basic_network_output_filter
379
+
380
+ # include all or none of the default handlers
381
+ ]
382
+ )
383
+ ```
384
+
385
+ Take a look at `src/koi_net/processor/default_handlers.py` to see some more in depth examples and better understand the default node behavior.
386
+
213
387
  # Implementation Reference
214
388
  This section provides high level explanations of the Python implementation. More detailed explanations of methods can be found in the docstrings within the codebase itself.
215
389
 
@@ -422,14 +596,13 @@ class ProcessorInterface:
422
596
  event: Event | None = None,
423
597
  kobj: KnowledgeObject | None = None,
424
598
  event_type: KnowledgeEventType = None,
425
- source: KnowledgeSource = KnowledgeSource.Internal,
426
- flush: bool = False
599
+ source: KnowledgeSource = KnowledgeSource.Internal
427
600
  ): ...
428
601
  ```
429
602
 
430
603
  The `register_handler` method is a decorator which can wrap a function to create a new `KnowledgeHandler` and add it to the processing pipeline in a single step. The `add_handler` method adds an existing `KnowledgeHandler` to the processining pipeline.
431
604
 
432
- The most commonly used functions in this class are `handle` and `flush_kobj_queue`. The `handle` method can be called on RIDs, manifests, bundles, and events to convert them to normalized to `KnowledgeObject` instances which are then added to the processing queue. The `flush` flag can be set to `True` to immediately start processing, or `flush_kobj_queue` can be called after queueing multiple knowledge objects. When calling the `handle` method, knowledge objects are marked as internally source by default. If you are handling RIDs, manifests, bundles, or events sourced from other nodes, `source` should be set to `KnowledgeSource.External`.
605
+ The most commonly used functions in this class are `handle` and `flush_kobj_queue`. The `handle` method can be called on RIDs, manifests, bundles, and events to convert them to normalized to `KnowledgeObject` instances which are then added to the processing queue. If you have enabled `use_kobj_processor_thread` then the queue will be automatically processed, otherwise you will need to regularly call `flush_kobj_queue` to process queued knolwedge objects. When calling the `handle` method, knowledge objects are marked as internally source by default. If you are handling RIDs, manifests, bundles, or events sourced from other nodes, `source` should be set to `KnowledgeSource.External`.
433
606
 
434
607
  Here is an example of how an event polling loop would be implemented using the knowledge processing pipeline:
435
608
  ```python
@@ -469,5 +642,5 @@ python -m build
469
642
  ```
470
643
  Push new package build to PyPI:
471
644
  ```shell
472
- python -m twine upload dist/*
645
+ python -m twine upload --skip-existing dist/*
473
646
  ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "koi-net"
7
- version = "1.0.0-beta.8"
7
+ version = "1.0.0-beta.10"
8
8
  description = "Implementation of KOI-net protocol in Python"
9
9
  authors = [
10
10
  {name = "Luke Miller", email = "luke@block.science"}
@@ -84,15 +84,15 @@ class NodeInterface:
84
84
  )
85
85
  )
86
86
 
87
- logger.info("Waiting for kobj queue to empty")
87
+ logger.debug("Waiting for kobj queue to empty")
88
88
  if self.use_kobj_processor_thread:
89
89
  self.processor.kobj_queue.join()
90
90
  else:
91
91
  self.processor.flush_kobj_queue()
92
- logger.info("Done")
92
+ logger.debug("Done")
93
93
 
94
94
  if not self.network.graph.get_neighbors() and self.first_contact:
95
- logger.info(f"I don't have any neighbors, reaching out to first contact {self.first_contact}")
95
+ logger.debug(f"I don't have any neighbors, reaching out to first contact {self.first_contact}")
96
96
 
97
97
  events = [
98
98
  Event.from_rid(EventType.FORGET, self.identity.rid),
@@ -106,7 +106,7 @@ class NodeInterface:
106
106
  )
107
107
 
108
108
  except httpx.ConnectError:
109
- logger.info("Failed to reach first contact")
109
+ logger.warning("Failed to reach first contact")
110
110
  return
111
111
 
112
112
 
@@ -118,7 +118,7 @@ class NodeInterface:
118
118
  logger.info("Stopping node...")
119
119
 
120
120
  if self.use_kobj_processor_thread:
121
- logger.info("Waiting for kobj queue to empty")
121
+ logger.info(f"Waiting for kobj queue to empty ({self.processor.kobj_queue.unfinished_tasks} tasks remaining)")
122
122
  self.processor.kobj_queue.join()
123
123
  else:
124
124
  self.processor.flush_kobj_queue()
@@ -25,12 +25,12 @@ class NetworkGraph:
25
25
 
26
26
  def generate(self):
27
27
  """Generates directed graph from cached KOI nodes and edges."""
28
- logger.info("Generating network graph")
28
+ logger.debug("Generating network graph")
29
29
  self.dg.clear()
30
30
  for rid in self.cache.list_rids():
31
31
  if type(rid) == KoiNetNode:
32
32
  self.dg.add_node(rid)
33
- logger.info(f"Added node {rid}")
33
+ logger.debug(f"Added node {rid}")
34
34
 
35
35
  elif type(rid) == KoiNetEdge:
36
36
  edge_profile = self.get_edge_profile(rid)
@@ -38,8 +38,8 @@ class NetworkGraph:
38
38
  logger.warning(f"Failed to load {rid!r}")
39
39
  continue
40
40
  self.dg.add_edge(edge_profile.source, edge_profile.target, rid=rid)
41
- logger.info(f"Added edge {rid} ({edge_profile.source} -> {edge_profile.target})")
42
- logger.info("Done")
41
+ logger.debug(f"Added edge {rid} ({edge_profile.source} -> {edge_profile.target})")
42
+ logger.debug("Done")
43
43
 
44
44
  def get_node_profile(self, rid: KoiNetNode) -> NodeProfile | None:
45
45
  """Returns node profile given its RID."""