koi-net 1.1.0b8__py3-none-any.whl → 1.2.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.

Files changed (48) hide show
  1. koi_net/__init__.py +2 -1
  2. koi_net/assembler.py +82 -0
  3. koi_net/cli/__init__.py +1 -0
  4. koi_net/cli/commands.py +99 -0
  5. koi_net/cli/models.py +41 -0
  6. koi_net/config.py +34 -0
  7. koi_net/context.py +11 -28
  8. koi_net/core.py +63 -179
  9. koi_net/default_actions.py +10 -1
  10. koi_net/effector.py +61 -34
  11. koi_net/handshaker.py +39 -0
  12. koi_net/identity.py +2 -3
  13. koi_net/interfaces/entrypoint.py +5 -0
  14. koi_net/interfaces/worker.py +17 -0
  15. koi_net/lifecycle.py +85 -48
  16. koi_net/logger.py +176 -0
  17. koi_net/network/error_handler.py +18 -16
  18. koi_net/network/event_queue.py +17 -185
  19. koi_net/network/graph.py +15 -10
  20. koi_net/network/poll_event_buffer.py +26 -0
  21. koi_net/network/request_handler.py +54 -47
  22. koi_net/network/resolver.py +18 -21
  23. koi_net/network/response_handler.py +79 -15
  24. koi_net/poller.py +18 -9
  25. koi_net/processor/event_worker.py +117 -0
  26. koi_net/processor/handler.py +4 -2
  27. koi_net/processor/{default_handlers.py → handlers.py} +109 -59
  28. koi_net/processor/knowledge_object.py +19 -7
  29. koi_net/processor/kobj_queue.py +51 -0
  30. koi_net/processor/kobj_worker.py +44 -0
  31. koi_net/processor/{knowledge_pipeline.py → pipeline.py} +31 -53
  32. koi_net/protocol/api_models.py +7 -3
  33. koi_net/protocol/envelope.py +5 -6
  34. koi_net/protocol/model_map.py +61 -0
  35. koi_net/protocol/node.py +3 -3
  36. koi_net/protocol/secure.py +8 -8
  37. koi_net/secure.py +33 -13
  38. koi_net/sentry.py +13 -0
  39. koi_net/server.py +44 -78
  40. koi_net/utils.py +18 -0
  41. {koi_net-1.1.0b8.dist-info → koi_net-1.2.0b2.dist-info}/METADATA +8 -3
  42. koi_net-1.2.0b2.dist-info/RECORD +52 -0
  43. koi_net-1.2.0b2.dist-info/entry_points.txt +2 -0
  44. koi_net/actor.py +0 -60
  45. koi_net/processor/interface.py +0 -101
  46. koi_net-1.1.0b8.dist-info/RECORD +0 -38
  47. {koi_net-1.1.0b8.dist-info → koi_net-1.2.0b2.dist-info}/WHEEL +0 -0
  48. {koi_net-1.1.0b8.dist-info → koi_net-1.2.0b2.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,4 @@
1
- import logging
1
+ import structlog
2
2
  import httpx
3
3
  from rid_lib import RID
4
4
  from rid_lib.core import RIDType
@@ -12,17 +12,15 @@ from ..protocol.event import Event
12
12
  from ..protocol.api_models import ErrorResponse
13
13
  from ..identity import NodeIdentity
14
14
  from ..config import NodeConfig
15
- from ..effector import Effector
16
15
 
17
- logger = logging.getLogger(__name__)
16
+ log = structlog.stdlib.get_logger()
18
17
 
19
18
 
20
19
  class NetworkResolver:
21
- """A collection of functions and classes to interact with the KOI network."""
20
+ """Handles resolving nodes or knowledge objects from the network."""
22
21
 
23
22
  config: NodeConfig
24
23
  identity: NodeIdentity
25
- effector: Effector
26
24
  cache: Cache
27
25
  graph: NetworkGraph
28
26
  request_handler: RequestHandler
@@ -32,7 +30,6 @@ class NetworkResolver:
32
30
  config: NodeConfig,
33
31
  cache: Cache,
34
32
  identity: NodeIdentity,
35
- effector: Effector,
36
33
  graph: NetworkGraph,
37
34
  request_handler: RequestHandler,
38
35
  ):
@@ -41,15 +38,14 @@ class NetworkResolver:
41
38
  self.cache = cache
42
39
  self.graph = graph
43
40
  self.request_handler = request_handler
44
- self.effector = effector
45
41
 
46
42
  self.poll_event_queue = dict()
47
43
  self.webhook_event_queue = dict()
