koi-net 1.0.0b1__py3-none-any.whl → 1.0.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.

koi_net/core.py CHANGED
@@ -3,7 +3,7 @@ import httpx
3
3
  from rid_lib.ext import Cache, Bundle
4
4
  from .network import NetworkInterface
5
5
  from .processor import ProcessorInterface
6
- from .processor import default_handlers as _default_handlers
6
+ from .processor import default_handlers
7
7
  from .processor.handler import KnowledgeHandler
8
8
  from .identity import NodeIdentity
9
9
  from .protocol.node import NodeProfile
@@ -12,13 +12,19 @@ from .protocol.event import Event, EventType
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
14
  class NodeInterface:
15
+ cache: Cache
16
+ identity: NodeIdentity
17
+ network: NetworkInterface
18
+ processor: ProcessorInterface
19
+ first_contact: str
20
+
15
21
  def __init__(
16
22
  self,
17
23
  name: str,
18
24
  profile: NodeProfile,
19
25
  identity_file_path: str = "identity.json",
20
26
  first_contact: str | None = None,
21
- default_handlers: list[KnowledgeHandler] | None = None,
27
+ handlers: list[KnowledgeHandler] | None = None,
22
28
  cache: Cache | None = None,
23
29
  network: NetworkInterface | None = None,
24
30
  processor: ProcessorInterface | None = None
@@ -39,9 +45,9 @@ class NodeInterface:
39
45
  )
40
46
 
41
47
  # pull all handlers defined in default_handlers module
42
- if not default_handlers:
43
- default_handlers = [
44
- obj for obj in vars(_default_handlers).values()
48
+ if not handlers:
49
+ handlers = [
50
+ obj for obj in vars(default_handlers).values()
45
51
  if isinstance(obj, KnowledgeHandler)
46
52
  ]
47
53
 
@@ -49,10 +55,16 @@ class NodeInterface:
49
55
  cache=self.cache,
50
56
  network=self.network,
51
57
  identity=self.identity,
52
- default_handlers=default_handlers
58
+ default_handlers=handlers
53
59
  )
54
60
 
55
- def initialize(self):
61
+ def initialize(self) -> None:
62
+ """Initializes node, call on startup.
63
+
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.
65
+ """
66
+ self.network._load_event_queues()
67
+
56
68
  self.network.graph.generate()
57
69
 
58
70
  self.processor.handle(
@@ -83,4 +95,8 @@ class NodeInterface:
83
95
 
84
96
 
85
97
  def finalize(self):
86
- self.network.save_event_queues()
98
+ """Finalizes node, call on shutdown.
99
+
100
+ Saves event queues to storage.
101
+ """
102
+ self.network._save_event_queues()
koi_net/identity.py CHANGED
@@ -13,6 +13,8 @@ class NodeIdentityModel(BaseModel):
13
13
  profile: NodeProfile
14
14
 
15
15
  class NodeIdentity:
16
+ """Represents a node's identity (RID, profile, bundle)."""
17
+
16
18
  _identity: NodeIdentityModel
17
19
  file_path: str
18
20
  cache: Cache
@@ -24,6 +26,12 @@ class NodeIdentity:
24
26
  cache: Cache,
25
27
  file_path: str = "identity.json"
26
28
  ):
29
+ """Initializes node identity from a name and profile.
30
+
31
+ Attempts to read identity from storage. If it doesn't already exist, a new RID is generated from the provided name, and that RID and profile are written to storage. Changes to the name or profile will update the stored identity.
32
+
33
+ WARNING: If the name is changed, the RID will be overwritten which will have consequences for the rest of the network.
34
+ """
27
35
  self.cache = cache
28
36
  self.file_path = file_path
29
37
 
koi_net/network/graph.py CHANGED
@@ -12,12 +12,15 @@ logger = logging.getLogger(__name__)
12
12
 
13
13
 
14
14
  class NetworkGraph:
15
+ """Graph functions for this node's view of its network."""
16
+
15
17
  def __init__(self, cache: Cache, identity: NodeIdentity):
16
18
  self.cache = cache
17
19
  self.dg = nx.DiGraph()
18
20
  self.identity = identity
19
21
 
20
22
  def generate(self):
23
+ """Generates directed graph from cached KOI nodes and edges."""
21
24
  logger.info("Generating network graph")
22
25
  self.dg.clear()
23
26
  for rid in self.cache.list_rids():
@@ -35,6 +38,7 @@ class NetworkGraph:
35
38
  logger.info("Done")
36
39
 
37
40
  def get_node_profile(self, rid: KoiNetNode) -> NodeProfile | None:
41
+ """Returns node profile given its RID."""
38
42
  bundle = self.cache.read(rid)
39
43
  if bundle:
40
44
  return bundle.validate_contents(NodeProfile)
@@ -45,6 +49,7 @@ class NetworkGraph:
45
49
  source: KoiNetNode | None = None,
46
50
  target: KoiNetNode | None = None,
47
51
  ) -> EdgeProfile | None:
52
+ """Returns edge profile given its RID, or source and target node RIDs."""
48
53
  if source and target:
49
54
  if (source, target) not in self.dg.edges: return
50
55
  edge_data = self.dg.get_edge_data(source, target)
@@ -62,6 +67,9 @@ class NetworkGraph:
62
67
  self,
63
68
  direction: Literal["in", "out"] | None = None,
64
69
  ) -> list[KoiNetEdge]:
70
+ """Returns edges this node belongs to.
71
+
72
+ All edges returned by default, specify `direction` to restrict to incoming or outgoing edges only."""
65
73
 
66
74
  edges = []
67
75
  if direction != "in":
@@ -88,6 +96,9 @@ class NetworkGraph:
88
96
  status: EdgeStatus | None = None,
89
97
  allowed_type: RIDType | None = None
90
98
  ) -> list[KoiNetNode]:
99
+ """Returns neighboring nodes this node shares an edge with.
100
+
101
+ All neighboring nodes returned by default, specify `direction` to restrict to neighbors connected by incoming or outgoing edges only."""
91
102
 
92
103
  neighbors = []
93
104
  for edge_rid in self.get_edges(direction):
