koi-net 1.0.0b1__py3-none-any.whl → 1.0.0b3__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,19 @@ logger = logging.getLogger(__name__)
12
12
 
13
13
 
14
14
  class NetworkGraph:
15
+ """Graph functions for this node's view of its network."""
16
+
17
+ cache: Cache
18
+ identity: NodeIdentity
19
+ dg: nx.DiGraph
20
+
15
21
  def __init__(self, cache: Cache, identity: NodeIdentity):
16
22
  self.cache = cache
17
23
  self.dg = nx.DiGraph()
18
24
  self.identity = identity
19
25
 
20
26
  def generate(self):
27
+ """Generates directed graph from cached KOI nodes and edges."""
21
28
  logger.info("Generating network graph")
22
29
  self.dg.clear()
23
30
  for rid in self.cache.list_rids():
@@ -35,6 +42,7 @@ class NetworkGraph:
35
42
  logger.info("Done")
36
43
 
37
44
  def get_node_profile(self, rid: KoiNetNode) -> NodeProfile | None:
45
+ """Returns node profile given its RID."""
38
46
  bundle = self.cache.read(rid)
39
47
  if bundle:
40
48
  return bundle.validate_contents(NodeProfile)
@@ -45,6 +53,7 @@ class NetworkGraph:
45
53
  source: KoiNetNode | None = None,
46
54
  target: KoiNetNode | None = None,
47
55
  ) -> EdgeProfile | None:
56
+ """Returns edge profile given its RID, or source and target node RIDs."""
48
57
  if source and target:
49
58
  if (source, target) not in self.dg.edges: return
50
59
  edge_data = self.dg.get_edge_data(source, target)
@@ -62,6 +71,9 @@ class NetworkGraph:
62
71
  self,
63
72
  direction: Literal["in", "out"] | None = None,
64
73
  ) -> list[KoiNetEdge]:
74
+ """Returns edges this node belongs to.
75
+
76
+ All edges returned by default, specify `direction` to restrict to incoming or outgoing edges only."""
65
77
 
66
78
  edges = []
67
79
  if direction != "in":
@@ -88,6 +100,9 @@ class NetworkGraph:
88
100
  status: EdgeStatus | None = None,
89
101
  allowed_type: RIDType | None = None
90
102
  ) -> list[KoiNetNode]:
103
+ """Returns neighboring nodes this node shares an edge with.
104
+
105
+ All neighboring nodes returned by default, specify `direction` to restrict to neighbors connected by incoming or outgoing edges only."""
91
106
 
92
107
  neighbors = []
