koi-net 1.2.4__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.

Files changed (59) hide show
  1. koi_net/__init__.py +1 -0
  2. koi_net/behaviors/handshaker.py +68 -0
  3. koi_net/behaviors/profile_monitor.py +23 -0
  4. koi_net/behaviors/sync_manager.py +68 -0
  5. koi_net/build/artifact.py +209 -0
  6. koi_net/build/assembler.py +60 -0
  7. koi_net/build/comp_order.py +6 -0
  8. koi_net/build/comp_type.py +7 -0
  9. koi_net/build/consts.py +18 -0
  10. koi_net/build/container.py +46 -0
  11. koi_net/cache.py +81 -0
  12. koi_net/config/core.py +113 -0
  13. koi_net/config/full_node.py +45 -0
  14. koi_net/config/loader.py +60 -0
  15. koi_net/config/partial_node.py +26 -0
  16. koi_net/config/proxy.py +20 -0
  17. koi_net/core.py +78 -0
  18. koi_net/effector.py +147 -0
  19. koi_net/entrypoints/__init__.py +2 -0
  20. koi_net/entrypoints/base.py +8 -0
  21. koi_net/entrypoints/poller.py +43 -0
  22. koi_net/entrypoints/server.py +85 -0
  23. koi_net/exceptions.py +107 -0
  24. koi_net/identity.py +20 -0
  25. koi_net/log_system.py +133 -0
  26. koi_net/network/__init__.py +0 -0
  27. koi_net/network/error_handler.py +63 -0
  28. koi_net/network/event_buffer.py +91 -0
  29. koi_net/network/event_queue.py +31 -0
  30. koi_net/network/graph.py +123 -0
  31. koi_net/network/request_handler.py +244 -0
  32. koi_net/network/resolver.py +152 -0
  33. koi_net/network/response_handler.py +130 -0
  34. koi_net/processor/__init__.py +0 -0
  35. koi_net/processor/context.py +36 -0
  36. koi_net/processor/handler.py +61 -0
  37. koi_net/processor/knowledge_handlers.py +302 -0
  38. koi_net/processor/knowledge_object.py +135 -0
  39. koi_net/processor/kobj_queue.py +51 -0
  40. koi_net/processor/pipeline.py +222 -0
  41. koi_net/protocol/__init__.py +0 -0
  42. koi_net/protocol/api_models.py +67 -0
  43. koi_net/protocol/consts.py +7 -0
  44. koi_net/protocol/edge.py +50 -0
  45. koi_net/protocol/envelope.py +65 -0
  46. koi_net/protocol/errors.py +24 -0
  47. koi_net/protocol/event.py +51 -0
  48. koi_net/protocol/model_map.py +62 -0
  49. koi_net/protocol/node.py +18 -0
  50. koi_net/protocol/secure.py +167 -0
  51. koi_net/secure_manager.py +115 -0
  52. koi_net/workers/__init__.py +2 -0
  53. koi_net/workers/base.py +26 -0
  54. koi_net/workers/event_worker.py +111 -0
  55. koi_net/workers/kobj_worker.py +51 -0
  56. koi_net-1.2.4.dist-info/METADATA +485 -0
  57. koi_net-1.2.4.dist-info/RECORD +59 -0
  58. koi_net-1.2.4.dist-info/WHEEL +4 -0
  59. koi_net-1.2.4.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,244 @@
