koi-net 1.0.0b19__py3-none-any.whl → 1.1.0__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,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,128 +25,188 @@ 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__)
30
38
 
31
39
 
40
+ # Custom error types for request handling
41
+ class SelfRequestError(Exception):
42
+ """Raised when a node tries to request itself."""
43
+ pass
44
+
45
+ class PartialNodeQueryError(Exception):
46
+ """Raised when attempting to query a partial node."""
47
+ pass
48
+
49
+ class NodeNotFoundError(Exception):
50
+ """Raised when a node URL cannot be found."""
51
+ pass
52
+
53
+ class UnknownPathError(Exception):
54
+ """Raised when an unknown path is requested."""
55
+ pass
56
+
32
57
  class RequestHandler:
33
58
  """Handles making requests to other KOI nodes."""
34
59
 
35
- cache: Cache
36
- graph: NetworkGraph
60
+ effector: Effector
61
+ identity: NodeIdentity
62
+ secure: Secure
63
+ error_handler: "ErrorHandler"
37
64
 
38
- def __init__(self, cache: Cache, graph: NetworkGraph):
39
- self.cache = cache
40
- self.graph = graph
65
+ def __init__(
66
+ self,
67
+ effector: Effector,
68
+ identity: NodeIdentity,
69
+ secure: Secure
70
+ ):
71
+ self.effector = effector
72
+ self.identity = identity
73
+ self.secure = secure
74
+
75
+ def set_error_handler(self, error_handler: "ErrorHandler"):
76
+ self.error_handler = error_handler
77
+
78
+ def get_url(self, node_rid: KoiNetNode) -> str:
79
+ """Retrieves URL of a node."""
80
+
81
+ logger.debug(f"Getting URL for {node_rid!r}")
82
+ node_url = None
83
+
84
+ if node_rid == self.identity.rid:
85
+ raise SelfRequestError("Don't talk to yourself")
86
+
87
+ node_bundle = self.effector.deref(node_rid)
41
88
 
89
+ if node_bundle:
90
+ node_profile = node_bundle.validate_contents(NodeProfile)
91
+ logger.debug(f"Found node profile: {node_profile}")
92
+ if node_profile.node_type != NodeType.FULL:
93
+ raise PartialNodeQueryError("Can't query partial node")
94
+ node_url = node_profile.base_url
95
+
96
+ else:
97
+ if node_rid == self.identity.config.koi_net.first_contact.rid:
98
+ logger.debug("Found URL of first contact")
99
+ node_url = self.identity.config.koi_net.first_contact.url
100
+
101
+ if not node_url:
102
+ raise NodeNotFoundError("Node not found")
103
+
104
+ logger.debug(f"Resolved {node_rid!r} to {node_url}")
105
+ return node_url
106
+
42
107
  def make_request(
43
- self,
44
- url: str,
108
+ self,
109
+ node: KoiNetNode,
110
+ path: str,
45
111
  request: RequestModels,
46
- response_model: type[ResponseModels] | None = None
47
112
  ) -> ResponseModels | None:
48
- logger.debug(f"Making request to {url}")
49
- resp = httpx.post(
50
- url=url,
51
- data=request.model_dump_json()
113
+ url = self.get_url(node) + path
114
+ logger.info(f"Making request to {url}")
115
+
116
+ signed_envelope = self.secure.create_envelope(
117
+ payload=request,
118
+ target=node
52
119
  )
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
120
 
59
- if not node_rid and not url:
60
- raise ValueError("One of 'node_rid' and 'url' must be provided")
121
+ try:
122
+ result = httpx.post(url, data=signed_envelope.model_dump_json(exclude_none=True))
123
+ except httpx.ConnectError as err:
124
+ logger.debug("Failed to connect")
125
+ self.error_handler.handle_connection_error(node)
126
+ raise err
61
127
 
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
128
+ if result.status_code != 200:
129
+ resp = ErrorResponse.model_validate_json(result.text)
130
+ self.error_handler.handle_protocol_error(resp.error, node)
131
+ return resp
132
+
133
+ if path == BROADCAST_EVENTS_PATH:
134
+ return None
135
+ elif path == POLL_EVENTS_PATH:
136
+ EnvelopeModel = SignedEnvelope[EventsPayload]
137
+ elif path == FETCH_RIDS_PATH:
138
+ EnvelopeModel = SignedEnvelope[RidsPayload]
139
+ elif path == FETCH_MANIFESTS_PATH:
140
+ EnvelopeModel = SignedEnvelope[ManifestsPayload]
141
+ elif path == FETCH_BUNDLES_PATH:
142
+ EnvelopeModel = SignedEnvelope[BundlesPayload]
70
143
  else:
71
- return url
144
+ raise UnknownPathError(f"Unknown path '{path}'")
145
+
146
+ resp_envelope = EnvelopeModel.model_validate_json(result.text)
147
+ self.secure.validate_envelope(resp_envelope)
148
+
149
+ return resp_envelope.payload
72
150
 
73
151
  def broadcast_events(
74
152
  self,
75
- node: RID = None,
76
- url: str = None,
153
+ node: RID,
77
154
  req: EventsPayload | None = None,
78
155
  **kwargs
79
156
  ) -> None:
80
157
  """See protocol.api_models.EventsPayload for available kwargs."""
81
158
  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}")
