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.
- koi_net/__init__.py +1 -0
- koi_net/behaviors/handshaker.py +68 -0
- koi_net/behaviors/profile_monitor.py +23 -0
- koi_net/behaviors/sync_manager.py +68 -0
- koi_net/build/artifact.py +209 -0
- koi_net/build/assembler.py +60 -0
- koi_net/build/comp_order.py +6 -0
- koi_net/build/comp_type.py +7 -0
- koi_net/build/consts.py +18 -0
- koi_net/build/container.py +46 -0
- koi_net/cache.py +81 -0
- koi_net/config/core.py +113 -0
- koi_net/config/full_node.py +45 -0
- koi_net/config/loader.py +60 -0
- koi_net/config/partial_node.py +26 -0
- koi_net/config/proxy.py +20 -0
- koi_net/core.py +78 -0
- koi_net/effector.py +147 -0
- koi_net/entrypoints/__init__.py +2 -0
- koi_net/entrypoints/base.py +8 -0
- koi_net/entrypoints/poller.py +43 -0
- koi_net/entrypoints/server.py +85 -0
- koi_net/exceptions.py +107 -0
- koi_net/identity.py +20 -0
- koi_net/log_system.py +133 -0
- koi_net/network/__init__.py +0 -0
- koi_net/network/error_handler.py +63 -0
- koi_net/network/event_buffer.py +91 -0
- koi_net/network/event_queue.py +31 -0
- koi_net/network/graph.py +123 -0
- koi_net/network/request_handler.py +244 -0
- koi_net/network/resolver.py +152 -0
- koi_net/network/response_handler.py +130 -0
- koi_net/processor/__init__.py +0 -0
- koi_net/processor/context.py +36 -0
- koi_net/processor/handler.py +61 -0
- koi_net/processor/knowledge_handlers.py +302 -0
- koi_net/processor/knowledge_object.py +135 -0
- koi_net/processor/kobj_queue.py +51 -0
- koi_net/processor/pipeline.py +222 -0
- koi_net/protocol/__init__.py +0 -0
- koi_net/protocol/api_models.py +67 -0
- koi_net/protocol/consts.py +7 -0
- koi_net/protocol/edge.py +50 -0
- koi_net/protocol/envelope.py +65 -0
- koi_net/protocol/errors.py +24 -0
- koi_net/protocol/event.py +51 -0
- koi_net/protocol/model_map.py +62 -0
- koi_net/protocol/node.py +18 -0
- koi_net/protocol/secure.py +167 -0
- koi_net/secure_manager.py +115 -0
- koi_net/workers/__init__.py +2 -0
- koi_net/workers/base.py +26 -0
- koi_net/workers/event_worker.py +111 -0
- koi_net/workers/kobj_worker.py +51 -0
- koi_net-1.2.4.dist-info/METADATA +485 -0
- koi_net-1.2.4.dist-info/RECORD +59 -0
- koi_net-1.2.4.dist-info/WHEEL +4 -0
- 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
|