Skip to content

Commit c454822

Browse files
committed
config_cls -> config_schema, switched assembler functions back to chain calls, implemented cache directly with config dependency (necessary for new config system), moved all code execution out of init functions, lifecycle start now calls config loader, and loads priv key to secure manager, removed lifespan from fastapi app, now done using lifecycle context manager in server class to allow for config loading before uvicorn starts, added kobj worker config
1 parent 65cd216 commit c454822

File tree

10 files changed

+139
-59
lines changed

10 files changed

+139
-59
lines changed

examples/coordinator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def handshake_handler(ctx: HandlerContext, kobj: KnowledgeObject):
6161
ctx.kobj_queue.push(bundle=edge_bundle)
6262

6363
class CoordinatorNode(FullNode):
64-
config_cls = CoordinatorConfig
64+
config_schema = CoordinatorConfig
6565
knowledge_handlers = FullNode.knowledge_handlers + [handshake_handler]
6666

6767
if __name__ == "__main__":

examples/partial.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class MyPartialNodeConfig(PartialNodeConfig):
99
)
1010

1111
class MyPartialNode(PartialNode):
12-
config_cls = MyPartialNodeConfig
12+
config_schema = MyPartialNodeConfig
1313

1414
if __name__ == "__main__":
1515
node = MyPartialNode()

src/koi_net/assembler.py

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,10 @@ class NodeAssembler(metaclass=BuildOrderer):
4545
# Self annotation lying to type checker to reflect typing set in node blueprints
4646
def __new__(self) -> Self:
4747
"""Returns assembled node container."""
48-
return self._build()
48+
return self._build_node()
4949

5050
@classmethod
51-
def _build_deps(
52-
cls,
53-
build_order: list[str]
54-
) -> dict[str, tuple[CompType, list[str]]]:
51+
def _build_deps(cls) -> dict[str, tuple[CompType, list[str]]]:
5552
"""Returns dependency graph for components defined in `cls_build_order`.
5653
5754
Graph representation is a dict where each key is a component name,
@@ -60,7 +57,7 @@ def _build_deps(
6057
"""
6158

