koi-net 1.0.0b18__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.

koi_net/config.py CHANGED
@@ -1,32 +1,42 @@
1
1
  import os
2
- from typing import TypeVar
3
2
  from ruamel.yaml import YAML
4
- from koi_net.protocol.node import NodeProfile
5
- from rid_lib.types import KoiNetNode
6
3
  from pydantic import BaseModel, Field, PrivateAttr
7
4
  from dotenv import load_dotenv
5
+ from rid_lib.ext.utils import sha256_hash
6
+ from rid_lib.types import KoiNetNode
7
+ from .protocol.secure import PrivateKey
8
+ from .protocol.node import NodeProfile, NodeType
8
9
 
9
10
 
10
11
  class ServerConfig(BaseModel):
11
- host: str | None = "127.0.0.1"
12
- port: int | None = 8000
12
+ host: str = "127.0.0.1"
13
+ port: int = 8000
13
14
  path: str | None = "/koi-net"
14
15
 
15
16
  @property
16
17
  def url(self) -> str:
17
18
  return f"http://{self.host}:{self.port}{self.path or ''}"
18
19
 
20
+ class NodeContact(BaseModel):
21
+ rid: KoiNetNode | None = None
22
+ url: str | None = None
23
+
19
24
  class KoiNetConfig(BaseModel):
20
25
  node_name: str
21
26
  node_rid: KoiNetNode | None = None
22
27
  node_profile: NodeProfile
23
28
 
24
- cache_directory_path: str | None = ".rid_cache"
25
- event_queues_path: str | None = "event_queues.json"
26
-
27
- first_contact: str | None = None
29
+ cache_directory_path: str = ".rid_cache"
30
+ event_queues_path: str = "event_queues.json"
31
+ private_key_pem_path: str = "priv_key.pem"
32
+
33
+ first_contact: NodeContact = Field(default_factory=NodeContact)
34
+
35
+ _priv_key: PrivateKey | None = PrivateAttr(default=None)
28
36
 
29
37
  class EnvConfig(BaseModel):
38
+ priv_key_password: str | None = "PRIV_KEY_PASSWORD"
39
+
30
40
  def __init__(self, **kwargs):
31
41
  super().__init__(**kwargs)
32
42
  load_dotenv()
@@ -41,8 +51,10 @@ class EnvConfig(BaseModel):
41
51
  return value
42
52
 
43
53
  class NodeConfig(BaseModel):
44
- server: ServerConfig | None = Field(default_factory=ServerConfig)
54
+ server: ServerConfig = Field(default_factory=ServerConfig)
45
55
  koi_net: KoiNetConfig
56
+ env: EnvConfig = Field(default_factory=EnvConfig)
57
+
46
58
  _file_path: str = PrivateAttr(default="config.yaml")
47
59
  _file_content: str | None = PrivateAttr(default=None)
48
60
 
@@ -72,13 +84,27 @@ class NodeConfig(BaseModel):
72
84
 
73
85
  config._file_path = file_path
74
86
 
75
- if generate_missing:
76
- config.koi_net.node_rid = (
77
- config.koi_net.node_rid or KoiNetNode.generate(config.koi_net.node_name)
78
- )
79
- config.koi_net.node_profile.base_url = (
80
- config.koi_net.node_profile.base_url or config.server.url
81
- )
87
+ if generate_missing:
88
+ if not config.koi_net.node_rid:
89
+ priv_key = PrivateKey.generate()
90
+ pub_key = priv_key.public_key()
91
+
92
+ config.koi_net.node_rid = KoiNetNode(
93
+ config.koi_net.node_name,
94
+ sha256_hash(pub_key.to_der())
95
+ )
96
+
97
+ with open(config.koi_net.private_key_pem_path, "w") as f:
98
+ f.write(
99
+ priv_key.to_pem(config.env.priv_key_password)
100
+ )
101
+
102
+ config.koi_net.node_profile.public_key = pub_key.to_der()
103
+
104
+ if config.koi_net.node_profile.node_type == NodeType.FULL:
105
+ config.koi_net.node_profile.base_url = (
106
+ config.koi_net.node_profile.base_url or config.server.url
107
+ )
82
108
 