@@ -24,6 +24,8 @@ class EventQueueModel(BaseModel):
24
24
  type EventQueue = dict[RID, Queue[Event]]
25
25
 
26
26
  class NetworkInterface:
27
+ """A collection of functions and classes to interact with the KOI network."""
28
+
27
29
  identity: NodeIdentity
28
30
  cache: Cache
29
31
  first_contact: str | None
@@ -45,15 +47,16 @@ class NetworkInterface:
45
47
  self.cache = cache
46
48
  self.first_contact = first_contact
47
49
  self.graph = NetworkGraph(cache, identity)
48
- self.request_handler = RequestHandler(cache)
50
+ self.request_handler = RequestHandler(cache, self.graph)
49
51
  self.response_handler = ResponseHandler(cache)
50
52
  self.event_queues_file_path = file_path
51
53
 
52
54
  self.poll_event_queue = dict()
53
55
  self.webhook_event_queue = dict()
54
- self.load_event_queues()
56
+ self._load_event_queues()
55
57
 
56
- def load_event_queues(self):
58
+ def _load_event_queues(self):
59
+ """Loads event queues from storage."""
57
60
  try:
58
61
  with open(self.event_queues_file_path, "r") as f:
59
62
  queues = EventQueueModel.model_validate_json(f.read())
@@ -71,7 +74,8 @@ class NetworkInterface:
71
74
  except FileNotFoundError:
72
75
  return
73
76
 
