koi-net 1.1.0b1__tar.gz → 1.1.0b3__tar.gz

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 (45) hide show
  1. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/PKG-INFO +3 -3
  2. koi_net-1.1.0b3/examples/coordinator.py +76 -0
  3. koi_net-1.1.0b1/examples/basic_partial_node.py → koi_net-1.1.0b3/examples/partial.py +8 -19
  4. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/pyproject.toml +4 -4
  5. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/config.py +1 -0
  6. koi_net-1.1.0b3/src/koi_net/core.py +194 -0
  7. koi_net-1.1.0b3/src/koi_net/lifecycle.py +76 -0
  8. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/network/response_handler.py +4 -3
  9. koi_net-1.1.0b3/src/koi_net/poller.py +47 -0
  10. koi_net-1.1.0b3/src/koi_net/server.py +128 -0
  11. koi_net-1.1.0b1/examples/basic_coordinator_node.py +0 -111
  12. koi_net-1.1.0b1/src/koi_net/core.py +0 -192
  13. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/.github/workflows/publish-to-pypi.yml +0 -0
  14. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/.gitignore +0 -0
  15. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/LICENSE +0 -0
  16. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/README.md +0 -0
  17. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/requirements.txt +0 -0
  18. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/__init__.py +0 -0
  19. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/context.py +0 -0
  20. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/default_actions.py +0 -0
  21. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/effector.py +0 -0
  22. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/identity.py +0 -0
  23. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/network/__init__.py +0 -0
  24. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/network/behavior.py +0 -0
  25. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/network/error_handler.py +0 -0
  26. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/network/event_queue.py +0 -0
  27. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/network/graph.py +0 -0
  28. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/network/request_handler.py +0 -0
  29. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/network/resolver.py +0 -0
  30. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/processor/__init__.py +0 -0
  31. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/processor/default_handlers.py +0 -0
  32. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/processor/handler.py +0 -0
  33. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/processor/interface.py +0 -0
  34. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/processor/knowledge_object.py +0 -0
  35. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/processor/knowledge_pipeline.py +0 -0
  36. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/protocol/__init__.py +0 -0
  37. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/protocol/api_models.py +0 -0
  38. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/protocol/consts.py +0 -0
  39. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/protocol/edge.py +0 -0
  40. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/protocol/envelope.py +0 -0
  41. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/protocol/errors.py +0 -0
  42. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/protocol/event.py +0 -0
  43. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/protocol/node.py +0 -0
  44. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/protocol/secure.py +0 -0
  45. {koi_net-1.1.0b1 → koi_net-1.1.0b3}/src/koi_net/secure.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: koi-net
3
- Version: 1.1.0b1
3
+ Version: 1.1.0b3
4
4
  Summary: Implementation of KOI-net protocol in Python
5
5
  Project-URL: Homepage, https://github.com/BlockScience/koi-net/
6
6
  Author-email: Luke Miller <luke@block.science>
@@ -28,19 +28,19 @@ License: MIT License
28
28
  License-File: LICENSE
29
29
  Requires-Python: >=3.10
30
30
  Requires-Dist: cryptography>=45.0.3
31
+ Requires-Dist: fastapi>=0.115.12
31
32
  Requires-Dist: httpx>=0.28.1
32
33
  Requires-Dist: networkx>=3.4.2
33
34
  Requires-Dist: pydantic>=2.10.6
34
35
  Requires-Dist: python-dotenv>=1.1.0
35
36
  Requires-Dist: rid-lib>=3.2.7
36
37
  Requires-Dist: ruamel-yaml>=0.18.10
38
+ Requires-Dist: uvicorn>=0.34.2
37
39
  Provides-Extra: dev
38
40
  Requires-Dist: build; extra == 'dev'
39
41
  Requires-Dist: twine>=6.0; extra == 'dev'
40
42
  Provides-Extra: examples
41
- Requires-Dist: fastapi; extra == 'examples'
42
43
  Requires-Dist: rich; extra == 'examples'
43
- Requires-Dist: uvicorn; extra == 'examples'
44
44
  Description-Content-Type: text/markdown
45
45
 
46
46
  # KOI-net
