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 +24 -8
- koi_net/identity.py +8 -0
- koi_net/network/graph.py +15 -0
- koi_net/network/interface.py +38 -10
- koi_net/network/request_handler.py +77 -41
- koi_net/network/response_handler.py +2 -0
- koi_net/processor/default_handlers.py +25 -6
- koi_net/processor/handler.py +37 -7
- koi_net/processor/interface.py +53 -32
- koi_net/processor/knowledge_object.py +26 -7
- koi_net/protocol/api_models.py +9 -1
- koi_net/protocol/consts.py +2 -0
- koi_net-1.0.0b3.dist-info/METADATA +510 -0
- koi_net-1.0.0b3.dist-info/RECORD +24 -0
- koi_net-1.0.0b1.dist-info/METADATA +0 -43
- koi_net-1.0.0b1.dist-info/RECORD +0 -24
- {koi_net-1.0.0b1.dist-info → koi_net-1.0.0b3.dist-info}/WHEEL +0 -0
- {koi_net-1.0.0b1.dist-info → koi_net-1.0.0b3.dist-info}/licenses/LICENSE +0 -0
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
|
|
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
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
obj for obj in vars(
|
|
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=
|
|
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
|
-
|
|
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):
|
koi_net/network/interface.py
CHANGED
|
@@ -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.
|
|
56
|
+
self._load_event_queues()
|
|
55
57
|
|
|
56
|
-
def
|
|
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
|
|
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
|
|
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.
|
|
147
|
+
return self._flush_queue(self.poll_event_queue, node)
|
|
138
148
|
|
|
139
|
-
def flush_webhook_queue(self, node:
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if
|
|
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 {
|
|
55
|
-
return
|
|
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,
|
|
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,
|
|
69
|
-
|
|
70
|
-
|
|
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,
|
|
79
|
-
|
|
80
|
-
|
|
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,
|
|
89
|
-
|
|
90
|
-
|
|
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,
|
|
99
|
-
|
|
100
|
-
|
|
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
|
+
)
|
|
@@ -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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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):
|
koi_net/processor/handler.py
CHANGED
|
@@ -4,19 +4,49 @@ from typing import Callable
|
|
|
4
4
|
from rid_lib import RIDType
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|