74
- def save_event_queues(self):
77
+ def _save_event_queues(self):
78
+ """Writes event queues to storage."""
75
79
  events_model = EventQueueModel(
76
80
  poll={
77
81
  node: list(queue.queue)
@@ -92,6 +96,10 @@ class NetworkInterface:
92
96
  f.write(events_model.model_dump_json(indent=2))
93
97
 
94
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
+ """
95
103
  logger.info(f"Pushing event {event.event_type} {event.rid} to {node}")
96
104
 
97
105
  node_profile = self.graph.get_node_profile(node)
@@ -121,7 +129,8 @@ class NetworkInterface:
121
129
  if flush and event_queue is self.webhook_event_queue:
122
130
  self.flush_webhook_queue(node)
123
131
 
124
- def flush_queue(self, event_queue: EventQueue, node: KoiNetNode) -> list[Event]:
132
+ def _flush_queue(self, event_queue: EventQueue, node: KoiNetNode) -> list[Event]:
133
+ """Flushes a node's queue, returning list of events."""
125
134
  queue = event_queue.get(node)
126
135
  events = list()
127
136
  if queue:
@@ -133,22 +142,29 @@ class NetworkInterface:
133
142
  return events
134
143
 
135
144
  def flush_poll_queue(self, node: KoiNetNode) -> list[Event]:
145
+ """Flushes a node's poll queue, returning list of events."""
136
146
  logger.info(f"Flushing poll queue for {node}")
137
- return self.flush_queue(self.poll_event_queue, node)
147
+ return self._flush_queue(self.poll_event_queue, node)
138
148
 
139
- def flush_webhook_queue(self, node: RID):
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
+
140
155
  logger.info(f"Flushing webhook queue for {node}")
141
156
 
142
157
  node_profile = self.graph.get_node_profile(node)
143
158
 
144
159
  if not node_profile:
145
160
  logger.warning(f"{node!r} not found")
161
+ return
146
162
 
147
163
  if node_profile.node_type != NodeType.FULL:
148
164
  logger.warning(f"{node!r} is a partial node!")
149
165
  return
150
166
 
151
- events = self.flush_queue(self.webhook_event_queue, node)
167
+ events = self._flush_queue(self.webhook_event_queue, node)
152
168
  logger.info(f"Broadcasting {len(events)} events")
153
169
 
154
170
  try:
@@ -159,10 +175,13 @@ class NetworkInterface:
159
175
  self.push_event_to(event, node)
160
176
 
161
177
  def flush_all_webhook_queues(self):
178
+ """Flushes all nodes' webhook queues and broadcasts events."""
162
179
  for node in self.webhook_event_queue.keys():
163
180
  self.flush_webhook_queue(node)
164
181
 
165
- def get_state_providers(self, rid_type: RIDType):
182
+ def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
183
+ """Returns list of node RIDs which provide state for the specified RID type."""
184
+
166
185
  logger.info(f"Looking for state providers of '{rid_type}'")
167
186
  provider_nodes = []
168
187
  for node_rid in self.cache.list_rids(rid_types=[KoiNetNode]):
@@ -177,6 +196,8 @@ class NetworkInterface:
177
196
  return provider_nodes
178
197
 
179
198
  def fetch_remote_bundle(self, rid: RID):
199
+ """Attempts to fetch a bundle by RID from known peer nodes."""
200
+
180
201
  logger.info(f"Fetching remote bundle '{rid}'")
181
202
  remote_bundle = None
182
203
  for node_rid in self.get_state_providers(type(rid)):
@@ -194,6 +215,8 @@ class NetworkInterface:
194
215
  return remote_bundle
195
216
 
196
217
  def fetch_remote_manifest(self, rid: RID):
218
+ """Attempts to fetch a manifest by RID from known peer nodes."""
219
+
197
220
  logger.info(f"Fetching remote manifest '{rid}'")
198
221
  remote_manifest = None
199
222
  for node_rid in self.get_state_providers(type(rid)):
@@ -211,9 +234,14 @@ class NetworkInterface:
211
234
  return remote_manifest
212
235
 
213
236
  def poll_neighbors(self) -> list[Event]:
237
+ """Polls all neighboring nodes and returns compiled list of events.
238
+
239
+ If this node has no neighbors, it will instead attempt to poll the provided first contact URL.
240
+ """
241
+
214
242
  neighbors = self.graph.get_neighbors()
215
243
 
216
- if not neighbors:
244
+ if not neighbors and self.first_contact:
217
245
  logger.info("No neighbors found, polling first contact")
218
246
  try:
219
247
  payload = self.request_handler.poll_events(
@@ -21,17 +21,22 @@ from ..protocol.consts import (
21
21
  FETCH_MANIFESTS_PATH,
22
22
  FETCH_BUNDLES_PATH
23
23
  )
24
- from ..protocol.node import NodeProfile, NodeType
24
+ from ..protocol.node import NodeType
25
+ from .graph import NetworkGraph
25
26
 
26
27
 
27
28
  logger = logging.getLogger(__name__)
28
29
 
29
30
 
30
31
  class RequestHandler:
32
+ """Handles making requests to other KOI nodes."""
33
+
31
34
  cache: Cache
35
+ graph: NetworkGraph
32
36
 
33
- def __init__(self, cache: Cache):
37
+ def __init__(self, cache: Cache, graph: NetworkGraph):
34
38
  self.cache = cache
39
+ self.graph = graph
35
40
 
36
41
  def make_request(self, url, request: BaseModel) -> httpx.Response:
37
42
  logger.info(f"Making request to {url}")
@@ -42,23 +47,26 @@ class RequestHandler:
42
47
  return resp
43
48
 
44
49
  def get_url(self, node_rid: KoiNetNode, url: str) -> str:
50
+ """Retrieves URL of a node, or returns provided URL."""
51
+
45
52
  if not node_rid and not url:
46
53
  raise ValueError("One of 'node_rid' and 'url' must be provided")
47
54
 
48
55
  if node_rid:
49
- # can't access get_node rn
50
- bundle = self.cache.read(node_rid)
51
- node = NodeProfile.model_validate(bundle.contents)
52
- if node.node_type != NodeType.FULL:
56
+ node_profile = self.graph.get_node_profile(node_rid)
57
+ if not node_profile:
58
+ raise Exception("Node not found")
59
+ if node_profile.node_type != NodeType.FULL:
53
60
  raise Exception("Can't query partial node")
54
- logger.info(f"Resolved {node_rid!r} to {node.base_url}")
55
- return node.base_url
61
+ logger.info(f"Resolved {node_rid!r} to {node_profile.base_url}")
62
+ return node_profile.base_url
56
63
  else:
57
64
  return url
58
65
 
59
66
  def broadcast_events(
60
67
  self, node: RID = None, url: str = None, **kwargs
61
68
  ) -> None:
69
+ """See protocol.api_models.EventsPayload for available kwargs."""
62
70
  self.make_request(
63
71
  self.get_url(node, url) + BROADCAST_EVENTS_PATH,
64
72
  EventsPayload.model_validate(kwargs)
@@ -66,7 +74,8 @@ class RequestHandler:
66
74
 
67
75
  def poll_events(
68
76
  self, node: RID = None, url: str = None, **kwargs
69
- ) -> EventsPayload:
77
+ ) -> EventsPayload:
78
+ """See protocol.api_models.PollEvents for available kwargs."""
70
79
  resp = self.make_request(
71
80
  self.get_url(node, url) + POLL_EVENTS_PATH,
72
81
  PollEvents.model_validate(kwargs)
@@ -76,7 +85,8 @@ class RequestHandler:
76
85
 
77
86
  def fetch_rids(
78
87
  self, node: RID = None, url: str = None, **kwargs
79
- ) -> RidsPayload:
88
+ ) -> RidsPayload:
89
+ """See protocol.api_models.FetchRids for available kwargs."""
80
90
  resp = self.make_request(
81
91
  self.get_url(node, url) + FETCH_RIDS_PATH,
82
92
  FetchRids.model_validate(kwargs)
@@ -86,7 +96,8 @@ class RequestHandler:
86
96
 
87
97
  def fetch_manifests(
88
98
  self, node: RID = None, url: str = None, **kwargs
89
- ) -> ManifestsPayload:
99
+ ) -> ManifestsPayload:
100
+ """See protocol.api_models.FetchManifests for available kwargs."""
90
101
  resp = self.make_request(
91
102
  self.get_url(node, url) + FETCH_MANIFESTS_PATH,
92
103
  FetchManifests.model_validate(kwargs)
@@ -96,7 +107,8 @@ class RequestHandler:
96
107
 
97
108
  def fetch_bundles(
98
109
  self, node: RID = None, url: str = None, **kwargs
99
- ) -> BundlesPayload:
110
+ ) -> BundlesPayload:
111
+ """See protocol.api_models.FetchBundles for available kwargs."""
100
112
  resp = self.make_request(
101
113
  self.get_url(node, url) + FETCH_BUNDLES_PATH,
102
114
  FetchBundles.model_validate(kwargs)
@@ -15,6 +15,8 @@ logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
17
  class ResponseHandler:
18
+ """Handles generating responses to requests from other KOI nodes."""
19
+
18
20
  cache: Cache
19
21
 
20
22
  def __init__(self, cache: Cache):
@@ -1,3 +1,5 @@
1
+ """Provides implementations of default knowledge handlers."""
2
+
1
3
  import logging
2
4
  from rid_lib.ext.bundle import Bundle
3
5
  from rid_lib.types import KoiNetNode, KoiNetEdge
@@ -14,6 +16,10 @@ logger = logging.getLogger(__name__)
14
16
 
15
17
  @ProcessorInterface.as_handler(handler_type=HandlerType.RID)
16
18
  def basic_rid_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
19
+ """Default RID handler.
20
+
21
+ Blocks external events about this node. Allows `FORGET` events if RID is known to this node.
22
+ """
17
23
  if (kobj.rid == processor.identity.rid and
18
24
  kobj.source == KnowledgeSource.External):
19
25
  logger.info("Don't let anyone else tell me who I am!")
@@ -34,6 +40,10 @@ def basic_rid_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
34
40
 
35
41
  @ProcessorInterface.as_handler(handler_type=HandlerType.Manifest)
36
42
  def basic_state_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
43
+ """Default manifest handler.
44
+
45
+ Blocks manifests with the same hash, or aren't newer than the cached version. Sets the normalized event type to `NEW` or `UPDATE` depending on whether the RID was previously known to this node.
46
+ """
37
47
  prev_bundle = processor.cache.read(kobj.rid)
38
48
 
39
49
  if prev_bundle:
@@ -58,6 +68,11 @@ def basic_state_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
58
68
 
59
69
  @ProcessorInterface.as_handler(HandlerType.Bundle, rid_types=[KoiNetEdge])
60
70
  def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
71
+ """Handles basic edge negotiation process.
72
+
73
+ Automatically approves proposed edges if they request RID types this node can provide (or KOI nodes/edges). Validates the edge type is allowed for the node type (partial nodes cannot use webhooks). If edge is invalid, a `FORGET` event is sent to the other node.
74
+ """
75
+
61
76
  edge_profile = EdgeProfile.model_validate(kobj.contents)
62
77
 
63
78
  # only want to handle external knowledge events (not edges this node created)
@@ -118,6 +133,10 @@ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObjec
118
133
 
119
134
  @ProcessorInterface.as_handler(HandlerType.Network)
120
135
  def basic_network_output_filter(processor: ProcessorInterface, kobj: KnowledgeObject):
136
+ """Default network handler.
137
+
138
+ Allows broadcasting of all RID types this node is an event provider for (set in node profile), and other nodes have subscribed to. All nodes will also broadcast about their own (internally sourced) KOI node, and KOI edges that they are part of, regardless of their node profile configuration. Finally, nodes will also broadcast about edges to the other node involved (regardless of if they are subscribed)."""
139
+
121
140
  involves_me = False
122
141
  if kobj.source == KnowledgeSource.Internal:
123
142
  if (type(kobj.rid) == KoiNetNode):
@@ -4,18 +4,33 @@ from typing import Callable
4
4
  from rid_lib import RIDType
5
5
 
6
6
 
7
- # sentinel
8
- STOP_CHAIN = object()
7
+ class StopChain:
8
+ """Class for a sentinel value by knowledge handlers."""
9
+ pass
10
+
11
+ STOP_CHAIN = StopChain()
12
+
9
13
 
10
14
  class HandlerType(StrEnum):
11
- RID = "rid", # guaranteed RID - decides whether validate manifest OR cache delete
12
- Manifest = "manifest", # guaranteed Manifest - decides whether to validate bundle
13
- Bundle = "bundle", # guaranteed Bundle - decides whether to write to cache
14
- Network = "network", # guaranteed Bundle, after cache write/delete - decides network targets
15
- Final = "final" # guaranteed Bundle, after network push - final action
15
+ """Types of handlers used in knowledge processing pipeline.
16
+
17
+ - RID - provided RID; if event type is `FORGET`, this handler decides whether to delete the knowledge from the cache by setting the normalized event type to `FORGET`, otherwise this handler decides whether to validate the manifest (and fetch it if not provided).
18
+ - Manifest - provided RID, manifest; decides whether to validate the bundle (and fetch it if not provided).
19
+ - Bundle - provided RID, manifest, contents (bundle); decides whether to write knowledge to the cache by setting the normalized event type to `NEW` or `UPDATE`.
20
+ - Network - provided RID, manifest, contents (bundle); decides which nodes (if any) to broadcast an event about this knowledge to. (Note, if event type is `FORGET`, the manifest and contents will be retrieved from the local cache, and indicate the last state of the knowledge before it was deleted.)
21
+ - Final - provided RID, manifests, contents (bundle); final action taken after network broadcast.
22
+ """
23
+
24
+ RID = "rid",
25
+ Manifest = "manifest",
26
+ Bundle = "bundle",
27
+ Network = "network",
28
+ Final = "final"
16
29
 
17
30
  @dataclass
18
31
  class KnowledgeHandler:
32
+ """Handles knowledge processing events of the provided types."""
33
+
19
34
  func: Callable
20
35
  handler_type: HandlerType
21
36
  rid_types: list[RIDType] | None
@@ -11,7 +11,8 @@ from ..protocol.event import Event, EventType
11
11
  from .handler import (
12
12
  KnowledgeHandler,
13
13
  HandlerType,
14
- STOP_CHAIN
14
+ STOP_CHAIN,
15
+ StopChain
15
16
  )
16
17
  from .knowledge_object import (
17
18
  KnowledgeObject,
@@ -23,6 +24,8 @@ logger = logging.getLogger(__name__)
23
24
 
24
25
 
25
26
  class ProcessorInterface:
27
+ """Provides access to this node's knowledge processing pipeline."""
28
+
26
29
  cache: Cache
27
30
  network: NetworkInterface
28
31
  identity: NodeIdentity
@@ -48,7 +51,10 @@ class ProcessorInterface:
48
51
  handler_type: HandlerType,
49
52
  rid_types: list[RIDType] | None = None
50
53
  ):
51
- """Special decorator that returns a Handler instead of a function."""
54
+ """Special decorator that returns a handler instead of a function.
55
+
56
+ The function symbol will redefined as a `KnowledgeHandler`, which can be passed into the `ProcessorInterface` constructor. This is used to register the default handlers.
57
+ """
52
58
  def decorator(func: Callable) -> KnowledgeHandler:
53
59
  handler = KnowledgeHandler(func, handler_type, rid_types, )
54
60
  return handler
@@ -59,7 +65,7 @@ class ProcessorInterface:
59
65
  handler_type: HandlerType,
60
66
  rid_types: list[RIDType] | None = None
61
67
  ):
62
- """Assigns decorated function as handler for this Processor."""
68
+ """Assigns decorated function as handler for this processor."""
63
69
  def decorator(func: Callable) -> Callable:
64
70
  handler = KnowledgeHandler(func, handler_type, rid_types)
65
71
  self.handlers.append(handler)
@@ -70,7 +76,17 @@ class ProcessorInterface:
70
76
  self,
71
77
  handler_type: HandlerType,
72
78
  kobj: KnowledgeObject
73
- ):
79
+ ) -> KnowledgeObject | StopChain:
80
+ """Calls handlers of provided type, chaining their inputs and outputs together.
81
+
82
+ The knowledge object provided when this function is called will be passed to the first handler. A handler may return one of three types:
83
+ - `KnowledgeObject` - to modify the knowledge object for the next handler in the chain
84
+ - `None` - to keep the same knowledge object for the next handler in the chain
85
+ - `STOP_CHAIN` - to stop the handler chain and immediately exit the processing pipeline
86
+
87
+ Handlers will only be called in the chain if their handler and RID type match that of the inputted knowledge object.
88
+ """
89
+
74
90
  for handler in self.handlers:
75
91
  if handler_type != handler.handler_type:
76
92
  continue
@@ -98,7 +114,19 @@ class ProcessorInterface:
98
114
  return kobj
99
115
 
100
116
 
101
- def handle_kobj(self, kobj: KnowledgeObject):
117
+ def handle_kobj(self, kobj: KnowledgeObject) -> None:
118
+ """Sends provided knowledge obejct through knowledge processing pipeline.
119
+
120
+ Handler chains are called in between major events in the pipeline, indicated by their handler type. Each handler type is guaranteed to have access to certain knowledge, and may affect a subsequent action in the pipeline. The five handler types are as follows:
121
+ - RID - provided RID; if event type is `FORGET`, this handler decides whether to delete the knowledge from the cache by setting the normalized event type to `FORGET`, otherwise this handler decides whether to validate the manifest (and fetch it if not provided).
122
+ - Manifest - provided RID, manifest; decides whether to validate the bundle (and fetch it if not provided).
123
+ - Bundle - provided RID, manifest, contents (bundle); decides whether to write knowledge to the cache by setting the normalized event type to `NEW` or `UPDATE`.
124
+ - Network - provided RID, manifest, contents (bundle); decides which nodes (if any) to broadcast an event about this knowledge to. (Note, if event type is `FORGET`, the manifest and contents will be retrieved from the local cache, and indicate the last state of the knowledge before it was deleted.)
125
+ - Final - provided RID, manifests, contents (bundle); final action taken after network broadcast.
126
+
127
+ The pipeline may be stopped by any point by a single handler returning the `STOP_CHAIN` sentinel. In that case, the process will exit immediately. Further handlers of that type and later handler chains will not be called.
128
+ """
129
+
102
130
  logger.info(f"Handling {kobj!r}")
103
131
  kobj = self.call_handler_chain(HandlerType.RID, kobj)
104
132
  if kobj is STOP_CHAIN: return
@@ -185,6 +213,7 @@ class ProcessorInterface:
185
213
  kobj = self.call_handler_chain(HandlerType.Final, kobj)
186
214
 
187
215
  def queue_kobj(self, kobj: KnowledgeObject, flush: bool = False):
216
+ """Queues a knowledge object to be put processed in the pipeline."""
188
217
  self.kobj_queue.put(kobj)
189
218
  logger.info(f"Queued {kobj!r}")
190
219
 
@@ -192,6 +221,7 @@ class ProcessorInterface:
192
221
  self.flush_kobj_queue()
193
222
 
194
223
  def flush_kobj_queue(self):
224
+ """Flushes all knowledge objects from queue and processes them."""
195
225
  while not self.kobj_queue.empty():
196
226
  kobj = self.kobj_queue.get()
197
227
  logger.info(f"Dequeued {kobj!r}")
@@ -208,6 +238,10 @@ class ProcessorInterface:
208
238
  source: KnowledgeSource = KnowledgeSource.Internal,
209
239
  flush: bool = False
210
240
  ):