83
109
  config.save_to_yaml()
84
110
 
@@ -98,4 +124,3 @@ class NodeConfig(BaseModel):
98
124
  f.write(self._file_content)
99
125
  raise e
100
126
 
101
- ConfigType = TypeVar("ConfigType", bound=NodeConfig)
koi_net/context.py ADDED
@@ -0,0 +1,55 @@
1
+
2
+ from koi_net.effector import Effector
3
+ from rid_lib.ext import Cache
4
+ from .network.graph import NetworkGraph
5
+ from .network.event_queue import NetworkEventQueue
6
+ from .network.request_handler import RequestHandler
7
+ from .identity import NodeIdentity
8
+ from .processor.interface import ProcessorInterface
9
+
10
+
11
+ class ActionContext:
12
+ identity: NodeIdentity
13
+ effector: Effector
14
+
15
+ def __init__(
16
+ self,
17
+ identity: NodeIdentity,
18
+ effector: Effector
19
+ ):
20
+ self.identity = identity
21
+ self.effector = effector
22
+
23
+
24
+ class HandlerContext:
25
+ identity: NodeIdentity
26
+ cache: Cache
27
+ event_queue: NetworkEventQueue
28
+ graph: NetworkGraph
29
+ request_handler: RequestHandler
30
+ effector: Effector
31
+ _processor: ProcessorInterface | None
32
+
33
+ def __init__(
34
+ self,
35
+ identity: NodeIdentity,
36
+ cache: Cache,
37
+ event_queue: NetworkEventQueue,
38
+ graph: NetworkGraph,
39
+ request_handler: RequestHandler,
40
+ effector: Effector
41
+ ):
42
+ self.identity = identity
43
+ self.cache = cache
44
+ self.event_queue = event_queue
45
+ self.graph = graph
46
+ self.request_handler = request_handler
47
+ self.effector = effector
48
+ self._processor = None
49
+
50
+ def set_processor(self, processor: ProcessorInterface):
51
+ self._processor = processor
52
+
53
+ @property
54
+ def handle(self):
55
+ return self._processor.handle
koi_net/core.py CHANGED
@@ -1,51 +1,100 @@
1
1
  import logging
2
- from typing import Generic
3
2
  import httpx
4
- from rid_lib.ext import Cache, Bundle
5
- from .network import NetworkInterface
6
- from .processor import ProcessorInterface
3
+ from rid_lib.ext import Cache
4
+ from .network.resolver import NetworkResolver
5
+ from .network.event_queue import NetworkEventQueue
6
+ from .network.graph import NetworkGraph
7
+ from .network.request_handler import RequestHandler
8
+ from .network.response_handler import ResponseHandler
9
+ from .network.error_handler import ErrorHandler
10
+ from .network.behavior import Actor
11
+ from .processor.interface import ProcessorInterface
7
12
  from .processor import default_handlers
8
13
  from .processor.handler import KnowledgeHandler
14
+ from .processor.knowledge_pipeline import KnowledgePipeline
9
15
  from .identity import NodeIdentity
10
- from .protocol.event import Event, EventType
11
- from .config import ConfigType
16
+ from .secure import Secure
17
+ from .config import NodeConfig
18
+ from .context import HandlerContext, ActionContext
19
+ from .effector import Effector
20
+ from . import default_actions
12
21
 
13
22
  logger = logging.getLogger(__name__)
14
23
 
15
24
 
16
25
 
17
- class NodeInterface(Generic[ConfigType]):
18
- config: ConfigType
26
+ class NodeInterface:
27
+ config: NodeConfig
19
28
  cache: Cache
20
29
  identity: NodeIdentity
21
- network: NetworkInterface
30
+ resolver: NetworkResolver
31
+ event_queue: NetworkEventQueue
32
+ graph: NetworkGraph
22
33
  processor: ProcessorInterface
34
+ secure: Secure
23
35
 
24
36
  use_kobj_processor_thread: bool
25
37
 
26
38
  def __init__(
27
39
  self,
28
- config: ConfigType,
40
+ config: NodeConfig,
29
41
  use_kobj_processor_thread: bool = False,
30
42
 
31
43
  handlers: list[KnowledgeHandler] | None = None,
32
44
 
33
45
  cache: Cache | None = None,
34
- network: NetworkInterface | None = None,
35
46
  processor: ProcessorInterface | None = None
36
47
  ):