159
+ self.make_request(node, BROADCAST_EVENTS_PATH, request)
160
+ logger.info(f"Broadcasted {len(request.events)} event(s) to {node!r}")
86
161
 
87
162
  def poll_events(
88
163
  self,
89
- node: RID = None,
90
- url: str = None,
164
+ node: RID,
91
165
  req: PollEvents | None = None,
92
166
  **kwargs
93
167
  ) -> EventsPayload:
94
168
  """See protocol.api_models.PollEvents for available kwargs."""
95
169
  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}")
170
+ resp = self.make_request(node, POLL_EVENTS_PATH, request)
171
+ if type(resp) != ErrorResponse:
172
+ logger.info(f"Polled {len(resp.events)} events from {node!r}")
101
173
  return resp
102
174
 
103
175
  def fetch_rids(
104
176
  self,
105
- node: RID = None,
106
- url: str = None,
177
+ node: RID,
107
178
  req: FetchRids | None = None,
108
179
  **kwargs
109
180
  ) -> RidsPayload:
110
181
  """See protocol.api_models.FetchRids for available kwargs."""
111
182
  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}")
183
+ resp = self.make_request(node, FETCH_RIDS_PATH, request)
184
+ if type(resp) != ErrorResponse:
185
+ logger.info(f"Fetched {len(resp.rids)} RID(s) from {node!r}")
117
186
  return resp
118
187
 
119
188
  def fetch_manifests(
120
189
  self,
121
- node: RID = None,
122
- url: str = None,
190
+ node: RID,
123
191
  req: FetchManifests | None = None,
124
192
  **kwargs
125
193
  ) -> ManifestsPayload:
126
194
  """See protocol.api_models.FetchManifests for available kwargs."""
127
195
  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}")
196
+ resp = self.make_request(node, FETCH_MANIFESTS_PATH, request)
197
+ if type(resp) != ErrorResponse:
198
+ logger.info(f"Fetched {len(resp.manifests)} manifest(s) from {node!r}")
133
199
  return resp
134
200
 
135
201
  def fetch_bundles(
136
202
  self,
137
- node: RID = None,
138
- url: str = None,
203
+ node: RID,
139
204
  req: FetchBundles | None = None,
140
205
  **kwargs
141
206
  ) -> BundlesPayload:
142
207
  """See protocol.api_models.FetchBundles for available kwargs."""
143
208
  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}")
209
+ resp = self.make_request(node, FETCH_BUNDLES_PATH, request)
210
+ if type(resp) != ErrorResponse:
211
+ logger.info(f"Fetched {len(resp.bundles)} bundle(s) from {node!r}")
149
212
  return resp