241
+ """Queues provided knowledge to be handled by processing pipeline.
242
+
243
+ Knowledge may take the form of an RID, manifest, bundle, or event (with an optional event type for non event objects). All objects will be normalized into knowledge objects and queued. If `flush` is `True`, the queue will be flushed immediately after adding the new knowledge.
244
+ """
211
245
  if rid:
212
246
  kobj = KnowledgeObject.from_rid(rid, event_type, source)
213
247
  elif manifest:
@@ -14,6 +14,18 @@ class KnowledgeSource(StrEnum):
14
14
  External = "EXTERNAL"
15
15
 
16
16
  class KnowledgeObject(BaseModel):
17
+ """A normalized knowledge representation for internal processing.
18
+
19
+ Capable of representing an RID, manifest, bundle, or event. Contains three additional fields use for decision making in the knowledge processing pipeline.
20
+
21
+ The source indicates whether this object was generated by this node, or sourced from another node in the network.
22
+
23
+ The normalized event type indicates how the knowledge object is viewed from the perspective of this node, and what cache actions will take place. `NEW`, `UPDATE` -> cache write, `FORGET` -> cache delete, `None` -> no cache action.
24
+
25
+ The network targets indicate other nodes in the network this knowledge object will be sent to. The event sent to them will be constructed from this knowledge object's RID, manifest, contents, and normalized event type.
26
+
27
+ Constructors are provided to create a knowledge object from an RID, manifest, bundle, or event.
28
+ """
17
29
  rid: RID