37
- self.config: ConfigType = config
48
+ self.config = config
38
49
  self.cache = cache or Cache(
39
- self.config.koi_net.cache_directory_path)
50
+ directory_path=self.config.koi_net.cache_directory_path
51
+ )
52
+
53
+ self.identity = NodeIdentity(config=self.config)
40
54
 
41
- self.identity = NodeIdentity(
55
+ self.effector = Effector(cache=self.cache)
56
+
57
+ self.graph = NetworkGraph(
58
+ cache=self.cache,
59
+ identity=self.identity
60
+ )
61
+
62
+ self.secure = Secure(
63
+ identity=self.identity,
64
+ effector=self.effector,
65
+ config=self.config
66
+ )
67
+
68
+ self.request_handler = RequestHandler(
69
+ effector=self.effector,
70
+ identity=self.identity,
71
+ secure=self.secure
72
+ )
73
+
74
+ self.response_handler = ResponseHandler(self.cache, self.effector)
75
+
76
+ self.resolver = NetworkResolver(
42
77
  config=self.config,
43
- cache=self.cache)
78
+ cache=self.cache,
79
+ identity=self.identity,
80
+ graph=self.graph,
81
+ request_handler=self.request_handler,
82
+ effector=self.effector
83
+ )
44
84
 
45
- self.network = network or NetworkInterface(
85
+ self.event_queue = NetworkEventQueue(
46
86
  config=self.config,
47
87
  cache=self.cache,
48
- identity=self.identity
88
+ identity=self.identity,
89
+ graph=self.graph,
90
+ request_handler=self.request_handler,
91
+ effector=self.effector
92
+ )
93
+
94
+ self.actor = Actor(
95
+ identity=self.identity,
96
+ effector=self.effector,
97
+ event_queue=self.event_queue
49
98
  )
50
99
 
51
100
  # pull all handlers defined in default_handlers module
@@ -56,14 +105,48 @@ class NodeInterface(Generic[ConfigType]):
56
105
  ]
57
106
 
58
107
  self.use_kobj_processor_thread = use_kobj_processor_thread