@@ -0,0 +1,76 @@
1
+ import logging
2
+ from rich.logging import RichHandler
3
+ from pydantic import Field
4
+ from rid_lib.types import KoiNetNode, KoiNetEdge
5
+ from koi_net.config import NodeConfig, KoiNetConfig
6
+ from koi_net.protocol.node import NodeProfile, NodeProvides, NodeType
7
+ from koi_net import NodeInterface
8
+ from koi_net.context import HandlerContext
9
+ from koi_net.processor.handler import HandlerType
10
+ from koi_net.processor.knowledge_object import KnowledgeObject
11
+ from koi_net.protocol.event import Event, EventType
12
+ from koi_net.protocol.edge import EdgeType, generate_edge_bundle
13
+
14
+ logging.basicConfig(
15
+ level=logging.INFO,
16
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
17
+ datefmt="%Y-%m-%d %H:%M:%S",
18
+ handlers=[RichHandler()]
19
+ )
20
+
21
+ logging.getLogger("koi_net").setLevel(logging.DEBUG)
22
+ logger = logging.getLogger(__name__)
23
+
24
+ class CoordinatorConfig(NodeConfig):
25
+ koi_net: KoiNetConfig = Field(default_factory = lambda:
26
+ KoiNetConfig(
27
+ node_name="coordinator",
28
+ node_profile=NodeProfile(
29
+ node_type=NodeType.FULL,
30
+ provides=NodeProvides(
31
+ event=[KoiNetNode, KoiNetEdge],
32
+ state=[KoiNetNode, KoiNetEdge]
33
+ )
34
+ ),
35
+ cache_directory_path=".coordinator_rid_cache",
36
+ event_queues_path="coordinator_event_queues.json",
37
+ private_key_pem_path="coordinator_priv_key.pem"
38
+ )
39
+ )
40
+
41
+ node = NodeInterface(
42
+ config=CoordinatorConfig.load_from_yaml("coordinator_config.yaml"),
43
+ use_kobj_processor_thread=True
44
+ )
45
+
46
+ @node.processor.pipeline.register_handler(HandlerType.Network, rid_types=[KoiNetNode])
47
+ def handshake_handler(ctx: HandlerContext, kobj: KnowledgeObject):
48
+ logger.info("Handling node handshake")
49
+
50
+ # only respond if node declares itself as NEW
51
+ if kobj.event_type != EventType.NEW:
52
+ return
53
+
54
+ logger.info("Sharing this node's bundle with peer")
55
+ identity_bundle = ctx.effector.deref(ctx.identity.rid)
56
+ ctx.event_queue.push_event_to(
57
+ event=Event.from_bundle(EventType.NEW, identity_bundle),
58
+ node=kobj.rid,
59
+ flush=True
60
+ )
61
+
62
+ logger.info("Proposing new edge")
63
+ # defer handling of proposed edge
64
+
65
+ edge_bundle = generate_edge_bundle(
66
+ source=kobj.rid,
67
+ target=ctx.identity.rid,
68
+ edge_type=EdgeType.WEBHOOK,
69
+ rid_types=[KoiNetNode, KoiNetEdge]
70
+ )
71
+
72
+ ctx.handle(rid=edge_bundle.rid, event_type=EventType.FORGET)
73
+ ctx.handle(bundle=edge_bundle)
74
+
75
+ if __name__ == "__main__":
76
+ node.server.run()
@@ -1,10 +1,9 @@
1
- import time
2
1
  import logging
3
2
  from pydantic import Field
4
3
  from rich.logging import RichHandler
5
4
  from koi_net import NodeInterface
6
5
  from koi_net.protocol.node import NodeProfile, NodeType
7
- from koi_net.config import NodeConfig, KoiNetConfig, NodeContact
6
+ from koi_net.config import NodeConfig, KoiNetConfig
8
7
 
