koi-net 1.0.0b1__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.
- koi_net-1.0.0b1/.gitignore +6 -0
- koi_net-1.0.0b1/LICENSE +21 -0
- koi_net-1.0.0b1/PKG-INFO +43 -0
- koi_net-1.0.0b1/README.md +1 -0
- koi_net-1.0.0b1/examples/full_node.py +134 -0
- koi_net-1.0.0b1/examples/partial_node.py +82 -0
- koi_net-1.0.0b1/pyproject.toml +31 -0
- koi_net-1.0.0b1/requirements.txt +9 -0
- koi_net-1.0.0b1/src/koi_net/__init__.py +1 -0
- koi_net-1.0.0b1/src/koi_net/core.py +86 -0
- koi_net-1.0.0b1/src/koi_net/identity.py +62 -0
- koi_net-1.0.0b1/src/koi_net/network/__init__.py +1 -0
- koi_net-1.0.0b1/src/koi_net/network/graph.py +112 -0
- koi_net-1.0.0b1/src/koi_net/network/interface.py +249 -0
- koi_net-1.0.0b1/src/koi_net/network/request_handler.py +105 -0
- koi_net-1.0.0b1/src/koi_net/network/response_handler.py +57 -0
- koi_net-1.0.0b1/src/koi_net/processor/__init__.py +1 -0
- koi_net-1.0.0b1/src/koi_net/processor/default_handlers.py +151 -0
- koi_net-1.0.0b1/src/koi_net/processor/handler.py +22 -0
- koi_net-1.0.0b1/src/koi_net/processor/interface.py +222 -0
- koi_net-1.0.0b1/src/koi_net/processor/knowledge_object.py +104 -0
- koi_net-1.0.0b1/src/koi_net/protocol/__init__.py +0 -0
- koi_net-1.0.0b1/src/koi_net/protocol/api_models.py +39 -0
- koi_net-1.0.0b1/src/koi_net/protocol/consts.py +5 -0
- koi_net-1.0.0b1/src/koi_net/protocol/edge.py +20 -0
- koi_net-1.0.0b1/src/koi_net/protocol/event.py +48 -0
- koi_net-1.0.0b1/src/koi_net/protocol/helpers.py +25 -0
- koi_net-1.0.0b1/src/koi_net/protocol/node.py +17 -0
koi_net-1.0.0b1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 BlockScience
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
koi_net-1.0.0b1/PKG-INFO
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: koi-net
|
|
3
|
+
Version: 1.0.0b1
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# koi-net
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import uvicorn
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from rich.logging import RichHandler
|
|
6
|
+
from fastapi import FastAPI, BackgroundTasks
|
|
7
|
+
from rid_lib.types import KoiNetNode, KoiNetEdge
|
|
8
|
+
from koi_net import NodeInterface
|
|
9
|
+
from koi_net.processor.handler import HandlerType
|
|
10
|
+
from koi_net.processor.knowledge_object import KnowledgeObject, KnowledgeSource
|
|
11
|
+
from koi_net.protocol.edge import EdgeType
|
|
12
|
+
from koi_net.protocol.event import Event, EventType
|
|
13
|
+
from koi_net.protocol.helpers import generate_edge_bundle
|
|
14
|
+
from koi_net.protocol.node import NodeProfile, NodeType, NodeProvides
|
|
15
|
+
from koi_net.processor import ProcessorInterface
|
|
16
|
+
from koi_net.protocol.api_models import (
|
|
17
|
+
PollEvents,
|
|
18
|
+
FetchRids,
|
|
19
|
+
FetchManifests,
|
|
20
|
+
FetchBundles,
|
|
21
|
+
EventsPayload,
|
|
22
|
+
RidsPayload,
|
|
23
|
+
ManifestsPayload,
|
|
24
|
+
BundlesPayload
|
|
25
|
+
)
|
|
26
|
+
from koi_net.protocol.consts import (
|
|
27
|
+
BROADCAST_EVENTS_PATH,
|
|
28
|
+
POLL_EVENTS_PATH,
|
|
29
|
+
FETCH_RIDS_PATH,
|
|
30
|
+
FETCH_MANIFESTS_PATH,
|
|
31
|
+
FETCH_BUNDLES_PATH
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
logging.basicConfig(
|
|
36
|
+
level=logging.INFO,
|
|
37
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
38
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
39
|
+
handlers=[RichHandler()]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
logging.getLogger("koi_net").setLevel(logging.DEBUG)
|
|
43
|
+
|
|
44
|
+
port = 8000
|
|
45
|
+
|
|
46
|
+
node = NodeInterface(
|
|
47
|
+
name="coordinator",
|
|
48
|
+
profile=NodeProfile(
|
|
49
|
+
base_url=f"http://127.0.0.1:{port}/koi-net",
|
|
50
|
+
node_type=NodeType.FULL,
|
|
51
|
+
provides=NodeProvides(
|
|
52
|
+
event=[KoiNetNode, KoiNetEdge],
|
|
53
|
+
state=[KoiNetNode, KoiNetEdge]
|
|
54
|
+
)
|
|
55
|
+
),
|
|
56
|
+
identity_file_path="coordinator_identity.json"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
logger = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@node.processor.register_handler(HandlerType.Network, rid_types=[KoiNetNode])
|
|
64
|
+
def handshake_handler(proc: ProcessorInterface, kobj: KnowledgeObject):
|
|
65
|
+
logger.info("Handling node handshake")
|
|
66
|
+
|
|
67
|
+
# only respond if node declares itself as NEW
|
|
68
|
+
if kobj.event_type != EventType.NEW:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
logger.info("Sharing this node's bundle with peer")
|
|
72
|
+
proc.network.push_event_to(
|
|
73
|
+
event=Event.from_bundle(EventType.NEW, proc.identity.bundle),
|
|
74
|
+
node=kobj.rid,
|
|
75
|
+
flush=True
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
logger.info("Proposing new edge")
|
|
79
|
+
# defer handling of proposed edge
|
|
80
|
+
proc.handle(bundle=generate_edge_bundle(
|
|
81
|
+
source=kobj.rid,
|
|
82
|
+
target=proc.identity.rid,
|
|
83
|
+
edge_type=EdgeType.WEBHOOK,
|
|
84
|
+
rid_types=[KoiNetNode, KoiNetEdge]
|
|
85
|
+
))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@asynccontextmanager
|
|
90
|
+
async def lifespan(app: FastAPI):
|
|
91
|
+
node.initialize()
|
|
92
|
+
yield
|
|
93
|
+
node.finalize()
|
|
94
|
+
|
|
95
|
+
app = FastAPI(
|
|
96
|
+
lifespan=lifespan,
|
|
97
|
+
root_path="/koi-net",
|
|
98
|
+
title="KOI-net Protocol API",
|
|
99
|
+
version="1.0.0"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@app.post(BROADCAST_EVENTS_PATH)
|
|
103
|
+
def broadcast_events(req: EventsPayload, background: BackgroundTasks):
|
|
104
|
+
logger.info(f"Request to {BROADCAST_EVENTS_PATH}, received {len(req.events)} event(s)")
|
|
105
|
+
for event in req.events:
|
|
106
|
+
node.processor.handle(event=event, source=KnowledgeSource.External)
|
|
107
|
+
|
|
108
|
+
background.add_task(node.processor.flush_kobj_queue)
|
|
109
|
+
|
|
110
|
+
@app.post(POLL_EVENTS_PATH)
|
|
111
|
+
def poll_events(req: PollEvents) -> EventsPayload:
|
|
112
|
+
logger.info(f"Request to {POLL_EVENTS_PATH}")
|
|
113
|
+
events = node.network.flush_poll_queue(req.rid)
|
|
114
|
+
return EventsPayload(events=events)
|
|
115
|
+
|
|
116
|
+
@app.post(FETCH_RIDS_PATH)
|
|
117
|
+
def fetch_rids(req: FetchRids) -> RidsPayload:
|
|
118
|
+
return node.network.response_handler.fetch_rids(req)
|
|
119
|
+
|
|
120
|
+
@app.post(FETCH_MANIFESTS_PATH)
|
|
121
|
+
def fetch_manifests(req: FetchManifests) -> ManifestsPayload:
|
|
122
|
+
return node.network.response_handler.fetch_manifests(req)
|
|
123
|
+
|
|
124
|
+
@app.post(FETCH_BUNDLES_PATH)
|
|
125
|
+
def fetch_bundles(req: FetchBundles) -> BundlesPayload:
|
|
126
|
+
return node.network.response_handler.fetch_bundles(req)
|
|
127
|
+
|
|
128
|
+
openapi_spec = app.openapi()
|
|
129
|
+
|
|
130
|
+
with open("koi-net-protocol-openapi.json", "w") as f:
|
|
131
|
+
json.dump(openapi_spec, f, indent=2)
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
uvicorn.run("examples.full_node:app", port=port)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import logging
|
|
3
|
+
from rich.logging import RichHandler
|
|
4
|
+
from koi_net import NodeInterface
|
|
5
|
+
from koi_net.processor.handler import HandlerType
|
|
6
|
+
from koi_net.processor.knowledge_object import KnowledgeSource, KnowledgeObject
|
|
7
|
+
from koi_net.processor.interface import ProcessorInterface
|
|
8
|
+
from koi_net.protocol.event import EventType
|
|
9
|
+
from koi_net.protocol.edge import EdgeType
|
|
10
|
+
from koi_net.protocol.node import NodeProfile, NodeType
|
|
11
|
+
from koi_net.protocol.helpers import generate_edge_bundle
|
|
12
|
+
from rid_lib.types import KoiNetNode
|
|
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
|
+
|
|
25
|
+
node = NodeInterface(
|
|
26
|
+
name="partial",
|
|
27
|
+
profile=NodeProfile(
|
|
28
|
+
node_type=NodeType.PARTIAL
|
|
29
|
+
),
|
|
30
|
+
identity_file_path="partial_identity.json",
|
|
31
|
+
first_contact="http://127.0.0.1:8000/koi-net"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@node.processor.register_handler(HandlerType.Network, rid_types=[KoiNetNode])
|
|
35
|
+
def coordinator_contact(processor: ProcessorInterface, kobj: KnowledgeObject):
|
|
36
|
+
# when I found out about a new node
|
|
37
|
+
if kobj.normalized_event_type != EventType.NEW:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
node_profile = kobj.bundle.validate_contents(NodeProfile)
|
|
41
|
+
|
|
42
|
+
# looking for event provider of nodes
|
|
43
|
+
if KoiNetNode not in node_profile.provides.event:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
logger.info("Identified a coordinator!")
|
|
47
|
+
logger.info("Proposing new edge")
|
|
48
|
+
|
|
49
|
+
# queued for processing
|
|
50
|
+
processor.handle(bundle=generate_edge_bundle(
|
|
51
|
+
source=kobj.rid,
|
|
52
|
+
target=node.identity.rid,
|
|
53
|
+
edge_type=EdgeType.POLL,
|
|
54
|
+
rid_types=[KoiNetNode]
|
|
55
|
+
))
|
|
56
|
+
|
|
57
|
+
logger.info("Catching up on network state")
|
|
58
|
+
|
|
59
|
+
payload = processor.network.request_handler.fetch_rids(kobj.rid, rid_types=[KoiNetNode])
|
|
60
|
+
for rid in payload.rids:
|
|
61
|
+
if rid == processor.identity.rid:
|
|
62
|
+
logger.info("Skipping myself")
|
|
63
|
+
continue
|
|
64
|
+
if processor.cache.exists(rid):
|
|
65
|
+
logger.info(f"Skipping known RID '{rid}'")
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
# marked as external since we are handling RIDs from another node
|
|
69
|
+
# will fetch remotely instead of checking local cache
|
|
70
|
+
processor.handle(rid=rid, source=KnowledgeSource.External)
|
|
71
|
+
logger.info("Done")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
node.initialize()
|
|
76
|
+
|
|
77
|
+
while True:
|
|
78
|
+
for event in node.network.poll_neighbors():
|
|
79
|
+
node.processor.handle(event=event, source=KnowledgeSource.External)
|
|
80
|
+
node.processor.flush_kobj_queue()
|
|
81
|
+
|
|
82
|
+
time.sleep(1)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "koi-net"
|
|
7
|
+
version = "1.0.0-beta.1"
|
|
8
|
+
description = "Implementation of KOI-net protocol in Python"
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Luke Miller", email = "luke@block.science"}
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
license = {file = "LICENSE"}
|
|
15
|
+
dependencies = [
|
|
16
|
+
"rid-lib>=3.2.1",
|
|
17
|
+
"networkx>=3.4.2",
|
|
18
|
+
"httpx>=0.28.1",
|
|
19
|
+
"pydantic>=2.10.6"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
dev = ["twine>=6.0", "build"]
|
|
24
|
+
examples = [
|
|
25
|
+
"rich",
|
|
26
|
+
"fastapi",
|
|
27
|
+
"uvicorn"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/BlockScience/koi-net/"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .core import NodeInterface
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import httpx
|
|
3
|
+
from rid_lib.ext import Cache, Bundle
|
|
4
|
+
from .network import NetworkInterface
|
|
5
|
+
from .processor import ProcessorInterface
|
|
6
|
+
from .processor import default_handlers as _default_handlers
|
|
7
|
+
from .processor.handler import KnowledgeHandler
|
|
8
|
+
from .identity import NodeIdentity
|
|
9
|
+
from .protocol.node import NodeProfile
|
|
10
|
+
from .protocol.event import Event, EventType
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
class NodeInterface:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
name: str,
|
|
18
|
+
profile: NodeProfile,
|
|
19
|
+
identity_file_path: str = "identity.json",
|
|
20
|
+
first_contact: str | None = None,
|
|
21
|
+
default_handlers: list[KnowledgeHandler] | None = None,
|
|
22
|
+
cache: Cache | None = None,
|
|
23
|
+
network: NetworkInterface | None = None,
|
|
24
|
+
processor: ProcessorInterface | None = None
|
|
25
|
+
):
|
|
26
|
+
self.cache = cache or Cache(directory_path=f"{name}_cache")
|
|
27
|
+
self.identity = NodeIdentity(
|
|
28
|
+
name=name,
|
|
29
|
+
profile=profile,
|
|
30
|
+
cache=self.cache,
|
|
31
|
+
file_path=identity_file_path
|
|
32
|
+
)
|
|
33
|
+
self.first_contact = first_contact
|
|
34
|
+
self.network = network or NetworkInterface(
|
|
35
|
+
file_path=f"{self.identity.rid.name}_event_queues.json",
|
|
36
|
+
first_contact=self.first_contact,
|
|
37
|
+
cache=self.cache,
|
|
38
|
+
identity=self.identity
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# pull all handlers defined in default_handlers module
|
|
42
|
+
if not default_handlers:
|
|
43
|
+
default_handlers = [
|
|
44
|
+
obj for obj in vars(_default_handlers).values()
|
|
45
|
+
if isinstance(obj, KnowledgeHandler)
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
self.processor = processor or ProcessorInterface(
|
|
49
|
+
cache=self.cache,
|
|
50
|
+
network=self.network,
|
|
51
|
+
identity=self.identity,
|
|
52
|
+
default_handlers=default_handlers
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def initialize(self):
|
|
56
|
+
self.network.graph.generate()
|
|
57
|
+
|
|
58
|
+
self.processor.handle(
|
|
59
|
+
bundle=Bundle.generate(
|
|
60
|
+
rid=self.identity.rid,
|
|
61
|
+
contents=self.identity.profile.model_dump()
|
|
62
|
+
),
|
|
63
|
+
flush=True
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if not self.network.graph.get_neighbors() and self.first_contact:
|
|
67
|
+
logger.info(f"I don't have any neighbors, reaching out to first contact {self.first_contact}")
|
|
68
|
+
|
|
69
|
+
events = [
|
|
70
|
+
Event.from_rid(EventType.FORGET, self.identity.rid),
|
|
71
|
+
Event.from_bundle(EventType.NEW, self.identity.bundle)
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
self.network.request_handler.broadcast_events(
|
|
76
|
+
url=self.first_contact,
|
|
77
|
+
events=events
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
except httpx.ConnectError:
|
|
81
|
+
logger.info("Failed to reach first contact")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def finalize(self):
|
|
86
|
+
self.network.save_event_queues()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from rid_lib.ext.bundle import Bundle
|
|
4
|
+
from rid_lib.ext.cache import Cache
|
|
5
|
+
from rid_lib.types.koi_net_node import KoiNetNode
|
|
6
|
+
from .protocol.node import NodeProfile
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NodeIdentityModel(BaseModel):
|
|
12
|
+
rid: KoiNetNode
|
|
13
|
+
profile: NodeProfile
|
|
14
|
+
|
|
15
|
+
class NodeIdentity:
|
|
16
|
+
_identity: NodeIdentityModel
|
|
17
|
+
file_path: str
|
|
18
|
+
cache: Cache
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
name: str,
|
|
23
|
+
profile: NodeProfile,
|
|
24
|
+
cache: Cache,
|
|
25
|
+
file_path: str = "identity.json"
|
|
26
|
+
):
|
|
27
|
+
self.cache = cache
|
|
28
|
+
self.file_path = file_path
|
|
29
|
+
|
|
30
|
+
self._identity = None
|
|
31
|
+
try:
|
|
32
|
+
with open(file_path, "r") as f:
|
|
33
|
+
self._identity = NodeIdentityModel.model_validate_json(f.read())
|
|
34
|
+
|
|
35
|
+
except FileNotFoundError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
if self._identity:
|
|
39
|
+
if self._identity.rid.name != name:
|
|
40
|
+
logger.warning("Node name changed which will change this node's RID, if you really want to do this manually delete the identity JSON file")
|
|
41
|
+
if self._identity.profile != profile:
|
|
42
|
+
self._identity.profile = profile
|
|
43
|
+
else:
|
|
44
|
+
self._identity = NodeIdentityModel(
|
|
45
|
+
rid=KoiNetNode.generate(name),
|
|
46
|
+
profile=profile,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
with open(file_path, "w") as f:
|
|
50
|
+
f.write(self._identity.model_dump_json(indent=2))
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def rid(self) -> KoiNetNode:
|
|
54
|
+
return self._identity.rid
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def profile(self) -> NodeProfile:
|
|
58
|
+
return self._identity.profile
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def bundle(self) -> Bundle:
|
|
62
|
+
return self.cache.read(self.rid)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .interface import NetworkInterface
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Literal
|
|
3
|
+
import networkx as nx
|
|
4
|
+
from rid_lib import RIDType
|
|
5
|
+
from rid_lib.ext import Cache
|
|
6
|
+
from rid_lib.types import KoiNetEdge, KoiNetNode
|
|
7
|
+
from ..identity import NodeIdentity
|
|
8
|
+
from ..protocol.edge import EdgeProfile, EdgeStatus
|
|
9
|
+
from ..protocol.node import NodeProfile
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NetworkGraph:
|
|
15
|
+
def __init__(self, cache: Cache, identity: NodeIdentity):
|
|
16
|
+
self.cache = cache
|
|
17
|
+
self.dg = nx.DiGraph()
|
|
18
|
+
self.identity = identity
|
|
19
|
+
|
|
20
|
+
def generate(self):
|
|
21
|
+
logger.info("Generating network graph")
|
|
22
|
+
self.dg.clear()
|
|
23
|
+
for rid in self.cache.list_rids():
|
|
24
|
+
if type(rid) == KoiNetNode:
|
|
25
|
+
self.dg.add_node(rid)
|
|
26
|
+
logger.info(f"Added node {rid}")
|
|
27
|
+
|
|
28
|
+
elif type(rid) == KoiNetEdge:
|
|
29
|
+
edge_profile = self.get_edge_profile(rid)
|
|
30
|
+
if not edge_profile:
|
|
31
|
+
logger.warning(f"Failed to load {rid!r}")
|
|
32
|
+
continue
|
|
33
|
+
self.dg.add_edge(edge_profile.source, edge_profile.target, rid=rid)
|
|
34
|
+
logger.info(f"Added edge {rid} ({edge_profile.source} -> {edge_profile.target})")
|
|
35
|
+
logger.info("Done")
|
|
36
|
+
|
|
37
|
+
def get_node_profile(self, rid: KoiNetNode) -> NodeProfile | None:
|
|
38
|
+
bundle = self.cache.read(rid)
|
|
39
|
+
if bundle:
|
|
40
|
+
return bundle.validate_contents(NodeProfile)
|
|
41
|
+
|
|
42
|
+
def get_edge_profile(
|
|
43
|
+
self,
|
|
44
|
+
rid: KoiNetEdge | None = None,
|
|
45
|
+
source: KoiNetNode | None = None,
|
|
46
|
+
target: KoiNetNode | None = None,
|
|
47
|
+
) -> EdgeProfile | None:
|
|
48
|
+
if source and target:
|
|
49
|
+
if (source, target) not in self.dg.edges: return
|
|
50
|
+
edge_data = self.dg.get_edge_data(source, target)
|
|
51
|
+
if not edge_data: return
|
|
52
|
+
rid = edge_data.get("rid")
|
|
53
|
+
if not rid: return
|
|
54
|
+
elif not rid:
|
|
55
|
+
raise ValueError("Either 'rid' or 'source' and 'target' must be provided")
|
|
56
|
+
|
|
57
|
+
bundle = self.cache.read(rid)
|
|
58
|
+
if bundle:
|
|
59
|
+
return bundle.validate_contents(EdgeProfile)
|
|
60
|
+
|
|
61
|
+
def get_edges(
|
|
62
|
+
self,
|
|
63
|
+
direction: Literal["in", "out"] | None = None,
|
|
64
|
+
) -> list[KoiNetEdge]:
|
|
65
|
+
|
|
66
|
+
edges = []
|
|
67
|
+
if direction != "in":
|
|
68
|
+
out_edges = self.dg.out_edges(self.identity.rid)
|
|
69
|
+
edges.extend([e for e in out_edges])
|
|
70
|
+
|
|
71
|
+
if direction != "out":
|
|
72
|
+
in_edges = self.dg.in_edges(self.identity.rid)
|
|
73
|
+
edges.extend([e for e in in_edges])
|
|
74
|
+
|
|
75
|
+
edge_rids = []
|
|
76
|
+
for edge in edges:
|
|
77
|
+
edge_data = self.dg.get_edge_data(*edge)
|
|
78
|
+
if not edge_data: continue
|
|
79
|
+
edge_rid = edge_data.get("rid")
|
|
80
|
+
if not edge_rid: continue
|
|
81
|
+
edge_rids.append(edge_rid)
|
|
82
|
+
|
|
83
|
+
return edge_rids
|
|
84
|
+
|
|
85
|
+
def get_neighbors(
|
|
86
|
+
self,
|
|
87
|
+
direction: Literal["in", "out"] | None = None,
|
|
88
|
+
status: EdgeStatus | None = None,
|
|
89
|
+
allowed_type: RIDType | None = None
|
|
90
|
+
) -> list[KoiNetNode]:
|
|
91
|
+
|
|
92
|
+
neighbors = []
|
|
93
|
+
for edge_rid in self.get_edges(direction):
|
|
94
|
+
edge_profile = self.get_edge_profile(edge_rid)
|
|
95
|
+
|
|
96
|
+
if not edge_profile:
|
|
97
|
+
logger.warning(f"Failed to find edge {edge_rid!r} in cache")
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
if status and edge_profile.status != status:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if allowed_type and allowed_type not in edge_profile.rid_types:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
if edge_profile.target == self.identity.rid:
|
|
107
|
+
neighbors.append(edge_profile.source)
|
|
108
|
+
elif edge_profile.source == self.identity.rid:
|
|
109
|
+
neighbors.append(edge_profile.target)
|
|
110
|
+
|
|
111
|
+
return list(neighbors)
|
|
112
|
+
|