6259
dep_graph = {}
63-
for comp_name in build_order:
60+
for comp_name in cls._build_order:
6461
try:
6562
comp = getattr(cls, comp_name)
6663
except AttributeError:
@@ -84,9 +81,9 @@ def _build_deps(
8481
return dep_graph
8582

8683
@classmethod
87-
def _visualize(cls, dep_graph) -> str:
84+
def _visualize(cls) -> str:
8885
"""Returns representation of dependency graph in Graphviz DOT language."""
89-
dep_graph = cls._build_deps(cls._build_order)
86+
dep_graph = cls._build_deps()
9087

9188
s = "digraph G {\n"
9289
for node, (_, neighbors) in dep_graph.items():
@@ -99,11 +96,10 @@ def _visualize(cls, dep_graph) -> str:
9996
return s
10097

10198
@classmethod
102-
def _build_comps(
103-
cls,
104-
dep_graph: dict[str, tuple[CompType, list[str]]]
105-
) -> dict[str, Any]:
99+
def _build_comps(cls) -> dict[str, Any]:
106100
"""Returns assembled components from dependency graph."""
101+
dep_graph = cls._build_deps()
102+
107103
components: dict[str, Any] = {}
108104
for comp_name, (comp_type, dep_names) in dep_graph.items():
109105
comp = getattr(cls, comp_name, None)
@@ -119,12 +115,14 @@ def _build_comps(
119115
raise Exception(f"Couldn't find required component '{dep_name}'")
120116
dependencies[dep_name] = components[dep_name]
121117
components[comp_name] = comp(**dependencies)
122-
118+
123119
return components
124120

125121
@classmethod
126-
def _build_node(cls, components: dict[str, Any]) -> NodeContainer:
122+
def _build_node(cls) -> NodeContainer:
127123
"""Returns node container from components."""
124+
components = cls._build_comps()
125+
128126
NodeContainer = make_dataclass(
129127
cls_name="NodeContainer",
130128
fields=[
@@ -136,11 +134,3 @@ def _build_node(cls, components: dict[str, Any]) -> NodeContainer:
136134
)
137135

138136
return NodeContainer(**components)
139-
140-
@classmethod
141-
def _build(cls) -> NodeContainer:
142-
"""Returns node container after calling full build process."""
143-
dep_graph = cls._build_deps(cls._build_order)
144-
comps = cls._build_comps(dep_graph)
145-
node = cls._build_node(comps)
146-
return node

src/koi_net/cache.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+

src/koi_net/config/loader.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ def __init__(
2020
):
2121
self.config_schema = config_schema
2222
self.proxy = config
23-
self.load_from_yaml()
24-
self.save_to_yaml()
2523

2624
def load_from_yaml(self):
2725
"""Loads config from YAML file, or generates it if missing."""

src/koi_net/core.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from rid_lib.ext import Cache
2-
1+
from .cache import Cache
32
from .log_system import LogSystem
43
from .assembler import NodeAssembler
54
from .config.core import NodeConfig
@@ -34,7 +33,6 @@
3433
secure_profile_handler
3534
)
3635

37-
3836
class BaseNode(NodeAssembler):
3937
log_system: LogSystem = LogSystem
4038
kobj_queue: KobjQueue = KobjQueue
@@ -54,8 +52,7 @@ class BaseNode(NodeAssembler):
5452
forget_edge_on_node_deletion
5553
]
5654
deref_handlers: list[DerefHandler] = []
57-
cache: Cache = lambda config: Cache(
58-
directory_path=config.koi_net.cache_directory_path)
55+
cache: Cache = Cache
5956
identity: NodeIdentity = NodeIdentity
6057
graph: NetworkGraph = NetworkGraph
6158
secure_manager: SecureManager = SecureManager

src/koi_net/entrypoints/server.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,8 @@ def __init__(
3232
self.config = config
3333
self.lifecycle = lifecycle
3434
self.response_handler = response_handler
35-
self._build_app()
36-
37-
def _build_endpoints(self, router: APIRouter):
35+
36+
def build_endpoints(self, router: APIRouter):
3837
"""Builds endpoints for API router."""
3938
for path, models in API_MODEL_MAP.items():
4039
def create_endpoint(path: str):
@@ -56,22 +55,16 @@ async def endpoint(req):
5655
response_model_exclude_none=True
5756
)
5857

59-
def _build_app(self):
58+
def build_app(self):
6059
"""Builds FastAPI app."""
61-
@asynccontextmanager
62-
async def lifespan(*args, **kwargs):
63-
async with self.lifecycle.async_run():
64-
yield
65-
6660
self.app = FastAPI(
67-
lifespan=lifespan,
6861
title="KOI-net Protocol API",
6962
version="1.1.0"
7063
)
7164

7265
self.app.add_exception_handler(ProtocolError, self.protocol_error_handler)
7366
self.router = APIRouter(prefix="/koi-net")
74-
self._build_endpoints(self.router)
67+
self.build_endpoints(self.router)
7568
self.app.include_router(self.router)
7669

7770
def protocol_error_handler(self, request, exc: ProtocolError):
@@ -86,10 +79,14 @@ def protocol_error_handler(self, request, exc: ProtocolError):
8679

8780
def run(self):
8881
"""Starts FastAPI server and event handler."""
89-
uvicorn.run(
90-
app=self.app,
91-
host=self.config.server.host,
92-
port=self.config.server.port,
93-
log_config=None,
94-
access_log=False
95-
)
82+
83+
with self.lifecycle.run():
84+
self.build_app()
85+
86+
uvicorn.run(
87+
app=self.app,
88+
host=self.config.server.host,
89+
port=self.config.server.port,
90+
log_config=None,
91+
access_log=False
92+
)

src/koi_net/lifecycle.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
from rid_lib.ext import Bundle
55
from rid_lib.types import KoiNetNode
66

7+
from koi_net.config.loader import ConfigLoader
8+
from koi_net.secure_manager import SecureManager
9+
710
from .sync_manager import SyncManager
811
from .handshaker import Handshaker
912
from .workers.kobj_worker import KnowledgeProcessingWorker
@@ -22,6 +25,7 @@ class NodeLifecycle:
2225
"""Manages node startup and shutdown processes."""
2326

2427
config: NodeConfig
28+
config_loader: ConfigLoader
2529
identity: NodeIdentity
2630
graph: NetworkGraph
2731
kobj_queue: KobjQueue
@@ -30,20 +34,24 @@ class NodeLifecycle:
3034
event_worker: EventProcessingWorker
3135
handshaker: Handshaker
3236
sync_manager: SyncManager
37+
secure_manager: SecureManager
3338

3439
def __init__(
3540
self,
3641
config: NodeConfig,
42+
config_loader: ConfigLoader,
3743
identity: NodeIdentity,
3844
graph: NetworkGraph,
3945
kobj_queue: KobjQueue,
4046
kobj_worker: KnowledgeProcessingWorker,
4147
event_queue: EventQueue,
4248
event_worker: EventProcessingWorker,
4349
handshaker: Handshaker,
44-
sync_manager: SyncManager
50+
sync_manager: SyncManager,
51+
secure_manager: SecureManager
4552
):
4653
self.config = config
54+
self.config_loader = config_loader
4755
self.identity = identity
4856
self.graph = graph
4957
self.kobj_queue = kobj_queue
@@ -52,6 +60,7 @@ def __init__(
5260
self.event_worker = event_worker
5361
self.handshaker = handshaker
5462
self.sync_manager = sync_manager
63+
self.secure_manager = secure_manager
5564

5665
@contextmanager
5766
def run(self):
@@ -86,7 +95,14 @@ def start(self):
8695
graph from nodes and edges in cache. Processes any state changes
8796
of node bundle. Initiates handshake with first contact if node
8897
doesn't have any neighbors. Catches up with coordinator state.
89-
"""
98+
"""
99+
100+
# attempt to load config from yaml, and write back changes (if any)
101+
self.config_loader.load_from_yaml()
102+
self.config_loader.save_to_yaml()
103+
104+
self.secure_manager.load_priv_key()
105+
90106
self.kobj_worker.thread.start()
91107
self.event_worker.thread.start()
92108
self.graph.generate()

src/koi_net/secure_manager.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,20 @@ def __init__(
3737
self.identity = identity
3838
self.cache = cache
3939
self.config = config
40-
41-
self.priv_key = self._load_priv_key()
4240

43-
def _load_priv_key(self) -> PrivateKey:
41+
def load_priv_key(self) -> PrivateKey:
4442
"""Loads private key from PEM file path in config."""
4543

4644
# TODO: handle missing private key
4745
with open(self.config.koi_net.private_key_pem_path, "r") as f:
4846
priv_key_pem = f.read()
4947

50-
return PrivateKey.from_pem(
48+
self.priv_key = PrivateKey.from_pem(
5149
priv_key_pem=priv_key_pem,
5250
password=self.config.env.priv_key_password
5351
)
5452

55-
def _handle_unknown_node(self, envelope: SignedEnvelope) -> Bundle | None:
53+
def handle_unknown_node(self, envelope: SignedEnvelope) -> Bundle | None:
5654
"""Attempts to find node profile in proided envelope.
5755
5856
If an unknown node sends an envelope, it may still be able to be
@@ -89,7 +87,7 @@ def validate_envelope(self, envelope: SignedEnvelope):
8987

9088
node_bundle = (
9189
self.cache.read(envelope.source_node) or
92-
self._handle_unknown_node(envelope)
90+
self.handle_unknown_node(envelope)
9391
)
9492

9593
if not node_bundle:

0 commit comments

Comments
 (0)