48
44
 
49
45
  def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
50
- """Returns list of node RIDs which provide state for the specified RID type."""
46
+ """Returns list of node RIDs which provide state for specified RID type."""
51
47
 
52
- logger.debug(f"Looking for state providers of {rid_type}")
48
+ log.debug(f"Looking for state providers of {rid_type}")
53
49
  provider_nodes = []
54
50
  for node_rid in self.cache.list_rids(rid_types=[KoiNetNode]):
55
51
  if node_rid == self.identity.rid:
@@ -60,17 +56,17 @@ class NetworkResolver:
60
56
  node_profile = node_bundle.validate_contents(NodeProfile)
61
57
 
62
58
  if (node_profile.node_type == NodeType.FULL) and (rid_type in node_profile.provides.state):
63
- logger.debug(f"Found provider {node_rid!r}")
59
+ log.debug(f"Found provider {node_rid!r}")
64
60
  provider_nodes.append(node_rid)
65
61
 
66
62
  if not provider_nodes:
67
- logger.debug("Failed to find providers")
63
+ log.debug("Failed to find providers")
68
64
  return provider_nodes
69
65
 
70
66
  def fetch_remote_bundle(self, rid: RID) -> tuple[Bundle | None, KoiNetNode | None]:
71
67
  """Attempts to fetch a bundle by RID from known peer nodes."""
72
68
 
73
- logger.debug(f"Fetching remote bundle {rid!r}")
69
+ log.debug(f"Fetching remote bundle {rid!r}")
74
70
  remote_bundle, node_rid = None, None
75
71
  for node_rid in self.get_state_providers(type(rid)):
