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.
- koi_net/__init__.py +1 -0
- koi_net/behaviors/handshaker.py +68 -0
- koi_net/behaviors/profile_monitor.py +23 -0
- koi_net/behaviors/sync_manager.py +68 -0
- koi_net/build/artifact.py +209 -0
- koi_net/build/assembler.py +60 -0
- koi_net/build/comp_order.py +6 -0
- koi_net/build/comp_type.py +7 -0
- koi_net/build/consts.py +18 -0
- koi_net/build/container.py +46 -0
- koi_net/cache.py +81 -0
- koi_net/config/core.py +113 -0
- koi_net/config/full_node.py +45 -0
- koi_net/config/loader.py +60 -0
- koi_net/config/partial_node.py +26 -0
- koi_net/config/proxy.py +20 -0
- koi_net/core.py +78 -0
- koi_net/effector.py +147 -0
- koi_net/entrypoints/__init__.py +2 -0
- koi_net/entrypoints/base.py +8 -0
- koi_net/entrypoints/poller.py +43 -0
- koi_net/entrypoints/server.py +85 -0
- koi_net/exceptions.py +107 -0
- koi_net/identity.py +20 -0
- koi_net/log_system.py +133 -0
- koi_net/network/__init__.py +0 -0
- koi_net/network/error_handler.py +63 -0
- koi_net/network/event_buffer.py +91 -0
- koi_net/network/event_queue.py +31 -0
- koi_net/network/graph.py +123 -0
- koi_net/network/request_handler.py +244 -0
- koi_net/network/resolver.py +152 -0
- koi_net/network/response_handler.py +130 -0
- koi_net/processor/__init__.py +0 -0
- koi_net/processor/context.py +36 -0
- koi_net/processor/handler.py +61 -0
- koi_net/processor/knowledge_handlers.py +302 -0
- koi_net/processor/knowledge_object.py +135 -0
- koi_net/processor/kobj_queue.py +51 -0
- koi_net/processor/pipeline.py +222 -0
- koi_net/protocol/__init__.py +0 -0
- koi_net/protocol/api_models.py +67 -0
- koi_net/protocol/consts.py +7 -0
- koi_net/protocol/edge.py +50 -0
- koi_net/protocol/envelope.py +65 -0
- koi_net/protocol/errors.py +24 -0
- koi_net/protocol/event.py +51 -0
- koi_net/protocol/model_map.py +62 -0
- koi_net/protocol/node.py +18 -0
- koi_net/protocol/secure.py +167 -0
- koi_net/secure_manager.py +115 -0
- koi_net/workers/__init__.py +2 -0
- koi_net/workers/base.py +26 -0
- koi_net/workers/event_worker.py +111 -0
- koi_net/workers/kobj_worker.py +51 -0
- koi_net-1.2.4.dist-info/METADATA +485 -0
- koi_net-1.2.4.dist-info/RECORD +59 -0
- koi_net-1.2.4.dist-info/WHEEL +4 -0
- koi_net-1.2.4.dist-info/licenses/LICENSE +21 -0
koi_net/config/core.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pydantic import BaseModel, model_validator
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from rid_lib import RIDType
|
|
5
|
+
from rid_lib.types import KoiNetNode
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
from ..build import comp_type
|
|
9
|
+
from ..protocol.secure import PrivateKey
|
|
10
|
+
from ..protocol.node import NodeProfile
|
|
11
|
+
|
|
12
|
+
log = structlog.stdlib.get_logger()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EventWorkerConfig(BaseModel):
|
|
16
|
+
queue_timeout: float = 0.1
|
|
17
|
+
max_buf_len: int = 5
|
|
18
|
+
max_wait_time: float = 1.0
|
|
19
|
+
|
|
20
|
+
class KobjWorkerConfig(BaseModel):
|
|
21
|
+
queue_timeout: float = 0.1
|
|
22
|
+
|
|
23
|
+
class NodeContact(BaseModel):
|
|
24
|
+
rid: KoiNetNode | None = None
|
|
25
|
+
url: str | None = None
|
|
26
|
+
|
|
27
|
+
class KoiNetConfig(BaseModel):
|
|
28
|
+
"""Config for KOI-net parameters."""
|
|
29
|
+
|
|
30
|
+
node_name: str
|
|
31
|
+
node_rid: KoiNetNode | None = None
|
|
32
|
+
node_profile: NodeProfile
|
|
33
|
+
|
|
34
|
+
rid_types_of_interest: list[RIDType] = [KoiNetNode]
|
|
35
|
+
|
|
36
|
+
cache_directory_path: str = ".rid_cache"
|
|
37
|
+
private_key_pem_path: str = "priv_key.pem"
|
|
38
|
+
|
|
39
|
+
event_worker: EventWorkerConfig = EventWorkerConfig()
|
|
40
|
+
kobj_worker: KobjWorkerConfig = KobjWorkerConfig()
|
|
41
|
+
|
|
42
|
+
first_contact: NodeContact = NodeContact()
|
|
43
|
+
|
|
44
|
+
class EnvConfig(BaseModel):
|
|
45
|
+
"""Config for environment variables.
|
|
46
|
+
|
|
47
|
+
Values set in the config are the variables names, and are loaded
|
|
48
|
+
from the environment at runtime. For example, if the config YAML
|
|
49
|
+
sets `priv_key_password: "PRIV_KEY_PASSWORD"` accessing
|
|
50
|
+
`priv_key_password` would retrieve the value of `PRIV_KEY_PASSWORD`
|
|
51
|
+
from the environment variables.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
priv_key_password: str = "PRIV_KEY_PASSWORD"
|
|
55
|
+
|
|
56
|
+
def __init__(self, **kwargs):
|
|
57
|
+
super().__init__(**kwargs)
|
|
58
|
+
load_dotenv()
|
|
59
|
+
|
|
60
|
+
def __getattribute__(self, name):
|
|
61
|
+
value = super().__getattribute__(name)
|
|
62
|
+
if name in type(self).model_fields:
|
|
63
|
+
env_val = os.getenv(value)
|
|
64
|
+
if env_val is None:
|
|
65
|
+
raise ValueError(f"Required environment variable {value} not set")
|
|
66
|
+
return env_val
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
# marking this component as static, classes are implicitly treated as
|
|
70
|
+
# factories, but this needs to be passed as is
|
|
71
|
+
@comp_type.object
|
|
72
|
+
class NodeConfig(BaseModel):
|
|
73
|
+
"""Base node config class, intended to be extended."""
|
|
74
|
+
|
|
75
|
+
koi_net: KoiNetConfig
|
|
76
|
+
env: EnvConfig = EnvConfig()
|
|
77
|
+
|
|
78
|
+
@model_validator(mode="after")
|
|
79
|
+
def generate_rid_cascade(self):
|
|
80
|
+
"""Generates node RID if missing."""
|
|
81
|
+
if self.koi_net.node_rid and self.koi_net.node_profile.public_key:
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
log.debug("Node RID or public key not found in config, attempting to generate")
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# attempts to read existing private key PEM file
|
|
88
|
+
with open(self.koi_net.private_key_pem_path, "r") as f:
|
|
89
|
+
priv_key_pem = f.read()
|
|
90
|
+
priv_key = PrivateKey.from_pem(
|
|
91
|
+
priv_key_pem,
|
|
92
|
+
password=self.env.priv_key_password)
|
|
93
|
+
log.debug("Used existing private key from PEM file")
|
|
94
|
+
|
|
95
|
+
except FileNotFoundError:
|
|
96
|
+
# generates new private key if PEM not found
|
|
97
|
+
priv_key = PrivateKey.generate()
|
|
98
|
+
|
|
99
|
+
with open(self.koi_net.private_key_pem_path, "w") as f:
|
|
100
|
+
f.write(priv_key.to_pem(self.env.priv_key_password))
|
|
101
|
+
log.debug("Generated new private key, no PEM file found")
|
|
102
|
+
|
|
103
|
+
pub_key = priv_key.public_key()
|
|
104
|
+
self.koi_net.node_rid = pub_key.to_node_rid(self.koi_net.node_name)
|
|
105
|
+
log.debug(f"Node RID set to {self.koi_net.node_rid}")
|
|
106
|
+
|
|
107
|
+
if self.koi_net.node_profile.public_key != pub_key.to_der():
|
|
108
|
+
if self.koi_net.node_profile.public_key:
|
|
109
|
+
log.warning("New private key overwriting old public key!")
|
|
110
|
+
|
|
111
|
+
self.koi_net.node_profile.public_key = pub_key.to_der()
|
|
112
|
+
|
|
113
|
+
return self
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from pydantic import BaseModel, model_validator
|
|
2
|
+
from .core import NodeConfig, KoiNetConfig as BaseKoiNetConfig
|
|
3
|
+
from ..protocol.node import (
|
|
4
|
+
NodeProfile as BaseNodeProfile,
|
|
5
|
+
NodeType,
|
|
6
|
+
NodeProvides
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NodeProfile(BaseNodeProfile):
|
|
11
|
+
"""Node profile config class for full nodes."""
|
|
12
|
+
node_type: NodeType = NodeType.FULL
|
|
13
|
+
|
|
14
|
+
class KoiNetConfig(BaseKoiNetConfig):
|
|
15
|
+
"""KOI-net config class for full nodes."""
|
|
16
|
+
node_profile: NodeProfile
|
|
17
|
+
|
|
18
|
+
class ServerConfig(BaseModel):
|
|
19
|
+
"""Server config for full nodes.
|
|
20
|
+
|
|
21
|
+
The parameters in this class represent how a server should be hosted,
|
|
22
|
+
not accessed. For example, a node may host a server at
|
|
23
|
+
`http://127.0.0.1:8000/koi-net`, but serve through nginx at
|
|
24
|
+
`https://example.com/koi-net`.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
host: str = "127.0.0.1"
|
|
28
|
+
port: int = 8000
|
|
29
|
+
path: str | None = "/koi-net"
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def url(self) -> str:
|
|
33
|
+
return f"http://{self.host}:{self.port}{self.path or ''}"
|
|
34
|
+
|
|
35
|
+
class FullNodeConfig(NodeConfig):
|
|
36
|
+
"""Node config class for full nodes."""
|
|
37
|
+
koi_net: KoiNetConfig
|
|
38
|
+
server: ServerConfig = ServerConfig()
|
|
39
|
+
|
|
40
|
+
@model_validator(mode="after")
|
|
41
|
+
def check_url(self):
|
|
42
|
+
"""Generates base URL if missing from node profile."""
|
|
43
|
+
if not self.koi_net.node_profile.base_url:
|
|
44
|
+
self.koi_net.node_profile.base_url = self.server.url
|
|
45
|
+
return self
|
koi_net/config/loader.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from ruamel.yaml import YAML
|
|
2
|
+
|
|
3
|
+
from .proxy import ConfigProxy
|
|
4
|
+
from .core import NodeConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConfigLoader:
|
|
8
|
+
"""Loads node config from a YAML file, and proxies access to it."""
|
|
9
|
+
|
|
10
|
+
file_path: str = "config.yaml"
|
|
11
|
+
file_content: str
|
|
12
|
+
|
|
13
|
+
config_schema: type[NodeConfig]
|
|
14
|
+
proxy: ConfigProxy
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
config_schema: type[NodeConfig],
|
|
19
|
+
config: ConfigProxy
|
|
20
|
+
):
|
|
21
|
+
self.config_schema = config_schema
|
|
22
|
+
self.proxy = config
|
|
23
|
+
|
|
24
|
+
# this is a special case to allow config state dependent components
|
|
25
|
+
# to initialize without a "lazy initialization" approach, in general
|
|
26
|
+
# components SHOULD NOT execute code in their init phase
|
|
27
|
+
self.load_from_yaml()
|
|
28
|
+
|
|
29
|
+
def start(self):
|
|
30
|
+
self.save_to_yaml()
|
|
31
|
+
|
|
32
|
+
def load_from_yaml(self):
|
|
33
|
+
"""Loads config from YAML file, or generates it if missing."""
|
|
34
|
+
yaml = YAML()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
with open(self.file_path, "r") as f:
|
|
38
|
+
self.file_content = f.read()
|
|
39
|
+
config_data = yaml.load(self.file_content)
|
|
40
|
+
self.proxy._config = self.config_schema.model_validate(config_data)
|
|
41
|
+
|
|
42
|
+
except FileNotFoundError:
|
|
43
|
+
self.proxy._config = self.config_schema()
|
|
44
|
+
|
|
45
|
+
def save_to_yaml(self):
|
|
46
|
+
"""Saves config to YAML file."""
|
|
47
|
+
yaml = YAML()
|
|
48
|
+
|
|
49
|
+
with open(self.file_path, "w") as f:
|
|
50
|
+
try:
|
|
51
|
+
config_data = self.proxy._config.model_dump(mode="json")
|
|
52
|
+
yaml.dump(config_data, f)
|
|
53
|
+
|
|
54
|
+
except Exception:
|
|
55
|
+
# rewrites original content if YAML dump fails
|
|
56
|
+
if self.file_content:
|
|
57
|
+
f.seek(0)
|
|
58
|
+
f.truncate()
|
|
59
|
+
f.write(self.file_content)
|
|
60
|
+
raise
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from .core import NodeConfig, KoiNetConfig as BaseKoiNetConfig
|
|
3
|
+
from ..protocol.node import (
|
|
4
|
+
NodeProfile as BaseNodeProfile,
|
|
5
|
+
NodeType,
|
|
6
|
+
NodeProvides
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NodeProfile(BaseNodeProfile):
|
|
11
|
+
"""Node profile config class for partial nodes."""
|
|
12
|
+
base_url: str | None = None
|
|
13
|
+
node_type: NodeType = NodeType.PARTIAL
|
|
14
|
+
|
|
15
|
+
class KoiNetConfig(BaseKoiNetConfig):
|
|
16
|
+
"""KOI-net config class for partial nodes."""
|
|
17
|
+
node_profile: NodeProfile
|
|
18
|
+
|
|
19
|
+
class PollerConfig(BaseModel):
|
|
20
|
+
"""Poller config for partial nodes."""
|
|
21
|
+
polling_interval: int = 5
|
|
22
|
+
|
|
23
|
+
class PartialNodeConfig(NodeConfig):
|
|
24
|
+
"""Node config class for partial nodes."""
|
|
25
|
+
koi_net: KoiNetConfig
|
|
26
|
+
poller: PollerConfig = PollerConfig()
|
koi_net/config/proxy.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .core import NodeConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ConfigProxy:
|
|
5
|
+
"""Proxy for config access.
|
|
6
|
+
|
|
7
|
+
Allows initialization of this component, and updating state without
|
|
8
|
+
destroying the original reference. Handled as if it were a config
|
|
9
|
+
model by other classes, loaded and saved by the `ConfigLoader`.
|
|
10
|
+
"""
|
|
11
|
+
_config: NodeConfig
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self._config = None
|
|
15
|
+
|
|
16
|
+
def __getattr__(self, name):
|
|
17
|
+
if not self._config:
|
|
18
|
+
raise RuntimeError("Proxy called before config loaded")
|
|
19
|
+
|
|
20
|
+
return getattr(self._config, name)
|
koi_net/core.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from .cache import Cache
|
|
2
|
+
from .build.assembler import NodeAssembler
|
|
3
|
+
from .config.core import NodeConfig
|
|
4
|
+
from .config.proxy import ConfigProxy
|
|
5
|
+
from .config.loader import ConfigLoader
|
|
6
|
+
from .config.full_node import FullNodeConfig
|
|
7
|
+
from .config.partial_node import PartialNodeConfig
|
|
8
|
+
from .processor.context import HandlerContext
|
|
9
|
+
from .effector import DerefHandler, Effector
|
|
10
|
+
from .behaviors.handshaker import Handshaker
|
|
11
|
+
from .behaviors.sync_manager import SyncManager
|
|
12
|
+
from .identity import NodeIdentity
|
|
13
|
+
from .workers import KnowledgeProcessingWorker, EventProcessingWorker
|
|
14
|
+
from .network.error_handler import ErrorHandler
|
|
15
|
+
from .network.event_queue import EventQueue
|
|
16
|
+
from .network.graph import NetworkGraph
|
|
17
|
+
from .network.request_handler import RequestHandler
|
|
18
|
+
from .network.resolver import NetworkResolver
|
|
19
|
+
from .network.response_handler import ResponseHandler
|
|
20
|
+
from .network.event_buffer import EventBuffer
|
|
21
|
+
from .processor.pipeline import KnowledgePipeline
|
|
22
|
+
from .processor.kobj_queue import KobjQueue
|
|
23
|
+
from .processor.handler import KnowledgeHandler
|
|
24
|
+
from .secure_manager import SecureManager
|
|
25
|
+
from .behaviors.profile_monitor import ProfileMonitor
|
|
26
|
+
from .entrypoints import NodeServer, NodePoller
|
|
27
|
+
from .processor.knowledge_handlers import (
|
|
28
|
+
basic_manifest_handler,
|
|
29
|
+
basic_network_output_filter,
|
|
30
|
+
basic_rid_handler,
|
|
31
|
+
node_contact_handler,
|
|
32
|
+
edge_negotiation_handler,
|
|
33
|
+
forget_edge_on_node_deletion,
|
|
34
|
+
secure_profile_handler
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
class BaseNode(NodeAssembler):
|
|
38
|
+
kobj_queue: KobjQueue = KobjQueue
|
|
39
|
+
event_queue: EventQueue = EventQueue
|
|
40
|
+
poll_event_buf: EventBuffer = EventBuffer
|
|
41
|
+
broadcast_event_buf: EventBuffer = EventBuffer
|
|
42
|
+
config_schema = NodeConfig
|
|
43
|
+
config: NodeConfig = ConfigProxy
|
|
44
|
+
config_loader: ConfigLoader = ConfigLoader
|
|
45
|
+
knowledge_handlers: list[KnowledgeHandler] = [
|
|
46
|
+
basic_rid_handler,
|
|
47
|
+
basic_manifest_handler,
|
|
48
|
+
secure_profile_handler,
|
|
49
|
+
edge_negotiation_handler,
|
|
50
|
+
node_contact_handler,
|
|
51
|
+
basic_network_output_filter,
|
|
52
|
+
forget_edge_on_node_deletion
|
|
53
|
+
]
|
|
54
|
+
deref_handlers: list[DerefHandler] = []
|
|
55
|
+
cache: Cache = Cache
|
|
56
|
+
identity: NodeIdentity = NodeIdentity
|
|
57
|
+
graph: NetworkGraph = NetworkGraph
|
|
58
|
+
secure_manager: SecureManager = SecureManager
|
|
59
|
+
handshaker: Handshaker = Handshaker
|
|
60
|
+
error_handler: ErrorHandler = ErrorHandler
|
|
61
|
+
request_handler: RequestHandler = RequestHandler
|
|
62
|
+
sync_manager: SyncManager = SyncManager
|
|
63
|
+
response_handler: ResponseHandler = ResponseHandler
|
|
64
|
+
resolver: NetworkResolver = NetworkResolver
|
|
65
|
+
handler_context: HandlerContext = HandlerContext
|
|
66
|
+
effector: Effector = Effector
|
|
67
|
+
pipeline: KnowledgePipeline = KnowledgePipeline
|
|
68
|
+
kobj_worker: KnowledgeProcessingWorker = KnowledgeProcessingWorker
|
|
69
|
+
event_worker: EventProcessingWorker = EventProcessingWorker
|
|
70
|
+
profile_monitor: ProfileMonitor = ProfileMonitor
|
|
71
|
+
|
|
72
|
+
class FullNode(BaseNode):
|
|
73
|
+
entrypoint: NodeServer = NodeServer
|
|
74
|
+
config: FullNodeConfig
|
|
75
|
+
|
|
76
|
+
class PartialNode(BaseNode):
|
|
77
|
+
entrypoint: NodePoller = NodePoller
|
|
78
|
+
config: PartialNodeConfig
|
koi_net/effector.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Callable
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
import structlog
|
|
6
|
+
from rid_lib.ext import Cache, Bundle
|
|
7
|
+
from rid_lib.core import RID, RIDType
|
|
8
|
+
from rid_lib.types import KoiNetNode
|
|
9
|
+
|
|
10
|
+
from .processor.context import HandlerContext
|
|
11
|
+
from .network.resolver import NetworkResolver
|
|
12
|
+
from .processor.kobj_queue import KobjQueue
|
|
13
|
+
|
|
14
|
+
log = structlog.stdlib.get_logger()
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class DerefHandler:
|
|
18
|
+
func: Callable[[HandlerContext, RID], Bundle | None]
|
|
19
|
+
rid_types: tuple[RIDType]
|
|
20
|
+
|
|
21
|
+
def __call__(self, ctx: HandlerContext, rid: RID) -> Bundle | None:
|
|
22
|
+
return self.func(ctx, rid)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def create(cls, rid_types: tuple[RIDType]):
|
|
26
|
+
def decorator(func: Callable) -> DerefHandler:
|
|
27
|
+
handler = cls(func, rid_types)
|
|
28
|
+
return handler
|
|
29
|
+
return decorator
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BundleSource(StrEnum):
|
|
33
|
+
CACHE = "CACHE"
|
|
34
|
+
ACTION = "ACTION"
|
|
35
|
+
|
|
36
|
+
class Effector:
|
|
37
|
+
"""Subsystem for dereferencing RIDs."""
|
|
38
|
+
|
|
39
|
+
cache: Cache
|
|
40
|
+
resolver: NetworkResolver
|
|
41
|
+
kobj_queue: KobjQueue
|
|
42
|
+
handler_context: HandlerContext
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
cache: Cache,
|
|
47
|
+
resolver: NetworkResolver,
|
|
48
|
+
kobj_queue: KobjQueue,
|
|
49
|
+
handler_context: HandlerContext,
|
|
50
|
+
deref_handlers: list[DerefHandler]
|
|
51
|
+
):
|
|
52
|
+
self.cache = cache
|
|
53
|
+
self.resolver = resolver
|
|
54
|
+
self.kobj_queue = kobj_queue
|
|
55
|
+
self.handler_context = handler_context
|
|
56
|
+
self.deref_handlers = deref_handlers
|
|
57
|
+
|
|
58
|
+
self.handler_context.set_effector(self)
|
|
59
|
+
|
|
60
|
+
def _try_cache(self, rid: RID) -> tuple[Bundle, BundleSource] | None:
|
|
61
|
+
bundle = self.cache.read(rid)
|
|
62
|
+
|
|
63
|
+
if bundle:
|
|
64
|
+
log.debug("Cache hit")
|
|
65
|
+
return bundle, BundleSource.CACHE
|
|
66
|
+
else:
|
|
67
|
+
log.debug("Cache miss")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def _try_action(self, rid: RID) -> tuple[Bundle, BundleSource] | None:
|
|
71
|
+
action = None
|
|
72
|
+
for handler in self.deref_handlers:
|
|
73
|
+
if type(rid) not in handler.rid_types:
|
|
74
|
+
continue
|
|
75
|
+
action = handler
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
if not action:
|
|
79
|
+
log.debug("No action found")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
bundle = action(ctx=self.handler_context, rid=rid)
|
|
83
|
+
|
|
84
|
+
if bundle:
|
|
85
|
+
log.debug("Action hit")
|
|
86
|
+
return bundle, BundleSource.ACTION
|
|
87
|
+
else:
|
|
88
|
+
log.debug("Action miss")
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def _try_network(self, rid: RID) -> tuple[Bundle, KoiNetNode] | None:
|
|
92
|
+
bundle, source = self.resolver.fetch_remote_bundle(rid)
|
|
93
|
+
|
|
94
|
+
if bundle:
|
|
95
|
+
log.debug("Network hit")
|
|
96
|
+
return bundle, source
|
|
97
|
+
else:
|
|
98
|
+
log.debug("Network miss")
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def deref(
|
|
102
|
+
self,
|
|
103
|
+
rid: RID,
|
|
104
|
+
refresh_cache: bool = False,
|
|
105
|
+
use_network: bool = False,
|
|
106
|
+
handle_result: bool = True,
|
|
107
|
+
write_through: bool = False
|
|
108
|
+
) -> Bundle | None:
|
|
109
|
+
"""Dereferences an RID.
|
|
110
|
+
|
|
111
|
+
Attempts to dereference an RID by (in order) reading the cache,
|
|
112
|
+
calling a bound action, or fetching from other nodes in the
|
|
113
|
+
newtork.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
rid: RID to dereference
|
|
117
|
+
refresh_cache: skips cache read when `True`
|
|
118
|
+
use_network: enables fetching from other nodes when `True`
|
|
119
|
+
handle_result: sends resulting bundle to kobj queue when `True`
|
|
120
|
+
write_through: waits for kobj queue to empty when `True`
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
log.debug(f"Dereferencing {rid!r}")
|
|
124
|
+
|
|
125
|
+
bundle, source = (
|
|
126
|
+
# if `refresh_cache`, skip try cache
|
|
127
|
+
not refresh_cache and self._try_cache(rid) or
|
|
128
|
+
self._try_action(rid) or
|
|
129
|
+
use_network and self._try_network(rid) or
|
|
130
|
+
# if not found, bundle and source set to None
|
|
131
|
+
(None, None)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
handle_result
|
|
136
|
+
and bundle is not None
|
|
137
|
+
and source != BundleSource.CACHE
|
|
138
|
+
):
|
|
139
|
+
self.kobj_queue.push(
|
|
140
|
+
bundle=bundle,
|
|
141
|
+
source=source if type(source) is KoiNetNode else None
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if write_through:
|
|
145
|
+
self.kobj_queue.q.join()
|
|
146
|
+
|
|
147
|
+
return bundle
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
import time
|
|
3
|
+
import structlog
|
|
4
|
+
|
|
5
|
+
from .base import EntryPoint
|
|
6
|
+
from ..processor.kobj_queue import KobjQueue
|
|
7
|
+
from ..network.resolver import NetworkResolver
|
|
8
|
+
from ..config.partial_node import PartialNodeConfig
|
|
9
|
+
|
|
10
|
+
log = structlog.stdlib.get_logger()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NodePoller(EntryPoint):
|
|
14
|
+
"""Entry point for partial nodes, manages polling event loop."""
|
|
15
|
+
kobj_queue: KobjQueue
|
|
16
|
+
resolver: NetworkResolver
|
|
17
|
+
config: PartialNodeConfig
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
config: PartialNodeConfig,
|
|
22
|
+
kobj_queue: KobjQueue,
|
|
23
|
+
resolver: NetworkResolver
|
|
24
|
+
):
|
|
25
|
+
self.kobj_queue = kobj_queue
|
|
26
|
+
self.resolver = resolver
|
|
27
|
+
self.config = config
|
|
28
|
+
|
|
29
|
+
def poll(self):
|
|
30
|
+
"""Polls neighbor nodes and processes returned events."""
|
|
31
|
+
for node_rid, events in self.resolver.poll_neighbors().items():
|
|
32
|
+
for event in events:
|
|
33
|
+
self.kobj_queue.push(event=event, source=node_rid)
|
|
34
|
+
|
|
35
|
+
def run(self):
|
|
36
|
+
"""Runs polling event loop."""
|
|
37
|
+
while True:
|
|
38
|
+
start_time = time.time()
|
|
39
|
+
self.poll()
|
|
40
|
+
elapsed = time.time() - start_time
|
|
41
|
+
sleep_time = self.config.poller.polling_interval - elapsed
|
|
42
|
+
if sleep_time > 0:
|
|
43
|
+
time.sleep(sleep_time)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import structlog
|
|
2
|
+
import uvicorn
|
|
3
|
+
from fastapi import FastAPI, APIRouter
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
from .base import EntryPoint
|
|
7
|
+
from ..network.response_handler import ResponseHandler
|
|
8
|
+
from ..protocol.model_map import API_MODEL_MAP
|
|
9
|
+
from ..protocol.api_models import ErrorResponse
|
|
10
|
+
from ..protocol.errors import EXCEPTION_TO_ERROR_TYPE, ProtocolError
|
|
11
|
+
from ..config.full_node import FullNodeConfig
|
|
12
|
+
|
|
13
|
+
log = structlog.stdlib.get_logger()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NodeServer(EntryPoint):
|
|
17
|
+
"""Entry point for full nodes, manages FastAPI server."""
|
|
18
|
+
config: FullNodeConfig
|
|
19
|
+
response_handler: ResponseHandler
|
|
20
|
+
app: FastAPI
|
|
21
|
+
router: APIRouter
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
config: FullNodeConfig,
|
|
26
|
+
response_handler: ResponseHandler,
|
|
27
|
+
):
|
|
28
|
+
self.config = config
|
|
29
|
+
self.response_handler = response_handler
|
|
30
|
+
|
|
31
|
+
self.build_app()
|
|
32
|
+
|
|
33
|
+
def build_endpoints(self, router: APIRouter):
|
|
34
|
+
"""Builds endpoints for API router."""
|
|
35
|
+
for path, models in API_MODEL_MAP.items():
|
|
36
|
+
def create_endpoint(path: str):
|
|
37
|
+
async def endpoint(req):
|
|
38
|
+
return self.response_handler.handle_response(path, req)
|
|
39
|
+
|
|
40
|
+
# programmatically setting type hint annotations for FastAPI's model validation
|
|
41
|
+
endpoint.__annotations__ = {
|
|
42
|
+
"req": models.request_envelope,
|
|
43
|
+
"return": models.response_envelope
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return endpoint
|
|
47
|
+
|
|
48
|
+
router.add_api_route(
|
|
49
|
+
path=path,
|
|
50
|
+
endpoint=create_endpoint(path),
|
|
51
|
+
methods=["POST"],
|
|
52
|
+
response_model_exclude_none=True
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def build_app(self):
|
|
56
|
+
"""Builds FastAPI app."""
|
|
57
|
+
self.app = FastAPI(
|
|
58
|
+
title="KOI-net Protocol API",
|
|
59
|
+
version="1.1.0"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self.app.add_exception_handler(ProtocolError, self.protocol_error_handler)
|
|
63
|
+
self.router = APIRouter(prefix="/koi-net")
|
|
64
|
+
self.build_endpoints(self.router)
|
|
65
|
+
self.app.include_router(self.router)
|
|
66
|
+
|
|
67
|
+
def protocol_error_handler(self, request, exc: ProtocolError):
|
|
68
|
+
"""Catches `ProtocolError` and returns an `ErrorResponse` payload."""
|
|
69
|
+
log.error(exc)
|
|
70
|
+
resp = ErrorResponse(error=EXCEPTION_TO_ERROR_TYPE[type(exc)])
|
|
71
|
+
log.info(f"Returning error response: {resp}")
|
|
72
|
+
return JSONResponse(
|
|
73
|
+
status_code=400,
|
|
74
|
+
content=resp.model_dump(mode="json")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def run(self):
|
|
78
|
+
"""Starts FastAPI server and event handler."""
|
|
79
|
+
|
|
80
|
+
uvicorn.run(
|
|
81
|
+
app=self.app,
|
|
82
|
+
host=self.config.server.host,
|
|
83
|
+
port=self.config.server.port,
|
|
84
|
+
log_config=None
|
|
85
|
+
)
|