1
+ from functools import wraps
2
+
3
+ import structlog
4
+ import httpx
5
+ from rid_lib import RID
6
+ from rid_lib.ext import Cache
7
+ from rid_lib.types import KoiNetNode
8
+ from pydantic import ValidationError
9
+
10
+ from ..identity import NodeIdentity
11
+ from ..protocol.api_models import (
12
+ RidsPayload,
13
+ ManifestsPayload,
14
+ BundlesPayload,
15
+ EventsPayload,
16
+ FetchRids,
17
+ FetchManifests,
18
+ FetchBundles,
19
+ PollEvents,
20
+ RequestModels,
21
+ ResponseModels,
22
+ ErrorResponse
23
+ )
24
+ from ..protocol.consts import (
25
+ BROADCAST_EVENTS_PATH,
26
+ POLL_EVENTS_PATH,
27
+ FETCH_RIDS_PATH,
28
+ FETCH_MANIFESTS_PATH,
29
+ FETCH_BUNDLES_PATH
30
+ )
31
+ from ..protocol.errors import ErrorType
32
+ from ..protocol.node import NodeProfile, NodeType
33
+ from ..protocol.model_map import API_MODEL_MAP
34
+ from ..secure_manager import SecureManager
35
+ from ..exceptions import (
36
+ RemoteInvalidKeyError,
37
+ RemoteInvalidSignatureError,
38
+ RemoteInvalidTargetError,
39
+ RequestError,
40
+ SelfRequestError,
41
+ PartialNodeQueryError,
42
+ NodeNotFoundError,
43
+ ServerError,
44
+ TransportError,
45
+ RemoteUnknownNodeError
46
+ )
47
+ from .error_handler import ErrorHandler
48
+
49
+ log = structlog.stdlib.get_logger()
50
+
51
+
52
+ def report_exception(func):
53
+ """Logs request errors as warnings."""
54
+ @wraps(func)
55
+ def wrapper(*args, **kwargs):
56
+ try:
57
+ return func(*args, **kwargs)
58
+ except RequestError as err:
59
+ log.warning(err)
60
+ raise
61
+ return wrapper
62
+
63
+ class RequestHandler:
64
+ """Handles making requests to other KOI nodes."""
65
+
66
+ cache: Cache
67
+ identity: NodeIdentity
68
+ secure_manager: SecureManager
69
+ error_handler: ErrorHandler
70
+
71
+ def __init__(
72
+ self,
73
+ cache: Cache,
74
+ identity: NodeIdentity,
75
+ secure_manager: SecureManager,
76
+ error_handler: ErrorHandler
77
+ ):
78
+ self.cache = cache
79
+ self.identity = identity
80
+ self.secure_manager = secure_manager
81
+ self.error_handler = error_handler
82
+
83
+ def get_base_url(self, node_rid: KoiNetNode) -> str:
84
+ """Retrieves URL of a node from its RID."""
85
+
86
+ node_bundle = self.cache.read(node_rid)
87
+ if node_bundle:
88
+ node_profile = node_bundle.validate_contents(NodeProfile)
89
+ if node_profile.node_type != NodeType.FULL:
90
+ raise PartialNodeQueryError("Partial nodes don't have URLs")
91
+ node_url = node_profile.base_url
92
+
93
+ elif node_rid == self.identity.config.koi_net.first_contact.rid:
94
+ node_url = self.identity.config.koi_net.first_contact.url
95
+
96
+ else:
97
+ raise NodeNotFoundError(f"URL not found for {node_rid!r}")
98
+
99
+ log.debug(f"Resolved {node_rid!r} to {node_url}")
100
+ return node_url
101
+
102
+ @report_exception
103
+ def make_request(
104
+ self,
105
+ node: KoiNetNode,
106
+ path: str,
107
+ request: RequestModels,
108
+ ) -> ResponseModels | None:
109
+ """Makes a request to a node."""
110
+ if node == self.identity.rid:
111
+ raise SelfRequestError("Don't talk to yourself")
112
+
113
+ url = self.get_base_url(node) + path
114
+ log.info(f"Making request to {url}")
115
+
116
+ signed_envelope = self.secure_manager.create_envelope(
117
+ payload=request,
118
+ target=node
119
+ )
120
+
121
+ data = signed_envelope.model_dump_json(exclude_none=True)
122
+
123
+ try:
124
+ result = httpx.post(url, data=data)
125
+ result.raise_for_status()
126
+ self.error_handler.reset_timeout_counter(node)
127
+
128
+ except httpx.RequestError as e:
129
+ log.debug("Failed to connect")
130
+ self.error_handler.handle_connection_error(node)
131
+ raise TransportError(e)
132
+
133
+ except httpx.HTTPStatusError:
134
+ """Possible errors:
135
+
136
+ 4xx - KOI-net protocol error, validate body
137
+ 404/405 - not implementing endpoints, or misconfigured URL
138
+
139
+ 500 - internal server error
140
+ """
141
+ try:
142
+ resp = ErrorResponse.model_validate_json(result.text)
143
+ self.error_handler.handle_protocol_error(resp.error, node)
144
+
145
+ match resp.error:
146
+ case ErrorType.UnknownNode:
147
+ raise RemoteUnknownNodeError(f"Peer couldn't resolve this node's RID")
148
+ case ErrorType.InvalidKey:
149
+ raise RemoteInvalidKeyError(f"Peer marked this node's public key as invalid")
150
+ case ErrorType.InvalidSignature:
151
+ raise RemoteInvalidSignatureError("Peer marked envelope signature as invalid")
152
+ case ErrorType.InvalidTarget:
153
+ raise RemoteInvalidTargetError("Envelope target is not the peer node")
154
+
155
+ except ValidationError as e:
156
+ raise ServerError(e)
157
+
158
+
159
+ resp_env_model = API_MODEL_MAP[path].response_envelope
160
+ if not resp_env_model:
161
+ return
162
+
163
+ try:
164
+ resp_envelope = resp_env_model.model_validate_json(result.text)
165
+ except ValidationError as e:
166
+ raise ServerError(e)
167
+
168
+ self.secure_manager.validate_envelope(resp_envelope)
169
+
170
+ return resp_envelope.payload
171
+
172
+ def broadcast_events(
173
+ self,
174
+ node: RID,
175
+ req: EventsPayload | None = None,
176
+ **kwargs
177
+ ) -> None:
178
+ """Broadcasts events to a node.
179
+
180
+ Pass `EventsPayload` object, or see `protocol.api_models.EventsPayload` for available kwargs.
181
+ """
182
+ request = req or EventsPayload.model_validate(kwargs)
183
+ self.make_request(node, BROADCAST_EVENTS_PATH, request)
184
+ log.info(f"Broadcasted {len(request.events)} event(s) to {node!r}")
185
+
186
+ def poll_events(
187
+ self,
188
+ node: RID,
189
+ req: PollEvents | None = None,
190
+ **kwargs
191
+ ) -> EventsPayload:
192
+ """Polls events from a node.
193
+
194
+ Pass `PollEvents` object as `req` or fields as kwargs.
195
+ """
196
+ request = req or PollEvents.model_validate(kwargs)
197
+ resp = self.make_request(node, POLL_EVENTS_PATH, request)
198
+ log.info(f"Polled {len(resp.events)} events from {node!r}")
199
+ return resp
200
+
201
+ def fetch_rids(
202
+ self,
203
+ node: RID,
204
+ req: FetchRids | None = None,
205
+ **kwargs
206
+ ) -> RidsPayload:
207
+ """Fetches RIDs from a node.
208
+
209
+ Pass `FetchRids` object as `req` or fields as kwargs.
210
+ """
211
+ request = req or FetchRids.model_validate(kwargs)
212
+ resp = self.make_request(node, FETCH_RIDS_PATH, request)
213
+ log.info(f"Fetched {len(resp.rids)} RID(s) from {node!r}")
214
+ return resp
215
+
216
+ def fetch_manifests(
217
+ self,
218
+ node: RID,
219
+ req: FetchManifests | None = None,
220
+ **kwargs
221
+ ) -> ManifestsPayload:
222
+ """Fetches manifests from a node.
223
+
224
+ Pass `FetchManifests` object as `req` or fields as kwargs.
225
+ """
226
+ request = req or FetchManifests.model_validate(kwargs)
227
+ resp = self.make_request(node, FETCH_MANIFESTS_PATH, request)
228
+ log.info(f"Fetched {len(resp.manifests)} manifest(s) from {node!r}")
229
+ return resp
230
+
231
+ def fetch_bundles(
232
+ self,
233
+ node: RID,
234
+ req: FetchBundles | None = None,
235
+ **kwargs
236
+ ) -> BundlesPayload:
237
+ """Fetches bundles from a node.
238
+
239
+ Pass `FetchBundles` object as `req` or fields as kwargs.
240
+ """
241
+ request = req or FetchBundles.model_validate(kwargs)
242
+ resp = self.make_request(node, FETCH_BUNDLES_PATH, request)
243
+ log.info(f"Fetched {len(resp.bundles)} bundle(s) from {node!r}")
244
+ return resp
@@ -0,0 +1,152 @@
1
+ import structlog
2
+ from rid_lib import RID
3
+ from rid_lib.core import RIDType
4
+ from rid_lib.ext import Cache, Bundle
5
+ from rid_lib.types import KoiNetNode
6
+
7
+ from .graph import NetworkGraph
8
+ from .request_handler import RequestHandler
9
+ from ..protocol.node import NodeProfile, NodeType
10
+ from ..protocol.event import Event
11
+ from ..identity import NodeIdentity
12
+ from ..config.core import NodeConfig
13
+ from ..exceptions import RequestError
14
+
15
+ log = structlog.stdlib.get_logger()
16
+
17
+
18
+ class NetworkResolver:
19
+ """Handles resolving nodes or knowledge objects from the network."""
20
+
21
+ config: NodeConfig
22
+ identity: NodeIdentity
23
+ cache: Cache
24
+ graph: NetworkGraph
25
+ request_handler: RequestHandler
26
+
27
+ def __init__(
28
+ self,
29
+ config: NodeConfig,
30
+ cache: Cache,
31
+ identity: NodeIdentity,
32
+ graph: NetworkGraph,
33
+ request_handler: RequestHandler,
34
+ ):
35
+ self.config = config
36
+ self.identity = identity
37
+ self.cache = cache
38
+ self.graph = graph
39
+ self.request_handler = request_handler
40
+
41
+ self.poll_event_queue = dict()
42
+ self.webhook_event_queue = dict()
43
+
44
+ def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
45
+ """Returns list of node RIDs which provide state for specified RID type."""
46
+
47
+ log.debug(f"Looking for state providers of {rid_type}")
48
+ provider_nodes = []
49
+ for node_rid in self.cache.list_rids(rid_types=[KoiNetNode]):
50
+ if node_rid == self.identity.rid:
51
+ continue
52
+
53
+ node_bundle = self.cache.read(node_rid)
54
+ node_profile = node_bundle.validate_contents(NodeProfile)
55
+
56
+ if node_profile.node_type != NodeType.FULL:
57
+ continue
58
+
59
+ if rid_type not in node_profile.provides.state:
60
+ continue
61
+
62
+ provider_nodes.append(node_rid)
63
+
64
+ if provider_nodes:
65
+ log.debug(f"Found provider(s) {provider_nodes}")
66
+ else:
67
+ log.debug("Failed to find providers")
68
+
69
+ return provider_nodes
70
+
71
+ def fetch_remote_bundle(self, rid: RID) -> tuple[Bundle | None, KoiNetNode | None]:
72
+ """Attempts to fetch a bundle by RID from known peer nodes."""
73
+
74
+ log.debug(f"Fetching remote bundle {rid!r}")
75
+ remote_bundle, node_rid = None, None
76
+ for node_rid in self.get_state_providers(type(rid)):
77
+ try:
78
+ payload = self.request_handler.fetch_bundles(
79
+ node=node_rid, rids=[rid])
80
+ except RequestError:
81
+ continue
82
+
83
+ if payload.bundles:
84
+ remote_bundle = payload.bundles[0]
85
+ log.debug(f"Got bundle from {node_rid!r}")
86
+ break
87
+
88
+ if not remote_bundle:
89
+ log.warning("Failed to fetch remote bundle")
90
+
91
+ return remote_bundle, node_rid
92
+
93
+ def fetch_remote_manifest(self, rid: RID) -> tuple[Bundle | None, KoiNetNode | None]:
94
+ """Attempts to fetch a manifest by RID from known peer nodes."""
95
+
96
+ log.debug(f"Fetching remote manifest {rid!r}")
97
+ remote_manifest, node_rid = None, None
98
+ for node_rid in self.get_state_providers(type(rid)):
99
+ try:
100
+ payload = self.request_handler.fetch_manifests(
101
+ node=node_rid, rids=[rid])
102
+ except RequestError:
103
+ continue
104
+
105
+ if payload.manifests:
106
+ remote_manifest = payload.manifests[0]
107
+ log.debug(f"Got bundle from {node_rid!r}")
108
+ break
109
+
110
+ if not remote_manifest:
111
+ log.warning("Failed to fetch remote bundle")
112
+
113
+ return remote_manifest, node_rid
114
+
115
+ def poll_neighbors(self) -> dict[KoiNetNode, list[Event]]:
116
+ """Polls all neighbor nodes and returns compiled list of events.
117
+
118
+ Neighbor nodes include any node this node shares an edge with,
119
+ or the first contact, if no neighbors are found.
120
+
121
+ NOTE: This function does not poll nodes that don't share edges
122
+ with this node. Events sent by non neighboring nodes will not
123
+ be polled.
124
+ """
125
+
126
+ neighbors: list[KoiNetNode] = []
127
+ for node_rid in self.graph.get_neighbors():
128
+ node_bundle = self.cache.read(node_rid)
129
+ if not node_bundle:
130
+ continue
131
+ node_profile = node_bundle.validate_contents(NodeProfile)
132
+ if node_profile.node_type != NodeType.FULL:
133
+ continue
134
+ neighbors.append(node_rid)
135
+
136
+ if not neighbors and self.config.koi_net.first_contact.rid:
137
+ neighbors.append(self.config.koi_net.first_contact.rid)
138
+
139
+ event_dict: dict[KoiNetNode, list[Event]] = {}
140
+ for node_rid in neighbors:
141
+ try:
142
+ payload = self.request_handler.poll_events(
143
+ node=node_rid,
144
+ rid=self.identity.rid
145
+ )
146
+ except RequestError:
147
+ continue
148
+
149
+ log.debug(f"Received {len(payload.events)} events from {node_rid!r}")
150
+ event_dict[node_rid] = payload.events
151
+
152
+ return event_dict
@@ -0,0 +1,130 @@
1
+ import structlog
2
+ from rid_lib import RID
3
+ from rid_lib.types import KoiNetNode
4
+ from rid_lib.ext import Manifest, Cache
5
+ from rid_lib.ext.bundle import Bundle
6
+
7
+ from ..processor.kobj_queue import KobjQueue
8
+ from ..protocol.consts import BROADCAST_EVENTS_PATH, FETCH_BUNDLES_PATH, FETCH_MANIFESTS_PATH, FETCH_RIDS_PATH, POLL_EVENTS_PATH
9
+ from ..protocol.envelope import SignedEnvelope
10
+ from ..secure_manager import SecureManager
11
+ from .event_buffer import EventBuffer
12
+
13
+
14
+ from ..protocol.api_models import (
15
+ EventsPayload,
16
+ PollEvents,
17
+ RidsPayload,
18
+ ManifestsPayload,
19
+ BundlesPayload,
20
+ FetchRids,
21
+ FetchManifests,
22
+ FetchBundles,
23
+ )
24
+
25
+ log = structlog.stdlib.get_logger()
26
+
27
+
28
+ class ResponseHandler:
29
+ """Handles generating responses to requests from other KOI nodes."""
30
+
31
+ cache: Cache
32
+ kobj_queue: KobjQueue
33
+ poll_event_buf: EventBuffer
34
+
35
+ def __init__(
36
+ self,
37
+ cache: Cache,
38
+ kobj_queue: KobjQueue,
39
+ poll_event_buf: EventBuffer,
40
+ secure_manager: SecureManager
41
+ ):
42
+ self.cache = cache
43
+ self.kobj_queue = kobj_queue
44
+ self.poll_event_buf = poll_event_buf
45
+ self.secure_manager = secure_manager
46
+
47
+ def handle_response(self, path: str, req: SignedEnvelope):
48
+ self.secure_manager.validate_envelope(req)
49
+
50
+ response_map = {
51
+ BROADCAST_EVENTS_PATH: self.broadcast_events_handler,
52
+ POLL_EVENTS_PATH: self.poll_events_handler,
53
+ FETCH_RIDS_PATH: self.fetch_rids_handler,
54
+ FETCH_MANIFESTS_PATH: self.fetch_manifests_handler,
55
+ FETCH_BUNDLES_PATH: self.fetch_bundles_handler
56
+ }
57
+
58
+ response = response_map[path](req.payload, req.source_node)
59
+
60
+ if response is None:
61
+ return
62
+
63
+ return self.secure_manager.create_envelope(
64
+ payload=response,
65
+ target=req.source_node
66
+ )
67
+
68
+ def broadcast_events_handler(self, req: EventsPayload, source: KoiNetNode):
69
+ log.info(f"Request to broadcast events, received {len(req.events)} event(s)")
70
+
71
+ for event in req.events:
72
+ self.kobj_queue.push(event=event, source=source)
73
+
74
+ def poll_events_handler(
75
+ self,
76
+ req: PollEvents,
77
+ source: KoiNetNode
78
+ ) -> EventsPayload:
79
+ events = self.poll_event_buf.flush(source, limit=req.limit)
80
+ log.info(f"Request to poll events, returning {len(events)} event(s)")
81
+ return EventsPayload(events=events)
82
+
83
+ def fetch_rids_handler(
84
+ self,
85
+ req: FetchRids,
86
+ source: KoiNetNode
87
+ ) -> RidsPayload:
88
+ """Returns response to fetch RIDs request."""
89
+ rids = self.cache.list_rids(req.rid_types)
90
+ log.info(f"Request to fetch rids, allowed types {req.rid_types}, returning {len(rids)} RID(s)")
91
+ return RidsPayload(rids=rids)
92
+
93
+ def fetch_manifests_handler(
94
+ self,
95
+ req: FetchManifests,
96
+ source: KoiNetNode
97
+ ) -> ManifestsPayload:
98
+ """Returns response to fetch manifests request."""
99
+ manifests: list[Manifest] = []
100
+ not_found: list[RID] = []
101
+
102
+ for rid in (req.rids or self.cache.list_rids(req.rid_types)):
103
+ bundle = self.cache.read(rid)
104
+ if bundle:
105
+ manifests.append(bundle.manifest)
106
+ else:
107
+ not_found.append(rid)
108
+
109
+ log.info(f"Request to fetch manifests, allowed types {req.rid_types}, rids {req.rids}, returning {len(manifests)} manifest(s)")
110
+ return ManifestsPayload(manifests=manifests, not_found=not_found)
111
+
112
+ def fetch_bundles_handler(
113
+ self,
114
+ req: FetchBundles,
115
+ source: KoiNetNode
116
+ ) -> BundlesPayload:
117
+ """Returns response to fetch bundles request."""
118
+
119
+ bundles: list[Bundle] = []
120
+ not_found: list[RID] = []
121
+
122
+ for rid in req.rids:
123
+ bundle = self.cache.read(rid)
124
+ if bundle:
125
+ bundles.append(bundle)
126
+ else:
127
+ not_found.append(rid)
128
+
129
+ log.info(f"Request to fetch bundles, requested rids {req.rids}, returning {len(bundles)} bundle(s)")
130
+ return BundlesPayload(bundles=bundles, not_found=not_found)
File without changes
@@ -0,0 +1,36 @@
1
+ from typing import TYPE_CHECKING
2
+ from dataclasses import dataclass, field
3
+
4
+ from rid_lib.ext import Cache
5
+
6
+ from ..network.resolver import NetworkResolver
7
+ from ..config.core import NodeConfig
8
+ from ..config.loader import ConfigLoader
9
+ from ..network.graph import NetworkGraph
10
+ from ..network.event_queue import EventQueue
11
+ from ..network.request_handler import RequestHandler
12
+ from ..identity import NodeIdentity
13
+ from .kobj_queue import KobjQueue
14
+
15
+ if TYPE_CHECKING:
16
+ from ..effector import Effector
17
+
18
+
19
+ @dataclass
20
+ class HandlerContext:
21
+ """Context object provides knowledge handlers access to other components."""
22
+
23
+ identity: NodeIdentity
24
+ config: NodeConfig
25
+ config_loader: ConfigLoader
26
+ cache: Cache
27
+ event_queue: EventQueue
28
+ kobj_queue: KobjQueue
29
+ graph: NetworkGraph
30
+ request_handler: RequestHandler
31
+ resolver: NetworkResolver
32
+ effector: "Effector" = field(init=False)
33
+
34
+ def set_effector(self, effector: "Effector"):
35
+ """Post initialization injection of effector component."""
36
+ self.effector = effector
@@ -0,0 +1,61 @@
1
+ from dataclasses import dataclass
2
+ from enum import StrEnum
3
+ from typing import Callable
4
+ from rid_lib.core import RIDType
5
+
6
+ from ..protocol.event import EventType
7
+ from .knowledge_object import KnowledgeObject
8
+ from .context import HandlerContext
9
+
10
+
11
+ class StopChain:
12
+ """Class for STOP_CHAIN sentinel returned by knowledge handlers."""
13
+ pass
14
+
15
+ STOP_CHAIN = StopChain()
16
+
17
+
18
+ class HandlerType(StrEnum):
19
+ """Types of handlers used in knowledge processing pipeline.
20
+
21
+ - 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).
22
+ - Manifest - provided RID, manifest; decides whether to validate the bundle (and fetch it if not provided).
23
+ - Bundle - provided RID, manifest, contents (bundle); decides whether to write knowledge to the cache by setting the normalized event type to `NEW` or `UPDATE`.
24
+ - 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.)
25
+ - Final - provided RID, manifests, contents (bundle); final action taken after network broadcast.
26
+ """
27
+
28
+ RID = "rid",
29
+ Manifest = "manifest",
30
+ Bundle = "bundle",
31
+ Network = "network",
32
+ Final = "final"
33
+
34
+ @dataclass
35
+ class KnowledgeHandler:
36
+ """Handles knowledge processing events of the provided types."""
37
+
38
+ func: Callable[[HandlerContext, KnowledgeObject], None | KnowledgeObject | StopChain]
39
+ handler_type: HandlerType
40
+ rid_types: tuple[RIDType] = ()
41
+ event_types: tuple[EventType | None] = ()
42
+
43
+ def __call__(
44
+ self,
45
+ ctx: HandlerContext,
46
+ kobj: KnowledgeObject
47
+ ) -> None | KnowledgeObject | StopChain:
48
+ return self.func(ctx, kobj)
49
+
50
+ @classmethod
51
+ def create(
52
+ cls,
53
+ handler_type: HandlerType,
54
+ rid_types: tuple[RIDType] = (),
55
+ event_types: tuple[EventType | None] = ()
56
+ ):
57
+ """Decorator wraps a function, returns a KnowledgeHandler."""
58
+ def decorator(func: Callable) -> KnowledgeHandler:
59
+ handler = cls(func, handler_type, rid_types, event_types)
60
+ return handler
61
+ return decorator