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 +95 -0
- koi_net/core.py +125 -125
- koi_net/identity.py +41 -69
- koi_net/network/graph.py +127 -127
- koi_net/network/interface.py +275 -275
- koi_net/network/request_handler.py +148 -148
- koi_net/network/response_handler.py +58 -58
- koi_net/processor/default_handlers.py +162 -162
- koi_net/processor/handler.py +59 -59
- koi_net/processor/interface.py +302 -298
- koi_net/processor/knowledge_object.py +122 -122
- koi_net/protocol/api_models.py +46 -46
- koi_net/protocol/consts.py +6 -6
- koi_net/protocol/edge.py +20 -20
- koi_net/protocol/event.py +50 -50
- koi_net/protocol/helpers.py +24 -24
- koi_net/protocol/node.py +16 -16
- {koi_net-1.0.0b11.dist-info → koi_net-1.0.0b13.dist-info}/METADATA +1 -1
- koi_net-1.0.0b13.dist-info/RECORD +25 -0
- {koi_net-1.0.0b11.dist-info → koi_net-1.0.0b13.dist-info}/licenses/LICENSE +21 -21
- koi_net-1.0.0b11.dist-info/RECORD +0 -24
- {koi_net-1.0.0b11.dist-info → koi_net-1.0.0b13.dist-info}/WHEEL +0 -0
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
|
|
3
|
-
|
|
4
|
-
from .
|
|
5
|
-
from .
|
|
6
|
-
from .processor import
|
|
7
|
-
from .processor
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
10
|
-
from .protocol.event import Event, EventType
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
cache: Cache | None = None,
|
|
34
|
-
network: NetworkInterface | None = None,
|
|
35
|
-
processor: ProcessorInterface | None = None
|
|
36
|
-
):
|
|
37
|
-
self.
|
|
38
|
-
self.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
self.network = network or NetworkInterface(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
self.
|
|
60
|
-
|
|
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
|
|
3
|
-
from rid_lib.ext.
|
|
4
|
-
from rid_lib.
|
|
5
|
-
|
|
6
|
-
from .
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
profile
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
profile
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
self.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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)
|