koi-net 1.2.4__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 (59) hide show
  1. koi_net/__init__.py +1 -0
  2. koi_net/behaviors/handshaker.py +68 -0
  3. koi_net/behaviors/profile_monitor.py +23 -0
  4. koi_net/behaviors/sync_manager.py +68 -0
  5. koi_net/build/artifact.py +209 -0
  6. koi_net/build/assembler.py +60 -0
  7. koi_net/build/comp_order.py +6 -0
  8. koi_net/build/comp_type.py +7 -0
  9. koi_net/build/consts.py +18 -0
  10. koi_net/build/container.py +46 -0
  11. koi_net/cache.py +81 -0
  12. koi_net/config/core.py +113 -0
  13. koi_net/config/full_node.py +45 -0
  14. koi_net/config/loader.py +60 -0
  15. koi_net/config/partial_node.py +26 -0
  16. koi_net/config/proxy.py +20 -0
  17. koi_net/core.py +78 -0
  18. koi_net/effector.py +147 -0
  19. koi_net/entrypoints/__init__.py +2 -0
  20. koi_net/entrypoints/base.py +8 -0
  21. koi_net/entrypoints/poller.py +43 -0
  22. koi_net/entrypoints/server.py +85 -0
  23. koi_net/exceptions.py +107 -0
  24. koi_net/identity.py +20 -0
  25. koi_net/log_system.py +133 -0
  26. koi_net/network/__init__.py +0 -0
  27. koi_net/network/error_handler.py +63 -0
  28. koi_net/network/event_buffer.py +91 -0
  29. koi_net/network/event_queue.py +31 -0
  30. koi_net/network/graph.py +123 -0
  31. koi_net/network/request_handler.py +244 -0
  32. koi_net/network/resolver.py +152 -0
  33. koi_net/network/response_handler.py +130 -0
  34. koi_net/processor/__init__.py +0 -0
  35. koi_net/processor/context.py +36 -0
  36. koi_net/processor/handler.py +61 -0
  37. koi_net/processor/knowledge_handlers.py +302 -0
  38. koi_net/processor/knowledge_object.py +135 -0
  39. koi_net/processor/kobj_queue.py +51 -0
  40. koi_net/processor/pipeline.py +222 -0
  41. koi_net/protocol/__init__.py +0 -0
  42. koi_net/protocol/api_models.py +67 -0
  43. koi_net/protocol/consts.py +7 -0
  44. koi_net/protocol/edge.py +50 -0
  45. koi_net/protocol/envelope.py +65 -0
  46. koi_net/protocol/errors.py +24 -0
  47. koi_net/protocol/event.py +51 -0
  48. koi_net/protocol/model_map.py +62 -0
  49. koi_net/protocol/node.py +18 -0
  50. koi_net/protocol/secure.py +167 -0
  51. koi_net/secure_manager.py +115 -0
  52. koi_net/workers/__init__.py +2 -0
  53. koi_net/workers/base.py +26 -0
  54. koi_net/workers/event_worker.py +111 -0
  55. koi_net/workers/kobj_worker.py +51 -0
  56. koi_net-1.2.4.dist-info/METADATA +485 -0
  57. koi_net-1.2.4.dist-info/RECORD +59 -0
  58. koi_net-1.2.4.dist-info/WHEEL +4 -0
  59. koi_net-1.2.4.dist-info/licenses/LICENSE +21 -0