@@ -0,0 +1,150 @@
1
+ import logging
2
+ import httpx
3
+ from rid_lib import RID
4
+ from rid_lib.core import RIDType
5
+ from rid_lib.ext import Cache, Bundle
6
+ from rid_lib.types import KoiNetNode
7
+
8
+ from .graph import NetworkGraph
9
+ from .request_handler import RequestHandler
10
+ from ..protocol.node import NodeProfile, NodeType
11
+ from ..protocol.event import Event
12
+ from ..protocol.api_models import ErrorResponse
13
+ from ..identity import NodeIdentity
14
+ from ..config import NodeConfig
15
+ from ..effector import Effector
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class NetworkResolver:
21
+ """A collection of functions and classes to interact with the KOI network."""
22
+
23
+ config: NodeConfig
24
+ identity: NodeIdentity
25
+ effector: Effector
26
+ cache: Cache
27
+ graph: NetworkGraph
28
+ request_handler: RequestHandler
29
+
30
+ def __init__(
31
+ self,
32
+ config: NodeConfig,
33
+ cache: Cache,
34
+ identity: NodeIdentity,
35
+ effector: Effector,
36
+ graph: NetworkGraph,
37
+ request_handler: RequestHandler,
38
+ ):
39
+ self.config = config
40
+ self.identity = identity
41
+ self.cache = cache
42
+ self.graph = graph
43
+ self.request_handler = request_handler
44
+ self.effector = effector
45
+
46
+ self.poll_event_queue = dict()
47
+ self.webhook_event_queue = dict()
48
+
49
+ def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
50
+ """Returns list of node RIDs which provide state for the specified RID type."""
51
+
52
+ logger.debug(f"Looking for state providers of {rid_type}")
53
+ provider_nodes = []
54
+ for node_rid in self.cache.list_rids(rid_types=[KoiNetNode]):
55
+ if node_rid == self.identity.rid:
56
+ continue
57
+
58
+ node_bundle = self.cache.read(node_rid)
59
+
60
+ node_profile = node_bundle.validate_contents(NodeProfile)
61
+
62
+ if (node_profile.node_type == NodeType.FULL) and (rid_type in node_profile.provides.state):
63
+ logger.debug(f"Found provider {node_rid!r}")
64
+ provider_nodes.append(node_rid)
65
+
66
+ if not provider_nodes:
67
+ logger.debug("Failed to find providers")
68
+ return provider_nodes
69
+
70
+ def fetch_remote_bundle(self, rid: RID) -> tuple[Bundle | None, KoiNetNode | None]:
71
+ """Attempts to fetch a bundle by RID from known peer nodes."""
72
+
73
+ logger.debug(f"Fetching remote bundle {rid!r}")
74
+ remote_bundle, node_rid = None, None
75
+ for node_rid in self.get_state_providers(type(rid)):
76
+ payload = self.request_handler.fetch_bundles(
77
+ node=node_rid, rids=[rid])
78
+
79
+ if payload.bundles:
80
+ remote_bundle = payload.bundles[0]
81
+ logger.debug(f"Got bundle from {node_rid!r}")
82
+ break
83
+
84
+ if not remote_bundle:
85
+ logger.warning("Failed to fetch remote bundle")
86
+
87
+ return remote_bundle, node_rid
88
+
89
+ def fetch_remote_manifest(self, rid: RID) -> tuple[Bundle | None, KoiNetNode | None]:
90
+ """Attempts to fetch a manifest by RID from known peer nodes."""
91
+
92
+ logger.debug(f"Fetching remote manifest {rid!r}")
93
+ remote_manifest, node_rid = None, None
94
+ for node_rid in self.get_state_providers(type(rid)):
95
+ payload = self.request_handler.fetch_manifests(
96
+ node=node_rid, rids=[rid])
97
+
98
+ if payload.manifests:
99
+ remote_manifest = payload.manifests[0]
100
+ logger.debug(f"Got bundle from {node_rid!r}")
101
+ break
102
+
103
+ if not remote_manifest:
104
+ logger.warning("Failed to fetch remote bundle")
105
+
106
+ return remote_manifest, node_rid
107
+
108
+ def poll_neighbors(self) -> dict[KoiNetNode, list[Event]]:
109
+ """Polls all neighboring nodes and returns compiled list of events.
110
+
111
+ If this node has no neighbors, it will instead attempt to poll the provided first contact URL.
112
+ """
113
+
114
+ graph_neighbors = self.graph.get_neighbors()
115
+ neighbors = []
116
+
117
+ if graph_neighbors:
118
+ for node_rid in graph_neighbors:
119
+ node_bundle = self.cache.read(node_rid)
120
+ if not node_bundle:
121
+ continue
122
+ node_profile = node_bundle.validate_contents(NodeProfile)
123
+ if node_profile.node_type != NodeType.FULL:
124
+ continue
125
+ neighbors.append(node_rid)
126
+
127
+ elif self.config.koi_net.first_contact.rid:
128
+ neighbors.append(self.config.koi_net.first_contact.rid)
129
+
130
+ event_dict = dict()
131
+ for node_rid in neighbors:
132
+ try:
133
+ payload = self.request_handler.poll_events(
134
+ node=node_rid,
135
+ rid=self.identity.rid
136
+ )
137
+
138
+ if type(payload) == ErrorResponse:
139
+ continue
140
+
141
+ if payload.events:
142
+ logger.debug(f"Received {len(payload.events)} events from {node_rid!r}")
143
+
144
+ event_dict[node_rid] = payload.events
145
+
146
+ except httpx.ConnectError:
147
+ logger.debug(f"Failed to reach node {node_rid!r}")
148
+ continue
149
+
150
+ return event_dict
@@ -1,7 +1,9 @@
1
1
  import logging