9
8
  logging.basicConfig(
10
9
  level=logging.INFO,
@@ -18,31 +17,21 @@ logger = logging.getLogger(__name__)
18
17
 
19
18
 
20
19
  class PartialNodeConfig(NodeConfig):
21
- koi_net: KoiNetConfig | None = Field(default_factory = lambda:
20
+ koi_net: KoiNetConfig = Field(default_factory = lambda:
22
21
  KoiNetConfig(
23
22
  node_name="partial",
24
23
  node_profile=NodeProfile(
25
24
  node_type=NodeType.PARTIAL
26
25
  ),
27
- cache_directory_path=".basic_partial_rid_cache",
28
- event_queues_path="basic_partial_event_queues.json"
26
+ cache_directory_path=".partial_rid_cache",
27
+ event_queues_path="partial_event_queues.json",
28
+ private_key_pem_path="partial_priv_key.pem"
29
29
  )
30
30
  )
31
31
 
32
-
33
32
  node = NodeInterface(
34
- config=PartialNodeConfig.load_from_yaml("basic_partial_config.yaml")
33
+ config=PartialNodeConfig.load_from_yaml("partial_config.yaml")
35
34
  )
36
35
 
37
-
38
- node.start()
39
-
40
- while True:
41
- neighbors = node.resolver.poll_neighbors()
42
- for node_rid in neighbors:
43
- events = neighbors[node_rid]
44
- for event in events:
45
- node.processor.handle(event=event, source=node_rid)
46
- node.processor.flush_kobj_queue()
47
-
48
- time.sleep(5)
36
+ if __name__ == "__main__":
37
+ node.lifecycle.start()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "koi-net"
7
- version = "1.1.0-beta.1"
7
+ version = "1.1.0-beta.3"
8
8
  description = "Implementation of KOI-net protocol in Python"
9
9
  authors = [
10
10
  {name = "Luke Miller", email = "luke@block.science"}
@@ -19,15 +19,15 @@ dependencies = [
19
19
  "pydantic>=2.10.6",
20
20
  "ruamel.yaml>=0.18.10",
21
21
  "python-dotenv>=1.1.0",
22
- "cryptography>=45.0.3"
22
+ "cryptography>=45.0.3",
23
+ "fastapi>=0.115.12",
24
+ "uvicorn>=0.34.2"
23
25
  ]
24
26
 
25
27
  [project.optional-dependencies]
26
28
  dev = ["twine>=6.0", "build"]
27
29
  examples = [
28
30
  "rich",
29
- "fastapi",
30
- "uvicorn"
31
31
  ]
32
32
 
33
33
  [project.urls]
@@ -29,6 +29,7 @@ class KoiNetConfig(BaseModel):
29
29
  cache_directory_path: str = ".rid_cache"
30
30
  event_queues_path: str = "event_queues.json"
31
31
  private_key_pem_path: str = "priv_key.pem"
32
+ polling_interval: int = 5
32
33
 
33
34
  first_contact: NodeContact = Field(default_factory=NodeContact)
34
35
 
@@ -0,0 +1,194 @@
1
+ import logging
2
+ from typing import Generic, TypeVar
3
+ from rid_lib.ext import Cache
4
+ from .network.resolver import NetworkResolver
5
+ from .network.event_queue import NetworkEventQueue
6
+ from .network.graph import NetworkGraph
7
+ from .network.request_handler import RequestHandler
8
+ from .network.response_handler import ResponseHandler
9
+ from .network.error_handler import ErrorHandler
10
+ from .network.behavior import Actor
11
+ from .processor.interface import ProcessorInterface
12
+ from .processor import default_handlers
13
+ from .processor.handler import KnowledgeHandler
14
+ from .processor.knowledge_pipeline import KnowledgePipeline
15
+ from .identity import NodeIdentity
16
+ from .secure import Secure
17
+ from .config import NodeConfig
18
+ from .context import HandlerContext, ActionContext
19
+ from .effector import Effector
20
+ from .server import NodeServer
21
+ from .lifecycle import NodeLifecycle
22
+ from .poller import NodePoller
23
+ from . import default_actions
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ T = TypeVar("T", bound=NodeConfig)
29
+
30
+ class NodeInterface(Generic[T]):
31
+ config: T
32
+ cache: Cache
33
+ identity: NodeIdentity
34
+ resolver: NetworkResolver
35
+ event_queue: NetworkEventQueue
36
+ graph: NetworkGraph
37
+ processor: ProcessorInterface
38
+ secure: Secure
39
+ server: NodeServer
40
+
41
+ use_kobj_processor_thread: bool
42
+
43
+ def __init__(
44
+ self,
45
+ config: T,
46
+ use_kobj_processor_thread: bool = False,
47
+ handlers: list[KnowledgeHandler] | None = None,
48
+
49
+ CacheOverride: type[Cache] | None = None,
50
+ NodeIdentityOverride: type[NodeIdentity] | None = None,
51
+ EffectorOverride: type[Effector] | None = None,
52
+ NetworkGraphOverride: type[NetworkGraph] | None = None,
53
+ SecureOverride: type[Secure] | None = None,
54
+ RequestHandlerOverride: type[RequestHandler] | None = None,
55
+ ResponseHandlerOverride: type[ResponseHandler] | None = None,
56
+ NetworkResolverOverride: type[NetworkResolver] | None = None,
57
+ NetworkEventQueueOverride: type[NetworkEventQueue] | None = None,
58
+ ActorOverride: type[Actor] | None = None,
59
+ ActionContextOverride: type[ActionContext] | None = None,
60
+ HandlerContextOverride: type[HandlerContext] | None = None,
61
+ KnowledgePipelineOverride: type[KnowledgePipeline] | None = None,
62
+ ProcessorInterfaceOverride: type[ProcessorInterface] | None = None,
63
+ ErrorHandlerOverride: type[ErrorHandler] | None = None,
64
+ NodeLifecycleOverride: type[NodeLifecycle] | None = None,
65
+ NodeServerOverride: type[NodeServer] | None = None,
66
+ NodePollerOverride: type[NodePoller] | None = None,
67
+ ):
68
+ self.config = config
69
+ self.cache = (CacheOverride or Cache)(
70
+ directory_path=self.config.koi_net.cache_directory_path
71
+ )
72
+
73
+ self.identity = (NodeIdentityOverride or NodeIdentity)(config=self.config)
74
+ self.effector = (EffectorOverride or Effector)(cache=self.cache)
75
+
76
+ self.graph = (NetworkGraphOverride or NetworkGraph)(
77
+ cache=self.cache,
78
+ identity=self.identity
79
+ )
80
+
81
+ self.secure = (SecureOverride or Secure)(
82
+ identity=self.identity,
83
+ effector=self.effector,
84
+ config=self.config
85
+ )
86
+
87
+ self.request_handler = (RequestHandlerOverride or RequestHandler)(
88
+ effector=self.effector,
89
+ identity=self.identity,
90
+ secure=self.secure
91
+ )
92
+
93
+ self.response_handler = (ResponseHandlerOverride or ResponseHandler)(self.cache, self.effector)
94
+
95
+ self.resolver = (NetworkResolverOverride or NetworkResolver)(
96
+ config=self.config,
97
+ cache=self.cache,
98
+ identity=self.identity,
99
+ graph=self.graph,
100
+ request_handler=self.request_handler,
101
+ effector=self.effector
102
+ )
103
+
104
+ self.event_queue = (NetworkEventQueueOverride or NetworkEventQueue)(
105
+ config=self.config,
106
+ cache=self.cache,
107
+ identity=self.identity,
108
+ graph=self.graph,
109
+ request_handler=self.request_handler,
110
+ effector=self.effector
111
+ )
112
+
113
+ self.actor = (ActorOverride or Actor)(
114
+ identity=self.identity,
115
+ effector=self.effector,
116
+ event_queue=self.event_queue
117
+ )
118
+
119
+ # pull all handlers defined in default_handlers module
120
+ if handlers is None:
121
+ handlers = [
122
+ obj for obj in vars(default_handlers).values()
123
+ if isinstance(obj, KnowledgeHandler)
124
+ ]
125
+
126
+ self.use_kobj_processor_thread = use_kobj_processor_thread
127
+
128
+ self.action_context = (ActionContextOverride or ActionContext)(
129
+ identity=self.identity,
130
+ effector=self.effector
131
+ )
132
+
133
+ self.handler_context = (HandlerContextOverride or HandlerContext)(
134
+ identity=self.identity,
135
+ cache=self.cache,
136
+ event_queue=self.event_queue,
137
+ graph=self.graph,
138
+ request_handler=self.request_handler,
139
+ effector=self.effector
140
+ )
141
+
142
+ self.pipeline = (KnowledgePipelineOverride or KnowledgePipeline)(
143
+ handler_context=self.handler_context,
144
+ cache=self.cache,
145
+ request_handler=self.request_handler,
146
+ event_queue=self.event_queue,
147
+ graph=self.graph,
148
+ default_handlers=handlers
149
+ )
150
+
151
+ self.processor = (ProcessorInterfaceOverride or ProcessorInterface)(
152
+ pipeline=self.pipeline,
153
+ use_kobj_processor_thread=self.use_kobj_processor_thread
154
+ )
155
+
156
+ self.error_handler = (ErrorHandlerOverride or ErrorHandler)(
157
+ processor=self.processor,
158
+ actor=self.actor
159
+ )
160
+
161
+ self.request_handler.set_error_handler(self.error_handler)
162
+
163
+ self.handler_context.set_processor(self.processor)
164
+
165
+ self.effector.set_processor(self.processor)
166
+ self.effector.set_resolver(self.resolver)
167
+ self.effector.set_action_context(self.action_context)
168
+
169
+ self.lifecycle = (NodeLifecycleOverride or NodeLifecycle)(
170
+ config=self.config,
171
+ identity=self.identity,
172
+ graph=self.graph,
173
+ processor=self.processor,
174
+ effector=self.effector,
175
+ actor=self.actor,
176
+ use_kobj_processor_thread=use_kobj_processor_thread
177
+ )
178
+
179
+ # if self.config.koi_net.node_profile.node_type == NodeType.FULL:
180
+ self.server = (NodeServerOverride or NodeServer)(
181
+ config=self.config,
182
+ lifecycle=self.lifecycle,
183
+ secure=self.secure,
184
+ processor=self.processor,
185
+ event_queue=self.event_queue,
186
+ response_handler=self.response_handler
187
+ )
188
+
189
+ self.poller = (NodePollerOverride or NodePoller)(
190
+ processor=self.processor,
191
+ lifecycle=self.lifecycle,
192
+ resolver=self.resolver,
193
+ config=self.config
194
+ )
@@ -0,0 +1,76 @@
1
+ import logging
2
+
3
+ from .network.behavior import Actor
4
+ from .effector import Effector
5
+ from .config import NodeConfig
6
+ from .processor.interface import ProcessorInterface
7
+ from .network.graph import NetworkGraph
8
+ from .identity import NodeIdentity
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class NodeLifecycle:
14
+ config: NodeConfig
15
+ graph: NetworkGraph
16
+ processor: ProcessorInterface
17
+ effector: Effector
18
+ actor: Actor
19
+
20
+ def __init__(
21
+ self,
22
+ config: NodeConfig,
23
+ identity: NodeIdentity,
24
+ graph: NetworkGraph,
25
+ processor: ProcessorInterface,
26
+ effector: Effector,
27
+ actor: Actor,
28
+ use_kobj_processor_thread: bool
29
+ ):
30
+ self.config = config
31
+ self.identity = identity
32
+ self.graph = graph
33
+ self.processor = processor
34
+ self.effector = effector
35
+ self.actor = actor
36
+ self.use_kobj_processor_thread = use_kobj_processor_thread
37
+
38
+ def start(self) -> None:
39
+ """Starts a node, call this method first.
40
+
41
+ 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.
42
+ """
43
+ if self.use_kobj_processor_thread:
44
+ logger.info("Starting processor worker thread")
45
+ self.processor.worker_thread.start()
46
+
47
+ self.graph.generate()
48
+
49
+ # refresh to reflect changes (if any) in config.yaml
50
+ self.effector.deref(self.identity.rid, refresh_cache=True)
51
+
52
+ logger.debug("Waiting for kobj queue to empty")
53
+ if self.use_kobj_processor_thread:
54
+ self.processor.kobj_queue.join()
55
+ else:
56
+ self.processor.flush_kobj_queue()
57
+ logger.debug("Done")
58
+
59
+ if not self.graph.get_neighbors() and self.config.koi_net.first_contact.rid:
60
+ logger.debug(f"I don't have any neighbors, reaching out to first contact {self.config.koi_net.first_contact.rid!r}")
61
+
62
+ self.actor.handshake_with(self.config.koi_net.first_contact.rid)
63
+
64
+
65
+ def stop(self):
66
+ """Stops a node, call this method last.
67
+
68
+ Finishes processing knowledge object queue. Saves event queues to storage.
69
+ """
70
+ logger.info("Stopping node...")
71
+
72
+ if self.use_kobj_processor_thread:
73
+ logger.info(f"Waiting for kobj queue to empty ({self.processor.kobj_queue.unfinished_tasks} tasks remaining)")
74
+ self.processor.kobj_queue.join()
75
+ else:
76
+ self.processor.flush_kobj_queue()
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  from rid_lib import RID
3
+ from rid_lib.types import KoiNetNode
3
4
  from rid_lib.ext import Manifest, Cache
4
5
  from rid_lib.ext.bundle import Bundle
5
6
 
@@ -30,13 +31,13 @@ class ResponseHandler:
30
31
  self.cache = cache
31
32
  self.effector = effector
32
33
 
33
- def fetch_rids(self, req: FetchRids) -> RidsPayload:
34
+ def fetch_rids(self, req: FetchRids, source: KoiNetNode) -> RidsPayload:
34
35
  logger.info(f"Request to fetch rids, allowed types {req.rid_types}")
35
36
  rids = self.cache.list_rids(req.rid_types)
36
37
 
37
38
  return RidsPayload(rids=rids)
38
39
 
39
- def fetch_manifests(self, req: FetchManifests) -> ManifestsPayload:
40
+ def fetch_manifests(self, req: FetchManifests, source: KoiNetNode) -> ManifestsPayload:
40
41
  logger.info(f"Request to fetch manifests, allowed types {req.rid_types}, rids {req.rids}")
41
42
 
42
43
  manifests: list[Manifest] = []
@@ -51,7 +52,7 @@ class ResponseHandler:
51
52
 
52
53
  return ManifestsPayload(manifests=manifests, not_found=not_found)
53
54
 
54
- def fetch_bundles(self, req: FetchBundles) -> BundlesPayload:
55
+ def fetch_bundles(self, req: FetchBundles, source: KoiNetNode) -> BundlesPayload:
55
56
  logger.info(f"Request to fetch bundles, requested rids {req.rids}")
56
57
 
57
58
  bundles: list[Bundle] = []
@@ -0,0 +1,47 @@
1
+
2
+ import time
3
+ import logging
4
+ from .processor.interface import ProcessorInterface
5
+ from .lifecycle import NodeLifecycle
6
+ from .network.resolver import NetworkResolver
7
+ from .config import NodeConfig
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class NodePoller:
13
+ def __init__(
14
+ self,
15
+ processor: ProcessorInterface,
16
+ lifecycle: NodeLifecycle,
17
+ resolver: NetworkResolver,
18
+ config: NodeConfig
19
+ ):
20
+ self.processor = processor
21
+ self.lifecycle = lifecycle
22
+ self.resolver = resolver
23
+ self.config = config
24
+
25
+ def poll(self):
26
+ neighbors = self.resolver.poll_neighbors()
27
+ for node_rid in neighbors:
28
+ for event in neighbors[node_rid]:
29
+ self.processor.handle(event=event, source=node_rid)
30
+ self.processor.flush_kobj_queue()
31
+
32
+ def run(self):
33
+ try:
34
+ self.lifecycle.start()
35
+ while True:
36
+ start_time = time.time()
37
+ self.poll()
38
+ elapsed = time.time() - start_time
39
+ sleep_time = self.config.koi_net.polling_interval - elapsed
40
+ if sleep_time > 0:
41
+ time.sleep(sleep_time)
42
+
43
+ except KeyboardInterrupt:
44
+ logger.info("Polling interrupted by user.")
45
+
46
+ finally:
47
+ self.lifecycle.stop()
@@ -0,0 +1,128 @@
1
+ import logging
2
+ import uvicorn
3
+ from contextlib import asynccontextmanager
4
+ from fastapi import FastAPI, APIRouter
5
+ from fastapi.responses import JSONResponse
6
+ from .network.event_queue import NetworkEventQueue
7
+ from .network.response_handler import ResponseHandler
8
+ from .processor.interface import ProcessorInterface
9
+ from .protocol.api_models import (
10
+ PollEvents,
11
+ FetchRids,
12
+ FetchManifests,
13
+ FetchBundles,
14
+ EventsPayload,
15
+ RidsPayload,
16
+ ManifestsPayload,
17
+ BundlesPayload,
18
+ ErrorResponse
19
+ )
20
+ from .protocol.errors import ProtocolError
21
+ from .protocol.envelope import SignedEnvelope
22
+ from .protocol.consts import (
23
+ BROADCAST_EVENTS_PATH,
24
+ POLL_EVENTS_PATH,
25
+ FETCH_RIDS_PATH,
26
+ FETCH_MANIFESTS_PATH,
27
+ FETCH_BUNDLES_PATH
28
+ )
29
+ from .secure import Secure
30
+ from .lifecycle import NodeLifecycle
31
+ from .config import NodeConfig
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class NodeServer:
37
+ lifecycle: NodeLifecycle
38
+
39
+ def __init__(
40
+ self,
41
+ config: NodeConfig,
42
+ lifecycle: NodeLifecycle,
43
+ secure: Secure,
44
+ processor: ProcessorInterface,
45
+ event_queue: NetworkEventQueue,
46
+ response_handler: ResponseHandler
47
+ ):
48
+ self.config = config
49
+ self.lifecycle = lifecycle
50
+ self.secure = secure
51
+ self.processor = processor
52
+ self.event_queue = event_queue
53
+ self.response_handler = response_handler
54
+ self._build_app()
55
+
56
+ def _build_app(self):
57
+ self.app = FastAPI(
58
+ lifespan=self.lifespan,
59
+ title="KOI-net Protocol API",
60
+ version="1.0.0"
61
+ )
62
+
63
+ self.router = APIRouter(prefix="/koi-net")
64
+ self.app.add_exception_handler(ProtocolError, self.protocol_error_handler)
65
+
66
+ def _add_endpoint(path, func):
67
+ self.router.add_api_route(
68
+ path=path,
69
+ endpoint=self.secure.envelope_handler(func),
70
+ methods=["POST"]
71
+ )
72
+
73
+ _add_endpoint(BROADCAST_EVENTS_PATH, self.broadcast_events)
74
+ _add_endpoint(POLL_EVENTS_PATH, self.poll_events)
75
+ _add_endpoint(FETCH_RIDS_PATH, self.fetch_rids)
76
+ _add_endpoint(FETCH_MANIFESTS_PATH, self.fetch_manifests)
77
+ _add_endpoint(FETCH_BUNDLES_PATH, self.fetch_bundles)
78
+
79
+ self.app.include_router(self.router)
80
+
81
+ def run(self):
82
+ uvicorn.run(
83
+ app=self.app,
84
+ host=self.config.server.host,
85
+ port=self.config.server.port
86
+ )
87
+
88
+ @asynccontextmanager
89
+ async def lifespan(self, app: FastAPI):
90
+ self.lifecycle.start()
91
+ yield
92
+ self.lifecycle.stop()
93
+
94
+ def protocol_error_handler(self, request, exc: ProtocolError):
95
+ logger.info(f"caught protocol error: {exc}")
96
+ resp = ErrorResponse(error=exc.error_type)
97
+ logger.info(f"returning error response: {resp}")
98
+ return JSONResponse(
99
+ status_code=400,
100
+ content=resp.model_dump(mode="json")
101
+ )
102
+
103
+ async def broadcast_events(self, req: SignedEnvelope[EventsPayload]):
104
+ logger.info(f"Request to {BROADCAST_EVENTS_PATH}, received {len(req.payload.events)} event(s)")
105
+ for event in req.payload.events:
106
+ self.processor.handle(event=event, source=req.source_node)
107
+
108
+ async def poll_events(
109
+ self, req: SignedEnvelope[PollEvents]
110
+ ) -> SignedEnvelope[EventsPayload] | ErrorResponse:
111
+ logger.info(f"Request to {POLL_EVENTS_PATH}")
112
+ events = self.event_queue.flush_poll_queue(req.source_node)
113
+ return EventsPayload(events=events)
114
+
115
+ async def fetch_rids(
116
+ self, req: SignedEnvelope[FetchRids]
117
+ ) -> SignedEnvelope[RidsPayload] | ErrorResponse:
118
+ return self.response_handler.fetch_rids(req.payload, req.source_node)
119
+
120
+ async def fetch_manifests(
121
+ self, req: SignedEnvelope[FetchManifests]
122
+ ) -> SignedEnvelope[ManifestsPayload] | ErrorResponse:
123
+ return self.response_handler.fetch_manifests(req.payload, req.source_node)
124
+
125
+ async def fetch_bundles(
126
+ self, req: SignedEnvelope[FetchBundles]
127
+ ) -> SignedEnvelope[BundlesPayload] | ErrorResponse:
128
+ return self.response_handler.fetch_bundles(req.payload, req.source_node)
@@ -1,111 +0,0 @@
1
- import json
2
- import logging
3
- from pydantic import Field
4
- import uvicorn
5
- from contextlib import asynccontextmanager
6
- from rich.logging import RichHandler
7
- from fastapi import FastAPI
8
- from rid_lib.types import KoiNetNode, KoiNetEdge
9
- from koi_net import NodeInterface
10
- from koi_net.config import NodeConfig, KoiNetConfig
11
- from koi_net.protocol.node import NodeProfile, NodeType, NodeProvides
12
- from koi_net.protocol.api_models import (
13
- PollEvents,
14
- FetchRids,
15
- FetchManifests,
16
- FetchBundles,
17
- EventsPayload,
18
- RidsPayload,
19
- ManifestsPayload,
20
- BundlesPayload
21
- )
22
- from koi_net.protocol.consts import (
23
- BROADCAST_EVENTS_PATH,
24
- POLL_EVENTS_PATH,
25
- FETCH_RIDS_PATH,
26
- FETCH_MANIFESTS_PATH,
27
- FETCH_BUNDLES_PATH
28
- )
29
-
30
-
31
- logging.basicConfig(
32
- level=logging.INFO,
33
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
34
- datefmt="%Y-%m-%d %H:%M:%S",
35
- handlers=[RichHandler()]
36
- )
37
-
38
- # TODO: UPDATE OR REMOVE, THIS EXAMPLE IS OUT OF DATE
39
-
40
- logging.getLogger("koi_net").setLevel(logging.DEBUG)
41
-
42
-
43
- class CoordinatorNodeConfig(NodeConfig):
44
- koi_net: KoiNetConfig | None = Field(default_factory = lambda:
45
- KoiNetConfig(
46
- node_name="coordinator",
47
- node_profile=NodeProfile(
48
- node_type=NodeType.FULL,
49
- provides=NodeProvides(
50
- event=[KoiNetNode, KoiNetEdge],
51
- state=[KoiNetNode, KoiNetEdge]
52
- )
53
- ),
54
- cache_directory_path=".coordinator_rid_cache",
55
- event_queues_path="coordinator_event_queues.json"
56
- )
57
- )
58
-
59
- node = NodeInterface(
60
- config=CoordinatorNodeConfig.load_from_yaml("coordinator_config.yaml"),
61
- use_kobj_processor_thread=True
62
- )
63
-
64
-
65
- logger = logging.getLogger(__name__)
66
-
67
-
68
- @asynccontextmanager
69
- async def lifespan(app: FastAPI):
70
- node.start()
71
- yield
72
- node.stop()
73
-
74
- app = FastAPI(
75
- lifespan=lifespan,
76
- root_path="/koi-net",
77
- title="KOI-net Protocol API",
78
- version="1.0.0"
79
- )
80
-
81
- @app.post(BROADCAST_EVENTS_PATH)
82
- def broadcast_events(req: EventsPayload):
83
- logger.info(f"Request to {BROADCAST_EVENTS_PATH}, received {len(req.events)} event(s)")
84
- for event in req.events:
85
- node.processor.handle(event=event, source=KnowledgeSource.External)
86
-
87
- @app.post(POLL_EVENTS_PATH)
88
- def poll_events(req: PollEvents) -> EventsPayload:
89
- logger.info(f"Request to {POLL_EVENTS_PATH}")
90
- events = node.event_queue.flush_poll_queue(req.rid)
91
- return EventsPayload(events=events)
92
-
93
- @app.post(FETCH_RIDS_PATH)
94
- def fetch_rids(req: FetchRids) -> RidsPayload:
95
- return node.response_handler.fetch_rids(req)
96
-
97
- @app.post(FETCH_MANIFESTS_PATH)
98
- def fetch_manifests(req: FetchManifests) -> ManifestsPayload:
99
- return node.response_handler.fetch_manifests(req)
100
-
101
- @app.post(FETCH_BUNDLES_PATH)
102
- def fetch_bundles(req: FetchBundles) -> BundlesPayload:
103
- return node.response_handler.fetch_bundles(req)
104
-
105
- if __name__ == "__main__":
106
- openapi_spec = app.openapi()
107
-
108
- with open("koi-net-protocol-openapi.json", "w") as f:
109
- json.dump(openapi_spec, f, indent=2)
110
-
111
- uvicorn.run("examples.basic_coordinator_node:app", port=node.config.server.port)
@@ -1,192 +0,0 @@
1
- import logging
2
- import httpx
3
- from rid_lib.ext import Cache
4
- from .network.resolver import NetworkResolver
5
- from .network.event_queue import NetworkEventQueue
6
- from .network.graph import NetworkGraph
7
- from .network.request_handler import RequestHandler
8
- from .network.response_handler import ResponseHandler
9
- from .network.error_handler import ErrorHandler
10
- from .network.behavior import Actor
11
- from .processor.interface import ProcessorInterface
12
- from .processor import default_handlers
13
- from .processor.handler import KnowledgeHandler
14
- from .processor.knowledge_pipeline import KnowledgePipeline
15
- from .identity import NodeIdentity
16
- from .secure import Secure
17
- from .config import NodeConfig
18
- from .context import HandlerContext, ActionContext
19
- from .effector import Effector
20
- from . import default_actions
21
-
22
- logger = logging.getLogger(__name__)
23
-
24
-
25
-
26
- class NodeInterface:
27
- config: NodeConfig
28
- cache: Cache
29
- identity: NodeIdentity
30
- resolver: NetworkResolver
31
- event_queue: NetworkEventQueue
32
- graph: NetworkGraph
33
- processor: ProcessorInterface
34
- secure: Secure
35
-
36
- use_kobj_processor_thread: bool
37
-
38
- def __init__(
39
- self,
40
- config: NodeConfig,
41
- use_kobj_processor_thread: bool = False,
42
-
43
- handlers: list[KnowledgeHandler] | None = None,
44
-
45
- cache: Cache | None = None,
46
- processor: ProcessorInterface | None = None
47
- ):
48
- self.config = config
49
- self.cache = cache or Cache(
50
- directory_path=self.config.koi_net.cache_directory_path
51
- )
52
-
53
- self.identity = NodeIdentity(config=self.config)
54
-
55
- self.effector = Effector(cache=self.cache)
56
-
57
- self.graph = NetworkGraph(
58
- cache=self.cache,
59
- identity=self.identity
60
- )
61
-
62
- self.secure = Secure(
63
- identity=self.identity,
64
- effector=self.effector,
65
- config=self.config
66
- )
67
-
68
- self.request_handler = RequestHandler(
69
- effector=self.effector,
70
- identity=self.identity,
71
- secure=self.secure
72
- )
73
-
74
- self.response_handler = ResponseHandler(self.cache, self.effector)
75
-
76
- self.resolver = NetworkResolver(
77
- config=self.config,
78
- cache=self.cache,
79
- identity=self.identity,
80
- graph=self.graph,
81
- request_handler=self.request_handler,
82
- effector=self.effector
83
- )
84
-
85
- self.event_queue = NetworkEventQueue(
86
- config=self.config,
87
- cache=self.cache,
88
- identity=self.identity,
89
- graph=self.graph,
90
- request_handler=self.request_handler,
91
- effector=self.effector
92
- )
93
-
94
- self.actor = Actor(
95
- identity=self.identity,
96
- effector=self.effector,
97
- event_queue=self.event_queue
98
- )
99
-
100
- # pull all handlers defined in default_handlers module
101
- if handlers is None:
102
- handlers = [
103
- obj for obj in vars(default_handlers).values()
104
- if isinstance(obj, KnowledgeHandler)
105
- ]
106
-
107
- self.use_kobj_processor_thread = use_kobj_processor_thread
108
-
109
- self.action_context = ActionContext(
110
- identity=self.identity,
111
- effector=self.effector
112
- )
113
-
114
- self.handler_context = HandlerContext(
115
- identity=self.identity,
116
- cache=self.cache,
117
- event_queue=self.event_queue,
118
- graph=self.graph,
119
- request_handler=self.request_handler,
120
- effector=self.effector
121
- )
122
-
123
- self.pipeline = KnowledgePipeline(
124
- handler_context=self.handler_context,
125
- cache=self.cache,
126
- request_handler=self.request_handler,
127
- event_queue=self.event_queue,
128
- graph=self.graph,
129
- default_handlers=handlers
130
- )
131
-
132
- self.processor = processor or ProcessorInterface(
133
- pipeline=self.pipeline,
134
- use_kobj_processor_thread=self.use_kobj_processor_thread
135
- )
136
-
137
- self.error_handler = ErrorHandler(
138
- processor=self.processor,
139
- actor=self.actor
140
- )
141
-
142
- self.request_handler.set_error_handler(self.error_handler)
143
-
144
- self.handler_context.set_processor(self.processor)
145
-
146
- self.effector.set_processor(self.processor)
147
- self.effector.set_resolver(self.resolver)
148
- self.effector.set_action_context(self.action_context)
149
-
150
-
151
- def start(self) -> None:
152
- """Starts a node, call this method first.
153
-
154
- 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.
155
- """
156
- if self.use_kobj_processor_thread:
157
- logger.info("Starting processor worker thread")
158
- self.processor.worker_thread.start()
159
-
160
- # self.network._load_event_queues()
161
- self.graph.generate()
162
-
163
- # refresh to reflect changes (if any) in config.yaml
164
- self.effector.deref(self.identity.rid, refresh_cache=True)
165
-
166
- logger.debug("Waiting for kobj queue to empty")
167
- if self.use_kobj_processor_thread:
168
- self.processor.kobj_queue.join()
169
- else:
170
- self.processor.flush_kobj_queue()
171
- logger.debug("Done")
172
-
173
- if not self.graph.get_neighbors() and self.config.koi_net.first_contact.rid:
174
- logger.debug(f"I don't have any neighbors, reaching out to first contact {self.config.koi_net.first_contact.rid!r}")
175
-
176
- self.actor.handshake_with(self.config.koi_net.first_contact.rid)
177
-
178
-
179
- def stop(self):
180
- """Stops a node, call this method last.
181
-
182
- Finishes processing knowledge object queue. Saves event queues to storage.
183
- """
184
- logger.info("Stopping node...")
185
-
186
- if self.use_kobj_processor_thread:
187
- logger.info(f"Waiting for kobj queue to empty ({self.processor.kobj_queue.unfinished_tasks} tasks remaining)")
188
- self.processor.kobj_queue.join()
189
- else:
190
- self.processor.flush_kobj_queue()
191
-
192
- # self.network._save_event_queues()
File without changes
File without changes
File without changes
File without changes