koi-net 1.0.0b11__py3-none-any.whl → 1.0.0b13__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 ADDED
@@ -0,0 +1,95 @@
1
+ import os
2
+ from typing import TypeVar
3
+ from ruamel.yaml import YAML
4
+ from koi_net.protocol.node import NodeProfile
5
+ from rid_lib.types import KoiNetNode
6
+ from pydantic import BaseModel, Field, PrivateAttr
7
+ from dotenv import load_dotenv
8
+
9
+
10
+ class ServerConfig(BaseModel):
11
+ host: str | None = "127.0.0.1"
12
+ port: int | None = 8000
13
+ path: str | None = None
14
+
15
+ @property
16
+ def url(self):
17
+ return f"http://{self.host}:{self.port}{self.path or ''}"
18
+
19
+ class KoiNetConfig(BaseModel):
20
+ node_name: str
21
+ node_rid: KoiNetNode | None = None
22
+ node_profile: NodeProfile
23
+
24
+ cache_directory_path: str | None = ".rid_cache"
25
+ event_queues_path: str | None = "event_queues.json"
26
+
27
+ first_contact: str | None = None
28
+
29
+ class EnvConfig(BaseModel):
30
+ def __init__(self, **kwargs):
31
+ super().__init__(**kwargs)
32
+ load_dotenv()
33
+
34
+ def __getattribute__(self, name):
35
+ value = super().__getattribute__(name)
36
+ if name in type(self).model_fields:
37
+ env_val = os.getenv(value)
38
+ if env_val is None:
39
+ raise ValueError(f"Required environment variable {value} not set")
40
+ return env_val
41
+ return value
42
+
43
+ class Config(BaseModel):
44
+ server: ServerConfig | None = Field(default_factory=ServerConfig)
45
+ koi_net: KoiNetConfig
46
+ _file_path: str = PrivateAttr(default="config.yaml")
47
+ _file_content: str | None = PrivateAttr(default=None)
48
+
49
+ @classmethod
50
+ def load_from_yaml(
51
+ cls,
52
+ file_path: str | None = None,
53
+ generate_missing: bool = True
54
+ ):
55
+ yaml = YAML()
56
+
57
+ try:
58
+ with open(file_path, "r") as f:
59
+ file_content = f.read()
60
+ config_data = yaml.load(file_content)
61
+ config = cls.model_validate(config_data)
62
+ config._file_content = file_content
63
+
64
+ except FileNotFoundError:
65
+ config = cls()
66
+
67
+ config._file_path = file_path
68
+
69
+ if generate_missing:
70
+ config.koi_net.node_rid = (
71
+ config.koi_net.node_rid or KoiNetNode.generate(config.koi_net.node_name)
72
+ )
73
+ config.koi_net.node_profile.base_url = (
74
+ config.koi_net.node_profile.base_url or config.server.url
75
+ )
76
+
77
+ config.save_to_yaml()
78
+
79
+ return config
80
+
81
+ def save_to_yaml(self):
82
+ yaml = YAML()
83
+
84
+ with open(self._file_path, "w") as f:
85
+ try:
86
+ config_data = self.model_dump(mode="json")
87
+ yaml.dump(config_data, f)
88
+ except Exception as e:
89
+ if self._file_content:
90
+ f.seek(0)
91
+ f.truncate()
92
+ f.write(self._file_content)
93
+ raise e
94
+
95
+ ConfigType = TypeVar("ConfigType", bound=Config)
koi_net/core.py CHANGED
@@ -1,126 +1,126 @@
1
- import logging
2
- import httpx
3
- from rid_lib.ext import Cache, Bundle
4
- from .network import NetworkInterface
5
- from .processor import ProcessorInterface
6
- from .processor import default_handlers
7
- from .processor.handler import KnowledgeHandler
8
- from .identity import NodeIdentity
9
- from .protocol.node import NodeProfile
10
- from .protocol.event import Event, EventType
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
-
15
- class NodeInterface:
16
- cache: Cache
17
- identity: NodeIdentity
18
- network: NetworkInterface
19
- processor: ProcessorInterface
20
- first_contact: str
21
- use_kobj_processor_thread: bool
22
-
23
- def __init__(
24
- self,
25
- name: str,
26
- profile: NodeProfile,
27
- identity_file_path: str = "identity.json",
28
- event_queues_file_path: str = "event_queues.json",
29
- cache_directory_path: str = "rid_cache",
30
- use_kobj_processor_thread: bool = False,
31
- first_contact: str | None = None,
32
- handlers: list[KnowledgeHandler] | None = None,
33
- cache: Cache | None = None,
34
- network: NetworkInterface | None = None,
35
- processor: ProcessorInterface | None = None
36
- ):
37
- self.cache = cache or Cache(cache_directory_path)
38
- self.identity = NodeIdentity(
39
- name=name,
40
- profile=profile,
41
- cache=self.cache,
42
- file_path=identity_file_path
43
- )
44
- self.first_contact = first_contact
45
- self.network = network or NetworkInterface(
46
- file_path=event_queues_file_path,
47
- first_contact=self.first_contact,
48
- cache=self.cache,
49
- identity=self.identity
50
- )
51
-
52
- # pull all handlers defined in default_handlers module
53
- if handlers is None:
54
- handlers = [
55
- obj for obj in vars(default_handlers).values()
56
- if isinstance(obj, KnowledgeHandler)
57
- ]
58
-
59
- self.use_kobj_processor_thread = use_kobj_processor_thread
60
- self.processor = processor or ProcessorInterface(
61
- cache=self.cache,
62
- network=self.network,
63
- identity=self.identity,
64
- use_kobj_processor_thread=self.use_kobj_processor_thread,
65
- default_handlers=handlers
66
- )
67
-
68
- def start(self) -> None:
69
- """Starts a node, call this method first.
70
-
71
- Starts the processor thread (if enabled). Loads event queues into memory. Generates network graph from nodes and edges in cache. Processes any state changes of node bundle. Initiates handshake with first contact (if provided) if node doesn't have any neighbors.
72
- """
73
- if self.use_kobj_processor_thread:
74
- logger.info("Starting processor worker thread")
75
- self.processor.worker_thread.start()
76
-
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
- )
86
-
87
- logger.debug("Waiting for kobj queue to empty")
88
- if self.use_kobj_processor_thread:
89
- self.processor.kobj_queue.join()
90
- else:
91
- self.processor.flush_kobj_queue()
92
- logger.debug("Done")
93
-
94
- if not self.network.graph.get_neighbors() and self.first_contact:
95
- logger.debug(f"I don't have any neighbors, reaching out to first contact {self.first_contact}")
96
-
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.first_contact,
105
- events=events
106
- )
107
-
108
- except httpx.ConnectError:
109
- logger.warning("Failed to reach first contact")
110
- return
111
-
112
-
113
- def stop(self):
114
- """Stops a node, call this method last.
115
-
116
- Finishes processing knowledge object queue. Saves event queues to storage.
117
- """
118
- logger.info("Stopping node...")
119
-
120
- if self.use_kobj_processor_thread:
121
- logger.info(f"Waiting for kobj queue to empty ({self.processor.kobj_queue.unfinished_tasks} tasks remaining)")
122
- self.processor.kobj_queue.join()
123
- else:
124
- self.processor.flush_kobj_queue()
125
-
1
+ import logging
2
+ from typing import Generic
3
+ import httpx
4
+ from rid_lib.ext import Cache, Bundle
5
+ from .network import NetworkInterface
6
+ from .processor import ProcessorInterface
7
+ from .processor import default_handlers
8
+ from .processor.handler import KnowledgeHandler
9
+ from .identity import NodeIdentity
10
+ from .protocol.event import Event, EventType
11
+ from .config import ConfigType
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+
17
+ class NodeInterface(Generic[ConfigType]):
18
+ config: ConfigType
19
+ cache: Cache
20
+ identity: NodeIdentity
21
+ network: NetworkInterface
22
+ processor: ProcessorInterface
23
+
24
+ use_kobj_processor_thread: bool
25
+
26
+ def __init__(
27
+ self,
28
+ config: ConfigType,
29
+ use_kobj_processor_thread: bool = False,
30
+
31
+ handlers: list[KnowledgeHandler] | None = None,
32
+
33
+ cache: Cache | None = None,
34
+ network: NetworkInterface | None = None,
35
+ processor: ProcessorInterface | None = None
36
+ ):
37
+ self.config: ConfigType = config
38
+ self.cache = cache or Cache(
39
+ self.config.koi_net.cache_directory_path)
40
+
41
+ self.identity = NodeIdentity(
42
+ config=self.config,
43
+ cache=self.cache)
44
+
45
+ self.network = network or NetworkInterface(
46
+ config=self.config,
47
+ cache=self.cache,
48
+ identity=self.identity
49
+ )
50
+
51
+ # pull all handlers defined in default_handlers module
52
+ if handlers is None:
53
+ handlers = [
54
+ obj for obj in vars(default_handlers).values()
55
+ if isinstance(obj, KnowledgeHandler)
56
+ ]
57
+
58
+ 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,
65
+ default_handlers=handlers
66
+ )
67
+
68
+ def start(self) -> None:
69
+ """Starts a node, call this method first.
70
+
71
+ Starts the processor thread (if enabled). Loads event queues into memory. Generates network graph from nodes and edges in cache. Processes any state changes of node bundle. Initiates handshake with first contact (if provided) if node doesn't have any neighbors.
72
+ """
73
+ if self.use_kobj_processor_thread:
74
+ logger.info("Starting processor worker thread")
75
+ self.processor.worker_thread.start()
76
+
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
+ )
86
+
87
+ logger.debug("Waiting for kobj queue to empty")
88
+ if self.use_kobj_processor_thread:
89
+ self.processor.kobj_queue.join()
90
+ else:
91
+ self.processor.flush_kobj_queue()
92
+ logger.debug("Done")
93
+
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}")
96
+
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
111
+
112
+
113
+ def stop(self):
114
+ """Stops a node, call this method last.
115
+
116
+ Finishes processing knowledge object queue. Saves event queues to storage.
117
+ """
118
+ logger.info("Stopping node...")
119
+
120
+ if self.use_kobj_processor_thread:
121
+ logger.info(f"Waiting for kobj queue to empty ({self.processor.kobj_queue.unfinished_tasks} tasks remaining)")
122
+ self.processor.kobj_queue.join()
123
+ else:
124
+ self.processor.flush_kobj_queue()
125
+
126
126
  self.network._save_event_queues()
