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.

@@ -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 .response_handler import ResponseHandler
14
- from ..protocol.node import NodeType
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 ConfigType
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 NetworkInterface(Generic[ConfigType]):
27
+ class NetworkEventQueue:
30
28
  """A collection of functions and classes to interact with the KOI network."""
31
29
 
32
- config: ConfigType
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: ConfigType,
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 = NetworkGraph(cache, identity)
51
- self.request_handler = RequestHandler(cache, self.graph)
52
- self.response_handler = ResponseHandler(cache)
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
- node_profile = self.graph.get_node_profile(node)
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
- edge_profile = self.graph.get_edge_profile(
108
+ edge_rid = self.graph.get_edge(
111
109
  source=self.identity.rid,
112
110
  target=node
113
111
  )
114
112
 
115
- if edge_profile:
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
- else:
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} '{event.rid}'")
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
- node_profile = self.graph.get_node_profile(node)
171
+ # node_bundle = self.effector.deref(node)
158
172
 
159
- if not node_profile:
160
- logger.warning(f"{node!r} not found")
161
- return
173
+ # if not node_bundle:
174
+ # logger.warning(f"{node!r} not found")
175
+ # return
162
176
 
163
- if node_profile.node_type != NodeType.FULL:
164
- logger.warning(f"{node!r} is a partial node!")
165
- return
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
- def fetch_remote_bundle(self, rid: RID):
198
- """Attempts to fetch a bundle by RID from known peer nodes."""
199
-
200
- logger.debug(f"Fetching remote bundle '{rid}'")
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
- edge_profile = self.get_edge_profile(rid)
37
- if not edge_profile:
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 get_node_profile(self, rid: KoiNetNode) -> NodeProfile | None:
45
- """Returns node profile given its RID."""
46
- bundle = self.cache.read(rid)
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 not edge_data: return
61
- rid = edge_data.get("rid")
62
- if not rid: return
63
- elif not rid:
64
- raise ValueError("Either 'rid' or 'source' and 'target' must be provided")
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
- edge_profile = self.get_edge_profile(edge_rid)
92
+ edge_bundle = self.cache.read(edge_rid)
110
93
 
111
- if not edge_profile:
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 .graph import NetworkGraph
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
- cache: Cache
36
- graph: NetworkGraph
43
+ effector: Effector
44
+ identity: NodeIdentity
45
+ secure: Secure
46
+ error_handler: "ErrorHandler"
37
47
 
38
- def __init__(self, cache: Cache, graph: NetworkGraph):
39
- self.cache = cache
40
- self.graph = graph
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
- url: str,
91
+ self,
92
+ node: KoiNetNode,
93
+ path: str,
45
94
  request: RequestModels,
46
- response_model: type[ResponseModels] | None = None
47
95
  ) -> ResponseModels | None:
48
- logger.debug(f"Making request to {url}")
49
- resp = httpx.post(
50
- url=url,
51
- data=request.model_dump_json()
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
- if not node_rid and not url:
60
- raise ValueError("One of 'node_rid' and 'url' must be provided")
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 node_rid:
63
- node_profile = self.graph.get_node_profile(node_rid)
64
- if not node_profile:
65
- raise Exception("Node not found")
66
- if node_profile.node_type != NodeType.FULL:
67
- raise Exception("Can't query partial node")
68
- logger.debug(f"Resolved {node_rid!r} to {node_profile.base_url}")
69
- return node_profile.base_url
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
- return url
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 = None,
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
- self.get_url(node, url) + BROADCAST_EVENTS_PATH, request
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 = None,
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
- self.get_url(node, url) + POLL_EVENTS_PATH, request,
98
- response_model=EventsPayload
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 = None,
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
- self.get_url(node, url) + FETCH_RIDS_PATH, request,
114
- response_model=RidsPayload
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 = None,
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
- self.get_url(node, url) + FETCH_MANIFESTS_PATH, request,
130
- response_model=ManifestsPayload
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 = None,
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
- self.get_url(node, url) + FETCH_BUNDLES_PATH, request,
146
- response_model=BundlesPayload
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