koi-net 1.0.0b19__py3-none-any.whl → 1.1.0b1__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/config.py +43 -18
- koi_net/context.py +55 -0
- koi_net/core.py +115 -49
- koi_net/default_actions.py +15 -0
- koi_net/effector.py +139 -0
- koi_net/identity.py +4 -22
- koi_net/network/__init__.py +0 -1
- koi_net/network/behavior.py +42 -0
- koi_net/network/error_handler.py +50 -0
- koi_net/network/{interface.py → event_queue.py} +52 -131
- koi_net/network/graph.py +18 -33
- koi_net/network/request_handler.py +112 -66
- koi_net/network/resolver.py +150 -0
- koi_net/network/response_handler.py +11 -3
- koi_net/processor/__init__.py +0 -1
- koi_net/processor/default_handlers.py +57 -41
- koi_net/processor/handler.py +3 -7
- koi_net/processor/interface.py +15 -214
- koi_net/processor/knowledge_object.py +10 -17
- koi_net/processor/knowledge_pipeline.py +220 -0
- koi_net/protocol/api_models.py +7 -1
- koi_net/protocol/edge.py +23 -1
- koi_net/protocol/envelope.py +54 -0
- koi_net/protocol/errors.py +23 -0
- koi_net/protocol/node.py +2 -1
- koi_net/protocol/secure.py +106 -0
- koi_net/secure.py +117 -0
- {koi_net-1.0.0b19.dist-info → koi_net-1.1.0b1.dist-info}/METADATA +3 -2
- koi_net-1.1.0b1.dist-info/RECORD +35 -0
- koi_net/protocol/helpers.py +0 -25
- koi_net-1.0.0b19.dist-info/RECORD +0 -25
- {koi_net-1.0.0b19.dist-info → koi_net-1.1.0b1.dist-info}/WHEEL +0 -0
- {koi_net-1.0.0b19.dist-info → koi_net-1.1.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from queue import Queue
|
|
3
|
-
from typing import Generic
|
|
4
3
|
import httpx
|
|
5
4
|
from pydantic import BaseModel
|
|
6
5
|
from rid_lib import RID
|
|
7
|
-
from rid_lib.core import RIDType
|
|
8
6
|
from rid_lib.ext import Cache
|
|
9
7
|
from rid_lib.types import KoiNetNode
|
|
10
8
|
|
|
11
9
|
from .graph import NetworkGraph
|
|
12
10
|
from .request_handler import RequestHandler
|
|
13
|
-
from .
|
|
14
|
-
from ..protocol.
|
|
15
|
-
from ..protocol.edge import EdgeType
|
|
11
|
+
from ..protocol.node import NodeProfile, NodeType
|
|
12
|
+
from ..protocol.edge import EdgeProfile, EdgeType
|
|
16
13
|
from ..protocol.event import Event
|
|
17
14
|
from ..identity import NodeIdentity
|
|
18
|
-
from ..config import
|
|
15
|
+
from ..config import NodeConfig
|
|
16
|
+
from ..effector import Effector
|
|
19
17
|
|
|
20
18
|
logger = logging.getLogger(__name__)
|
|
21
19
|
|
|
@@ -26,34 +24,36 @@ class EventQueueModel(BaseModel):
|
|
|
26
24
|
|
|
27
25
|
type EventQueue = dict[RID, Queue[Event]]
|
|
28
26
|
|
|
29
|
-
class
|
|
27
|
+
class NetworkEventQueue:
|
|
30
28
|
"""A collection of functions and classes to interact with the KOI network."""
|
|
31
29
|
|
|
32
|
-
config:
|
|
30
|
+
config: NodeConfig
|
|
33
31
|
identity: NodeIdentity
|
|
32
|
+
effector: Effector
|
|
34
33
|
cache: Cache
|
|
35
34
|
graph: NetworkGraph
|
|
36
35
|
request_handler: RequestHandler
|
|
37
|
-
response_handler: ResponseHandler
|
|
38
36
|
poll_event_queue: EventQueue
|
|
39
37
|
webhook_event_queue: EventQueue
|
|
40
38
|
|
|
41
39
|
def __init__(
|
|
42
40
|
self,
|
|
43
|
-
config:
|
|
41
|
+
config: NodeConfig,
|
|
44
42
|
cache: Cache,
|
|
45
|
-
identity: NodeIdentity
|
|
43
|
+
identity: NodeIdentity,
|
|
44
|
+
effector: Effector,
|
|
45
|
+
graph: NetworkGraph,
|
|
46
|
+
request_handler: RequestHandler,
|
|
46
47
|
):
|
|
47
48
|
self.config = config
|
|
48
49
|
self.identity = identity
|
|
49
50
|
self.cache = cache
|
|
50
|
-
self.graph =
|
|
51
|
-
self.request_handler =
|
|
52
|
-
self.
|
|
51
|
+
self.graph = graph
|
|
52
|
+
self.request_handler = request_handler
|
|
53
|
+
self.effector = effector
|
|
53
54
|
|
|
54
55
|
self.poll_event_queue = dict()
|
|
55
56
|
self.webhook_event_queue = dict()
|
|
56
|
-
self._load_event_queues()
|
|
57
57
|
|
|
58
58
|
def _load_event_queues(self):
|
|
59
59
|
"""Loads event queues from storage."""
|
|
@@ -100,29 +100,43 @@ class NetworkInterface(Generic[ConfigType]):
|
|
|
100
100
|
|
|
101
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
102
|
"""
|
|
103
|
-
logger.debug(f"Pushing event {event.event_type} {event.rid} to {node}")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if not node_profile:
|
|
107
|
-
logger.warning(f"Node {node!r} unknown to me")
|
|
103
|
+
logger.debug(f"Pushing event {event.event_type} {event.rid!r} to {node}")
|
|
104
|
+
|
|
105
|
+
node_bundle = self.effector.deref(node)
|
|
108
106
|
|
|
109
107
|
# if there's an edge from me to the target node, override broadcast type
|
|
110
|
-
|
|
108
|
+
edge_rid = self.graph.get_edge(
|
|
111
109
|
source=self.identity.rid,
|
|
112
110
|
target=node
|
|
113
111
|
)
|
|
114
112
|
|
|
115
|
-
if
|
|
113
|
+
edge_bundle = self.effector.deref(edge_rid) if edge_rid else None
|
|
114
|
+
|
|
115
|
+
if edge_bundle:
|
|
116
|
+
logger.debug(f"Found edge from me to {node!r}")
|
|
117
|
+
edge_profile = edge_bundle.validate_contents(EdgeProfile)
|
|
116
118
|
if edge_profile.edge_type == EdgeType.WEBHOOK:
|
|
117
119
|
event_queue = self.webhook_event_queue
|
|
118
120
|
elif edge_profile.edge_type == EdgeType.POLL:
|
|
119
121
|
event_queue = self.poll_event_queue
|
|
120
|
-
|
|
122
|
+
|
|
123
|
+
elif node_bundle:
|
|
124
|
+
logger.debug(f"Found bundle for {node!r}")
|
|
125
|
+
node_profile = node_bundle.validate_contents(NodeProfile)
|
|
121
126
|
if node_profile.node_type == NodeType.FULL:
|
|
122
127
|
event_queue = self.webhook_event_queue
|
|
123
128
|
elif node_profile.node_type == NodeType.PARTIAL:
|
|
124
129
|
event_queue = self.poll_event_queue
|
|
125
130
|
|
|
131
|
+
elif node == self.config.koi_net.first_contact.rid:
|
|
132
|
+
logger.debug(f"Node {node!r} is my first contact")
|
|
133
|
+
# first contact node is always a webhook node
|
|
134
|
+
event_queue = self.webhook_event_queue
|
|
135
|
+
|
|
136
|
+
else:
|
|
137
|
+
logger.warning(f"Node {node!r} unknown to me")
|
|
138
|
+
return
|
|
139
|
+
|
|
126
140
|
queue = event_queue.setdefault(node, Queue())
|
|
127
141
|
queue.put(event)
|
|
128
142
|
|
|
@@ -136,7 +150,7 @@ class NetworkInterface(Generic[ConfigType]):
|
|
|
136
150
|
if queue:
|
|
137
151
|
while not queue.empty():
|
|
138
152
|
event = queue.get()
|
|
139
|
-
logger.debug(f"Dequeued {event.event_type}
|
|
153
|
+
logger.debug(f"Dequeued {event.event_type} {event.rid!r}")
|
|
140
154
|
events.append(event)
|
|
141
155
|
|
|
142
156
|
return events
|
|
@@ -146,7 +160,7 @@ class NetworkInterface(Generic[ConfigType]):
|
|
|
146
160
|
logger.debug(f"Flushing poll queue for {node}")
|
|
147
161
|
return self._flush_queue(self.poll_event_queue, node)
|
|
148
162
|
|
|
149
|
-
def flush_webhook_queue(self, node: KoiNetNode):
|
|
163
|
+
def flush_webhook_queue(self, node: KoiNetNode, requeue_on_fail: bool = True):
|
|
150
164
|
"""Flushes a node's webhook queue, and broadcasts events.
|
|
151
165
|
|
|
152
166
|
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.
|
|
@@ -154,15 +168,17 @@ class NetworkInterface(Generic[ConfigType]):
|
|
|
154
168
|
|
|
155
169
|
logger.debug(f"Flushing webhook queue for {node}")
|
|
156
170
|
|
|
157
|
-
|
|
171
|
+
# node_bundle = self.effector.deref(node)
|
|
158
172
|
|
|
159
|
-
if not
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
# if not node_bundle:
|
|
174
|
+
# logger.warning(f"{node!r} not found")
|
|
175
|
+
# return
|
|
162
176
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
# node_profile = node_bundle.validate_contents(NodeProfile)
|
|
178
|
+
|
|
179
|
+
# if node_profile.node_type != NodeType.FULL:
|
|
180
|
+
# logger.warning(f"{node!r} is a partial node!")
|
|
181
|
+
# return
|
|
166
182
|
|
|
167
183
|
events = self._flush_queue(self.webhook_event_queue, node)
|
|
168
184
|
if not events: return
|
|
@@ -174,103 +190,8 @@ class NetworkInterface(Generic[ConfigType]):
|
|
|
174
190
|
return True
|
|
175
191
|
except httpx.ConnectError:
|
|
176
192
|
logger.warning("Broadcast failed")
|
|
177
|
-
for event in events:
|
|
178
|
-
self.push_event_to(event, node)
|
|
179
|
-
return False
|
|
180
|
-
|
|
181
|
-
def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
|
|
182
|
-
"""Returns list of node RIDs which provide state for the specified RID type."""
|
|
183
|
-
|
|
184
|
-
logger.debug(f"Looking for state providers of '{rid_type}'")
|
|
185
|
-
provider_nodes = []
|
|
186
|
-
for node_rid in self.cache.list_rids(rid_types=[KoiNetNode]):
|
|
187
|
-
node = self.graph.get_node_profile(node_rid)
|
|
188
|
-
|
|
189
|
-
if node.node_type == NodeType.FULL and rid_type in node.provides.state:
|
|
190
|
-
logger.debug(f"Found provider '{node_rid}'")
|
|
191
|
-
provider_nodes.append(node_rid)
|
|
192
|
-
|
|
193
|
-
if not provider_nodes:
|
|
194
|
-
logger.debug("Failed to find providers")
|
|
195
|
-
return provider_nodes
|
|
196
193
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
remote_bundle = None
|
|
202
|
-
for node_rid in self.get_state_providers(type(rid)):
|
|
203
|
-
payload = self.request_handler.fetch_bundles(
|
|
204
|
-
node=node_rid, rids=[rid])
|
|
205
|
-
|
|
206
|
-
if payload.bundles:
|
|
207
|
-
remote_bundle = payload.bundles[0]
|
|
208
|
-
logger.debug(f"Got bundle from '{node_rid}'")
|
|
209
|
-
break
|
|
210
|
-
|
|
211
|
-
if not remote_bundle:
|
|
212
|
-
logger.warning("Failed to fetch remote bundle")
|
|
213
|
-
|
|
214
|
-
return remote_bundle
|
|
215
|
-
|
|
216
|
-
def fetch_remote_manifest(self, rid: RID):
|
|
217
|
-
"""Attempts to fetch a manifest by RID from known peer nodes."""
|
|
218
|
-
|
|
219
|
-
logger.debug(f"Fetching remote manifest '{rid}'")
|
|
220
|
-
remote_manifest = None
|
|
221
|
-
for node_rid in self.get_state_providers(type(rid)):
|
|
222
|
-
payload = self.request_handler.fetch_manifests(
|
|
223
|
-
node=node_rid, rids=[rid])
|
|
224
|
-
|
|
225
|
-
if payload.manifests:
|
|
226
|
-
remote_manifest = payload.manifests[0]
|
|
227
|
-
logger.debug(f"Got bundle from '{node_rid}'")
|
|
228
|
-
break
|
|
229
|
-
|
|
230
|
-
if not remote_manifest:
|
|
231
|
-
logger.warning("Failed to fetch remote bundle")
|
|
232
|
-
|
|
233
|
-
return remote_manifest
|
|
234
|
-
|
|
235
|
-
def poll_neighbors(self) -> list[Event]:
|
|
236
|
-
"""Polls all neighboring nodes and returns compiled list of events.
|
|
237
|
-
|
|
238
|
-
If this node has no neighbors, it will instead attempt to poll the provided first contact URL.
|
|
239
|
-
"""
|
|
240
|
-
|
|
241
|
-
neighbors = self.graph.get_neighbors()
|
|
242
|
-
|
|
243
|
-
if not neighbors and self.config.koi_net.first_contact:
|
|
244
|
-
logger.debug("No neighbors found, polling first contact")
|
|
245
|
-
try:
|
|
246
|
-
payload = self.request_handler.poll_events(
|
|
247
|
-
url=self.config.koi_net.first_contact,
|
|
248
|
-
rid=self.identity.rid
|
|
249
|
-
)
|
|
250
|
-
if payload.events:
|
|
251
|
-
logger.debug(f"Received {len(payload.events)} events from '{self.config.koi_net.first_contact}'")
|
|
252
|
-
return payload.events
|
|
253
|
-
except httpx.ConnectError:
|
|
254
|
-
logger.debug(f"Failed to reach first contact '{self.config.koi_net.first_contact}'")
|
|
255
|
-
|
|
256
|
-
events = []
|
|
257
|
-
for node_rid in neighbors:
|
|
258
|
-
node = self.graph.get_node_profile(node_rid)
|
|
259
|
-
if not node: continue
|
|
260
|
-
if node.node_type != NodeType.FULL: continue
|
|
261
|
-
|
|
262
|
-
try:
|
|
263
|
-
payload = self.request_handler.poll_events(
|
|
264
|
-
node=node_rid,
|
|
265
|
-
rid=self.identity.rid
|
|
266
|
-
)
|
|
267
|
-
if payload.events:
|
|
268
|
-
logger.debug(f"Received {len(payload.events)} events from {node_rid!r}")
|
|
269
|
-
events.extend(payload.events)
|
|
270
|
-
except httpx.ConnectError:
|
|
271
|
-
logger.debug(f"Failed to reach node '{node_rid}'")
|
|
272
|
-
continue
|
|
273
|
-
|
|
274
|
-
return events
|
|
275
|
-
|
|
276
|
-
|
|
194
|
+
if requeue_on_fail:
|
|
195
|
+
for event in events:
|
|
196
|
+
self.push_event_to(event, node)
|
|
197
|
+
return False
|
koi_net/network/graph.py
CHANGED
|
@@ -6,7 +6,6 @@ from rid_lib.ext import Cache
|
|
|
6
6
|
from rid_lib.types import KoiNetEdge, KoiNetNode
|
|
7
7
|
from ..identity import NodeIdentity
|
|
8
8
|
from ..protocol.edge import EdgeProfile, EdgeStatus
|
|
9
|
-
from ..protocol.node import NodeProfile
|
|
10
9
|
|
|
11
10
|
logger = logging.getLogger(__name__)
|
|
12
11
|
|
|
@@ -30,43 +29,27 @@ class NetworkGraph:
|
|
|
30
29
|
for rid in self.cache.list_rids():
|
|
31
30
|
if type(rid) == KoiNetNode:
|
|
32
31
|
self.dg.add_node(rid)
|
|
33
|
-
logger.debug(f"Added node {rid}")
|
|
32
|
+
logger.debug(f"Added node {rid!r}")
|
|
34
33
|
|
|
35
34
|
elif type(rid) == KoiNetEdge:
|
|
36
|
-
|
|
37
|
-
if not
|
|
35
|
+
edge_bundle = self.cache.read(rid)
|
|
36
|
+
if not edge_bundle:
|
|
38
37
|
logger.warning(f"Failed to load {rid!r}")
|
|
39
38
|
continue
|
|
39
|
+
edge_profile = edge_bundle.validate_contents(EdgeProfile)
|
|
40
40
|
self.dg.add_edge(edge_profile.source, edge_profile.target, rid=rid)
|
|
41
|
-
logger.debug(f"Added edge {rid} ({edge_profile.source} -> {edge_profile.target})")
|
|
41
|
+
logger.debug(f"Added edge {rid!r} ({edge_profile.source} -> {edge_profile.target})")
|
|
42
42
|
logger.debug("Done")
|
|
43
43
|
|
|
44
|
-
def
|
|
45
|
-
"""Returns
|
|
46
|
-
|
|
47
|
-
if bundle:
|
|
48
|
-
return bundle.validate_contents(NodeProfile)
|
|
49
|
-
|
|
50
|
-
def get_edge_profile(
|
|
51
|
-
self,
|
|
52
|
-
rid: KoiNetEdge | None = None,
|
|
53
|
-
source: KoiNetNode | None = None,
|
|
54
|
-
target: KoiNetNode | None = None,
|
|
55
|
-
) -> EdgeProfile | None:
|
|
56
|
-
"""Returns edge profile given its RID, or source and target node RIDs."""
|
|
57
|
-
if source and target:
|
|
58
|
-
if (source, target) not in self.dg.edges: return
|
|
44
|
+
def get_edge(self, source: KoiNetNode, target: KoiNetNode,) -> EdgeProfile | None:
|
|
45
|
+
"""Returns edge RID given the RIDs of a source and target node."""
|
|
46
|
+
if (source, target) in self.dg.edges:
|
|
59
47
|
edge_data = self.dg.get_edge_data(source, target)
|
|
60
|
-
if
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
bundle = self.cache.read(rid)
|
|
67
|
-
if bundle:
|
|
68
|
-
return bundle.validate_contents(EdgeProfile)
|
|
69
|
-
|
|
48
|
+
if edge_data:
|
|
49
|
+
return edge_data.get("rid")
|
|
50
|
+
|
|
51
|
+
return None
|
|
52
|
+
|
|
70
53
|
def get_edges(
|
|
71
54
|
self,
|
|
72
55
|
direction: Literal["in", "out"] | None = None,
|
|
@@ -106,12 +89,14 @@ class NetworkGraph:
|
|
|
106
89
|
|
|
107
90
|
neighbors = []
|
|
108
91
|
for edge_rid in self.get_edges(direction):
|
|
109
|
-
|
|
92
|
+
edge_bundle = self.cache.read(edge_rid)
|
|
110
93
|
|
|
111
|
-
if not
|
|
94
|
+
if not edge_bundle:
|
|
112
95
|
logger.warning(f"Failed to find edge {edge_rid!r} in cache")
|
|
113
96
|
continue
|
|
114
|
-
|
|
97
|
+
|
|
98
|
+
edge_profile = edge_bundle.validate_contents(EdgeProfile)
|
|
99
|
+
|
|
115
100
|
if status and edge_profile.status != status:
|
|
116
101
|
continue
|
|
117
102
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import httpx
|
|
3
3
|
from rid_lib import RID
|
|
4
|
-
from rid_lib.ext import Cache
|
|
5
4
|
from rid_lib.types.koi_net_node import KoiNetNode
|
|
5
|
+
|
|
6
|
+
from ..identity import NodeIdentity
|
|
6
7
|
from ..protocol.api_models import (
|
|
7
8
|
RidsPayload,
|
|
8
9
|
ManifestsPayload,
|
|
@@ -13,8 +14,10 @@ from ..protocol.api_models import (
|
|
|
13
14
|
FetchBundles,
|
|
14
15
|
PollEvents,
|
|
15
16
|
RequestModels,
|
|
16
|
-
ResponseModels
|
|
17
|
+
ResponseModels,
|
|
18
|
+
ErrorResponse
|
|
17
19
|
)
|
|
20
|
+
from ..protocol.envelope import SignedEnvelope
|
|
18
21
|
from ..protocol.consts import (
|
|
19
22
|
BROADCAST_EVENTS_PATH,
|
|
20
23
|
POLL_EVENTS_PATH,
|
|
@@ -22,8 +25,13 @@ from ..protocol.consts import (
|
|
|
22
25
|
FETCH_MANIFESTS_PATH,
|
|
23
26
|
FETCH_BUNDLES_PATH
|
|
24
27
|
)
|
|
25
|
-
from ..protocol.node import NodeType
|
|
26
|
-
from
|
|
28
|
+
from ..protocol.node import NodeProfile, NodeType
|
|
29
|
+
from ..secure import Secure
|
|
30
|
+
from ..effector import Effector
|
|
31
|
+
|
|
32
|
+
from typing import TYPE_CHECKING
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from .error_handler import ErrorHandler
|
|
27
35
|
|
|
28
36
|
|
|
29
37
|
logger = logging.getLogger(__name__)
|
|
@@ -32,118 +40,156 @@ logger = logging.getLogger(__name__)
|
|
|
32
40
|
class RequestHandler:
|
|
33
41
|
"""Handles making requests to other KOI nodes."""
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
effector: Effector
|
|
44
|
+
identity: NodeIdentity
|
|
45
|
+
secure: Secure
|
|
46
|
+
error_handler: "ErrorHandler"
|
|
37
47
|
|
|
38
|
-
def __init__(
|
|
39
|
-
self
|
|
40
|
-
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
effector: Effector,
|
|
51
|
+
identity: NodeIdentity,
|
|
52
|
+
secure: Secure
|
|
53
|
+
):
|
|
54
|
+
self.effector = effector
|
|
55
|
+
self.identity = identity
|
|
56
|
+
self.secure = secure
|
|
57
|
+
|
|
58
|
+
def set_error_handler(self, error_handler: "ErrorHandler"):
|
|
59
|
+
self.error_handler = error_handler
|
|
60
|
+
|
|
61
|
+
def get_url(self, node_rid: KoiNetNode) -> str:
|
|
62
|
+
"""Retrieves URL of a node."""
|
|
63
|
+
|
|
64
|
+
logger.debug(f"Getting URL for {node_rid!r}")
|
|
65
|
+
node_url = None
|
|
66
|
+
|
|
67
|
+
if node_rid == self.identity.rid:
|
|
68
|
+
raise Exception("Don't talk to yourself")
|
|
69
|
+
|
|
70
|
+
node_bundle = self.effector.deref(node_rid)
|
|
41
71
|
|
|
72
|
+
if node_bundle:
|
|
73
|
+
node_profile = node_bundle.validate_contents(NodeProfile)
|
|
74
|
+
logger.debug(f"Found node profile: {node_profile}")
|
|
75
|
+
if node_profile.node_type != NodeType.FULL:
|
|
76
|
+
raise Exception("Can't query partial node")
|
|
77
|
+
node_url = node_profile.base_url
|
|
78
|
+
|
|
79
|
+
else:
|
|
80
|
+
if node_rid == self.identity.config.koi_net.first_contact.rid:
|
|
81
|
+
logger.debug("Found URL of first contact")
|
|
82
|
+
node_url = self.identity.config.koi_net.first_contact.url
|
|
83
|
+
|
|
84
|
+
if not node_url:
|
|
85
|
+
raise Exception("Node not found")
|
|
86
|
+
|
|
87
|
+
logger.debug(f"Resolved {node_rid!r} to {node_url}")
|
|
88
|
+
return node_url
|
|
89
|
+
|
|
42
90
|
def make_request(
|
|
43
|
-
self,
|
|
44
|
-
|
|
91
|
+
self,
|
|
92
|
+
node: KoiNetNode,
|
|
93
|
+
path: str,
|
|
45
94
|
request: RequestModels,
|
|
46
|
-
response_model: type[ResponseModels] | None = None
|
|
47
95
|
) -> ResponseModels | None:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
96
|
+
url = self.get_url(node) + path
|
|
97
|
+
logger.info(f"Making request to {url}")
|
|
98
|
+
|
|
99
|
+
signed_envelope = self.secure.create_envelope(
|
|
100
|
+
payload=request,
|
|
101
|
+
target=node
|
|
52
102
|
)
|
|
53
|
-
if response_model:
|
|
54
|
-
return response_model.model_validate_json(resp.text)
|
|
55
|
-
|
|
56
|
-
def get_url(self, node_rid: KoiNetNode, url: str) -> str:
|
|
57
|
-
"""Retrieves URL of a node, or returns provided URL."""
|
|
58
103
|
|
|
59
|
-
|
|
60
|
-
|
|
104
|
+
try:
|
|
105
|
+
result = httpx.post(url, data=signed_envelope.model_dump_json())
|
|
106
|
+
except httpx.ConnectError as err:
|
|
107
|
+
logger.debug("Failed to connect")
|
|
108
|
+
self.error_handler.handle_connection_error(node)
|
|
109
|
+
raise err
|
|
61
110
|
|
|
62
|
-
if
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
111
|
+
if result.status_code != 200:
|
|
112
|
+
resp = ErrorResponse.model_validate_json(result.text)
|
|
113
|
+
self.error_handler.handle_protocol_error(resp.error, node)
|
|
114
|
+
return resp
|
|
115
|
+
|
|
116
|
+
if path == BROADCAST_EVENTS_PATH:
|
|
117
|
+
return None
|
|
118
|
+
elif path == POLL_EVENTS_PATH:
|
|
119
|
+
EnvelopeModel = SignedEnvelope[EventsPayload]
|
|
120
|
+
elif path == FETCH_RIDS_PATH:
|
|
121
|
+
EnvelopeModel = SignedEnvelope[RidsPayload]
|
|
122
|
+
elif path == FETCH_MANIFESTS_PATH:
|
|
123
|
+
EnvelopeModel = SignedEnvelope[ManifestsPayload]
|
|
124
|
+
elif path == FETCH_BUNDLES_PATH:
|
|
125
|
+
EnvelopeModel = SignedEnvelope[BundlesPayload]
|
|
70
126
|
else:
|
|
71
|
-
|
|
127
|
+
raise Exception(f"Unknown path '{path}'")
|
|
128
|
+
|
|
129
|
+
resp_envelope = EnvelopeModel.model_validate_json(result.text)
|
|
130
|
+
self.secure.validate_envelope(resp_envelope)
|
|
131
|
+
|
|
132
|
+
return resp_envelope.payload
|
|
72
133
|
|
|
73
134
|
def broadcast_events(
|
|
74
135
|
self,
|
|
75
|
-
node: RID
|
|
76
|
-
url: str = None,
|
|
136
|
+
node: RID,
|
|
77
137
|
req: EventsPayload | None = None,
|
|
78
138
|
**kwargs
|
|
79
139
|
) -> None:
|
|
80
140
|
"""See protocol.api_models.EventsPayload for available kwargs."""
|
|
81
141
|
request = req or EventsPayload.model_validate(kwargs)
|
|
82
|
-
self.make_request(
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
logger.info(f"Broadcasted {len(request.events)} event(s) to {node or url!r}")
|
|
142
|
+
self.make_request(node, BROADCAST_EVENTS_PATH, request)
|
|
143
|
+
logger.info(f"Broadcasted {len(request.events)} event(s) to {node!r}")
|
|
86
144
|
|
|
87
145
|
def poll_events(
|
|
88
146
|
self,
|
|
89
|
-
node: RID
|
|
90
|
-
url: str = None,
|
|
147
|
+
node: RID,
|
|
91
148
|
req: PollEvents | None = None,
|
|
92
149
|
**kwargs
|
|
93
150
|
) -> EventsPayload:
|
|
94
151
|
"""See protocol.api_models.PollEvents for available kwargs."""
|
|
95
152
|
request = req or PollEvents.model_validate(kwargs)
|
|
96
|
-
resp = self.make_request(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
100
|
-
logger.info(f"Polled {len(resp.events)} events from {node or url!r}")
|
|
153
|
+
resp = self.make_request(node, POLL_EVENTS_PATH, request)
|
|
154
|
+
if type(resp) != ErrorResponse:
|
|
155
|
+
logger.info(f"Polled {len(resp.events)} events from {node!r}")
|
|
101
156
|
return resp
|
|
102
157
|
|
|
103
158
|
def fetch_rids(
|
|
104
159
|
self,
|
|
105
|
-
node: RID
|
|
106
|
-
url: str = None,
|
|
160
|
+
node: RID,
|
|
107
161
|
req: FetchRids | None = None,
|
|
108
162
|
**kwargs
|
|
109
163
|
) -> RidsPayload:
|
|
110
164
|
"""See protocol.api_models.FetchRids for available kwargs."""
|
|
111
165
|
request = req or FetchRids.model_validate(kwargs)
|
|
112
|
-
resp = self.make_request(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
logger.info(f"Fetched {len(resp.rids)} RID(s) from {node or url!r}")
|
|
166
|
+
resp = self.make_request(node, FETCH_RIDS_PATH, request)
|
|
167
|
+
if type(resp) != ErrorResponse:
|
|
168
|
+
logger.info(f"Fetched {len(resp.rids)} RID(s) from {node!r}")
|
|
117
169
|
return resp
|
|
118
170
|
|
|
119
171
|
def fetch_manifests(
|
|
120
172
|
self,
|
|
121
|
-
node: RID
|
|
122
|
-
url: str = None,
|
|
173
|
+
node: RID,
|
|
123
174
|
req: FetchManifests | None = None,
|
|
124
175
|
**kwargs
|
|
125
176
|
) -> ManifestsPayload:
|
|
126
177
|
"""See protocol.api_models.FetchManifests for available kwargs."""
|
|
127
178
|
request = req or FetchManifests.model_validate(kwargs)
|
|
128
|
-
resp = self.make_request(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
132
|
-
logger.info(f"Fetched {len(resp.manifests)} manifest(s) from {node or url!r}")
|
|
179
|
+
resp = self.make_request(node, FETCH_MANIFESTS_PATH, request)
|
|
180
|
+
if type(resp) != ErrorResponse:
|
|
181
|
+
logger.info(f"Fetched {len(resp.manifests)} manifest(s) from {node!r}")
|
|
133
182
|
return resp
|
|
134
183
|
|
|
135
184
|
def fetch_bundles(
|
|
136
185
|
self,
|
|
137
|
-
node: RID
|
|
138
|
-
url: str = None,
|
|
186
|
+
node: RID,
|
|
139
187
|
req: FetchBundles | None = None,
|
|
140
188
|
**kwargs
|
|
141
189
|
) -> BundlesPayload:
|
|
142
190
|
"""See protocol.api_models.FetchBundles for available kwargs."""
|
|
143
191
|
request = req or FetchBundles.model_validate(kwargs)
|
|
144
|
-
resp = self.make_request(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
logger.info(f"Fetched {len(resp.bundles)} bundle(s) from {node or url!r}")
|
|
192
|
+
resp = self.make_request(node, FETCH_BUNDLES_PATH, request)
|
|
193
|
+
if type(resp) != ErrorResponse:
|
|
194
|
+
logger.info(f"Fetched {len(resp.bundles)} bundle(s) from {node!r}")
|
|
149
195
|
return resp
|