koi_net/identity.py CHANGED
@@ -1,70 +1,42 @@
1
- import logging
2
- from pydantic import BaseModel
3
- from rid_lib.ext.bundle import Bundle
4
- from rid_lib.ext.cache import Cache
5
- from rid_lib.types.koi_net_node import KoiNetNode
6
- from .protocol.node import NodeProfile
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
-
11
- class NodeIdentityModel(BaseModel):
12
- rid: KoiNetNode
13
- profile: NodeProfile
14
-
15
- class NodeIdentity:
16
- """Represents a node's identity (RID, profile, bundle)."""
17
-
18
- _identity: NodeIdentityModel
19
- file_path: str
20
- cache: Cache
21
-
22
- def __init__(
23
- self,
24
- name: str,
25
- profile: NodeProfile,
26
- cache: Cache,
27
- file_path: str = "identity.json"
28
- ):
29
- """Initializes node identity from a name and profile.
30
-
31
- 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.
32
-
33
- WARNING: If the name is changed, the RID will be overwritten which will have consequences for the rest of the network.
34
- """
35
- self.cache = cache
36
- self.file_path = file_path
37
-
38
- self._identity = None
39
- try:
40
- with open(file_path, "r") as f:
41
- self._identity = NodeIdentityModel.model_validate_json(f.read())
42
-
43
- except FileNotFoundError:
44
- pass
45
-
46
- if self._identity:
47
- if self._identity.rid.name != name:
48
- logger.warning("Node name changed which will change this node's RID, if you really want to do this manually delete the identity JSON file")
49
- if self._identity.profile != profile:
50
- self._identity.profile = profile
51
- else:
52
- self._identity = NodeIdentityModel(
53
- rid=KoiNetNode.generate(name),
54
- profile=profile,
55
- )
56
-
57
- with open(file_path, "w") as f:
58
- f.write(self._identity.model_dump_json(indent=2))
59
-
60
- @property
61
- def rid(self) -> KoiNetNode:
62
- return self._identity.rid
63
-
64
- @property
65
- def profile(self) -> NodeProfile:
66
- return self._identity.profile
67
-
68
- @property
69
- def bundle(self) -> Bundle:
1
+ import logging
2
+ from rid_lib.ext.bundle import Bundle
3
+ from rid_lib.ext.cache import Cache
4
+ from rid_lib.types.koi_net_node import KoiNetNode
5
+
6
+ from .config import Config
7
+ from .protocol.node import NodeProfile
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class NodeIdentity:
13
+ """Represents a node's identity (RID, profile, bundle)."""
14
+
15
+ config: Config
16
+ cache: Cache
17
+
18
+ def __init__(
19
+ self,
20
+ config: Config,
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
+ """
29
+ self.config = config
30
+ self.cache = cache
31
+
32
+ @property
33
+ def rid(self) -> KoiNetNode:
34
+ return self.config.koi_net.node_rid
35
+
36
+ @property
37
+ def profile(self) -> NodeProfile:
38
+ return self.config.koi_net.node_profile
39
+
40
+ @property
41
+ def bundle(self) -> Bundle:
70
42
  return self.cache.read(self.rid)