93
108
  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(
@@ -12,7 +12,9 @@ from ..protocol.api_models import (
12
12
  FetchRids,
13
13
  FetchManifests,
14
14
  FetchBundles,
15
- PollEvents
15
+ PollEvents,
16
+ RequestModels,
17
+ ResponseModels
16
18
  )
17
19
  from ..protocol.consts import (
18
20
  BROADCAST_EVENTS_PATH,
@@ -21,85 +23,119 @@ from ..protocol.consts import (
21
23
  FETCH_MANIFESTS_PATH,
22
24
  FETCH_BUNDLES_PATH
23
25
  )
24
- from ..protocol.node import NodeProfile, NodeType
26
+ from ..protocol.node import NodeType
27
+ from .graph import NetworkGraph
25
28
 
26
29
 
27
30
  logger = logging.getLogger(__name__)
28
31
 
29
32
 
30
33
  class RequestHandler:
34
+ """Handles making requests to other KOI nodes."""
35
+
31
36
  cache: Cache
37
+ graph: NetworkGraph
32
38
 
33
- def __init__(self, cache: Cache):
39
+ def __init__(self, cache: Cache, graph: NetworkGraph):
34
40
  self.cache = cache
41
+ self.graph = graph
35
42
 
36
- def make_request(self, url, request: BaseModel) -> httpx.Response:
43
+ def make_request(
44
+ self,
45
+ url: str,
46
+ request: RequestModels,
47
+ response_model: type[ResponseModels] | None = None
48
+ ) -> ResponseModels | None:
37
49
  logger.info(f"Making request to {url}")
38
50
  resp = httpx.post(
39
51
  url=url,
40
52
  data=request.model_dump_json()
41
53
  )
42
- return resp
54
+ if response_model:
55
+ return response_model.model_validate_json(resp.text)
43
56
 
44
57
  def get_url(self, node_rid: KoiNetNode, url: str) -> str:
58
+ """Retrieves URL of a node, or returns provided URL."""
59
+
45
60
  if not node_rid and not url:
46
61
  raise ValueError("One of 'node_rid' and 'url' must be provided")
47
62
 
48
63
  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:
64
+ node_profile = self.graph.get_node_profile(node_rid)
65
+ if not node_profile:
66
+ raise Exception("Node not found")
67
+ if node_profile.node_type != NodeType.FULL:
53
68
  raise Exception("Can't query partial node")
54
- logger.info(f"Resolved {node_rid!r} to {node.base_url}")
55
- return node.base_url
69
+ logger.info(f"Resolved {node_rid!r} to {node_profile.base_url}")
70
+ return node_profile.base_url
56
71
  else:
57
72
  return url
58
73
 
59
74
  def broadcast_events(
60
- self, node: RID = None, url: str = None, **kwargs
75
+ self,
76
+ node: RID = None,
77
+ url: str = None,
78
+ req: EventsPayload | None = None,
79
+ **kwargs
61
80
  ) -> None:
81
+ """See protocol.api_models.EventsPayload for available kwargs."""
62
82
  self.make_request(
63
83
  self.get_url(node, url) + BROADCAST_EVENTS_PATH,
64
- EventsPayload.model_validate(kwargs)
84
+ req or EventsPayload.model_validate(kwargs)
65
85
  )
66
86
 
67
87
  def poll_events(
68
- self, node: RID = None, url: str = None, **kwargs
69
- ) -> EventsPayload:
70
- resp = self.make_request(
88
+ self,
89
+ node: RID = None,
90
+ url: str = None,
91
+ req: PollEvents | None = None,
92
+ **kwargs
93
+ ) -> EventsPayload:
94
+ """See protocol.api_models.PollEvents for available kwargs."""
95
+ return self.make_request(
71
96
  self.get_url(node, url) + POLL_EVENTS_PATH,
72
- PollEvents.model_validate(kwargs)
97
+ req or PollEvents.model_validate(kwargs),
98
+ response_model=EventsPayload
73
99
  )
74
-
75
- return EventsPayload.model_validate_json(resp.text)
76
-
100
+
77
101
  def fetch_rids(
78
- self, node: RID = None, url: str = None, **kwargs
79
- ) -> RidsPayload:
80
- resp = self.make_request(
102
+ self,
103
+ node: RID = None,
104
+ url: str = None,
105
+ req: FetchRids | None = None,
106
+ **kwargs
107
+ ) -> RidsPayload:
108
+ """See protocol.api_models.FetchRids for available kwargs."""
109
+ return self.make_request(
81
110
  self.get_url(node, url) + FETCH_RIDS_PATH,
82
- FetchRids.model_validate(kwargs)
111
+ req or FetchRids.model_validate(kwargs),
112
+ response_model=RidsPayload
83
113
  )
84
-
85
- return RidsPayload.model_validate_json(resp.text)
86
-
114
+
87
115
  def fetch_manifests(
88
- self, node: RID = None, url: str = None, **kwargs
89
- ) -> ManifestsPayload:
90
- resp = self.make_request(
116
+ self,
117
+ node: RID = None,
118
+ url: str = None,
119
+ req: FetchManifests | None = None,
120
+ **kwargs
121
+ ) -> ManifestsPayload:
122
+ """See protocol.api_models.FetchManifests for available kwargs."""
123
+ return self.make_request(
91
124
  self.get_url(node, url) + FETCH_MANIFESTS_PATH,
92
- FetchManifests.model_validate(kwargs)
125
+ req or FetchManifests.model_validate(kwargs),
126
+ response_model=ManifestsPayload
93
127
  )
94
-
95
- return ManifestsPayload.model_validate_json(resp.text)
96
-
128
+
97
129
  def fetch_bundles(
98
- self, node: RID = None, url: str = None, **kwargs
99
- ) -> BundlesPayload:
100
- resp = self.make_request(
130
+ self,
131
+ node: RID = None,
132
+ url: str = None,
133
+ req: FetchBundles | None = None,
134
+ **kwargs
135
+ ) -> BundlesPayload:
136
+ """See protocol.api_models.FetchBundles for available kwargs."""
137
+ return self.make_request(
101
138
  self.get_url(node, url) + FETCH_BUNDLES_PATH,
102
- FetchBundles.model_validate(kwargs)
103
- )
104
-
105
- return BundlesPayload.model_validate_json(resp.text)
139
+ req or FetchBundles.model_validate(kwargs),
140
+ response_model=BundlesPayload
141
+ )
@@ -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,9 +1,11 @@
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
4
6
  from koi_net.protocol.node import NodeType
5
7
  from .interface import ProcessorInterface
6
- from .handler import HandlerType, STOP_CHAIN
8
+ from .handler import KnowledgeHandler, HandlerType, STOP_CHAIN
7
9
  from .knowledge_object import KnowledgeObject, KnowledgeSource
8
10
  from ..protocol.event import Event, EventType
9
11
  from ..protocol.edge import EdgeProfile, EdgeStatus, EdgeType
@@ -12,14 +14,18 @@ logger = logging.getLogger(__name__)
12
14
 
13
15
  # RID handlers
14
16
 
15
- @ProcessorInterface.as_handler(handler_type=HandlerType.RID)
17
+ @KnowledgeHandler.create(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!")
20
26
  return STOP_CHAIN
21
27
 
22
- if kobj.event_type == EventType.FORGET:
28
+ if kobj.event_type == EventType.FORGET:
23
29
  if processor.cache.exists(kobj.rid):
24
30
  logger.info("Allowing cache forget")
25
31
  kobj.normalized_event_type = EventType.FORGET
@@ -32,8 +38,12 @@ def basic_rid_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
32
38
 
33
39
  # Manifest handlers
34
40
 
35
- @ProcessorInterface.as_handler(handler_type=HandlerType.Manifest)
41
+ @KnowledgeHandler.create(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:
@@ -56,8 +66,13 @@ def basic_state_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
56
66
 
57
67
  # Bundle handlers
58
68
 
59
- @ProcessorInterface.as_handler(HandlerType.Bundle, rid_types=[KoiNetEdge])
69
+ @KnowledgeHandler.create(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)
@@ -116,8 +131,12 @@ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObjec
116
131
 
117
132
  # Network handlers
118
133
 
119
- @ProcessorInterface.as_handler(HandlerType.Network)
134
+ @KnowledgeHandler.create(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,19 +4,49 @@ 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
37
+
38
+ @classmethod
39
+ def create(
40
+ cls,
41
+ handler_type: HandlerType,
42
+ rid_types: list[RIDType] | None = None
43
+ ):
44
+ """Special decorator that returns a KnowledgeHandler instead of a function.
45
+
46
+ The function symbol will redefined as a `KnowledgeHandler`, which can be passed into the `ProcessorInterface` constructor. This is used to register default handlers.
47
+ """
48
+ def decorator(func: Callable) -> KnowledgeHandler:
49
+ handler = cls(func, handler_type, rid_types)
50
+ return handler
51
+ return decorator
22
52