76
72
  payload = self.request_handler.fetch_bundles(
@@ -78,18 +74,18 @@ class NetworkResolver:
78
74
 
79
75
  if payload.bundles:
80
76
  remote_bundle = payload.bundles[0]
81
- logger.debug(f"Got bundle from {node_rid!r}")
77
+ log.debug(f"Got bundle from {node_rid!r}")
82
78
  break
83
79
 
84
80
  if not remote_bundle:
85
- logger.warning("Failed to fetch remote bundle")
81
+ log.warning("Failed to fetch remote bundle")
86
82
 
87
83
  return remote_bundle, node_rid
88
84
 
89
85
  def fetch_remote_manifest(self, rid: RID) -> tuple[Bundle | None, KoiNetNode | None]:
90
86
  """Attempts to fetch a manifest by RID from known peer nodes."""
91
87
 
92
- logger.debug(f"Fetching remote manifest {rid!r}")
88
+ log.debug(f"Fetching remote manifest {rid!r}")
93
89
  remote_manifest, node_rid = None, None
94
90
  for node_rid in self.get_state_providers(type(rid)):
95
91
  payload = self.request_handler.fetch_manifests(
@@ -97,18 +93,19 @@ class NetworkResolver:
97
93
 
98
94
  if payload.manifests:
99
95
  remote_manifest = payload.manifests[0]
100
- logger.debug(f"Got bundle from {node_rid!r}")
96
+ log.debug(f"Got bundle from {node_rid!r}")
101
97
  break
102
98
 
103
99
  if not remote_manifest:
104
- logger.warning("Failed to fetch remote bundle")
100
+ log.warning("Failed to fetch remote bundle")
105
101
 
106
102
  return remote_manifest, node_rid
107
103
 
108
104
  def poll_neighbors(self) -> dict[KoiNetNode, list[Event]]:
109
- """Polls all neighboring nodes and returns compiled list of events.
105
+ """Polls all neighbor nodes and returns compiled list of events.
110
106
 
111
- If this node has no neighbors, it will instead attempt to poll the provided first contact URL.
107
+ Neighbor nodes also include the first contact, regardless of
108
+ whether the first contact profile is known to this node.
112
109
  """
113
110
 
114
111
  graph_neighbors = self.graph.get_neighbors()
@@ -139,12 +136,12 @@ class NetworkResolver:
139
136
  continue
140
137
 
141
138
  if payload.events:
142
- logger.debug(f"Received {len(payload.events)} events from {node_rid!r}")
139
+ log.debug(f"Received {len(payload.events)} events from {node_rid!r}")
143
140
 
144
141
  event_dict[node_rid] = payload.events
145
142
 
146
143
  except httpx.ConnectError:
147
- logger.debug(f"Failed to reach node {node_rid!r}")
144
+ log.debug(f"Failed to reach node {node_rid!r}")
148
145
  continue
149
146
 
150
147
  return event_dict
@@ -1,10 +1,20 @@
1
- import logging
1
+ import structlog
2
2
  from rid_lib import RID
3
3
  from rid_lib.types import KoiNetNode
4
4
  from rid_lib.ext import Manifest, Cache
5
5
  from rid_lib.ext.bundle import Bundle
6
6
 
7
+ from koi_net.network.poll_event_buffer import PollEventBuffer
8
+ from koi_net.processor.kobj_queue import KobjQueue
9
+ from koi_net.protocol.consts import BROADCAST_EVENTS_PATH, FETCH_BUNDLES_PATH, FETCH_MANIFESTS_PATH, FETCH_RIDS_PATH, POLL_EVENTS_PATH
10
+ from koi_net.protocol.envelope import SignedEnvelope
11
+ from koi_net.protocol.model_map import API_MODEL_MAP
12
+ from koi_net.secure import Secure
13
+
7
14
  from ..protocol.api_models import (
15
+ ApiModels,
16
+ EventsPayload,
17
+ PollEvents,
8
18
  RidsPayload,
9
19
  ManifestsPayload,
10
20
  BundlesPayload,
@@ -12,39 +22,88 @@ from ..protocol.api_models import (
12
22
  FetchManifests,
13
23
  FetchBundles,
14
24
  )
15
- from ..effector import Effector
16
25
 
17
- logger = logging.getLogger(__name__)
26
+ log = structlog.stdlib.get_logger()
18
27
 
19
28
 
20
29
  class ResponseHandler:
21
30
  """Handles generating responses to requests from other KOI nodes."""
22
31
 
23
32
  cache: Cache
24
- effector: Effector
33
+ kobj_queue: KobjQueue
34
+ poll_event_buf: PollEventBuffer
25
35
 
26
36
  def __init__(
27
37
  self,
28
- cache: Cache,
29
- effector: Effector,
38
+ cache: Cache,
39
+ kobj_queue: KobjQueue,
40
+ poll_event_buf: PollEventBuffer,
41
+ secure: Secure
30
42
  ):
31
43
  self.cache = cache
32
- self.effector = effector
44
+ self.kobj_queue = kobj_queue
45
+ self.poll_event_buf = poll_event_buf
46
+ self.secure = secure
47
+
48
+ def handle_response(self, path: str, req: SignedEnvelope):
49
+ self.secure.validate_envelope(req)
50
+
51
+ response_map = {
52
+ BROADCAST_EVENTS_PATH: self.broadcast_events_handler,
53
+ POLL_EVENTS_PATH: self.poll_events_handler,
54
+ FETCH_RIDS_PATH: self.fetch_rids_handler,
55
+ FETCH_MANIFESTS_PATH: self.fetch_manifests_handler,
56
+ FETCH_BUNDLES_PATH: self.fetch_bundles_handler
57
+ }
58
+
59
+ response = response_map[path](req.payload, req.source_node)
60
+
61
+ if response is None:
62
+ return
63
+
64
+ return self.secure.create_envelope(
65
+ payload=response,
66
+ target=req.source_node
67
+ )
33
68
 
34
- def fetch_rids(self, req: FetchRids, source: KoiNetNode) -> RidsPayload:
35
- logger.info(f"Request to fetch rids, allowed types {req.rid_types}")
69
+ def broadcast_events_handler(self, req: EventsPayload, source: KoiNetNode):
70
+ log.info(f"Request to broadcast events, received {len(req.events)} event(s)")
71
+
72
+ for event in req.events:
73
+ self.kobj_queue.put_kobj(event=event, source=source)
74
+
75
+ def poll_events_handler(
76
+ self,
77
+ req: PollEvents,
78
+ source: KoiNetNode
79
+ ) -> EventsPayload:
80
+ log.info(f"Request to poll events")
81
+ events = self.poll_event_buf.flush(source, limit=req.limit)
82
+ return EventsPayload(events=events)
83
+
84
+ def fetch_rids_handler(
85
+ self,
86
+ req: FetchRids,
87
+ source: KoiNetNode
88
+ ) -> RidsPayload:
89
+ """Returns response to fetch RIDs request."""
90
+ log.info(f"Request to fetch rids, allowed types {req.rid_types}")
36
91
  rids = self.cache.list_rids(req.rid_types)
37
92
 
38
93
  return RidsPayload(rids=rids)
39
94
 
40
- def fetch_manifests(self, req: FetchManifests, source: KoiNetNode) -> ManifestsPayload:
41
- logger.info(f"Request to fetch manifests, allowed types {req.rid_types}, rids {req.rids}")
95
+ def fetch_manifests_handler(self,
96
+ req: FetchManifests,
97
+ source: KoiNetNode
98
+ ) -> ManifestsPayload:
99
+ """Returns response to fetch manifests request."""
100
+ log.info(f"Request to fetch manifests, allowed types {req.rid_types}, rids {req.rids}")
42
101
 
43
102
  manifests: list[Manifest] = []
44
103
  not_found: list[RID] = []
45
104
 
46
105
  for rid in (req.rids or self.cache.list_rids(req.rid_types)):
47
- bundle = self.effector.deref(rid)
106
+ bundle = self.cache.read(rid)
48
107
  if bundle:
49
108
  manifests.append(bundle.manifest)
50
109
  else:
@@ -52,14 +111,19 @@ class ResponseHandler:
52
111
 
53
112
  return ManifestsPayload(manifests=manifests, not_found=not_found)
54
113
 
55
- def fetch_bundles(self, req: FetchBundles, source: KoiNetNode) -> BundlesPayload:
56
- logger.info(f"Request to fetch bundles, requested rids {req.rids}")
114
+ def fetch_bundles_handler(
115
+ self,
116
+ req: FetchBundles,
117
+ source: KoiNetNode
118
+ ) -> BundlesPayload:
119
+ """Returns response to fetch bundles request."""
120
+ log.info(f"Request to fetch bundles, requested rids {req.rids}")
57
121
 
58
122
  bundles: list[Bundle] = []
59
123
  not_found: list[RID] = []
60
124
 
61
125
  for rid in req.rids:
62
- bundle = self.effector.deref(rid)
126
+ bundle = self.cache.read(rid)
63
127
  if bundle:
64
128
  bundles.append(bundle)
65
129
  else:
koi_net/poller.py CHANGED
@@ -1,35 +1,44 @@
1
1
 
2
2
  import time
3
- import logging
4
- from .processor.interface import ProcessorInterface
3
+ import structlog
4
+
5
+ from koi_net.interfaces.entrypoint import EntryPoint
6
+ from .processor.kobj_queue import KobjQueue
5
7
  from .lifecycle import NodeLifecycle
6
8
  from .network.resolver import NetworkResolver
7
9
  from .config import NodeConfig
8
10
 
9
- logger = logging.getLogger(__name__)
11
+ log = structlog.stdlib.get_logger()
10
12
 
11
13
 
12
- class NodePoller:
14
+ class NodePoller(EntryPoint):
15
+ """Manages polling based event loop for partial nodes."""
16
+ kobj_queue: KobjQueue
17
+ lifecycle: NodeLifecycle
18
+ resolver: NetworkResolver
19
+ config: NodeConfig
20
+
13
21
  def __init__(
14
22
  self,
15
- processor: ProcessorInterface,
23
+ config: NodeConfig,
16
24
  lifecycle: NodeLifecycle,
25
+ kobj_queue: KobjQueue,
17
26
  resolver: NetworkResolver,
18
- config: NodeConfig
19
27
  ):
20
- self.processor = processor
28
+ self.kobj_queue = kobj_queue
21
29
  self.lifecycle = lifecycle
22
30
  self.resolver = resolver
23
31
  self.config = config
24
32
 
25
33
  def poll(self):
34
+ """Polls neighbors and processes returned events."""
26
35
  neighbors = self.resolver.poll_neighbors()
27
36
  for node_rid in neighbors:
28
37
  for event in neighbors[node_rid]:
29
- self.processor.handle(event=event, source=node_rid)
30
- self.processor.flush_kobj_queue()
38
+ self.kobj_queue.put_kobj(event=event, source=node_rid)
31
39
 
32
40
  def run(self):
41
+ """Runs polling event loop."""
33
42
  with self.lifecycle.run():
34
43
  while True:
35
44
  start_time = time.time()
@@ -0,0 +1,117 @@
1
+ import queue
2
+ import traceback
3
+ import time
4
+ import structlog
5
+
6
+ from rid_lib.ext import Cache
7
+ from rid_lib.types import KoiNetNode
8
+
9
+ from koi_net.config import NodeConfig
10
+ from koi_net.network.event_queue import EventQueue, QueuedEvent
11
+ from koi_net.network.request_handler import RequestHandler
12
+ from koi_net.network.poll_event_buffer import PollEventBuffer
13
+ from koi_net.protocol.event import Event
14
+ from koi_net.protocol.node import NodeProfile, NodeType
15
+ from koi_net.interfaces.worker import ThreadWorker, STOP_WORKER
16
+
17
+ log = structlog.stdlib.get_logger()
18
+
19
+
20
+ class EventProcessingWorker(ThreadWorker):
21
+ event_buffer: dict[KoiNetNode, list[Event]]
22
+ buffer_times: dict[KoiNetNode, float]
23
+
24
+ def __init__(
25
+ self,
26
+ event_queue: EventQueue,
27
+ request_handler: RequestHandler,
28
+ config: NodeConfig,
29
+ cache: Cache,
30
+ poll_event_buf: PollEventBuffer
31
+ ):
32
+ self.event_queue = event_queue
33
+ self.request_handler = request_handler
34
+
35
+ self.config = config
36
+ self.cache = cache
37
+ self.poll_event_buf = poll_event_buf
38
+
39
+ self.timeout: float = 0.1
40
+ self.max_buf_len: int = 5
41
+ self.max_wait_time: float = 1.0
42
+
43
+ self.event_buffer = dict()
44
+ self.buffer_times = dict()
45
+
46
+ super().__init__()
47
+
48
+ def flush_buffer(self, target: KoiNetNode, buffer: list[Event]):
49
+ try:
50
+ self.request_handler.broadcast_events(target, events=buffer)
51
+ except Exception as e:
52
+ traceback.print_exc()
53
+
54
+ self.event_buffer[target] = []
55
+ self.buffer_times[target] = None
56
+
57
+ def decide_event(self, item: QueuedEvent) -> bool:
58
+ node_bundle = self.cache.read(item.target)
59
+ if node_bundle:
60
+ node_profile = node_bundle.validate_contents(NodeProfile)
61
+
62
+ if node_profile.node_type == NodeType.FULL:
63
+ return True
64
+
65
+ elif node_profile.node_type == NodeType.PARTIAL:
66
+ self.poll_event_buf.put(item.target, item.event)
67
+ return False
68
+
69
+ elif item.target == self.config.koi_net.first_contact.rid:
70
+ return True
71
+
72
+ else:
73
+ log.warning(f"Couldn't handle event {item.event!r} in queue, node {item.target!r} unknown to me")
74
+ return False
75
+
76
+
77
+ def run(self):
78
+ log.info("Started event worker")
79
+ while True:
80
+ now = time.time()
81
+ try:
82
+ item = self.event_queue.q.get(timeout=self.timeout)
83
+
84
+ try:
85
+ if item is STOP_WORKER:
86
+ log.info(f"Received 'STOP_WORKER' signal, flushing buffer...")
87
+ for target in self.event_buffer.keys():
88
+ self.flush_buffer(target, self.event_buffer[target])
89
+ return
90
+
91
+ log.info(f"Dequeued {item.event!r} -> {item.target!r}")
92
+
93
+ if not self.decide_event(item):
94
+ continue
95
+
96
+ event_buf = self.event_buffer.setdefault(item.target, [])
97
+ if not event_buf:
98
+ self.buffer_times[item.target] = now
99
+
100
+ event_buf.append(item.event)
101
+
102
+ # When new events are dequeued, check buffer for max length
103
+ if len(event_buf) >= self.max_buf_len:
104
+ self.flush_buffer(item.target, event_buf)
105
+ finally:
106
+ self.event_queue.q.task_done()
107
+
108
+ except queue.Empty:
109
+ # On timeout, check all buffers for max wait time
110
+ for target, event_buf in self.event_buffer.items():
111
+ if (len(event_buf) == 0) or (self.buffer_times.get(target) is None):
112
+ continue
113
+ if (now - self.buffer_times[target]) >= self.max_wait_time:
114
+ self.flush_buffer(target, event_buf)
115
+
116
+ except Exception as e:
117
+ traceback.print_exc()
@@ -44,9 +44,11 @@ class KnowledgeHandler:
44
44
  rid_types: list[RIDType] | None = None,
45
45
  event_types: list[EventType | None] | None = None
46
46
  ):
47
- """Special decorator that returns a KnowledgeHandler instead of a function.
47
+ """Decorator wraps a function, returns a KnowledgeHandler.
48
48
 
49
- The function symbol will redefined as a `KnowledgeHandler`, which can be passed into the `ProcessorInterface` constructor. This is used to register default handlers.
49
+ The function symbol will redefined as a `KnowledgeHandler`,
50
+ which can be passed into the `ProcessorInterface` constructor.
51
+ This is used to register default handlers.
50
52
  """
51
53
  def decorator(func: Callable) -> KnowledgeHandler:
52
54
  handler = cls(func, handler_type, rid_types, event_types)