59
- self.processor = processor or ProcessorInterface(
60
- config=self.config,
61
- cache=self.cache,
62
- network=self.network,
63
- identity=self.identity,
64
- use_kobj_processor_thread=self.use_kobj_processor_thread,
108
+
109
+ self.action_context = ActionContext(
110
+ identity=self.identity,
111
+ effector=self.effector
112
+ )
113
+
114
+ self.handler_context = HandlerContext(
115
+ identity=self.identity,
116
+ cache=self.cache,
117
+ event_queue=self.event_queue,
118
+ graph=self.graph,
119
+ request_handler=self.request_handler,
120
+ effector=self.effector
121
+ )
122
+
123
+ self.pipeline = KnowledgePipeline(
124
+ handler_context=self.handler_context,
125
+ cache=self.cache,
126
+ request_handler=self.request_handler,
127
+ event_queue=self.event_queue,
128
+ graph=self.graph,
65
129
  default_handlers=handlers
66
130
  )
131
+
132
+ self.processor = processor or ProcessorInterface(
133
+ pipeline=self.pipeline,
134
+ use_kobj_processor_thread=self.use_kobj_processor_thread
135
+ )
136
+
137
+ self.error_handler = ErrorHandler(
138
+ processor=self.processor,
139
+ actor=self.actor
140
+ )
141
+
142
+ self.request_handler.set_error_handler(self.error_handler)
143
+
144
+ self.handler_context.set_processor(self.processor)
145
+
146
+ self.effector.set_processor(self.processor)
147
+ self.effector.set_resolver(self.resolver)
148
+ self.effector.set_action_context(self.action_context)
149
+
67
150
 
68
151
  def start(self) -> None:
69
152
  """Starts a node, call this method first.
@@ -74,16 +157,12 @@ class NodeInterface(Generic[ConfigType]):
74
157
  logger.info("Starting processor worker thread")
75
158
  self.processor.worker_thread.start()
76
159
 
77
- self.network._load_event_queues()
78
- self.network.graph.generate()
79
-
80
- self.processor.handle(
81
- bundle=Bundle.generate(
82
- rid=self.identity.rid,
83
- contents=self.identity.profile.model_dump()
84
- )
85
- )
160
+ # self.network._load_event_queues()
161
+ self.graph.generate()
86
162
 
163
+ # refresh to reflect changes (if any) in config.yaml
164
+ self.effector.deref(self.identity.rid, refresh_cache=True)
165
+
87
166
  logger.debug("Waiting for kobj queue to empty")
88
167
  if self.use_kobj_processor_thread:
89
168
  self.processor.kobj_queue.join()
@@ -91,23 +170,10 @@ class NodeInterface(Generic[ConfigType]):
91
170
  self.processor.flush_kobj_queue()
92
171
  logger.debug("Done")
93
172
 
94
- if not self.network.graph.get_neighbors() and self.config.koi_net.first_contact:
95
- logger.debug(f"I don't have any neighbors, reaching out to first contact {self.config.koi_net.first_contact}")
173
+ if not self.graph.get_neighbors() and self.config.koi_net.first_contact.rid:
174
+ logger.debug(f"I don't have any neighbors, reaching out to first contact {self.config.koi_net.first_contact.rid!r}")
96
175
 
97
- events = [
98
- Event.from_rid(EventType.FORGET, self.identity.rid),
99
- Event.from_bundle(EventType.NEW, self.identity.bundle)
100
- ]
101
-
102
- try:
103
- self.network.request_handler.broadcast_events(
104
- url=self.config.koi_net.first_contact,
105
- events=events
106
- )
107
-
108
- except httpx.ConnectError:
109
- logger.warning("Failed to reach first contact")
110
- return
176
+ self.actor.handshake_with(self.config.koi_net.first_contact.rid)
111
177
 
112
178
 
113
179
  def stop(self):
@@ -123,4 +189,4 @@ class NodeInterface(Generic[ConfigType]):
123
189
  else:
124
190
  self.processor.flush_kobj_queue()
125
191
 
126
- self.network._save_event_queues()
192
+ # self.network._save_event_queues()
@@ -0,0 +1,15 @@
1
+ from .context import ActionContext
2
+ from rid_lib.types import KoiNetNode
3
+ from rid_lib.ext import Bundle
4
+ from .effector import Effector
5
+
6
+
7
+ @Effector.register_default_action(KoiNetNode)
8
+ def dereference_koi_node(ctx: ActionContext, rid: KoiNetNode) -> Bundle:
9
+ if rid != ctx.identity.rid:
10
+ return
11
+
12
+ return Bundle.generate(
13
+ rid=ctx.identity.rid,
14
+ contents=ctx.identity.profile.model_dump()
15
+ )
koi_net/effector.py ADDED
@@ -0,0 +1,139 @@
1
+ import logging
2
+ from typing import Callable
3
+ from enum import StrEnum
4
+ from rid_lib.ext import Cache, Bundle
5
+ from rid_lib.core import RID, RIDType
6
+ from rid_lib.types import KoiNetNode
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from .network.resolver import NetworkResolver
12
+ from .processor.interface import ProcessorInterface
13
+ from .context import ActionContext
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class BundleSource(StrEnum):
19
+ CACHE = "CACHE"
20
+ ACTION = "ACTION"
21
+
22
+ class Effector:
23
+ cache: Cache
24
+ resolver: "NetworkResolver | None"
25
+ processor: "ProcessorInterface | None"
26
+ action_context: "ActionContext | None"
27
+ _action_table: dict[
28
+ type[RID],
29
+ Callable[
30
+ ["ActionContext", RID],
31
+ Bundle | None
32
+ ]
33
+ ] = dict()
34
+
35
+ def __init__(
36
+ self,
37
+ cache: Cache,
38
+ ):
39
+ self.cache = cache
40
+ self.resolver = None
41
+ self.processor = None
42
+ self.action_context = None
43
+ self._action_table = self.__class__._action_table.copy()
44
+
45
+ def set_processor(self, processor: "ProcessorInterface"):
46
+ self.processor = processor
47
+
48
+ def set_resolver(self, resolver: "NetworkResolver"):
49
+ self.resolver = resolver
50
+
51
+ def set_action_context(self, action_context: "ActionContext"):
52
+ self.action_context = action_context
53
+
54
+ @classmethod
55
+ def register_default_action(cls, rid_type: RIDType):
56
+ def decorator(func: Callable) -> Callable:
57
+ cls._action_table[rid_type] = func
58
+ return func
59
+ return decorator
60
+
61
+ def register_action(self, rid_type: RIDType):
62
+ def decorator(func: Callable) -> Callable:
63
+ self._action_table[rid_type] = func
64
+ return func
65
+ return decorator
66
+
67
+ def _try_cache(self, rid: RID) -> tuple[Bundle, BundleSource] | None:
68
+ bundle = self.cache.read(rid)
69
+
70
+ if bundle:
71
+ logger.debug("Cache hit")
72
+ return bundle, BundleSource.CACHE
73
+ else:
74
+ logger.debug("Cache miss")
75
+ return None
76
+
77
+ def _try_action(self, rid: RID) -> tuple[Bundle, BundleSource] | None:
78
+ if type(rid) not in self._action_table:
79
+ logger.debug("No action available")
80
+ return None
81
+
82
+ logger.debug("Action available")
83
+ func = self._action_table[type(rid)]
84
+ bundle = func(
85
+ ctx=self.action_context,
86
+ rid=rid
87
+ )
88
+
89
+ if bundle:
90
+ logger.debug("Action hit")
91
+ return bundle, BundleSource.ACTION
92
+ else:
93
+ logger.debug("Action miss")
94
+ return None
95
+
96
+
97
+ def _try_network(self, rid: RID) -> tuple[Bundle, KoiNetNode] | None:
98
+ bundle, source = self.resolver.fetch_remote_bundle(rid)
99
+
100
+ if bundle:
101
+ logger.debug("Network hit")
102
+ return bundle, source
103
+ else:
104
+ logger.debug("Network miss")
105
+ return None
106
+
107
+
108
+ def deref(
109
+ self,
110
+ rid: RID,
111
+ refresh_cache: bool = False,
112
+ use_network: bool = False,
113
+ handle_result: bool = True
114
+ ) -> Bundle | None:
115
+ logger.debug(f"Dereferencing {rid!r}")
116
+
117
+ bundle, source = (
118
+ # if `refresh_cache`, skip try cache
119
+ not refresh_cache and self._try_cache(rid) or
120
+ self._try_action(rid) or
121
+ use_network and self._try_network(rid) or
122
+ # if not found, bundle and source set to None
123
+ (None, None)
124
+ )
125
+
126
+ if (
127
+ handle_result
128
+ and bundle is not None
129
+ and source != BundleSource.CACHE
130
+ ):
131
+ self.processor.handle(
132
+ bundle=bundle,
133
+ source=source if type(source) is KoiNetNode else None
134
+ )
135
+
136
+ # TODO: refactor for general solution, param to write through to cache before continuing
137
+ # like `self.processor.kobj_queue.join()``
138
+
139
+ return bundle
koi_net/identity.py CHANGED
@@ -1,33 +1,19 @@
1
1
  import logging
2
- from rid_lib.ext.bundle import Bundle
3
- from rid_lib.ext.cache import Cache
4
2
  from rid_lib.types.koi_net_node import KoiNetNode
5
-
6
3
  from .config import NodeConfig
7
4
  from .protocol.node import NodeProfile
8
5
 
6
+
9
7
  logger = logging.getLogger(__name__)
10
8
 
11
9
 
12
10
  class NodeIdentity:
13
- """Represents a node's identity (RID, profile, bundle)."""
11
+ """Represents a node's identity (RID, profile)."""
14
12
 
15
13
  config: NodeConfig
16
- cache: Cache
17
14
 
18
- def __init__(
19
- self,
20
- config: NodeConfig,
21
- cache: Cache
22
- ):
23
- """Initializes node identity from a name and profile.
24
-
25
- Attempts to read identity from storage. If it doesn't already exist, a new RID is generated from the provided name, and that RID and profile are written to storage. Changes to the name or profile will update the stored identity.
26
-
27
- WARNING: If the name is changed, the RID will be overwritten which will have consequences for the rest of the network.
28
- """
15
+ def __init__(self, config: NodeConfig):
29
16
  self.config = config
30
- self.cache = cache
31
17
 
32
18
  @property
33
19
  def rid(self) -> KoiNetNode:
@@ -35,8 +21,4 @@ class NodeIdentity:
35
21
 
36
22
  @property
37
23
  def profile(self) -> NodeProfile:
38
- return self.config.koi_net.node_profile
39
-
40
- @property
41
- def bundle(self) -> Bundle:
42
- return self.cache.read(self.rid)
24
+ return self.config.koi_net.node_profile
@@ -1 +0,0 @@
1
- from .interface import NetworkInterface
@@ -0,0 +1,42 @@
1
+ from logging import getLogger
2
+ from rid_lib.types import KoiNetNode
3
+ from ..protocol.event import Event, EventType
4
+ from ..identity import NodeIdentity
5
+ from ..effector import Effector
6
+ from ..network.event_queue import NetworkEventQueue
7
+
8
+ logger = getLogger(__name__)
9
+
10
+
11
+ class Actor:
12
+ identity: NodeIdentity
13
+ effector: Effector
14
+ event_queue: NetworkEventQueue
15
+
16
+ def __init__(
17
+ self,
18
+ identity: NodeIdentity,
19
+ effector: Effector,
20
+ event_queue: NetworkEventQueue
21
+ ):
22
+ self.identity = identity
23
+ self.effector = effector
24
+ self.event_queue = event_queue
25
+
26
+ def handshake_with(self, target: KoiNetNode):
27
+ logger.debug(f"Initiating handshake with {target}")
28
+ self.event_queue.push_event_to(
29
+ Event.from_rid(
30
+ event_type=EventType.FORGET,
31
+ rid=self.identity.rid),
32
+ node=target
33
+ )
34
+
35
+ self.event_queue.push_event_to(
36
+ event=Event.from_bundle(
37
+ event_type=EventType.NEW,
38
+ bundle=self.effector.deref(self.identity.rid)),
39
+ node=target
40
+ )
41
+
42
+ self.event_queue.flush_webhook_queue(target)
@@ -0,0 +1,50 @@
1
+ from logging import getLogger
2
+ from koi_net.protocol.errors import ErrorTypes
3
+ from koi_net.protocol.event import EventType
4
+ from rid_lib.types import KoiNetNode
5
+ from ..processor.interface import ProcessorInterface
6
+ from ..network.behavior import Actor
7
+
8
+ logger = getLogger(__name__)
9
+
10
+
11
+ class ErrorHandler:
12
+ timeout_counter: dict[KoiNetNode, int]
13
+ processor: ProcessorInterface
14
+ actor: Actor
15
+
16
+ def __init__(
17
+ self,
18
+ processor: ProcessorInterface,
19
+ actor: Actor
20
+ ):
21
+ self.processor = processor
22
+ self.actor = actor
23
+ self.timeout_counter = {}
24
+
25
+ def handle_connection_error(self, node: KoiNetNode):
26
+ self.timeout_counter.setdefault(node, 0)
27
+ self.timeout_counter[node] += 1
28
+
29
+ logger.debug(f"{node} has timed out {self.timeout_counter[node]} time(s)")
30
+
31
+ if self.timeout_counter[node] > 3:
32
+ logger.debug(f"Exceeded time out limit, forgetting node")
33
+ self.processor.handle(rid=node, event_type=EventType.FORGET)
34
+ # do something
35
+
36
+
37
+ def handle_protocol_error(
38
+ self,
39
+ error_type: ErrorTypes,
40
+ node: KoiNetNode
41
+ ):
42
+ logger.info(f"Handling protocol error {error_type} for node {node!r}")
43
+ match error_type:
44
+ case ErrorTypes.UnknownNode:
45
+ logger.info("Peer doesn't know me, attempting handshake...")
46
+ self.actor.handshake_with(node)
47
+
48
+ case ErrorTypes.InvalidKey: ...
49
+ case ErrorTypes.InvalidSignature: ...
50
+ case ErrorTypes.InvalidTarget: ...