koi-net 1.0.0b2__py3-none-any.whl → 1.0.0b4__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/network/graph.py +4 -0
- koi_net/network/request_handler.py +53 -29
- koi_net/processor/default_handlers.py +6 -6
- koi_net/processor/handler.py +15 -0
- koi_net/processor/interface.py +20 -33
- koi_net/protocol/api_models.py +7 -1
- koi_net-1.0.0b4.dist-info/METADATA +510 -0
- {koi_net-1.0.0b2.dist-info → koi_net-1.0.0b4.dist-info}/RECORD +10 -10
- koi_net-1.0.0b2.dist-info/METADATA +0 -209
- {koi_net-1.0.0b2.dist-info → koi_net-1.0.0b4.dist-info}/WHEEL +0 -0
- {koi_net-1.0.0b2.dist-info → koi_net-1.0.0b4.dist-info}/licenses/LICENSE +0 -0
koi_net/network/graph.py
CHANGED
|
@@ -14,6 +14,10 @@ logger = logging.getLogger(__name__)
|
|
|
14
14
|
class NetworkGraph:
|
|
15
15
|
"""Graph functions for this node's view of its network."""
|
|
16
16
|
|
|
17
|
+
cache: Cache
|
|
18
|
+
identity: NodeIdentity
|
|
19
|
+
dg: nx.DiGraph
|
|
20
|
+
|
|
17
21
|
def __init__(self, cache: Cache, identity: NodeIdentity):
|
|
18
22
|
self.cache = cache
|
|
19
23
|
self.dg = nx.DiGraph()
|
|
@@ -12,7 +12,9 @@ from ..protocol.api_models import (
|
|
|
12
12
|
FetchRids,
|
|
13
13
|
FetchManifests,
|
|
14
14
|
FetchBundles,
|
|
15
|
-
PollEvents
|
|
15
|
+
PollEvents,
|
|
16
|
+
RequestModels,
|
|
17
|
+
ResponseModels
|
|
16
18
|
)
|
|
17
19
|
from ..protocol.consts import (
|
|
18
20
|
BROADCAST_EVENTS_PATH,
|
|
@@ -38,13 +40,19 @@ class RequestHandler:
|
|
|
38
40
|
self.cache = cache
|
|
39
41
|
self.graph = graph
|
|
40
42
|
|
|
41
|
-
def make_request(
|
|
43
|
+
def make_request(
|
|
44
|
+
self,
|
|
45
|
+
url: str,
|
|
46
|
+
request: RequestModels,
|
|
47
|
+
response_model: type[ResponseModels] | None = None
|
|
48
|
+
) -> ResponseModels | None:
|
|
42
49
|
logger.info(f"Making request to {url}")
|
|
43
50
|
resp = httpx.post(
|
|
44
51
|
url=url,
|
|
45
52
|
data=request.model_dump_json()
|
|
46
53
|
)
|
|
47
|
-
|
|
54
|
+
if response_model:
|
|
55
|
+
return response_model.model_validate_json(resp.text)
|
|
48
56
|
|
|
49
57
|
def get_url(self, node_rid: KoiNetNode, url: str) -> str:
|
|
50
58
|
"""Retrieves URL of a node, or returns provided URL."""
|
|
@@ -64,54 +72,70 @@ class RequestHandler:
|
|
|
64
72
|
return url
|
|
65
73
|
|
|
66
74
|
def broadcast_events(
|
|
67
|
-
self,
|
|
75
|
+
self,
|
|
76
|
+
node: RID = None,
|
|
77
|
+
url: str = None,
|
|
78
|
+
req: EventsPayload | None = None,
|
|
79
|
+
**kwargs
|
|
68
80
|
) -> None:
|
|
69
81
|
"""See protocol.api_models.EventsPayload for available kwargs."""
|
|
70
82
|
self.make_request(
|
|
71
83
|
self.get_url(node, url) + BROADCAST_EVENTS_PATH,
|
|
72
|
-
EventsPayload.model_validate(kwargs)
|
|
84
|
+
req or EventsPayload.model_validate(kwargs)
|
|
73
85
|
)
|
|
74
86
|
|
|
75
87
|
def poll_events(
|
|
76
|
-
self,
|
|
88
|
+
self,
|
|
89
|
+
node: RID = None,
|
|
90
|
+
url: str = None,
|
|
91
|
+
req: PollEvents | None = None,
|
|
92
|
+
**kwargs
|
|
77
93
|
) -> EventsPayload:
|
|
78
94
|
"""See protocol.api_models.PollEvents for available kwargs."""
|
|
79
|
-
|
|
95
|
+
return self.make_request(
|
|
80
96
|
self.get_url(node, url) + POLL_EVENTS_PATH,
|
|
81
|
-
PollEvents.model_validate(kwargs)
|
|
97
|
+
req or PollEvents.model_validate(kwargs),
|
|
98
|
+
response_model=EventsPayload
|
|
82
99
|
)
|
|
83
|
-
|
|
84
|
-
return EventsPayload.model_validate_json(resp.text)
|
|
85
|
-
|
|
100
|
+
|
|
86
101
|
def fetch_rids(
|
|
87
|
-
self,
|
|
102
|
+
self,
|
|
103
|
+
node: RID = None,
|
|
104
|
+
url: str = None,
|
|
105
|
+
req: FetchRids | None = None,
|
|
106
|
+
**kwargs
|
|
88
107
|
) -> RidsPayload:
|
|
89
108
|
"""See protocol.api_models.FetchRids for available kwargs."""
|
|
90
|
-
|
|
109
|
+
return self.make_request(
|
|
91
110
|
self.get_url(node, url) + FETCH_RIDS_PATH,
|
|
92
|
-
FetchRids.model_validate(kwargs)
|
|
111
|
+
req or FetchRids.model_validate(kwargs),
|
|
112
|
+
response_model=RidsPayload
|
|
93
113
|
)
|
|
94
|
-
|
|
95
|
-
return RidsPayload.model_validate_json(resp.text)
|
|
96
|
-
|
|
114
|
+
|
|
97
115
|
def fetch_manifests(
|
|
98
|
-
self,
|
|
116
|
+
self,
|
|
117
|
+
node: RID = None,
|
|
118
|
+
url: str = None,
|
|
119
|
+
req: FetchManifests | None = None,
|
|
120
|
+
**kwargs
|
|
99
121
|
) -> ManifestsPayload:
|
|
100
122
|
"""See protocol.api_models.FetchManifests for available kwargs."""
|
|
101
|
-
|
|
123
|
+
return self.make_request(
|
|
102
124
|
self.get_url(node, url) + FETCH_MANIFESTS_PATH,
|
|
103
|
-
FetchManifests.model_validate(kwargs)
|
|
125
|
+
req or FetchManifests.model_validate(kwargs),
|
|
126
|
+
response_model=ManifestsPayload
|
|
104
127
|
)
|
|
105
|
-
|
|
106
|
-
return ManifestsPayload.model_validate_json(resp.text)
|
|
107
|
-
|
|
128
|
+
|
|
108
129
|
def fetch_bundles(
|
|
109
|
-
self,
|
|
130
|
+
self,
|
|
131
|
+
node: RID = None,
|
|
132
|
+
url: str = None,
|
|
133
|
+
req: FetchBundles | None = None,
|
|
134
|
+
**kwargs
|
|
110
135
|
) -> BundlesPayload:
|
|
111
136
|
"""See protocol.api_models.FetchBundles for available kwargs."""
|
|
112
|
-
|
|
137
|
+
return self.make_request(
|
|
113
138
|
self.get_url(node, url) + FETCH_BUNDLES_PATH,
|
|
114
|
-
FetchBundles.model_validate(kwargs)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return BundlesPayload.model_validate_json(resp.text)
|
|
139
|
+
req or FetchBundles.model_validate(kwargs),
|
|
140
|
+
response_model=BundlesPayload
|
|
141
|
+
)
|
|
@@ -5,7 +5,7 @@ from rid_lib.ext.bundle import Bundle
|
|
|
5
5
|
from rid_lib.types import KoiNetNode, KoiNetEdge
|
|
6
6
|
from koi_net.protocol.node import NodeType
|
|
7
7
|
from .interface import ProcessorInterface
|
|
8
|
-
from .handler import HandlerType, STOP_CHAIN
|
|
8
|
+
from .handler import KnowledgeHandler, HandlerType, STOP_CHAIN
|
|
9
9
|
from .knowledge_object import KnowledgeObject, KnowledgeSource
|
|
10
10
|
from ..protocol.event import Event, EventType
|
|
11
11
|
from ..protocol.edge import EdgeProfile, EdgeStatus, EdgeType
|
|
@@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
|
|
|
14
14
|
|
|
15
15
|
# RID handlers
|
|
16
16
|
|
|
17
|
-
@
|
|
17
|
+
@KnowledgeHandler.create(HandlerType.RID)
|
|
18
18
|
def basic_rid_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
|
|
19
19
|
"""Default RID handler.
|
|
20
20
|
|
|
@@ -25,7 +25,7 @@ def basic_rid_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
|
|
|
25
25
|
logger.info("Don't let anyone else tell me who I am!")
|
|
26
26
|
return STOP_CHAIN
|
|
27
27
|
|
|
28
|
-
if kobj.event_type == EventType.FORGET:
|
|
28
|
+
if kobj.event_type == EventType.FORGET:
|
|
29
29
|
if processor.cache.exists(kobj.rid):
|
|
30
30
|
logger.info("Allowing cache forget")
|
|
31
31
|
kobj.normalized_event_type = EventType.FORGET
|
|
@@ -38,7 +38,7 @@ def basic_rid_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
|
|
|
38
38
|
|
|
39
39
|
# Manifest handlers
|
|
40
40
|
|
|
41
|
-
@
|
|
41
|
+
@KnowledgeHandler.create(HandlerType.Manifest)
|
|
42
42
|
def basic_state_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
|
|
43
43
|
"""Default manifest handler.
|
|
44
44
|
|
|
@@ -66,7 +66,7 @@ def basic_state_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
|
|
|
66
66
|
|
|
67
67
|
# Bundle handlers
|
|
68
68
|
|
|
69
|
-
@
|
|
69
|
+
@KnowledgeHandler.create(HandlerType.Bundle, rid_types=[KoiNetEdge])
|
|
70
70
|
def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
|
|
71
71
|
"""Handles basic edge negotiation process.
|
|
72
72
|
|
|
@@ -131,7 +131,7 @@ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObjec
|
|
|
131
131
|
|
|
132
132
|
# Network handlers
|
|
133
133
|
|
|
134
|
-
@
|
|
134
|
+
@KnowledgeHandler.create(HandlerType.Network)
|
|
135
135
|
def basic_network_output_filter(processor: ProcessorInterface, kobj: KnowledgeObject):
|
|
136
136
|
"""Default network handler.
|
|
137
137
|
|
koi_net/processor/handler.py
CHANGED
|
@@ -34,4 +34,19 @@ class KnowledgeHandler:
|
|
|
34
34
|
func: Callable
|
|
35
35
|
handler_type: HandlerType
|
|
36
36
|
rid_types: list[RIDType] | None
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def create(
|
|
40
|
+
cls,
|
|
41
|
+
handler_type: HandlerType,
|
|
42
|
+
rid_types: list[RIDType] | None = None
|
|
43
|
+
):
|
|
44
|
+
"""Special decorator that returns a KnowledgeHandler instead of a function.
|
|
45
|
+
|
|
46
|
+
The function symbol will redefined as a `KnowledgeHandler`, which can be passed into the `ProcessorInterface` constructor. This is used to register default handlers.
|
|
47
|
+
"""
|
|
48
|
+
def decorator(func: Callable) -> KnowledgeHandler:
|
|
49
|
+
handler = cls(func, handler_type, rid_types)
|
|
50
|
+
return handler
|
|
51
|
+
return decorator
|
|
37
52
|
|
koi_net/processor/interface.py
CHANGED
|
@@ -44,21 +44,9 @@ class ProcessorInterface:
|
|
|
44
44
|
self.identity = identity
|
|
45
45
|
self.handlers: list[KnowledgeHandler] = default_handlers
|
|
46
46
|
self.kobj_queue = Queue()
|
|
47
|
-
|
|
48
|
-
@classmethod
|
|
49
|
-
def as_handler(
|
|
50
|
-
cls,
|
|
51
|
-
handler_type: HandlerType,
|
|
52
|
-
rid_types: list[RIDType] | None = None
|
|
53
|
-
):
|
|
54
|
-
"""Special decorator that returns a handler instead of a function.
|
|
55
47
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def decorator(func: Callable) -> KnowledgeHandler:
|
|
59
|
-
handler = KnowledgeHandler(func, handler_type, rid_types, )
|
|
60
|
-
return handler
|
|
61
|
-
return decorator
|
|
48
|
+
def add_handler(self, handler: KnowledgeHandler):
|
|
49
|
+
self.handlers.append(handler)
|
|
62
50
|
|
|
63
51
|
def register_handler(
|
|
64
52
|
self,
|
|
@@ -68,7 +56,7 @@ class ProcessorInterface:
|
|
|
68
56
|
"""Assigns decorated function as handler for this processor."""
|
|
69
57
|
def decorator(func: Callable) -> Callable:
|
|
70
58
|
handler = KnowledgeHandler(func, handler_type, rid_types)
|
|
71
|
-
self.
|
|
59
|
+
self.add_handler(handler)
|
|
72
60
|
return func
|
|
73
61
|
return decorator
|
|
74
62
|
|
|
@@ -114,7 +102,7 @@ class ProcessorInterface:
|
|
|
114
102
|
return kobj
|
|
115
103
|
|
|
116
104
|
|
|
117
|
-
def
|
|
105
|
+
def process_kobj(self, kobj: KnowledgeObject) -> None:
|
|
118
106
|
"""Sends provided knowledge obejct through knowledge processing pipeline.
|
|
119
107
|
|
|
120
108
|
Handler chains are called in between major events in the pipeline, indicated by their handler type. Each handler type is guaranteed to have access to certain knowledge, and may affect a subsequent action in the pipeline. The five handler types are as follows:
|
|
@@ -211,21 +199,13 @@ class ProcessorInterface:
|
|
|
211
199
|
self.network.flush_all_webhook_queues()
|
|
212
200
|
|
|
213
201
|
kobj = self.call_handler_chain(HandlerType.Final, kobj)
|
|
214
|
-
|
|
215
|
-
def queue_kobj(self, kobj: KnowledgeObject, flush: bool = False):
|
|
216
|
-
"""Queues a knowledge object to be put processed in the pipeline."""
|
|
217
|
-
self.kobj_queue.put(kobj)
|
|
218
|
-
logger.info(f"Queued {kobj!r}")
|
|
219
|
-
|
|
220
|
-
if flush:
|
|
221
|
-
self.flush_kobj_queue()
|
|
222
202
|
|
|
223
203
|
def flush_kobj_queue(self):
|
|
224
204
|
"""Flushes all knowledge objects from queue and processes them."""
|
|
225
205
|
while not self.kobj_queue.empty():
|
|
226
206
|
kobj = self.kobj_queue.get()
|
|
227
207
|
logger.info(f"Dequeued {kobj!r}")
|
|
228
|
-
self.
|
|
208
|
+
self.process_kobj(kobj)
|
|
229
209
|
logger.info("Done handling")
|
|
230
210
|
|
|
231
211
|
def handle(
|
|
@@ -234,23 +214,30 @@ class ProcessorInterface:
|
|
|
234
214
|
manifest: Manifest | None = None,
|
|
235
215
|
bundle: Bundle | None = None,
|
|
236
216
|
event: Event | None = None,
|
|
217
|
+
kobj: KnowledgeObject | None = None,
|
|
237
218
|
event_type: KnowledgeEventType = None,
|
|
238
219
|
source: KnowledgeSource = KnowledgeSource.Internal,
|
|
239
220
|
flush: bool = False
|
|
240
221
|
):
|
|
241
222
|
"""Queues provided knowledge to be handled by processing pipeline.
|
|
242
223
|
|
|
243
|
-
Knowledge may take the form of an RID, manifest, bundle, or
|
|
224
|
+
Knowledge may take the form of an RID, manifest, bundle, event, or knowledge object (with an optional event type for RID, manifest, or bundle objects). All objects will be normalized into knowledge objects and queued. If `flush` is `True`, the queue will be flushed immediately after adding the new knowledge.
|
|
244
225
|
"""
|
|
245
226
|
if rid:
|
|
246
|
-
|
|
227
|
+
_kobj = KnowledgeObject.from_rid(rid, event_type, source)
|
|
247
228
|
elif manifest:
|
|
248
|
-
|
|
229
|
+
_kobj = KnowledgeObject.from_manifest(manifest, event_type, source)
|
|
249
230
|
elif bundle:
|
|
250
|
-
|
|
231
|
+
_kobj = KnowledgeObject.from_bundle(bundle, event_type, source)
|
|
251
232
|
elif event:
|
|
252
|
-
|
|
233
|
+
_kobj = KnowledgeObject.from_event(event, source)
|
|
234
|
+
elif _kobj:
|
|
235
|
+
_kobj = kobj
|
|
253
236
|
else:
|
|
254
|
-
raise ValueError("One of 'rid', 'manifest', 'bundle', or '
|
|
255
|
-
|
|
256
|
-
self.
|
|
237
|
+
raise ValueError("One of 'rid', 'manifest', 'bundle', 'event', or 'kobj' must be provided")
|
|
238
|
+
|
|
239
|
+
self.kobj_queue.put(kobj)
|
|
240
|
+
logger.info(f"Queued {kobj!r}")
|
|
241
|
+
|
|
242
|
+
if flush:
|
|
243
|
+
self.flush_kobj_queue()
|
koi_net/protocol/api_models.py
CHANGED
|
@@ -38,4 +38,10 @@ class BundlesPayload(BaseModel):
|
|
|
38
38
|
deferred: list[RID] = []
|
|
39
39
|
|
|
40
40
|
class EventsPayload(BaseModel):
|
|
41
|
-
events: list[Event]
|
|
41
|
+
events: list[Event]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# TYPES
|
|
45
|
+
|
|
46
|
+
type RequestModels = EventsPayload | PollEvents | FetchRids | FetchManifests | FetchBundles
|
|
47
|
+
type ResponseModels = RidsPayload | ManifestsPayload | BundlesPayload | EventsPayload
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: koi-net
|
|
3
|
+
Version: 1.0.0b4
|
|
4
|
+
Summary: Implementation of KOI-net protocol in Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/BlockScience/koi-net/
|
|
6
|
+
Author-email: Luke Miller <luke@block.science>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2025 BlockScience
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Requires-Dist: httpx>=0.28.1
|
|
31
|
+
Requires-Dist: networkx>=3.4.2
|
|
32
|
+
Requires-Dist: pydantic>=2.10.6
|
|
33
|
+
Requires-Dist: rid-lib>=3.2.1
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: build; extra == 'dev'
|
|
36
|
+
Requires-Dist: twine>=6.0; extra == 'dev'
|
|
37
|
+
Provides-Extra: examples
|
|
38
|
+
Requires-Dist: fastapi; extra == 'examples'
|
|
39
|
+
Requires-Dist: rich; extra == 'examples'
|
|
40
|
+
Requires-Dist: uvicorn; extra == 'examples'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# KOI-net
|
|
44
|
+
|
|
45
|
+
*This specification is the result of several iterations of KOI research, [read more here](https://github.com/BlockScience/koi).*
|
|
46
|
+
|
|
47
|
+
# Protocol
|
|
48
|
+
## Introduction
|
|
49
|
+
|
|
50
|
+
*This project builds upon and uses the [RID protocol](https://github.com/BlockScience/rid-lib) to identify and coordinate around knowledge objects.*
|
|
51
|
+
|
|
52
|
+
This protocol defines the standard communication patterns and coordination norms needed to establish and maintain Knowledge Organization Infrastructure (KOI) networks. KOI-nets are heterogenous compositions of KOI nodes, each of which is capable of autonomously inputting, processing, and outputting knowledge. The behavior of each node and configuration of each network can vary greatly, thus the protocol is designed to be a simple and flexible but interoperable foundation for future projects to build on. The protocol only governs communication between nodes, not how they operate internally. As a result we consider KOI-nets to be fractal-like, in that a network of nodes may act like a single node from an outside perspective.
|
|
53
|
+
|
|
54
|
+
Generated OpenAPI documentation is provided in this repository, and can be [viewed interactively with Swagger](https://generator.swagger.io/?url=https://raw.githubusercontent.com/BlockScience/koi-net/refs/heads/main/koi-net-protocol-openapi.json).
|
|
55
|
+
|
|
56
|
+
## Communication Methods
|
|
57
|
+
|
|
58
|
+
There are two classes of communication methods, event and state communication.
|
|
59
|
+
- Event communication is one way, a node send an event to another node.
|
|
60
|
+
- State communication is two way, a node asks another node for RIDs, manifests, or bundles and receives a response containing the requested resource (if available).
|
|
61
|
+
|
|
62
|
+
There are also two types of nodes, full and partial nodes.
|
|
63
|
+
- Full nodes are web servers, implementing the endpoints defined in the KOi-net protocol. They are capable of receiving events via webhooks (another node calls their endpoint), and serving state queries. They can also call the endpoints of other full nodes to broadcast events or retrieve state.
|
|
64
|
+
- Partial nodes are web clients and don't implement any API endpoints. They are capable of receiving events via polling (asking another node for events). They can also call the endpoints of full nodes to broadcast events or retrieve state.
|
|
65
|
+
|
|
66
|
+
There are five endpoints defined by the API spec. The first two are for event communication with full and partial nodes respectively. The remaining three are for state communication with full nodes. As a result, partial nodes are unable to directly transfer state and may only output events to other nodes.
|
|
67
|
+
- Broadcast events - `/events/broadcast`
|
|
68
|
+
- Poll events - `/events/poll`
|
|
69
|
+
- Fetch bundles - `/bundles/fetch`
|
|
70
|
+
- Fetch manifests - `/manifests/fetch`
|
|
71
|
+
- Fetch RIDs - `/rids/fetch`
|
|
72
|
+
|
|
73
|
+
All endpoints are called with via POST request with a JSON body, and will receive a response containing a JSON payload (with the exception of broadcast events, which won't return anything). The JSON schemas can be found in the attached OpenAPI specification or the Pydantic models in the "protocol" module.
|
|
74
|
+
|
|
75
|
+
The request and payload JSON objects are composed of the fundamental "knowledge types" from the RID / KOI-net system: RIDs, manifests, bundles, and events. RIDs, manifests, and bundles are defined by the RID protocol and imported from rid-lib, which you can [read about here](https://github.com/BlockScience/rid-lib). Events are now part of the KOI-net protocol, and are defined as an RID and an event type with an optional manifest and contents.
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"rid": "...",
|
|
80
|
+
"event_type": "NEW | UPDATE | FORGET",
|
|
81
|
+
"manifest": {
|
|
82
|
+
"rid": "...",
|
|
83
|
+
"timestamp": "...",
|
|
84
|
+
"sha256_hash": "...",
|
|
85
|
+
},
|
|
86
|
+
"contents": {}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This means that events are essentially just an RID, manifest, or bundle with an event type attached. Event types can be one of `FORGET`, `UPDATE`, or `NEW` forming the "FUN" acronym. While these types roughly correspond to delete, update, and create from CRUD operations, but they are not commands, they are signals. A node emits an event to indicate that its internal state has changed:
|
|
91
|
+
- `NEW` - indicates an previously unknown RID was cached
|
|
92
|
+
- `UPDATE` - indicates a previously known RID was cached
|
|
93
|
+
- `FORGET` - indicates a previously known RID was deleted
|
|
94
|
+
|
|
95
|
+
Nodes may broadcast events to other nodes to indicate their internal state changed. Conversely, nodes may also listen to events from other nodes and as a result decide to change their internal state, take some other action, or do nothing.
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Quickstart
|
|
99
|
+
## Setup
|
|
100
|
+
|
|
101
|
+
The bulk of the code in this repo is taken up by the Python reference implementation, which can be used in other projects to easily set up and configure your own KOI-net node.
|
|
102
|
+
|
|
103
|
+
This package can be installed with pip:
|
|
104
|
+
```shell
|
|
105
|
+
pip install koi-net
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Creating a Node
|
|
109
|
+
|
|
110
|
+
*Check out the `examples/` folder to follow along!*
|
|
111
|
+
|
|
112
|
+
All of the KOI-net functionality comes from the `NodeInterface` class which provides methods to interact with the protocol API, a local RID cache, a view of the network, and an internal processing pipeline. To create a new node, you will need to give it a name and a profile. The name will be used to generate its unique node RID, and the profile stores basic configuration data which will be shared with the other nodes that you communciate with.
|
|
113
|
+
|
|
114
|
+
Your first decision will be whether to setup a partial or full node:
|
|
115
|
+
- Partial nodes only need to indicate their type, and optionally the RID types of events they provide.
|
|
116
|
+
- Full nodes need to indicate their type, the base URL for their KOI-net API, and optionally the RID types of events and state they provide.
|
|
117
|
+
|
|
118
|
+
### Partial Node
|
|
119
|
+
```python
|
|
120
|
+
from koi_net import NodeInterface
|
|
121
|
+
from koi_net.protocol.node import NodeProfile, NodeProvides, NodeType
|
|
122
|
+
|
|
123
|
+
node = NodeInterface(
|
|
124
|
+
name="mypartialnode",
|
|
125
|
+
profile=NodeProfile(
|
|
126
|
+
node_type=NodeType.PARTIAL,
|
|
127
|
+
provides=NodeProvides(
|
|
128
|
+
event=[]
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
### Full Node
|
|
134
|
+
```python
|
|
135
|
+
from koi_net import NodeInterface
|
|
136
|
+
from koi_net.protocol.node import NodeProfile, NodeProvides, NodeType
|
|
137
|
+
|
|
138
|
+
node = NodeInterface(
|
|
139
|
+
name="myfullnode",
|
|
140
|
+
profile=NodeProfile(
|
|
141
|
+
base_url="http://127.0.0.1:8000",
|
|
142
|
+
node_type=NodeType.FULL,
|
|
143
|
+
provides=NodeProvides(
|
|
144
|
+
event=[],
|
|
145
|
+
state=[]
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Knowledge Processing
|
|
152
|
+
|
|
153
|
+
Next we'll set up the knowledge processing flow for our node. This is where most of the node's logic and behavior will come into play. For partial nodes this will be an event loop, and for full nodes we will use webhooks. Make sure to call `node.initialize()` and `node.finalize()` at the beginning and end of your node's life cycle.
|
|
154
|
+
|
|
155
|
+
### Partial Node
|
|
156
|
+
Make sure to set `source=KnowledgeSource.External`, this indicates to the knowledge processing pipeline that the incoming knowledge was received from an external source. Where the knowledge is sourced from will impact decisions in the node's knowledge handlers.
|
|
157
|
+
```python
|
|
158
|
+
import time
|
|
159
|
+
from koi_net.processor.knowledge_object import KnowledgeSource
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
node.initialize()
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
while True:
|
|
166
|
+
for event in node.network.poll_neighbors():
|
|
167
|
+
node.processor.handle(event=event, source=KnowledgeSource.External)
|
|
168
|
+
node.processor.flush_kobj_queue()
|
|
169
|
+
|
|
170
|
+
time.sleep(5)
|
|
171
|
+
|
|
172
|
+
finally:
|
|
173
|
+
node.finalize()
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Full Node
|
|
177
|
+
Setting up a full node is slightly more complex as we'll need a webserver. For this example, we'll use FastAPI and uvicorn. First we need to setup the "lifespan" of the server, to initialize and finalize the node before and after execution, as well as the FastAPI app which will be our web server.
|
|
178
|
+
```python
|
|
179
|
+
from contextlib import asynccontextmanager
|
|
180
|
+
from fastapi import FastAPI
|
|
181
|
+
|
|
182
|
+
@asynccontextmanager
|
|
183
|
+
async def lifespan(app: FastAPI):
|
|
184
|
+
node.initialize()
|
|
185
|
+
yield
|
|
186
|
+
node.finalize()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
app = FastAPI(lifespan=lifespan, root_path="/koi-net")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Next we'll add our event handling webhook endpoint, which will allow other nodes to broadcast events to us. You'll notice that we have a similar loop to our partial node, but instead of polling periodicially, we handle events asynchronously as we receive them from other nodes.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from koi_net.protocol.api_models import *
|
|
196
|
+
from koi_net.protocol.consts import *
|
|
197
|
+
|
|
198
|
+
@app.post(BROADCAST_EVENTS_PATH)
|
|
199
|
+
def broadcast_events(req: EventsPayload, background: BackgroundTasks):
|
|
200
|
+
for event in req.events:
|
|
201
|
+
node.processor.handle(event=event, source=KnowledgeSource.External)
|
|
202
|
+
|
|
203
|
+
background.add_task(node.processor.flush_kobj_queue)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Next we can add the event polling endpoint, this allows partial nodes to receive events from us.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
@app.post(POLL_EVENTS_PATH)
|
|
210
|
+
def poll_events(req: PollEvents) -> EventsPayload:
|
|
211
|
+
events = node.network.flush_poll_queue(req.rid)
|
|
212
|
+
return EventsPayload(events=events)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Now for the state transfer "fetch" endpoints:
|
|
216
|
+
```python
|
|
217
|
+
@app.post(FETCH_RIDS_PATH)
|
|
218
|
+
def fetch_rids(req: FetchRids) -> RidsPayload:
|
|
219
|
+
return node.network.response_handler.fetch_rids(req)
|
|
220
|
+
|
|
221
|
+
@app.post(FETCH_MANIFESTS_PATH)
|
|
222
|
+
def fetch_manifests(req: FetchManifests) -> ManifestsPayload:
|
|
223
|
+
return node.network.response_handler.fetch_manifests(req)
|
|
224
|
+
|
|
225
|
+
@app.post(FETCH_BUNDLES_PATH)
|
|
226
|
+
def fetch_bundles(req: FetchBundles) -> BundlesPayload:
|
|
227
|
+
return node.network.response_handler.fetch_bundles(req)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Finally we can run the server!
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
import uvicorn
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
# update this path to the Python module that defines "app"
|
|
237
|
+
uvicorn.run("examples.full_node_template:app", port=8000)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
*Note: If your node is not the first node in the network, you'll also want to set up a "first contact" in the `NodeInterface`. This is the URL of another full node that can be used to make your first connection and find out about other nodes in the network.*
|
|
241
|
+
|
|
242
|
+
## Try It Out!
|
|
243
|
+
|
|
244
|
+
In addition to the partial and full node templates, there's also example implementations that showcase a coordinator + partial node setup. You can run both of them locally after cloning this repository. First, install the koi-net library with the optional examples requirements from the root directory in the repo:
|
|
245
|
+
```shell
|
|
246
|
+
pip install .[examples]
|
|
247
|
+
```
|
|
248
|
+
Then you can start each node in a separate terminal:
|
|
249
|
+
```shell
|
|
250
|
+
python -m examples.basic_coordinator_node
|
|
251
|
+
```
|
|
252
|
+
```shell
|
|
253
|
+
python -m examples.basic_partial_node
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
# Implementation Reference
|
|
257
|
+
This section provides high level explanations of the Python implementation. More detailed explanations of methods can be found in the docstrings within the codebase itself.
|
|
258
|
+
|
|
259
|
+
## Node Interface
|
|
260
|
+
The node class mostly acts as a container for other classes with more specialized behavior, with special functions that should be called to start up and shut down a node. We'll take a look at each of these components in turn, but here is the class stub:
|
|
261
|
+
```python
|
|
262
|
+
class NodeInterface:
|
|
263
|
+
cache: Cache
|
|
264
|
+
identity: NodeIdentity
|
|
265
|
+
network: NetworkInterface
|
|
266
|
+
processor: ProcessorInterface
|
|
267
|
+
first_contact: str
|
|
268
|
+
|
|
269
|
+
def __init__(
|
|
270
|
+
self,
|
|
271
|
+
name: str,
|
|
272
|
+
profile: NodeProfile,
|
|
273
|
+
identity_file_path: str = "identity.json",
|
|
274
|
+
first_contact: str | None = None,
|
|
275
|
+
handlers: list[KnowledgeHandler] | None = None,
|
|
276
|
+
cache: Cache | None = None,
|
|
277
|
+
network: NetworkInterface | None = None,
|
|
278
|
+
processor: ProcessorInterface | None = None
|
|
279
|
+
): ...
|
|
280
|
+
|
|
281
|
+
def initialize(self): ...
|
|
282
|
+
def finalize(self): ...
|
|
283
|
+
```
|
|
284
|
+
As you can see, only a name and profile are required. The other fields allow for additional customization if needed.
|
|
285
|
+
|
|
286
|
+
## Node Identity
|
|
287
|
+
The `NodeIdentity` class provides easy access to a node's own RID, profile, and bundle. It provides access to the following properties after initialization, accessed with `node.identity`.
|
|
288
|
+
```python
|
|
289
|
+
class NodeIdentity:
|
|
290
|
+
rid: KoiNetNode # an RID type
|
|
291
|
+
profile: NodeProfile
|
|
292
|
+
bundle: Bundle
|
|
293
|
+
```
|
|
294
|
+
This it what is initialized from the required `name` and `profile` fields in the `NodeInterface` constructor. Node RIDs take the form of `orn:koi-net.node:<name>+<uuid>`, and are generated on first use to the identity JSON file along with a the node profile.
|
|
295
|
+
|
|
296
|
+
## Network Interface
|
|
297
|
+
The `NetworkInterface` class provides access to high level network actions, and contains several other network related classes. It is accessed with `node.network`.
|
|
298
|
+
```python
|
|
299
|
+
class NetworkInterface:
|
|
300
|
+
graph: NetworkGraph
|
|
301
|
+
request_handler: RequestHandler
|
|
302
|
+
response_handler: ResponseHandler
|
|
303
|
+
|
|
304
|
+
def __init__(
|
|
305
|
+
self,
|
|
306
|
+
file_path: str,
|
|
307
|
+
first_contact: str | None,
|
|
308
|
+
cache: Cache,
|
|
309
|
+
identity: NodeIdentity
|
|
310
|
+
): ...
|
|
311
|
+
|
|
312
|
+
def push_event_to(self, event: Event, node: KoiNetNode, flush=False): ...
|
|
313
|
+
|
|
314
|
+
def flush_poll_queue(self, node: KoiNetNode) -> list[Event]: ...
|
|
315
|
+
def flush_webhook_queue(self, node: RID): ...
|
|
316
|
+
def flush_all_webhook_queues(self): ...
|
|
317
|
+
|
|
318
|
+
def fetch_remote_bundle(self, rid: RID): ...
|
|
319
|
+
def fetch_remote_manifest(self, rid: RID): ...
|
|
320
|
+
|
|
321
|
+
def get_state_providers(self, rid_type: RIDType): ...
|
|
322
|
+
def poll_neighbors(self) -> list[Event]: ...
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Most of the provided functions are abstractions for KOI-net protocol actions. It also contains three lower level classes: `NetworkGraph`, `RequestHandler`, and `ResponseHandler`.
|
|
326
|
+
|
|
327
|
+
### Network Graph
|
|
328
|
+
The `NetworkGraph` class provides access to a graph view of the node's KOI network: all of the KOI-net node and edge objects it knows about (stored in local cache). This view allows us to query nodes that we have edges with to make networking decisions.
|
|
329
|
+
```python
|
|
330
|
+
class NetworkGraph:
|
|
331
|
+
dg: nx.DiGraph
|
|
332
|
+
|
|
333
|
+
def __init__(
|
|
334
|
+
self,
|
|
335
|
+
cache: Cache,
|
|
336
|
+
identity: NodeIdentity
|
|
337
|
+
): ...
|
|
338
|
+
|
|
339
|
+
def generate(self): ...
|
|
340
|
+
|
|
341
|
+
def get_edges(
|
|
342
|
+
self,
|
|
343
|
+
direction: Literal["in", "out"] | None = None
|
|
344
|
+
) -> list[KoiNetEdge]: ...
|
|
345
|
+
|
|
346
|
+
def get_neighbors(
|
|
347
|
+
self,
|
|
348
|
+
direction: Literal["in", "out"] | None = None,
|
|
349
|
+
status: EdgeStatus | None = None,
|
|
350
|
+
allowed_type: RIDType | None = None
|
|
351
|
+
) -> list[KoiNetNode]: ...
|
|
352
|
+
|
|
353
|
+
def get_node_profile(self, rid: KoiNetNode) -> NodeProfile | None: ...
|
|
354
|
+
def get_edge_profile(
|
|
355
|
+
self,
|
|
356
|
+
rid: KoiNetEdge | None = None,
|
|
357
|
+
source: KoiNetNode | None = None,
|
|
358
|
+
target: KoiNetNode | None = None
|
|
359
|
+
) -> EdgeProfile | None: ...
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Request Handler
|
|
363
|
+
Handles raw API requests to other nodes through the KOI-net protocol. Accepts a node RID or direct URL as the target. Each method requires either a valid request model, or `kwargs` which will be converted to the correct model in `koi_net.protocol.api_models`.
|
|
364
|
+
```python
|
|
365
|
+
class RequestHandler:
|
|
366
|
+
def __init__(self, cache: Cache, graph: NetworkGraph): ...
|
|
367
|
+
|
|
368
|
+
def broadcast_events(
|
|
369
|
+
self,
|
|
370
|
+
node: RID = None,
|
|
371
|
+
url: str = None,
|
|
372
|
+
req: EventsPayload | None = None,
|
|
373
|
+
**kwargs
|
|
374
|
+
) -> None: ...
|
|
375
|
+
|
|
376
|
+
def poll_events(
|
|
377
|
+
self,
|
|
378
|
+
node: RID = None,
|
|
379
|
+
url: str = None,
|
|
380
|
+
req: PollEvents | None = None,
|
|
381
|
+
**kwargs
|
|
382
|
+
) -> EventsPayload: ...
|
|
383
|
+
|
|
384
|
+
def fetch_rids(
|
|
385
|
+
self,
|
|
386
|
+
node: RID = None,
|
|
387
|
+
url: str = None,
|
|
388
|
+
req: FetchRids | None = None,
|
|
389
|
+
**kwargs
|
|
390
|
+
) -> RidsPayload: ...
|
|
391
|
+
|
|
392
|
+
def fetch_manifests(
|
|
393
|
+
self,
|
|
394
|
+
node: RID = None,
|
|
395
|
+
url: str = None,
|
|
396
|
+
req: FetchManifests | None = None,
|
|
397
|
+
**kwargs
|
|
398
|
+
) -> ManifestsPayload: ...
|
|
399
|
+
|
|
400
|
+
def fetch_bundles(
|
|
401
|
+
self,
|
|
402
|
+
node: RID = None,
|
|
403
|
+
url: str = None,
|
|
404
|
+
req: FetchBundles | None = None,
|
|
405
|
+
**kwargs
|
|
406
|
+
) -> BundlesPayload: ...
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Response Handler
|
|
410
|
+
Handles raw API responses to requests from other nodes through the KOI-net protocol.
|
|
411
|
+
```python
|
|
412
|
+
class ResponseHandler:
|
|
413
|
+
def __init__(self, cache: Cache): ...
|
|
414
|
+
|
|
415
|
+
def fetch_rids(self, req: FetchRids) -> RidsPayload:
|
|
416
|
+
def fetch_manifests(self, req: FetchManifests) -> ManifestsPayload:
|
|
417
|
+
def fetch_bundles(self, req: FetchBundles) -> BundlesPayload:
|
|
418
|
+
```
|
|
419
|
+
Only fetch methods are provided right now, event polling and broadcasting can be handled like this:
|
|
420
|
+
```python
|
|
421
|
+
def broadcast_events(req: EventsPayload) -> None:
|
|
422
|
+
for event in req.events:
|
|
423
|
+
node.processor.handle(event=event, source=KnowledgeSource.External)
|
|
424
|
+
node.processor.flush_kobj_queue()
|
|
425
|
+
|
|
426
|
+
def poll_events(req: PollEvents) -> EventsPayload:
|
|
427
|
+
events = node.network.flush_poll_queue(req.rid)
|
|
428
|
+
return EventsPayload(events=events)
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Processor Interface
|
|
432
|
+
The `ProcessorInterface` class provides access to a node's internal knowledge processing pipeline.
|
|
433
|
+
```python
|
|
434
|
+
class ProcessorInterface:
|
|
435
|
+
def __init__(
|
|
436
|
+
self,
|
|
437
|
+
cache: Cache,
|
|
438
|
+
network: NetworkInterface,
|
|
439
|
+
identity: NodeIdentity,
|
|
440
|
+
default_handlers: list[KnowledgeHandler] = []
|
|
441
|
+
): ...
|
|
442
|
+
|
|
443
|
+
def add_handler(self, handler: KnowledgeHandler): ...
|
|
444
|
+
|
|
445
|
+
def register_handler(
|
|
446
|
+
self,
|
|
447
|
+
handler_type: HandlerType,
|
|
448
|
+
rid_types: list[RIDType] | None = None
|
|
449
|
+
): ...
|
|
450
|
+
|
|
451
|
+
def process_kobj(self, kobj: KnowledgeObject) -> None:
|
|
452
|
+
def flush_kobj_queue(self): ...
|
|
453
|
+
|
|
454
|
+
def handle(
|
|
455
|
+
self,
|
|
456
|
+
rid: RID | None = None,
|
|
457
|
+
manifest: Manifest | None = None,
|
|
458
|
+
bundle: Bundle | None = None,
|
|
459
|
+
event: Event | None = None,
|
|
460
|
+
kobj: KnowledgeObject | None = None,
|
|
461
|
+
event_type: KnowledgeEventType = None,
|
|
462
|
+
source: KnowledgeSource = KnowledgeSource.Internal,
|
|
463
|
+
flush: bool = False
|
|
464
|
+
): ...
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
The `register_handler` method is a decorator which can wrap a function to create a new `KnowledgeHandler` and add it to the processing pipeline in a single step. The `add_handler` method adds an existing `KnowledgeHandler` to the processining pipeline.
|
|
468
|
+
|
|
469
|
+
The most commonly used functions in this class are `handle` and `flush_kobj_queue`. The `handle` method can be called on RIDs, manifests, bundles, and events to convert them to normalized to `KnowledgeObject` instances which are then added to the processing queue. The `flush` flag can be set to `True` to immediately start processing, or `flush_kobj_queue` can be called after queueing multiple knowledge objects. When calling the `handle` method, knowledge objects are marked as internally source by default. If you are handling RIDs, manifests, bundles, or events sourced from other nodes, `source` should be set to `KnowledgeSource.External`.
|
|
470
|
+
|
|
471
|
+
Here is an example of how an event polling loop would be implemented using the knowledge processing pipeline:
|
|
472
|
+
```python
|
|
473
|
+
for event in node.network.poll_neighbors():
|
|
474
|
+
node.processor.handle(event=event, source=KnowledgeSource.External)
|
|
475
|
+
node.processor.flush_kobj_queue()
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
# Development
|
|
479
|
+
## Setup
|
|
480
|
+
Clone this repository:
|
|
481
|
+
```console
|
|
482
|
+
git clone https://github.com/BlockScience/koi-net
|
|
483
|
+
```
|
|
484
|
+
Set up and activate virtual environment:
|
|
485
|
+
```shell
|
|
486
|
+
python -m venv venv
|
|
487
|
+
```
|
|
488
|
+
Windows:
|
|
489
|
+
```shell
|
|
490
|
+
.\venv\Scripts\activate
|
|
491
|
+
```
|
|
492
|
+
Linux:
|
|
493
|
+
```shell
|
|
494
|
+
source venv/bin/activate
|
|
495
|
+
```
|
|
496
|
+
Install koi-net with dev dependencies:
|
|
497
|
+
```shell
|
|
498
|
+
pip install -e .[dev]
|
|
499
|
+
```
|
|
500
|
+
## Distribution
|
|
501
|
+
*Be careful! All files not in `.gitignore` will be included in the distribution, even if they aren't tracked by git! Double check the `.tar.gz` after building to make sure you didn't accidently include other files.*
|
|
502
|
+
|
|
503
|
+
Build package:
|
|
504
|
+
```shell
|
|
505
|
+
python -m build
|
|
506
|
+
```
|
|
507
|
+
Push new package build to PyPI:
|
|
508
|
+
```shell
|
|
509
|
+
python -m twine upload -r pypi dist/*
|
|
510
|
+
```
|
|
@@ -2,23 +2,23 @@ koi_net/__init__.py,sha256=b0Ze0pZmJAuygpWUFHM6Kvqo3DkU_uzmkptv1EpAArw,31
|
|
|
2
2
|
koi_net/core.py,sha256=ZsBoTay7Z1_7JKzKt-vB3x_zl9GEit-fFkCSifLPoOk,3582
|
|
3
3
|
koi_net/identity.py,sha256=PBgmAx5f3zzQmHASB1TJW2g19n9TLfmSJMXg2eQFg0A,2386
|
|
4
4
|
koi_net/network/__init__.py,sha256=r_RN-q_mDYC-2RAkN-lJoMUX76TXyfEUc_MVKW87z0g,39
|
|
5
|
-
koi_net/network/graph.py,sha256=
|
|
5
|
+
koi_net/network/graph.py,sha256=SbA0ATIYaiBnq6PCEN2Mu7dgmfyZW3p5tRhZu23UR4E,4782
|
|
6
6
|
koi_net/network/interface.py,sha256=paBJjQFJC8UkUz-BWeRwvuWD2cv8WFt7PyhoV7VOhWI,10823
|
|
7
|
-
koi_net/network/request_handler.py,sha256=
|
|
7
|
+
koi_net/network/request_handler.py,sha256=fhuCDsxI8fZ4p5TntcTZR4mnLrLQ61zDy7Oca3ooFCE,4402
|
|
8
8
|
koi_net/network/response_handler.py,sha256=mA3FtrN3aTZATcLaHQhJUWrJdIKNv6d24fhvOl-nDKY,1890
|
|
9
9
|
koi_net/processor/__init__.py,sha256=x4fAY0hvQEDcpfdTB3POIzxBQjYAtn0qQazPo1Xm0m4,41
|
|
10
|
-
koi_net/processor/default_handlers.py,sha256=
|
|
11
|
-
koi_net/processor/handler.py,sha256=
|
|
12
|
-
koi_net/processor/interface.py,sha256=
|
|
10
|
+
koi_net/processor/default_handlers.py,sha256=Yc7a9n5sAOYMHzzY59VMXYOxQL-6O9zbMQzd61XbIEs,7184
|
|
11
|
+
koi_net/processor/handler.py,sha256=APCECwU7MFcgP7Vu6UTngs0XIjaXSQ_f8rqy8cH5_rM,2242
|
|
12
|
+
koi_net/processor/interface.py,sha256=4D2M09rfLcjBBfAnA1sXBzKlEjhcd_o7xOgwonSc0zc,11092
|
|
13
13
|
koi_net/processor/knowledge_object.py,sha256=cGv33fwNZQMylkhlTaQTbk96FVIVbdOUaBsG06u0m4k,4187
|
|
14
14
|
koi_net/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
koi_net/protocol/api_models.py,sha256=
|
|
15
|
+
koi_net/protocol/api_models.py,sha256=79B5IWQ7gsJ_QIsSRv9424F1frF_DMGkhBbYWkXgtOI,1118
|
|
16
16
|
koi_net/protocol/consts.py,sha256=zeWJvRpqcERrqJq39heyNHb6f_9QrvoBZJHd70yE914,249
|
|
17
17
|
koi_net/protocol/edge.py,sha256=G3D9Ie0vbTSMJdoTw9g_oBmFCqzJ1gO7U1PVrw7p3j8,447
|
|
18
18
|
koi_net/protocol/event.py,sha256=dzJmcHbimo7p5NwH2drccF0vMcAj9oQRj3iZ9Bjf7kg,1275
|
|
19
19
|
koi_net/protocol/helpers.py,sha256=9E9PaoIuSNrTBATGCLJ_kSBMZ2z-KIMnLJzGOTqQDC0,719
|
|
20
20
|
koi_net/protocol/node.py,sha256=Ntrx01dbm39ViKGtr4gLmztcMwKpTIweS6rRL-zoU_Y,391
|
|
21
|
-
koi_net-1.0.
|
|
22
|
-
koi_net-1.0.
|
|
23
|
-
koi_net-1.0.
|
|
24
|
-
koi_net-1.0.
|
|
21
|
+
koi_net-1.0.0b4.dist-info/METADATA,sha256=gDXK_D6NTFeIxcK0XFY5kxHuMyU4b6WKNwU9RnNElgo,21219
|
|
22
|
+
koi_net-1.0.0b4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
+
koi_net-1.0.0b4.dist-info/licenses/LICENSE,sha256=XBcvl8yjCAezfuqN1jadQykrX7H2g4nr2WRDmHLW6ik,1090
|
|
24
|
+
koi_net-1.0.0b4.dist-info/RECORD,,
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: koi-net
|
|
3
|
-
Version: 1.0.0b2
|
|
4
|
-
Summary: Implementation of KOI-net protocol in Python
|
|
5
|
-
Project-URL: Homepage, https://github.com/BlockScience/koi-net/
|
|
6
|
-
Author-email: Luke Miller <luke@block.science>
|
|
7
|
-
License: MIT License
|
|
8
|
-
|
|
9
|
-
Copyright (c) 2025 BlockScience
|
|
10
|
-
|
|
11
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
-
in the Software without restriction, including without limitation the rights
|
|
14
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
-
furnished to do so, subject to the following conditions:
|
|
17
|
-
|
|
18
|
-
The above copyright notice and this permission notice shall be included in all
|
|
19
|
-
copies or substantial portions of the Software.
|
|
20
|
-
|
|
21
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
-
SOFTWARE.
|
|
28
|
-
License-File: LICENSE
|
|
29
|
-
Requires-Python: >=3.10
|
|
30
|
-
Requires-Dist: httpx>=0.28.1
|
|
31
|
-
Requires-Dist: networkx>=3.4.2
|
|
32
|
-
Requires-Dist: pydantic>=2.10.6
|
|
33
|
-
Requires-Dist: rid-lib>=3.2.1
|
|
34
|
-
Provides-Extra: dev
|
|
35
|
-
Requires-Dist: build; extra == 'dev'
|
|
36
|
-
Requires-Dist: twine>=6.0; extra == 'dev'
|
|
37
|
-
Provides-Extra: examples
|
|
38
|
-
Requires-Dist: fastapi; extra == 'examples'
|
|
39
|
-
Requires-Dist: rich; extra == 'examples'
|
|
40
|
-
Requires-Dist: uvicorn; extra == 'examples'
|
|
41
|
-
Description-Content-Type: text/markdown
|
|
42
|
-
|
|
43
|
-
# KOI-net
|
|
44
|
-
|
|
45
|
-
*This specification is the result of several iterations of KOI research, [read more here](https://github.com/BlockScience/koi).*
|
|
46
|
-
|
|
47
|
-
# Protocol
|
|
48
|
-
## Introduction
|
|
49
|
-
|
|
50
|
-
*This project builds upon and uses the [RID protocol](https://github.com/BlockScience/rid-lib) to identify and coordinate around knowledge objects.*
|
|
51
|
-
|
|
52
|
-
This protocol defines the standard communication patterns and coordination norms needed to establish and maintain Knowledge Organization Infrastructure (KOI) networks. KOI-nets are heterogenous compositions of KOI nodes, each of which is capable of autonomously inputting, processing, and outputting knowledge. The behavior of each node and configuration of each network can vary greatly, thus the protocol is designed to be a simple and flexible but interoperable foundation for future projects to build on. The protocol only governs communication between nodes, not how they operate internally. As a result we consider KOI-nets to be fractal-like, in that a network of nodes may act like a single node from an outside perspective.
|
|
53
|
-
|
|
54
|
-
Generated OpenAPI documentation is provided in this repository, and can be [viewed interactively with Swagger](https://generator.swagger.io/?url=https://raw.githubusercontent.com/BlockScience/koi/refs/heads/main/koi_v3_node_api.yaml).
|
|
55
|
-
|
|
56
|
-
## Communication Methods
|
|
57
|
-
|
|
58
|
-
There are two classes of communication methods, event and state communication.
|
|
59
|
-
- Event communication is one way, a node send an event to another node.
|
|
60
|
-
- State communication is two way, a node asks another node for RIDs, manifests, or bundles and receives a response containing the requested resource (if available).
|
|
61
|
-
|
|
62
|
-
There are also two types of nodes, full and partial nodes.
|
|
63
|
-
- Full nodes are web servers, implementing the endpoints defined in the KOi-net protocol. They are capable of receiving events via webhooks (another node calls their endpoint), and serving state queries. They can also call the endpoints of other full nodes to broadcast events or retrieve state.
|
|
64
|
-
- Partial nodes are web clients and don't implement any API endpoints. They are capable of receiving events via polling (asking another node for events). They can also call the endpoints of full nodes to broadcast events or retrieve state.
|
|
65
|
-
|
|
66
|
-
There are five endpoints defined by the API spec. The first two are for event communication with full and partial nodes respectively. The remaining three are for state communication with full nodes. As a result, partial nodes are unable to directly transfer state and may only output events to other nodes.
|
|
67
|
-
- Broadcast events - `/events/broadcast`
|
|
68
|
-
- Poll events - `/events/poll`
|
|
69
|
-
- Fetch bundles - `/bundles/fetch`
|
|
70
|
-
- Fetch manifests - `/manifests/fetch`
|
|
71
|
-
- Fetch RIDs - `/rids/fetch`
|
|
72
|
-
|
|
73
|
-
All endpoints are called with via POST request with a JSON body, and will receive a response containing a JSON payload (with the exception of broadcast events, which won't return anything). The JSON schemas can be found in the attached OpenAPI specification or the Pydantic models in the "protocol" module.
|
|
74
|
-
|
|
75
|
-
The request and payload JSON objects are composed of the fundamental "knowledge types" from the RID / KOI-net system: RIDs, manifests, bundles, and events. RIDs, manifests, and bundles are defined by the RID protocol and imported from rid-lib, which you can [read about here](https://github.com/BlockScience/rid-lib). Events are now part of the KOI-net protocol, and are defined as an RID and an event type with an optional manifest and contents.
|
|
76
|
-
|
|
77
|
-
```json
|
|
78
|
-
{
|
|
79
|
-
"rid": "...",
|
|
80
|
-
"event_type": "NEW | UPDATE | FORGET",
|
|
81
|
-
"manifest": {
|
|
82
|
-
"rid": "...",
|
|
83
|
-
"timestamp": "...",
|
|
84
|
-
"sha256_hash": "...",
|
|
85
|
-
},
|
|
86
|
-
"contents": {}
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
This means that events are essentially just an RID, manifest, or bundle with an event type attached. Event types can be one of `FORGET`, `UPDATE`, or `NEW` forming the "FUN" acronym. While these types roughly correspond to delete, update, and create from CRUD operations, but they are not commands, they are signals. A node emits an event to indicate that its internal state has changed:
|
|
91
|
-
- `NEW` - indicates an previously unknown RID was cached
|
|
92
|
-
- `UPDATE` - indicates a previously known RID was cached
|
|
93
|
-
- `FORGET` - indicates a previously known RID was deleted
|
|
94
|
-
|
|
95
|
-
Nodes may broadcast events to other nodes to indicate its internal state changed. Conversely, nodes may also listen to events from other nodes and decide to change their internal state, take some other action, or do nothing.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# Implementation
|
|
99
|
-
## Setup
|
|
100
|
-
|
|
101
|
-
The bulk of the code in this repo is taken up by the Python reference implementation, which can be used in other projects to easily set up and configure your own KOI node.
|
|
102
|
-
|
|
103
|
-
This package can be installed with pip:
|
|
104
|
-
```
|
|
105
|
-
pip install koi-net
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## Node Interface
|
|
109
|
-
All of the KOI-net functionality comes from the `NodeInterface` class which provides methods to interact with the protocol API, a local RID cache, a view of the network, and an internal processing pipeline. To create a new node, you will need to give it a name and a profile. The name will be used to generate its unique node RID, and the profile stores basic configuration data which will be shared with other nodes you communciate with (it will be stored in the bundle associated with your node's RID).
|
|
110
|
-
- Partial nodes only need to indicate their type, and optionally the RID types of events they provide.
|
|
111
|
-
- Full nodes need to indicate their type, the base URL for their KOI-net API, and optionally the RID types of events and state they provide.
|
|
112
|
-
|
|
113
|
-
```python
|
|
114
|
-
from koi_net import NodeInterface
|
|
115
|
-
from koi_net.protocol.node import NodeProfile, NodeProvides, NodeType
|
|
116
|
-
|
|
117
|
-
# partial node configuration
|
|
118
|
-
|
|
119
|
-
partial_node = NodeInterface(
|
|
120
|
-
name="mypartialnode",
|
|
121
|
-
profile=NodeProfile(
|
|
122
|
-
node_type=NodeType.PARTIAL,
|
|
123
|
-
provides=NodeProvides(
|
|
124
|
-
event=[]
|
|
125
|
-
)
|
|
126
|
-
)
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
# full node configuration
|
|
130
|
-
|
|
131
|
-
full_node = NodeInterface(
|
|
132
|
-
name="myfullnode",
|
|
133
|
-
profile=NodeProfile(
|
|
134
|
-
base_url="http://127.0.0.1:8000",
|
|
135
|
-
node_type=NodeType.FULL,
|
|
136
|
-
provides=NodeProvides(
|
|
137
|
-
event=[],
|
|
138
|
-
state=[]
|
|
139
|
-
)
|
|
140
|
-
)
|
|
141
|
-
)
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
The node class mostly acts as a container for other classes with more specialized behavior, with special functions that should be called to start up and shut down a node. We'll take a look at each of these components in turn, but here is the class stub:
|
|
145
|
-
```python
|
|
146
|
-
class NodeInterface:
|
|
147
|
-
cache: Cache
|
|
148
|
-
identity: NodeIdentity
|
|
149
|
-
network: NetworkInterface
|
|
150
|
-
processor: ProcessorInterface
|
|
151
|
-
first_contact: str
|
|
152
|
-
|
|
153
|
-
def __init__(
|
|
154
|
-
self,
|
|
155
|
-
name: str,
|
|
156
|
-
profile: NodeProfile,
|
|
157
|
-
identity_file_path: str = "identity.json",
|
|
158
|
-
first_contact: str | None = None,
|
|
159
|
-
handlers: list[KnowledgeHandler] | None = None,
|
|
160
|
-
cache: Cache | None = None,
|
|
161
|
-
network: NetworkInterface | None = None,
|
|
162
|
-
processor: ProcessorInterface | None = None
|
|
163
|
-
): ...
|
|
164
|
-
|
|
165
|
-
def initialize(self): ...
|
|
166
|
-
def finalize(self): ...
|
|
167
|
-
```
|
|
168
|
-
As you can see, only a name and profile are required. The other fields allow for additional customization if needed.
|
|
169
|
-
|
|
170
|
-
## Node Identity
|
|
171
|
-
The `NodeIdentity` class provides easy access to a node's own RID, profile, and bundle. It provides access to the following properties after initialization, accessed with `node.identity`.
|
|
172
|
-
```
|
|
173
|
-
class NodeIdentity:
|
|
174
|
-
rid: KoiNetNode # an RID type
|
|
175
|
-
profile: NodeProfile
|
|
176
|
-
bundle: Bundle
|
|
177
|
-
```
|
|
178
|
-
This it what is initialized from the required `name` and `profile` fields in the `NodeInterface` constructor.
|
|
179
|
-
|
|
180
|
-
## Network Interface
|
|
181
|
-
The `NetworkInterface` class provides access to high level network actions, and contains several other network related classes. It is accessed with `node.network`.
|
|
182
|
-
```python
|
|
183
|
-
class NetworkInterface:
|
|
184
|
-
graph: NetworkGraph
|
|
185
|
-
request_handler: RequestHandler
|
|
186
|
-
response_handler: ResponseHandler
|
|
187
|
-
|
|
188
|
-
def __init__(
|
|
189
|
-
self,
|
|
190
|
-
file_path: str,
|
|
191
|
-
first_contact: str | None,
|
|
192
|
-
cache: Cache,
|
|
193
|
-
identity: NodeIdentity
|
|
194
|
-
): ...
|
|
195
|
-
|
|
196
|
-
def push_event_to(self, event: Event, node: KoiNetNode, flush=False): ...
|
|
197
|
-
|
|
198
|
-
def flush_poll_queue(self, node: KoiNetNode) -> list[Event]: ...
|
|
199
|
-
def flush_webhook_queue(self, node: RID): ...
|
|
200
|
-
def flush_all_webhook_queues(self): ...
|
|
201
|
-
|
|
202
|
-
def get_state_providers(self, rid_type: RIDType): ...
|
|
203
|
-
|
|
204
|
-
def fetch_remote_bundle(self, rid: RID): ...
|
|
205
|
-
|
|
206
|
-
def fetch_remote_manifest(self, rid: RID): ...
|
|
207
|
-
|
|
208
|
-
def poll_neighbors(self) -> list[Event]: ...
|
|
209
|
-
|
|
File without changes
|
|
File without changes
|