koi-net 1.0.0b19__py3-none-any.whl → 1.1.0b2__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,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
@@ -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
@@ -2,6 +2,7 @@ import logging
2
2
  from rid_lib import RID
3
3
  from rid_lib.ext import Manifest, Cache
4
4
  from rid_lib.ext.bundle import Bundle
5
+
5
6
  from ..protocol.api_models import (
6
7
  RidsPayload,
7
8
  ManifestsPayload,
@@ -10,6 +11,7 @@ from ..protocol.api_models import (
10
11
  FetchManifests,
11
12
  FetchBundles,
12
13
  )
14
+ from ..effector import Effector
13
15
 
14
16
  logger = logging.getLogger(__name__)
15
17
 
@@ -18,9 +20,15 @@ class ResponseHandler:
18
20
  """Handles generating responses to requests from other KOI nodes."""
19
21
 
20
22
  cache: Cache
23
+ effector: Effector
21
24
 
22
- def __init__(self, cache: Cache):
25
+ def __init__(
26
+ self,
27
+ cache: Cache,
28
+ effector: Effector,
29
+ ):
23
30
  self.cache = cache
31
+ self.effector = effector
24
32
 
25
33
  def fetch_rids(self, req: FetchRids) -> RidsPayload:
26
34
  logger.info(f"Request to fetch rids, allowed types {req.rid_types}")
@@ -35,7 +43,7 @@ class ResponseHandler:
35
43
  not_found: list[RID] = []
36
44
 
37
45
  for rid in (req.rids or self.cache.list_rids(req.rid_types)):
38
- bundle = self.cache.read(rid)
46
+ bundle = self.effector.deref(rid)
39
47
  if bundle:
40
48
  manifests.append(bundle.manifest)
41
49
  else:
@@ -50,7 +58,7 @@ class ResponseHandler:
50
58
  not_found: list[RID] = []
51
59
 
52
60
  for rid in req.rids:
53
- bundle = self.cache.read(rid)
61
+ bundle = self.effector.deref(rid)
54
62
  if bundle:
55
63
  bundles.append(bundle)
56
64
  else:
koi_net/poller.py ADDED
@@ -0,0 +1,45 @@
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 run(self):
26
+ try:
27
+ self.lifecycle.start()
28
+ while True:
29
+ start_time = time.time()
30
+ neighbors = self.resolver.poll_neighbors()
31
+ for node_rid in neighbors:
32
+ for event in neighbors[node_rid]:
33
+ self.processor.handle(event=event, source=node_rid)
34
+ self.processor.flush_kobj_queue()
35
+
36
+ elapsed = time.time() - start_time
37
+ sleep_time = self.config.koi_net.polling_interval - elapsed
38
+ if sleep_time > 0:
39
+ time.sleep(sleep_time)
40
+
41
+ except KeyboardInterrupt:
42
+ logger.info("Polling interrupted by user.")
43
+
44
+ finally:
45
+ self.lifecycle.stop()
@@ -1 +0,0 @@
1
- from .interface import ProcessorInterface