koi_net/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from . import log_system
@@ -0,0 +1,68 @@
1
+ import structlog
2
+ from rid_lib.ext import Cache
3
+ from rid_lib.types import KoiNetNode
4
+
5
+ from ..network.graph import NetworkGraph
6
+ from ..config.core import NodeConfig
7
+ from ..identity import NodeIdentity
8
+ from ..network.event_queue import EventQueue
9
+ from ..protocol.event import Event, EventType
10
+
11
+ log = structlog.stdlib.get_logger()
12
+
13
+
14
+ class Handshaker:
15
+ """Handles handshaking with other nodes."""
16
+ def __init__(
17
+ self,
18
+ cache: Cache,
19
+ identity: NodeIdentity,
20
+ event_queue: EventQueue,
21
+ config: NodeConfig,
22
+ graph: NetworkGraph
23
+ ):
24
+ self.config = config
25
+ self.cache = cache
26
+ self.identity = identity
27
+ self.event_queue = event_queue
28
+ self.graph = graph
29
+
30
+ def start(self):
31
+ """Attempts handshake with first contact on startup.
32
+
33
+ Handshake occurs if first contact is set in the config, the first
34
+ contact is not already known to this node, and this node does not
35
+ already have incoming edges with node providers.
36
+ """
37
+ if not self.config.koi_net.first_contact.rid:
38
+ return
39
+
40
+ if self.cache.read(self.config.koi_net.first_contact.rid):
41
+ return
42
+
43
+ if self.graph.get_neighbors(
44
+ direction="in", allowed_type=KoiNetNode):
45
+ return
46
+
47
+ self.handshake_with(self.config.koi_net.first_contact.rid)
48
+
49
+ def handshake_with(self, target: KoiNetNode):
50
+ """Initiates a handshake with target node.
51
+
52
+ Pushes successive `FORGET` and `NEW` events to target node to
53
+ reset the target's cache in case it already knew this node.
54
+ """
55
+
56
+ log.debug(f"Initiating handshake with {target}")
57
+ self.event_queue.push(
58
+ Event.from_rid(
59
+ event_type=EventType.FORGET,
60
+ rid=self.identity.rid),
61
+ target=target
62
+ )
63
+ self.event_queue.push(
64
+ event=Event.from_bundle(
65
+ event_type=EventType.NEW,
66
+ bundle=self.cache.read(self.identity.rid)),
67
+ target=target
68
+ )
@@ -0,0 +1,23 @@
1
+ from rid_lib.ext import Bundle
2
+ from ..identity import NodeIdentity
3
+ from ..processor.kobj_queue import KobjQueue
4
+
5
+
6
+ class ProfileMonitor:
7
+ """Processes changes to node profile in the config."""
8
+ def __init__(
9
+ self,
10
+ kobj_queue: KobjQueue,
11
+ identity: NodeIdentity
12
+ ):
13
+ self.kobj_queue = kobj_queue
14
+ self.identity = identity
15
+
16
+ def start(self):
17
+ """Processes identity bundle generated from config."""
18
+ self_bundle = Bundle.generate(
19
+ rid=self.identity.rid,
20
+ contents=self.identity.profile.model_dump()
21
+ )
22
+
23
+ self.kobj_queue.push(bundle=self_bundle)
@@ -0,0 +1,68 @@
1
+ import structlog
2
+ from rid_lib.ext import Cache
3
+ from rid_lib.types import KoiNetNode
4
+
5
+ from ..exceptions import RequestError
6
+ from ..network.graph import NetworkGraph
7
+ from ..network.request_handler import RequestHandler
8
+ from ..processor.kobj_queue import KobjQueue
9
+ from ..protocol.node import NodeProfile, NodeType
10
+
11
+ log = structlog.stdlib.get_logger()
12
+
13
+
14
+ class SyncManager:
15
+ """Handles state synchronization actions with other nodes."""
16
+ graph: NetworkGraph
17
+ cache: Cache
18
+ request_handler: RequestHandler
19
+ kobj_queue: KobjQueue
20
+
21
+ def __init__(
22
+ self,
23
+ graph: NetworkGraph,
24
+ cache: Cache,
25
+ request_handler: RequestHandler,
26
+ kobj_queue: KobjQueue
27
+ ):
28
+ self.graph = graph
29
+ self.cache = cache
30
+ self.request_handler = request_handler
31
+ self.kobj_queue = kobj_queue
32
+
33
+ def start(self):
34
+ """Catches up with node providers on startup."""
35
+
36
+ node_providers = self.graph.get_neighbors(
37
+ direction="in",
38
+ allowed_type=KoiNetNode
39
+ )
40
+
41
+ if not node_providers:
42
+ return
43
+
44
+ log.debug(f"Catching up with `orn:koi-net.node` providers: {node_providers}")
45
+ self.catch_up_with(node_providers, [KoiNetNode])
46
+
47
+ def catch_up_with(self, nodes, rid_types):
48
+ """Catches up with the state of RID types within other nodes."""
49
+
50
+ for node in nodes:
51
+ node_bundle = self.cache.read(node)
52
+ node_profile = node_bundle.validate_contents(NodeProfile)
53
+
54
+ # can't catch up with partial nodes
55
+ if node_profile.node_type != NodeType.FULL:
56
+ continue
57
+
58
+ try:
59
+ payload = self.request_handler.fetch_manifests(
60
+ node, rid_types=rid_types)
61
+ except RequestError:
62
+ continue
63
+
64
+ for manifest in payload.manifests:
65
+ self.kobj_queue.push(
66
+ manifest=manifest,
67
+ source=node
68
+ )
@@ -0,0 +1,209 @@
1
+ import inspect
2
+ from collections import deque
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ import structlog
6
+
7
+ from ..exceptions import BuildError
8
+ from .consts import (
9
+ COMP_ORDER_OVERRIDE,
10
+ COMP_TYPE_OVERRIDE,
11
+ START_FUNC_NAME,
12
+ START_ORDER_OVERRIDE,
13
+ STOP_FUNC_NAME,
14
+ STOP_ORDER_OVERRIDE,
15
+ CompOrder,
16
+ CompType
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from .assembler import NodeAssembler
21
+
22
+ log = structlog.stdlib.get_logger()
23
+
24
+
25
+ class BuildArtifact:
26
+ assembler: "NodeAssembler"
27
+ comp_dict: dict[str, Any]
28
+ dep_graph: dict[str, list[str]]
29
+ comp_types: dict[str, CompType]
30
+ init_order: list[str]
31
+ start_order: list[str]
32
+ stop_order: list[str]
33
+ graphviz: str
34
+
35
+ def __init__(self, assembler: "NodeAssembler"):
36
+ self.assembler = assembler
37
+
38
+ def collect_comps(self):
39
+ """Collects components from class definition."""
40
+
41
+ self.comp_dict = {}
42
+ # adds components from class and all base classes. skips `type`, and runs in reverse so that sub classes override super class values
43
+ for base in reversed(inspect.getmro(self.assembler)[:-1]):
44
+ for k, v in vars(base).items():
45
+ # excludes built in, private, and `None` attributes
46
+ if k.startswith("_") or v is None:
47
+ continue
48
+
49
+ self.comp_dict[k] = v
50
+ log.debug(f"Collected {len(self.comp_dict)} components")
51
+
52
+ def build_dependencies(self):
53
+ """Builds dependency graph and component type map.
54
+
55
+ Graph representation is an adjacency list: the key is a component
56
+ name, and the value is a tuple containing names of the depedencies.
57
+ """
58
+
59
+ self.comp_types = {}
60
+ self.dep_graph = {}
61
+ for comp_name, comp in self.comp_dict.items():
62
+
63
+ dep_names = []
64
+
65
+ explicit_type = getattr(comp, COMP_TYPE_OVERRIDE, None)
66
+ if explicit_type:
67
+ self.comp_types[comp_name] = explicit_type
68
+
69
+ # non callable components are objects treated "as is"
70
+ elif not callable(comp):
71
+ self.comp_types[comp_name] = CompType.OBJECT
72
+
73
+ # callable components default to singletons
74
+ else:
75
+ sig = inspect.signature(comp)
76
+ self.comp_types[comp_name] = CompType.SINGLETON
77
+ dep_names = list(sig.parameters)
78
+
79
+ invalid_deps = set(dep_names) - set(self.comp_dict)
80
+ if invalid_deps:
81
+ raise BuildError(f"Dependencies {invalid_deps} of component '{comp_name}' are undefined")
82
+
83
+ self.dep_graph[comp_name] = dep_names
84
+
85
+ log.debug("Built dependency graph")
86
+
87
+ def build_init_order(self):
88
+ """Builds component initialization order using Kahn's algorithm."""
89
+
90
+ # adj list: n -> outgoing neighbors
91
+ adj = self.dep_graph
92
+ # reverse adj list: n -> incoming neighbors
93
+ r_adj: dict[str, list[str]] = {}
94
+
95
+ # computes reverse adjacency list
96
+ for node in adj:
97
+ r_adj.setdefault(node, [])
98
+ for n in adj[node]:
99
+ r_adj.setdefault(n, [])
100
+ r_adj[n].append(node)
101
+
102
+ # how many outgoing edges each node has
103
+ out_degree = {
104
+ n: len(neighbors)
105
+ for n, neighbors in adj.items()
106
+ }
107
+
108
+ # initializing queue: nodes w/o dependencies
109
+ queue = deque()
110
+ for node in out_degree:
111
+ if out_degree[node] == 0:
112
+ queue.append(node)
113
+
114
+ self.init_order = []
115
+ while queue:
116
+ # removes node from graph
117
+ n = queue.popleft()
118
+ self.init_order.append(n)
119
+
120
+ # updates out degree for nodes dependent on this node
121
+ for next_n in r_adj[n]:
122
+ out_degree[next_n] -= 1
123
+ # adds nodes now without dependencies to queue
124
+ if out_degree[next_n] == 0:
125
+ queue.append(next_n)
126
+
127
+ if len(self.init_order) != len(self.dep_graph):
128
+ cycle_nodes = set(self.dep_graph) - set(self.init_order)
129
+ raise BuildError(f"Found cycle in dependency graph, the following nodes could not be ordered: {cycle_nodes}")
130
+
131
+ log.debug(f"Resolved initialization order: {' -> '.join(self.init_order)}")
132
+
133
+ def build_start_order(self):
134
+ """Builds component start order.
135
+
136
+ Checks if components define a start function in init order. Can
137
+ be overridden by setting start order override in the `NodeAssembler`.
138
+ """
139
+
140
+ self.start_order = getattr(self.assembler, START_ORDER_OVERRIDE, None)
141
+
142
+ if self.start_order:
143
+ return
144
+
145
+ workers = []
146
+ start_order = []
147
+ for comp_name in self.init_order:
148
+ comp = self.comp_dict[comp_name]
149
+ if getattr(comp, START_FUNC_NAME, None):
150
+ if getattr(comp, COMP_ORDER_OVERRIDE, None) == CompOrder.WORKER:
151
+ workers.append(comp_name)
152
+ else:
153
+ start_order.append(comp_name)
154
+
155
+ # order workers first
156
+ self.start_order = workers + start_order
157
+
158
+ log.debug(f"Resolved start order: {' -> '.join(self.start_order)}")
159
+
160
+ def build_stop_order(self):
161
+ """Builds component stop order.
162
+
163
+ Checks if components define a stop function in init order. Can
164
+ be overridden by setting stop order override in the `NodeAssembler`.
165
+ """
166
+ self.stop_order = getattr(self.assembler, STOP_ORDER_OVERRIDE, None)
167
+
168
+ if self.stop_order:
169
+ return
170
+
171
+ workers = []
172
+ stop_order = []
173
+ for comp_name in self.init_order:
174
+ comp = self.comp_dict[comp_name]
175
+ if getattr(comp, STOP_FUNC_NAME, None):
176
+ if getattr(comp, COMP_ORDER_OVERRIDE, None) == CompOrder.WORKER:
177
+ workers.append(comp_name)
178
+ else:
179
+ stop_order.append(comp_name)
180
+
181
+ # order workers first (last)
182
+ self.stop_order = workers + stop_order
183
+ # reverse order from start order
184
+ self.stop_order.reverse()
185
+
186
+ log.debug(f"Resolved stop order: {' -> '.join(self.stop_order)}")
187
+
188
+ def visualize(self) -> str:
189
+ """Creates representation of dependency graph in Graphviz DOT language."""
190
+
191
+ s = "digraph G {\n"
192
+ for node, neighbors in self.dep_graph.items():
193
+ sub_s = node
194
+ if neighbors:
195
+ sub_s += f"-> {', '.join(neighbors)}"
196
+ sub_s = sub_s.replace("graph", "graph_") + ";"
197
+ s += " " * 4 + sub_s + "\n"
198
+ s += "}"
199
+ self.graphviz = s
200
+
201
+ def build(self):
202
+ log.debug("Creating build artifact...")
203
+ self.collect_comps()
204
+ self.build_dependencies()
205
+ self.build_init_order()
206
+ self.build_start_order()
207
+ self.build_stop_order()
208
+ self.visualize()
209
+ log.debug("Done")
@@ -0,0 +1,60 @@
1
+
2
+ from typing import Any, Self
3
+
4
+ import structlog
5
+
6
+ from ..exceptions import BuildError
7
+ from .artifact import BuildArtifact, CompType
8
+ from .container import NodeContainer
9
+
10
+ log = structlog.stdlib.get_logger()
11
+
12
+
13
+ class NodeAssembler:
14
+ _artifact: BuildArtifact = None
15
+
16
+ # optional order overrides:
17
+ _start_order: list[str]
18
+ _stop_order: list[str]
19
+
20
+ # annotation hack to show the components and container methods
21
+ def __new__(cls) -> Self | NodeContainer:
22
+ """Returns assembled node container."""
23
+
24
+ log.debug(f"Assembling '{cls.__name__}'")
25
+
26
+ # builds assembly artifact if it doesn't exist
27
+ if not cls._artifact:
28
+ cls._artifact = BuildArtifact(cls)
29
+ cls._artifact.build()
30
+
31
+ components = cls._build_components(cls._artifact)
32
+
33
+ log.debug("Returning assembled node")
34
+ return NodeContainer(cls._artifact, **components)
35
+
36
+ @staticmethod
37
+ def _build_components(artifact: BuildArtifact):
38
+ """Returns assembled components as a dict."""
39
+
40
+ log.debug("Building components...")
41
+ components: dict[str, Any] = {}
42
+ for comp_name in artifact.init_order:
43
+ # for comp_name, (comp_type, dep_names) in dep_graph.items():
44
+ comp = artifact.comp_dict[comp_name]
45
+ comp_type = artifact.comp_types[comp_name]
46
+
47
+ if comp_type == CompType.OBJECT:
48
+ components[comp_name] = comp
49
+
50
+ elif comp_type == CompType.SINGLETON:
51
+ # builds depedency dict for current component
52
+ dependencies = {}
53
+ for dep in artifact.dep_graph[comp_name]:
54
+ if dep not in components:
55
+ raise BuildError(f"Couldn't find required component '{dep}'")
56
+ dependencies[dep] = components[dep]
57
+ components[comp_name] = comp(**dependencies)
58
+ log.debug("Done")
59
+
60
+ return components
@@ -0,0 +1,6 @@
1
+ from .consts import COMP_ORDER_OVERRIDE, CompOrder
2
+
3
+
4
+ def worker(cls):
5
+ setattr(cls, COMP_ORDER_OVERRIDE, CompOrder.WORKER)
6
+ return cls
@@ -0,0 +1,7 @@
1
+ from .consts import COMP_TYPE_OVERRIDE, CompType
2
+
3
+
4
+ def object(cls):
5
+ """Sets a component's type to `CompType.OBJECT`."""
6
+ setattr(cls, COMP_TYPE_OVERRIDE, CompType.OBJECT)
7
+ return cls
@@ -0,0 +1,18 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ START_FUNC_NAME = "start"
5
+ STOP_FUNC_NAME = "stop"
6
+
7
+ START_ORDER_OVERRIDE = "_start_order"
8
+ STOP_ORDER_OVERRIDE = "_stop_order"
9
+
10
+ COMP_TYPE_OVERRIDE = "_comp_type"
11
+ COMP_ORDER_OVERRIDE = "_comp_order"
12
+
13
+ class CompType(StrEnum):
14
+ SINGLETON = "SINGLETON"
15
+ OBJECT = "OBJECT"
16
+
17
+ class CompOrder(StrEnum):
18
+ WORKER = "WORKER"
@@ -0,0 +1,46 @@
1
+ import structlog
2
+
3
+ from ..entrypoints.base import EntryPoint
4
+ from .artifact import BuildArtifact
5
+ from .consts import START_FUNC_NAME, STOP_FUNC_NAME
6
+
7
+ log = structlog.stdlib.get_logger()
8
+
9
+
10
+ class NodeContainer:
11
+ """Dummy 'shape' for node containers built by assembler."""
12
+ _artifact: BuildArtifact
13
+
14
+ entrypoint: EntryPoint
15
+
16
+ def __init__(self, artifact, **kwargs):
17
+ self._artifact = artifact
18
+
19
+ # adds all components as attributes of this instance
20
+ for name, comp in kwargs.items():
21
+ setattr(self, name, comp)
22
+
23
+ def run(self):
24
+ try:
25
+ self.start()
26
+ self.entrypoint.run()
27
+ except KeyboardInterrupt:
28
+ log.info("Keyboard interrupt!")
29
+ finally:
30
+ self.stop()
31
+
32
+ def start(self):
33
+ log.info("Starting node...")
34
+ for comp_name in self._artifact.start_order:
35
+ comp = getattr(self, comp_name)
36
+ start_func = getattr(comp, START_FUNC_NAME)
37
+ log.info(f"Starting {comp_name}...")
38
+ start_func()
39
+
40
+ def stop(self):
41
+ log.info("Stopping node...")
42
+ for comp_name in self._artifact.stop_order:
43
+ comp = getattr(self, comp_name)
44
+ stop_func = getattr(comp, STOP_FUNC_NAME)
45
+ log.info(f"Stopping {comp_name}...")
46
+ stop_func()
koi_net/cache.py ADDED
@@ -0,0 +1,81 @@
1
+ import os
2
+ import shutil
3
+ from rid_lib.core import RID, RIDType
4
+ from rid_lib.ext import Bundle
5
+ from rid_lib.ext.utils import b64_encode, b64_decode
6
+
7
+ from .config.core import NodeConfig
8
+
9
+
10
+ class Cache:
11
+ def __init__(self, config: NodeConfig):
12
+ self.config = config
13
+
14
+ @property
15
+ def directory_path(self):
16
+ return self.config.koi_net.cache_directory_path
17
+
18
+ def file_path_to(self, rid: RID) -> str:
19
+ encoded_rid_str = b64_encode(str(rid))
20
+ return f"{self.directory_path}/{encoded_rid_str}.json"
21
+
22
+ def write(self, bundle: Bundle) -> Bundle:
23
+ """Writes bundle to cache, returns a Bundle."""
24
+ if not os.path.exists(self.directory_path):
25
+ os.makedirs(self.directory_path)
26
+
27
+ with open(
28
+ file=self.file_path_to(bundle.manifest.rid),
29
+ mode="w",
30
+ encoding="utf-8"
31
+ ) as f:
32
+ f.write(bundle.model_dump_json(indent=2))
33
+
34
+ return bundle
35
+
36
+ def exists(self, rid: RID) -> bool:
37
+ return os.path.exists(
38
+ self.file_path_to(rid)
39
+ )
40
+
41
+ def read(self, rid: RID) -> Bundle | None:
42
+ """Reads and returns CacheEntry from RID cache."""
43
+ try:
44
+ with open(
45
+ file=self.file_path_to(rid),
46
+ mode="r",
47
+ encoding="utf-8"
48
+ ) as f:
49
+ return Bundle.model_validate_json(f.read())
50
+ except FileNotFoundError:
51
+ return None
52
+
53
+ def list_rids(self, rid_types: list[RIDType] | None = None) -> list[RID]:
54
+ if not os.path.exists(self.directory_path):
55
+ return []
56
+
57
+ rids = []
58
+ for filename in os.listdir(self.directory_path):
59
+ encoded_rid_str = filename.split(".")[0]
60
+ rid_str = b64_decode(encoded_rid_str)
61
+ rid = RID.from_string(rid_str)
62
+
63
+ if not rid_types or type(rid) in rid_types:
64
+ rids.append(rid)
65
+
66
+ return rids
67
+
68
+ def delete(self, rid: RID) -> None:
69
+ """Deletes cache bundle."""
70
+ try:
71
+ os.remove(self.file_path_to(rid))
72
+ except FileNotFoundError:
73
+ return
74
+
75
+ def drop(self) -> None:
76
+ """Deletes all cache bundles."""
77
+ try:
78
+ shutil.rmtree(self.directory_path)
79
+ except FileNotFoundError:
80
+ return
81
+