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.

Files changed (59) hide show
  1. koi_net/__init__.py +1 -0
  2. koi_net/behaviors/handshaker.py +68 -0
  3. koi_net/behaviors/profile_monitor.py +23 -0
  4. koi_net/behaviors/sync_manager.py +68 -0
  5. koi_net/build/artifact.py +209 -0
  6. koi_net/build/assembler.py +60 -0
  7. koi_net/build/comp_order.py +6 -0
  8. koi_net/build/comp_type.py +7 -0
  9. koi_net/build/consts.py +18 -0
  10. koi_net/build/container.py +46 -0
  11. koi_net/cache.py +81 -0
  12. koi_net/config/core.py +113 -0
  13. koi_net/config/full_node.py +45 -0
  14. koi_net/config/loader.py +60 -0
  15. koi_net/config/partial_node.py +26 -0
  16. koi_net/config/proxy.py +20 -0
  17. koi_net/core.py +78 -0
  18. koi_net/effector.py +147 -0
  19. koi_net/entrypoints/__init__.py +2 -0
  20. koi_net/entrypoints/base.py +8 -0
  21. koi_net/entrypoints/poller.py +43 -0
  22. koi_net/entrypoints/server.py +85 -0
  23. koi_net/exceptions.py +107 -0
  24. koi_net/identity.py +20 -0
  25. koi_net/log_system.py +133 -0
  26. koi_net/network/__init__.py +0 -0
  27. koi_net/network/error_handler.py +63 -0
  28. koi_net/network/event_buffer.py +91 -0
  29. koi_net/network/event_queue.py +31 -0
  30. koi_net/network/graph.py +123 -0
  31. koi_net/network/request_handler.py +244 -0
  32. koi_net/network/resolver.py +152 -0
  33. koi_net/network/response_handler.py +130 -0
  34. koi_net/processor/__init__.py +0 -0
  35. koi_net/processor/context.py +36 -0
  36. koi_net/processor/handler.py +61 -0
  37. koi_net/processor/knowledge_handlers.py +302 -0
  38. koi_net/processor/knowledge_object.py +135 -0
  39. koi_net/processor/kobj_queue.py +51 -0
  40. koi_net/processor/pipeline.py +222 -0
  41. koi_net/protocol/__init__.py +0 -0
  42. koi_net/protocol/api_models.py +67 -0
  43. koi_net/protocol/consts.py +7 -0
  44. koi_net/protocol/edge.py +50 -0
  45. koi_net/protocol/envelope.py +65 -0
  46. koi_net/protocol/errors.py +24 -0
  47. koi_net/protocol/event.py +51 -0
  48. koi_net/protocol/model_map.py +62 -0
  49. koi_net/protocol/node.py +18 -0
  50. koi_net/protocol/secure.py +167 -0
  51. koi_net/secure_manager.py +115 -0
  52. koi_net/workers/__init__.py +2 -0
  53. koi_net/workers/base.py +26 -0
  54. koi_net/workers/event_worker.py +111 -0
  55. koi_net/workers/kobj_worker.py +51 -0
  56. koi_net-1.2.4.dist-info/METADATA +485 -0
  57. koi_net-1.2.4.dist-info/RECORD +59 -0
  58. koi_net-1.2.4.dist-info/WHEEL +4 -0
  59. 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
@@ -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()
@@ -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,2 @@
1
+ from .poller import NodePoller
2
+ from .server import NodeServer
@@ -0,0 +1,8 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class EntryPoint(ABC):
5
+ """Abstract class for entry point components."""
6
+ @abstractmethod
7
+ def run(self):
8
+ ...
@@ -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
+ )