18
30
  manifest: Manifest | None = None
19
31
  contents: dict | None = None
@@ -93,12 +105,19 @@ class KnowledgeObject(BaseModel):
93
105
 
94
106
  @property
95
107
  def normalized_event(self):
96
- if not self.normalized_event_type:
108
+ if self.normalized_event_type is None:
97
109
  raise ValueError("Internal event's normalized event type is None, cannot convert to Event")
98
110
 
99
- return Event(
100
- rid=self.rid,
101
- event_type=self.normalized_event_type,
102
- manifest=self.manifest,
103
- contents=self.contents
104
- )
111
+ elif self.normalized_event_type == EventType.FORGET:
112
+ return Event(
113
+ rid=self.rid,
114
+ event_type=EventType.FORGET
115
+ )
116
+
117
+ else:
118
+ return Event(
119
+ rid=self.rid,
120
+ event_type=self.normalized_event_type,
121
+ manifest=self.manifest,
122
+ contents=self.contents
123
+ )
@@ -1,3 +1,5 @@
1
+ """Pydantic models for request and response/payload objects in the KOI-net API."""
2
+
1
3
  from pydantic import BaseModel
2
4
  from rid_lib import RID, RIDType
3
5
  from rid_lib.ext import Bundle, Manifest
@@ -1,3 +1,5 @@
1
+ """API paths for KOI-net protocol."""
2
+
1
3
  BROADCAST_EVENTS_PATH = "/events/broadcast"