2
2
  from rid_lib import RID
3
+ from rid_lib.types import KoiNetNode
3
4
  from rid_lib.ext import Manifest, Cache
4
5
  from rid_lib.ext.bundle import Bundle
6
+
5
7
  from ..protocol.api_models import (
6
8
  RidsPayload,
7
9
  ManifestsPayload,
@@ -10,6 +12,7 @@ from ..protocol.api_models import (
10
12
  FetchManifests,
11
13
  FetchBundles,
12
14
  )
15
+ from ..effector import Effector
13
16
 
14
17
  logger = logging.getLogger(__name__)
15
18
 
@@ -18,24 +21,30 @@ class ResponseHandler:
18
21
  """Handles generating responses to requests from other KOI nodes."""
19
22
 
20
23
  cache: Cache
24
+ effector: Effector
21
25
 
22
- def __init__(self, cache: Cache):
26
+ def __init__(
27
+ self,
28
+ cache: Cache,
29
+ effector: Effector,
30
+ ):
23
31
  self.cache = cache
32
+ self.effector = effector
24
33
 
25
- def fetch_rids(self, req: FetchRids) -> RidsPayload:
34
+ def fetch_rids(self, req: FetchRids, source: KoiNetNode) -> RidsPayload:
26
35
  logger.info(f"Request to fetch rids, allowed types {req.rid_types}")
27
36
  rids = self.cache.list_rids(req.rid_types)
28
37
 
29
38
  return RidsPayload(rids=rids)
30
39
 
31
- def fetch_manifests(self, req: FetchManifests) -> ManifestsPayload:
40
+ def fetch_manifests(self, req: FetchManifests, source: KoiNetNode) -> ManifestsPayload:
32
41
  logger.info(f"Request to fetch manifests, allowed types {req.rid_types}, rids {req.rids}")
33
42
 
34
43
  manifests: list[Manifest] = []
35
44
  not_found: list[RID] = []
36
45
 
37
46
  for rid in (req.rids or self.cache.list_rids(req.rid_types)):
38
- bundle = self.cache.read(rid)
47
+ bundle = self.effector.deref(rid)
39
48
  if bundle:
40
49
  manifests.append(bundle.manifest)
41
50
  else:
@@ -43,14 +52,14 @@ class ResponseHandler:
43
52
 
44
53
  return ManifestsPayload(manifests=manifests, not_found=not_found)
45
54
 
46
- def fetch_bundles(self, req: FetchBundles) -> BundlesPayload:
55
+ def fetch_bundles(self, req: FetchBundles, source: KoiNetNode) -> BundlesPayload:
47
56
  logger.info(f"Request to fetch bundles, requested rids {req.rids}")
48
57
 
49
58
  bundles: list[Bundle] = []
50
59
  not_found: list[RID] = []
51
60
 
52
61
  for rid in req.rids:
53
- bundle = self.cache.read(rid)
62
+ bundle = self.effector.deref(rid)
54
63
  if bundle:
55
64
  bundles.append(bundle)
56
65
  else:
koi_net/poller.py ADDED
@@ -0,0 +1,40 @@
1
+
2
+ import time
3
+ import logging
4
+ from .processor.interface import ProcessorInterface
5
+ from .lifecycle import NodeLifecycle
6
+ from .network.resolver import NetworkResolver
7
+ from .config import NodeConfig
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class NodePoller:
13
+ def __init__(
14
+ self,
15
+ processor: ProcessorInterface,
16
+ lifecycle: NodeLifecycle,
17
+ resolver: NetworkResolver,
18
+ config: NodeConfig
19
+ ):
20
+ self.processor = processor
21
+ self.lifecycle = lifecycle
22
+ self.resolver = resolver
23
+ self.config = config
24
+
25
+ def poll(self):
26
+ neighbors = self.resolver.poll_neighbors()
27
+ for node_rid in neighbors:
28
+ for event in neighbors[node_rid]:
29
+ self.processor.handle(event=event, source=node_rid)
30
+ self.processor.flush_kobj_queue()
31
+
32
+ def run(self):
33
+ with self.lifecycle.run():
34
+ while True:
35
+ start_time = time.time()
36
+ self.poll()
37
+ elapsed = time.time() - start_time
38
+ sleep_time = self.config.koi_net.polling_interval - elapsed
39
+ if sleep_time > 0:
40
+ time.sleep(sleep_time)
@@ -1 +0,0 @@
1
- from .interface import ProcessorInterface