2
4
  POLL_EVENTS_PATH = "/events/poll"
3
5
  FETCH_RIDS_PATH = "/rids/fetch"
@@ -0,0 +1,209 @@
1
+ Metadata-Version: 2.4
2
+ Name: koi-net
3
+ Version: 1.0.0b2
4
+ Summary: Implementation of KOI-net protocol in Python
5
+ Project-URL: Homepage, https://github.com/BlockScience/koi-net/
6
+ Author-email: Luke Miller <luke@block.science>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2025 BlockScience
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ License-File: LICENSE
29
+ Requires-Python: >=3.10
30
+ Requires-Dist: httpx>=0.28.1
31
+ Requires-Dist: networkx>=3.4.2
32
+ Requires-Dist: pydantic>=2.10.6
33
+ Requires-Dist: rid-lib>=3.2.1
34
+ Provides-Extra: dev
35
+ Requires-Dist: build; extra == 'dev'
36
+ Requires-Dist: twine>=6.0; extra == 'dev'
37
+ Provides-Extra: examples
38
+ Requires-Dist: fastapi; extra == 'examples'
39
+ Requires-Dist: rich; extra == 'examples'
40
+ Requires-Dist: uvicorn; extra == 'examples'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # KOI-net
44
+
45
+ *This specification is the result of several iterations of KOI research, [read more here](https://github.com/BlockScience/koi).*
46
+
47
+ # Protocol
48
+ ## Introduction
49
+
50
+ *This project builds upon and uses the [RID protocol](https://github.com/BlockScience/rid-lib) to identify and coordinate around knowledge objects.*
51
+
52
+ This protocol defines the standard communication patterns and coordination norms needed to establish and maintain Knowledge Organization Infrastructure (KOI) networks. KOI-nets are heterogenous compositions of KOI nodes, each of which is capable of autonomously inputting, processing, and outputting knowledge. The behavior of each node and configuration of each network can vary greatly, thus the protocol is designed to be a simple and flexible but interoperable foundation for future projects to build on. The protocol only governs communication between nodes, not how they operate internally. As a result we consider KOI-nets to be fractal-like, in that a network of nodes may act like a single node from an outside perspective.
53
+
54
+ Generated OpenAPI documentation is provided in this repository, and can be [viewed interactively with Swagger](https://generator.swagger.io/?url=https://raw.githubusercontent.com/BlockScience/koi/refs/heads/main/koi_v3_node_api.yaml).
55
+
56
+ ## Communication Methods
57
+
58
+ There are two classes of communication methods, event and state communication.
59
+ - Event communication is one way, a node send an event to another node.
60
+ - State communication is two way, a node asks another node for RIDs, manifests, or bundles and receives a response containing the requested resource (if available).
61
+
62
+ There are also two types of nodes, full and partial nodes.
63
+ - Full nodes are web servers, implementing the endpoints defined in the KOi-net protocol. They are capable of receiving events via webhooks (another node calls their endpoint), and serving state queries. They can also call the endpoints of other full nodes to broadcast events or retrieve state.
64
+ - Partial nodes are web clients and don't implement any API endpoints. They are capable of receiving events via polling (asking another node for events). They can also call the endpoints of full nodes to broadcast events or retrieve state.
65
+
66
+ There are five endpoints defined by the API spec. The first two are for event communication with full and partial nodes respectively. The remaining three are for state communication with full nodes. As a result, partial nodes are unable to directly transfer state and may only output events to other nodes.
67
+ - Broadcast events - `/events/broadcast`
68
+ - Poll events - `/events/poll`
69
+ - Fetch bundles - `/bundles/fetch`
70
+ - Fetch manifests - `/manifests/fetch`
71
+ - Fetch RIDs - `/rids/fetch`
72
+
73
+ All endpoints are called with via POST request with a JSON body, and will receive a response containing a JSON payload (with the exception of broadcast events, which won't return anything). The JSON schemas can be found in the attached OpenAPI specification or the Pydantic models in the "protocol" module.
74
+
75
+ The request and payload JSON objects are composed of the fundamental "knowledge types" from the RID / KOI-net system: RIDs, manifests, bundles, and events. RIDs, manifests, and bundles are defined by the RID protocol and imported from rid-lib, which you can [read about here](https://github.com/BlockScience/rid-lib). Events are now part of the KOI-net protocol, and are defined as an RID and an event type with an optional manifest and contents.
76
+
77
+ ```json
78
+ {
79
+ "rid": "...",
80
+ "event_type": "NEW | UPDATE | FORGET",
81
+ "manifest": {
82
+ "rid": "...",
83
+ "timestamp": "...",
84
+ "sha256_hash": "...",
85
+ },
86
+ "contents": {}
87
+ }
88
+ ```
89
+
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
94
+
95
+ Nodes may broadcast events to other nodes to indicate its internal state changed. Conversely, nodes may also listen to events from other nodes and decide to change their internal state, take some other action, or do nothing.
96
+
97
+
98
+ # Implementation
99
+ ## Setup
100
+
101
+ The bulk of the code in this repo is taken up by the Python reference implementation, which can be used in other projects to easily set up and configure your own KOI node.
102
+
103
+ This package can be installed with pip:
104
+ ```
105
+ pip install koi-net
106
+ ```
107
+
108
+ ## Node Interface
109
+ All of the KOI-net functionality comes from the `NodeInterface` class which provides methods to interact with the protocol API, a local RID cache, a view of the network, and an internal processing pipeline. To create a new node, you will need to give it a name and a profile. The name will be used to generate its unique node RID, and the profile stores basic configuration data which will be shared with other nodes you communciate with (it will be stored in the bundle associated with your node's RID).
110
+ - Partial nodes only need to indicate their type, and optionally the RID types of events they provide.
111
+ - Full nodes need to indicate their type, the base URL for their KOI-net API, and optionally the RID types of events and state they provide.
112
+
113
+ ```python
114
+ from koi_net import NodeInterface
115
+ from koi_net.protocol.node import NodeProfile, NodeProvides, NodeType
116
+
117
+ # partial node configuration
118
+
119
+ partial_node = NodeInterface(
120
+ name="mypartialnode",
121
+ profile=NodeProfile(
122
+ node_type=NodeType.PARTIAL,
123
+ provides=NodeProvides(
124
+ event=[]
125
+ )
126
+ )
127
+ )
128
+
129
+ # full node configuration
130
+
131
+ full_node = NodeInterface(
132
+ name="myfullnode",
133
+ profile=NodeProfile(
134
+ base_url="http://127.0.0.1:8000",
135
+ node_type=NodeType.FULL,
136
+ provides=NodeProvides(
137
+ event=[],
138
+ state=[]
139
+ )
140
+ )
141
+ )
142
+ ```
143
+
144
+ The node class mostly acts as a container for other classes with more specialized behavior, with special functions that should be called to start up and shut down a node. We'll take a look at each of these components in turn, but here is the class stub:
145
+ ```python
146
+ class NodeInterface:
147
+ cache: Cache
148
+ identity: NodeIdentity
149
+ network: NetworkInterface
150
+ processor: ProcessorInterface
151
+ first_contact: str
152
+
153
+ def __init__(
154
+ self,
155
+ name: str,
156
+ profile: NodeProfile,
157
+ identity_file_path: str = "identity.json",
158
+ first_contact: str | None = None,
159
+ handlers: list[KnowledgeHandler] | None = None,
160
+ cache: Cache | None = None,
161
+ network: NetworkInterface | None = None,
162
+ processor: ProcessorInterface | None = None
163
+ ): ...
164
+
165
+ def initialize(self): ...
166
+ def finalize(self): ...
167
+ ```
168
+ As you can see, only a name and profile are required. The other fields allow for additional customization if needed.
169
+
170
+ ## Node Identity
171
+ The `NodeIdentity` class provides easy access to a node's own RID, profile, and bundle. It provides access to the following properties after initialization, accessed with `node.identity`.
172
+ ```
173
+ class NodeIdentity:
174
+ rid: KoiNetNode # an RID type
175
+ profile: NodeProfile
176
+ bundle: Bundle
177
+ ```
178
+ This it what is initialized from the required `name` and `profile` fields in the `NodeInterface` constructor.
179
+
180
+ ## Network Interface
181
+ The `NetworkInterface` class provides access to high level network actions, and contains several other network related classes. It is accessed with `node.network`.
182
+ ```python
183
+ class NetworkInterface:
184
+ graph: NetworkGraph
185
+ request_handler: RequestHandler
186
+ response_handler: ResponseHandler
187
+
188
+ def __init__(
189
+ self,
190
+ file_path: str,
191
+ first_contact: str | None,
192
+ cache: Cache,
193
+ identity: NodeIdentity
194
+ ): ...
195
+
196
+ def push_event_to(self, event: Event, node: KoiNetNode, flush=False): ...
197
+
198
+ def flush_poll_queue(self, node: KoiNetNode) -> list[Event]: ...
199
+ def flush_webhook_queue(self, node: RID): ...
200
+ def flush_all_webhook_queues(self): ...
201
+
202
+ def get_state_providers(self, rid_type: RIDType): ...
203
+
204
+ def fetch_remote_bundle(self, rid: RID): ...
205
+
206
+ def fetch_remote_manifest(self, rid: RID): ...
207
+
208
+ def poll_neighbors(self) -> list[Event]: ...
209
+
@@ -0,0 +1,24 @@
1
+ koi_net/__init__.py,sha256=b0Ze0pZmJAuygpWUFHM6Kvqo3DkU_uzmkptv1EpAArw,31
2
+ koi_net/core.py,sha256=ZsBoTay7Z1_7JKzKt-vB3x_zl9GEit-fFkCSifLPoOk,3582
3
+ koi_net/identity.py,sha256=PBgmAx5f3zzQmHASB1TJW2g19n9TLfmSJMXg2eQFg0A,2386
4
+ koi_net/network/__init__.py,sha256=r_RN-q_mDYC-2RAkN-lJoMUX76TXyfEUc_MVKW87z0g,39
5
+ koi_net/network/graph.py,sha256=VSvjF2p_EsuJsNPFhK4MEjgXW9oZL0Vg9Tn6X4-c3WY,4710
6
+ koi_net/network/interface.py,sha256=paBJjQFJC8UkUz-BWeRwvuWD2cv8WFt7PyhoV7VOhWI,10823
7
+ koi_net/network/request_handler.py,sha256=FDS6b3MLjeM_w6josbDtHZZUg0D1kuGDwE1H0si8mus,3870
8
+ koi_net/network/response_handler.py,sha256=mA3FtrN3aTZATcLaHQhJUWrJdIKNv6d24fhvOl-nDKY,1890
9
+ koi_net/processor/__init__.py,sha256=x4fAY0hvQEDcpfdTB3POIzxBQjYAtn0qQazPo1Xm0m4,41
10
+ koi_net/processor/default_handlers.py,sha256=kmXBKBCAs6ygsPWl9IFbjZe1KDC_6M0YgkRF7hfI7kU,7224
11
+ koi_net/processor/handler.py,sha256=9ZHsoPyAwFhtJPmvrLY-7C9dQ7KhLwI3DS5_Ms4HWAY,1640
12
+ koi_net/processor/interface.py,sha256=4oiSWz91WWQywbfRdT2H2-aVh_6BTYE2AHbZSovzd0U,11671
13
+ koi_net/processor/knowledge_object.py,sha256=cGv33fwNZQMylkhlTaQTbk96FVIVbdOUaBsG06u0m4k,4187
14
+ koi_net/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ koi_net/protocol/api_models.py,sha256=Vm2Szp2OZ3EsZrYRwkAiBi_smk4MF1dAt-xPGOLi6ME,919
16
+ koi_net/protocol/consts.py,sha256=zeWJvRpqcERrqJq39heyNHb6f_9QrvoBZJHd70yE914,249
17
+ koi_net/protocol/edge.py,sha256=G3D9Ie0vbTSMJdoTw9g_oBmFCqzJ1gO7U1PVrw7p3j8,447
18
+ koi_net/protocol/event.py,sha256=dzJmcHbimo7p5NwH2drccF0vMcAj9oQRj3iZ9Bjf7kg,1275
19
+ koi_net/protocol/helpers.py,sha256=9E9PaoIuSNrTBATGCLJ_kSBMZ2z-KIMnLJzGOTqQDC0,719
20
+ koi_net/protocol/node.py,sha256=Ntrx01dbm39ViKGtr4gLmztcMwKpTIweS6rRL-zoU_Y,391
21
+ koi_net-1.0.0b2.dist-info/METADATA,sha256=Gry4tH3P7Ed1_d0EiC3oBnLIwHa6BmQaM4gTw6V1cbc,10358
22
+ koi_net-1.0.0b2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ koi_net-1.0.0b2.dist-info/licenses/LICENSE,sha256=XBcvl8yjCAezfuqN1jadQykrX7H2g4nr2WRDmHLW6ik,1090
24
+ koi_net-1.0.0b2.dist-info/RECORD,,
@@ -1,43 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: koi-net
3
- Version: 1.0.0b1
4
- Summary: Implementation of KOI-net protocol in Python
5
- Project-URL: Homepage, https://github.com/BlockScience/koi-net/
6
- Author-email: Luke Miller <luke@block.science>
7
- License: MIT License
8
-
9
- Copyright (c) 2025 BlockScience
10
-
11
- Permission is hereby granted, free of charge, to any person obtaining a copy
12
- of this software and associated documentation files (the "Software"), to deal
13
- in the Software without restriction, including without limitation the rights
14
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
- copies of the Software, and to permit persons to whom the Software is
16
- furnished to do so, subject to the following conditions:
17
-
18
- The above copyright notice and this permission notice shall be included in all
19
- copies or substantial portions of the Software.
20
-
21
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
- SOFTWARE.
28
- License-File: LICENSE
29
- Requires-Python: >=3.10
30
- Requires-Dist: httpx>=0.28.1
31
- Requires-Dist: networkx>=3.4.2
32
- Requires-Dist: pydantic>=2.10.6
33
- Requires-Dist: rid-lib>=3.2.1
34
- Provides-Extra: dev
35
- Requires-Dist: build; extra == 'dev'
36
- Requires-Dist: twine>=6.0; extra == 'dev'
37
- Provides-Extra: examples
38
- Requires-Dist: fastapi; extra == 'examples'
39
- Requires-Dist: rich; extra == 'examples'
40
- Requires-Dist: uvicorn; extra == 'examples'
41
- Description-Content-Type: text/markdown
42
-
43
- # koi-net
@@ -1,24 +0,0 @@
1
- koi_net/__init__.py,sha256=b0Ze0pZmJAuygpWUFHM6Kvqo3DkU_uzmkptv1EpAArw,31
2
- koi_net/core.py,sha256=71ntaewhGBZ2zFQ7BxoSu2hhzPHQRsYYcRZGgTcdLhU,3024
3
- koi_net/identity.py,sha256=0ISIzQHUbwtPhXl6ZzrphzvwhBmb3K9t5wHmupr2_eA,1854
4
- koi_net/network/__init__.py,sha256=r_RN-q_mDYC-2RAkN-lJoMUX76TXyfEUc_MVKW87z0g,39
5
- koi_net/network/graph.py,sha256=qSHlilXZe2ikbDkE1tDRhazQfE2S_bxkNcExYmbxXqs,4039
6
- koi_net/network/interface.py,sha256=8WADUb5qADgw65fyPbUagYwK0rjYRa1XId7zegGJSa4,9316
7
- koi_net/network/request_handler.py,sha256=y7E80jgGfrXYxrOrHMMMzQCKrm5yV_GDessRE0GwOq4,3273
8
- koi_net/network/response_handler.py,sha256=EWOJzSBdbMYfsUx51avhZXAJEU0QqeR6USfKafHK-f8,1810
9
- koi_net/processor/__init__.py,sha256=x4fAY0hvQEDcpfdTB3POIzxBQjYAtn0qQazPo1Xm0m4,41
10
- koi_net/processor/default_handlers.py,sha256=djgPWj_bcNsT8Wxp41JcCk-kjlDIHTK6r4mcjdQWasQ,5973
11
- koi_net/processor/handler.py,sha256=6WkBJavbAs61cUwShcMjMb6lcWXh1Xu4g1PlMa3lu_A,747
12
- koi_net/processor/interface.py,sha256=uEAktg8cfFYJGO9rbCSja0wsdcnbDhe44iBXKCxyT9k,8511
13
- koi_net/processor/knowledge_object.py,sha256=fr0TBqSbjd8tdgbAxG_B5onKHRez52-dtsrz71K8Vew,2998
14
- koi_net/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- koi_net/protocol/api_models.py,sha256=dUvNtOor8cI_xhhMvuQuIgcEY7JhVrBg-6KoC_Jih6I,833
16
- koi_net/protocol/consts.py,sha256=GmaODi2V-BGZ5rMyFgRsrPz4iDfJZwkNpul2aSTQY5Q,208
17
- koi_net/protocol/edge.py,sha256=G3D9Ie0vbTSMJdoTw9g_oBmFCqzJ1gO7U1PVrw7p3j8,447
18
- koi_net/protocol/event.py,sha256=dzJmcHbimo7p5NwH2drccF0vMcAj9oQRj3iZ9Bjf7kg,1275
19
- koi_net/protocol/helpers.py,sha256=9E9PaoIuSNrTBATGCLJ_kSBMZ2z-KIMnLJzGOTqQDC0,719
20
- koi_net/protocol/node.py,sha256=Ntrx01dbm39ViKGtr4gLmztcMwKpTIweS6rRL-zoU_Y,391
21
- koi_net-1.0.0b1.dist-info/METADATA,sha256=7nR4xHkuAJI3yctTJEaEZGtLjkYXOlpN43DEZsukehc,1927
22
- koi_net-1.0.0b1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- koi_net-1.0.0b1.dist-info/licenses/LICENSE,sha256=XBcvl8yjCAezfuqN1jadQykrX7H2g4nr2WRDmHLW6ik,1090
24
- koi_net-1.0.0b1.dist-info/RECORD,,