koi-net 1.0.0b19__py3-none-any.whl → 1.1.0__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/actor.py +60 -0
- koi_net/config.py +44 -18
- koi_net/context.py +63 -0
- koi_net/core.py +152 -84
- koi_net/default_actions.py +15 -0
- koi_net/effector.py +139 -0
- koi_net/identity.py +4 -22
- koi_net/lifecycle.py +104 -0
- koi_net/network/__init__.py +0 -1
- koi_net/network/error_handler.py +50 -0
- koi_net/network/event_queue.py +199 -0
- koi_net/network/graph.py +23 -38
- koi_net/network/request_handler.py +129 -66
- koi_net/network/resolver.py +150 -0
- koi_net/network/response_handler.py +15 -6
- koi_net/poller.py +40 -0
- koi_net/processor/__init__.py +0 -1
- koi_net/processor/default_handlers.py +71 -42
- koi_net/processor/handler.py +3 -7
- koi_net/processor/interface.py +15 -214
- koi_net/processor/knowledge_object.py +10 -17
- koi_net/processor/knowledge_pipeline.py +220 -0
- koi_net/protocol/api_models.py +18 -3
- koi_net/protocol/edge.py +26 -1
- koi_net/protocol/envelope.py +58 -0
- koi_net/protocol/errors.py +23 -0
- koi_net/protocol/event.py +0 -3
- koi_net/protocol/node.py +2 -1
- koi_net/protocol/secure.py +160 -0
- koi_net/secure.py +117 -0
- koi_net/server.py +129 -0
- {koi_net-1.0.0b19.dist-info → koi_net-1.1.0.dist-info}/METADATA +5 -4
- koi_net-1.1.0.dist-info/RECORD +38 -0
- koi_net/network/interface.py +0 -276
- koi_net/protocol/helpers.py +0 -25
- koi_net-1.0.0b19.dist-info/RECORD +0 -25
- {koi_net-1.0.0b19.dist-info → koi_net-1.1.0.dist-info}/WHEEL +0 -0
- {koi_net-1.0.0b19.dist-info → koi_net-1.1.0.dist-info}/licenses/LICENSE +0 -0
koi_net/identity.py
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from rid_lib.ext.bundle import Bundle
|
|
3
|
-
from rid_lib.ext.cache import Cache
|
|
4
2
|
from rid_lib.types.koi_net_node import KoiNetNode
|
|
5
|
-
|
|
6
3
|
from .config import NodeConfig
|
|
7
4
|
from .protocol.node import NodeProfile
|
|
8
5
|
|
|
6
|
+
|
|
9
7
|
logger = logging.getLogger(__name__)
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
class NodeIdentity:
|
|
13
|
-
"""Represents a node's identity (RID, profile
|
|
11
|
+
"""Represents a node's identity (RID, profile)."""
|
|
14
12
|
|
|
15
13
|
config: NodeConfig
|
|
16
|
-
cache: Cache
|
|
17
14
|
|
|
18
|
-
def __init__(
|
|
19
|
-
self,
|
|
20
|
-
config: NodeConfig,
|
|
21
|
-
cache: Cache
|
|
22
|
-
):
|
|
23
|
-
"""Initializes node identity from a name and profile.
|
|
24
|
-
|
|
25
|
-
Attempts to read identity from storage. If it doesn't already exist, a new RID is generated from the provided name, and that RID and profile are written to storage. Changes to the name or profile will update the stored identity.
|
|
26
|
-
|
|
27
|
-
WARNING: If the name is changed, the RID will be overwritten which will have consequences for the rest of the network.
|
|
28
|
-
"""
|
|
15
|
+
def __init__(self, config: NodeConfig):
|
|
29
16
|
self.config = config
|
|
30
|
-
self.cache = cache
|
|
31
17
|
|
|
32
18
|
@property
|
|
33
19
|
def rid(self) -> KoiNetNode:
|
|
@@ -35,8 +21,4 @@ class NodeIdentity:
|
|
|
35
21
|
|
|
36
22
|
@property
|
|
37
23
|
def profile(self) -> NodeProfile:
|
|
38
|
-
return self.config.koi_net.node_profile
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def bundle(self) -> Bundle:
|
|
42
|
-
return self.cache.read(self.rid)
|
|
24
|
+
return self.config.koi_net.node_profile
|
koi_net/lifecycle.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from contextlib import contextmanager, asynccontextmanager
|
|
3
|
+
|
|
4
|
+
from rid_lib.types import KoiNetNode
|
|
5
|
+
|
|
6
|
+
from .actor import Actor
|
|
7
|
+
from .effector import Effector
|
|
8
|
+
from .config import NodeConfig
|
|
9
|
+
from .processor.interface import ProcessorInterface
|
|
10
|
+
from .network.graph import NetworkGraph
|
|
11
|
+
from .identity import NodeIdentity
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NodeLifecycle:
|
|
17
|
+
config: NodeConfig
|
|
18
|
+
graph: NetworkGraph
|
|
19
|
+
processor: ProcessorInterface
|
|
20
|
+
effector: Effector
|
|
21
|
+
actor: Actor
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
config: NodeConfig,
|
|
26
|
+
identity: NodeIdentity,
|
|
27
|
+
graph: NetworkGraph,
|
|
28
|
+
processor: ProcessorInterface,
|
|
29
|
+
effector: Effector,
|
|
30
|
+
actor: Actor,
|
|
31
|
+
use_kobj_processor_thread: bool
|
|
32
|
+
):
|
|
33
|
+
self.config = config
|
|
34
|
+
self.identity = identity
|
|
35
|
+
self.graph = graph
|
|
36
|
+
self.processor = processor
|
|
37
|
+
self.effector = effector
|
|
38
|
+
self.actor = actor
|
|
39
|
+
self.use_kobj_processor_thread = use_kobj_processor_thread
|
|
40
|
+
|
|
41
|
+
@contextmanager
|
|
42
|
+
def run(self):
|
|
43
|
+
try:
|
|
44
|
+
logger.info("Starting node lifecycle...")
|
|
45
|
+
self.start()
|
|
46
|
+
yield
|
|
47
|
+
except KeyboardInterrupt:
|
|
48
|
+
logger.info("Keyboard interrupt!")
|
|
49
|
+
finally:
|
|
50
|
+
logger.info("Stopping node lifecycle...")
|
|
51
|
+
self.stop()
|
|
52
|
+
|
|
53
|
+
@asynccontextmanager
|
|
54
|
+
async def async_run(self):
|
|
55
|
+
try:
|
|
56
|
+
logger.info("Starting async node lifecycle...")
|
|
57
|
+
self.start()
|
|
58
|
+
yield
|
|
59
|
+
except KeyboardInterrupt:
|
|
60
|
+
logger.info("Keyboard interrupt!")
|
|
61
|
+
finally:
|
|
62
|
+
logger.info("Stopping async node lifecycle...")
|
|
63
|
+
self.stop()
|
|
64
|
+
|
|
65
|
+
def start(self):
|
|
66
|
+
"""Starts a node, call this method first.
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
"""
|
|
70
|
+
if self.use_kobj_processor_thread:
|
|
71
|
+
logger.info("Starting processor worker thread")
|
|
72
|
+
self.processor.worker_thread.start()
|
|
73
|
+
|
|
74
|
+
self.graph.generate()
|
|
75
|
+
|
|
76
|
+
# refresh to reflect changes (if any) in config.yaml
|
|
77
|
+
self.effector.deref(self.identity.rid, refresh_cache=True)
|
|
78
|
+
|
|
79
|
+
logger.debug("Waiting for kobj queue to empty")
|
|
80
|
+
if self.use_kobj_processor_thread:
|
|
81
|
+
self.processor.kobj_queue.join()
|
|
82
|
+
else:
|
|
83
|
+
self.processor.flush_kobj_queue()
|
|
84
|
+
logger.debug("Done")
|
|
85
|
+
|
|
86
|
+
if not self.graph.get_neighbors() and self.config.koi_net.first_contact.rid:
|
|
87
|
+
logger.debug(f"I don't have any neighbors, reaching out to first contact {self.config.koi_net.first_contact.rid!r}")
|
|
88
|
+
|
|
89
|
+
self.actor.handshake_with(self.config.koi_net.first_contact.rid)
|
|
90
|
+
|
|
91
|
+
for coordinator in self.actor.identify_coordinators():
|
|
92
|
+
self.actor.catch_up_with(coordinator, rid_types=[KoiNetNode])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def stop(self):
|
|
96
|
+
"""Stops a node, call this method last.
|
|
97
|
+
|
|
98
|
+
Finishes processing knowledge object queue. Saves event queues to storage.
|
|
99
|
+
"""
|
|
100
|
+
if self.use_kobj_processor_thread:
|
|
101
|
+
logger.info(f"Waiting for kobj queue to empty ({self.processor.kobj_queue.unfinished_tasks} tasks remaining)")
|
|
102
|
+
self.processor.kobj_queue.join()
|
|
103
|
+
else:
|
|
104
|
+
self.processor.flush_kobj_queue()
|
koi_net/network/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .interface import NetworkInterface
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from koi_net.protocol.errors import ErrorType
|
|
3
|
+
from koi_net.protocol.event import EventType
|
|
4
|
+
from rid_lib.types import KoiNetNode
|
|
5
|
+
from ..processor.interface import ProcessorInterface
|
|
6
|
+
from ..actor import Actor
|
|
7
|
+
|
|
8
|
+
logger = getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ErrorHandler:
|
|
12
|
+
timeout_counter: dict[KoiNetNode, int]
|
|
13
|
+
processor: ProcessorInterface
|
|
14
|
+
actor: Actor
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
processor: ProcessorInterface,
|
|
19
|
+
actor: Actor
|
|
20
|
+
):
|
|
21
|
+
self.processor = processor
|
|
22
|
+
self.actor = actor
|
|
23
|
+
self.timeout_counter = {}
|
|
24
|
+
|
|
25
|
+
def handle_connection_error(self, node: KoiNetNode):
|
|
26
|
+
self.timeout_counter.setdefault(node, 0)
|
|
27
|
+
self.timeout_counter[node] += 1
|
|
28
|
+
|
|
29
|
+
logger.debug(f"{node} has timed out {self.timeout_counter[node]} time(s)")
|
|
30
|
+
|
|
31
|
+
if self.timeout_counter[node] > 3:
|
|
32
|
+
logger.debug(f"Exceeded time out limit, forgetting node")
|
|
33
|
+
self.processor.handle(rid=node, event_type=EventType.FORGET)
|
|
34
|
+
# do something
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def handle_protocol_error(
|
|
38
|
+
self,
|
|
39
|
+
error_type: ErrorType,
|
|
40
|
+
node: KoiNetNode
|
|
41
|
+
):
|
|
42
|
+
logger.info(f"Handling protocol error {error_type} for node {node!r}")
|
|
43
|
+
match error_type:
|
|
44
|
+
case ErrorType.UnknownNode:
|
|
45
|
+
logger.info("Peer doesn't know me, attempting handshake...")
|
|
46
|
+
self.actor.handshake_with(node)
|
|
47
|
+
|
|
48
|
+
case ErrorType.InvalidKey: ...
|
|
49
|
+
case ErrorType.InvalidSignature: ...
|
|
50
|
+
case ErrorType.InvalidTarget: ...
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from queue import Queue
|
|
3
|
+
import httpx
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from rid_lib import RID
|
|
6
|
+
from rid_lib.ext import Cache
|
|
7
|
+
from rid_lib.types import KoiNetNode
|
|
8
|
+
|
|
9
|
+
from .graph import NetworkGraph
|
|
10
|
+
from .request_handler import NodeNotFoundError, RequestHandler
|
|
11
|
+
from ..protocol.node import NodeProfile, NodeType
|
|
12
|
+
from ..protocol.edge import EdgeProfile, EdgeType
|
|
13
|
+
from ..protocol.event import Event
|
|
14
|
+
from ..identity import NodeIdentity
|
|
15
|
+
from ..config import NodeConfig
|
|
16
|
+
from ..effector import Effector
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EventQueueModel(BaseModel):
|
|
22
|
+
webhook: dict[KoiNetNode, list[Event]]
|
|
23
|
+
poll: dict[KoiNetNode, list[Event]]
|
|
24
|
+
|
|
25
|
+
type EventQueue = dict[RID, Queue[Event]]
|
|
26
|
+
|
|
27
|
+
class NetworkEventQueue:
|
|
28
|
+
"""A collection of functions and classes to interact with the KOI network."""
|
|
29
|
+
|
|
30
|
+
config: NodeConfig
|
|
31
|
+
identity: NodeIdentity
|
|
32
|
+
effector: Effector
|
|
33
|
+
cache: Cache
|
|
34
|
+
graph: NetworkGraph
|
|
35
|
+
request_handler: RequestHandler
|
|
36
|
+
poll_event_queue: EventQueue
|
|
37
|
+
webhook_event_queue: EventQueue
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
config: NodeConfig,
|
|
42
|
+
cache: Cache,
|
|
43
|
+
identity: NodeIdentity,
|
|
44
|
+
effector: Effector,
|
|
45
|
+
graph: NetworkGraph,
|
|
46
|
+
request_handler: RequestHandler,
|
|
47
|
+
):
|
|
48
|
+
self.config = config
|
|
49
|
+
self.identity = identity
|
|
50
|
+
self.cache = cache
|
|
51
|
+
self.graph = graph
|
|
52
|
+
self.request_handler = request_handler
|
|
53
|
+
self.effector = effector
|
|
54
|
+
|
|
55
|
+
self.poll_event_queue = dict()
|
|
56
|
+
self.webhook_event_queue = dict()
|
|
57
|
+
|
|
58
|
+
def _load_event_queues(self):
|
|
59
|
+
"""Loads event queues from storage."""
|
|
60
|
+
try:
|
|
61
|
+
with open(self.config.koi_net.event_queues_path, "r") as f:
|
|
62
|
+
queues = EventQueueModel.model_validate_json(f.read())
|
|
63
|
+
|
|
64
|
+
for node in queues.poll.keys():
|
|
65
|
+
for event in queues.poll[node]:
|
|
66
|
+
queue = self.poll_event_queue.setdefault(node, Queue())
|
|
67
|
+
queue.put(event)
|
|
68
|
+
|
|
69
|
+
for node in queues.webhook.keys():
|
|
70
|
+
for event in queues.webhook[node]:
|
|
71
|
+
queue = self.webhook_event_queue.setdefault(node, Queue())
|
|
72
|
+
queue.put(event)
|
|
73
|
+
|
|
74
|
+
except FileNotFoundError:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
def _save_event_queues(self):
|
|
78
|
+
"""Writes event queues to storage."""
|
|
79
|
+
events_model = EventQueueModel(
|
|
80
|
+
poll={
|
|
81
|
+
node: list(queue.queue)
|
|
82
|
+
for node, queue in self.poll_event_queue.items()
|
|
83
|
+
if not queue.empty()
|
|
84
|
+
},
|
|
85
|
+
webhook={
|
|
86
|
+
node: list(queue.queue)
|
|
87
|
+
for node, queue in self.webhook_event_queue.items()
|
|
88
|
+
if not queue.empty()
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if len(events_model.poll) == 0 and len(events_model.webhook) == 0:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
with open(self.config.koi_net.event_queues_path, "w") as f:
|
|
96
|
+
f.write(events_model.model_dump_json(indent=2))
|
|
97
|
+
|
|
98
|
+
def push_event_to(self, event: Event, node: KoiNetNode, flush=False):
|
|
99
|
+
"""Pushes event to queue of specified node.
|
|
100
|
+
|
|
101
|
+
Event will be sent to webhook or poll queue depending on the node type and edge type of the specified node. If `flush` is set to `True`, the webhook queued will be flushed after pushing the event.
|
|
102
|
+
"""
|
|
103
|
+
logger.debug(f"Pushing event {event.event_type} {event.rid!r} to {node}")
|
|
104
|
+
|
|
105
|
+
node_bundle = self.effector.deref(node)
|
|
106
|
+
|
|
107
|
+
# if there's an edge from me to the target node, override broadcast type
|
|
108
|
+
edge_rid = self.graph.get_edge(
|
|
109
|
+
source=self.identity.rid,
|
|
110
|
+
target=node
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
edge_bundle = self.effector.deref(edge_rid) if edge_rid else None
|
|
114
|
+
|
|
115
|
+
if edge_bundle:
|
|
116
|
+
logger.debug(f"Found edge from me to {node!r}")
|
|
117
|
+
edge_profile = edge_bundle.validate_contents(EdgeProfile)
|
|
118
|
+
if edge_profile.edge_type == EdgeType.WEBHOOK:
|
|
119
|
+
event_queue = self.webhook_event_queue
|
|
120
|
+
elif edge_profile.edge_type == EdgeType.POLL:
|
|
121
|
+
event_queue = self.poll_event_queue
|
|
122
|
+
|
|
123
|
+
elif node_bundle:
|
|
124
|
+
logger.debug(f"Found bundle for {node!r}")
|
|
125
|
+
node_profile = node_bundle.validate_contents(NodeProfile)
|
|
126
|
+
if node_profile.node_type == NodeType.FULL:
|
|
127
|
+
event_queue = self.webhook_event_queue
|
|
128
|
+
elif node_profile.node_type == NodeType.PARTIAL:
|
|
129
|
+
event_queue = self.poll_event_queue
|
|
130
|
+
|
|
131
|
+
elif node == self.config.koi_net.first_contact.rid:
|
|
132
|
+
logger.debug(f"Node {node!r} is my first contact")
|
|
133
|
+
# first contact node is always a webhook node
|
|
134
|
+
event_queue = self.webhook_event_queue
|
|
135
|
+
|
|
136
|
+
else:
|
|
137
|
+
logger.warning(f"Node {node!r} unknown to me")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
queue = event_queue.setdefault(node, Queue())
|
|
141
|
+
queue.put(event)
|
|
142
|
+
|
|
143
|
+
if flush and event_queue is self.webhook_event_queue:
|
|
144
|
+
self.flush_webhook_queue(node)
|
|
145
|
+
|
|
146
|
+
def _flush_queue(self, event_queue: EventQueue, node: KoiNetNode) -> list[Event]:
|
|
147
|
+
"""Flushes a node's queue, returning list of events."""
|
|
148
|
+
queue = event_queue.get(node)
|
|
149
|
+
events = list()
|
|
150
|
+
if queue:
|
|
151
|
+
while not queue.empty():
|
|
152
|
+
event = queue.get()
|
|
153
|
+
logger.debug(f"Dequeued {event.event_type} {event.rid!r}")
|
|
154
|
+
events.append(event)
|
|
155
|
+
|
|
156
|
+
return events
|
|
157
|
+
|
|
158
|
+
def flush_poll_queue(self, node: KoiNetNode) -> list[Event]:
|
|
159
|
+
"""Flushes a node's poll queue, returning list of events."""
|
|
160
|
+
logger.debug(f"Flushing poll queue for {node}")
|
|
161
|
+
return self._flush_queue(self.poll_event_queue, node)
|
|
162
|
+
|
|
163
|
+
def flush_webhook_queue(self, node: KoiNetNode, requeue_on_fail: bool = True):
|
|
164
|
+
"""Flushes a node's webhook queue, and broadcasts events.
|
|
165
|
+
|
|
166
|
+
If node profile is unknown, or node type is not `FULL`, this operation will fail silently. If the remote node cannot be reached, all events will be requeued.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
logger.debug(f"Flushing webhook queue for {node}")
|
|
170
|
+
|
|
171
|
+
# node_bundle = self.effector.deref(node)
|
|
172
|
+
|
|
173
|
+
# if not node_bundle:
|
|
174
|
+
# logger.warning(f"{node!r} not found")
|
|
175
|
+
# return
|
|
176
|
+
|
|
177
|
+
# node_profile = node_bundle.validate_contents(NodeProfile)
|
|
178
|
+
|
|
179
|
+
# if node_profile.node_type != NodeType.FULL:
|
|
180
|
+
# logger.warning(f"{node!r} is a partial node!")
|
|
181
|
+
# return
|
|
182
|
+
|
|
183
|
+
events = self._flush_queue(self.webhook_event_queue, node)
|
|
184
|
+
if not events: return
|
|
185
|
+
|
|
186
|
+
logger.debug(f"Broadcasting {len(events)} events")
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
self.request_handler.broadcast_events(node, events=events)
|
|
190
|
+
|
|
191
|
+
except NodeNotFoundError:
|
|
192
|
+
logger.warning("Broadcast failed (node not found)")
|
|
193
|
+
|
|
194
|
+
except httpx.ConnectError:
|
|
195
|
+
logger.warning("Broadcast failed (couldn't connect)")
|
|
196
|
+
|
|
197
|
+
if requeue_on_fail:
|
|
198
|
+
for event in events:
|
|
199
|
+
self.push_event_to(event, node)
|
koi_net/network/graph.py
CHANGED
|
@@ -6,7 +6,6 @@ from rid_lib.ext import Cache
|
|
|
6
6
|
from rid_lib.types import KoiNetEdge, KoiNetNode
|
|
7
7
|
from ..identity import NodeIdentity
|
|
8
8
|
from ..protocol.edge import EdgeProfile, EdgeStatus
|
|
9
|
-
from ..protocol.node import NodeProfile
|
|
10
9
|
|
|
11
10
|
logger = logging.getLogger(__name__)
|
|
12
11
|
|
|
@@ -30,43 +29,27 @@ class NetworkGraph:
|
|
|
30
29
|
for rid in self.cache.list_rids():
|
|
31
30
|
if type(rid) == KoiNetNode:
|
|
32
31
|
self.dg.add_node(rid)
|
|
33
|
-
logger.debug(f"Added node {rid}")
|
|
32
|
+
logger.debug(f"Added node {rid!r}")
|
|
34
33
|
|
|
35
34
|
elif type(rid) == KoiNetEdge:
|
|
36
|
-
|
|
37
|
-
if not
|
|
35
|
+
edge_bundle = self.cache.read(rid)
|
|
36
|
+
if not edge_bundle:
|
|
38
37
|
logger.warning(f"Failed to load {rid!r}")
|
|
39
38
|
continue
|
|
39
|
+
edge_profile = edge_bundle.validate_contents(EdgeProfile)
|
|
40
40
|
self.dg.add_edge(edge_profile.source, edge_profile.target, rid=rid)
|
|
41
|
-
logger.debug(f"Added edge {rid} ({edge_profile.source} -> {edge_profile.target})")
|
|
41
|
+
logger.debug(f"Added edge {rid!r} ({edge_profile.source} -> {edge_profile.target})")
|
|
42
42
|
logger.debug("Done")
|
|
43
43
|
|
|
44
|
-
def
|
|
45
|
-
"""Returns
|
|
46
|
-
|
|
47
|
-
if bundle:
|
|
48
|
-
return bundle.validate_contents(NodeProfile)
|
|
49
|
-
|
|
50
|
-
def get_edge_profile(
|
|
51
|
-
self,
|
|
52
|
-
rid: KoiNetEdge | None = None,
|
|
53
|
-
source: KoiNetNode | None = None,
|
|
54
|
-
target: KoiNetNode | None = None,
|
|
55
|
-
) -> EdgeProfile | None:
|
|
56
|
-
"""Returns edge profile given its RID, or source and target node RIDs."""
|
|
57
|
-
if source and target:
|
|
58
|
-
if (source, target) not in self.dg.edges: return
|
|
44
|
+
def get_edge(self, source: KoiNetNode, target: KoiNetNode,) -> KoiNetEdge | None:
|
|
45
|
+
"""Returns edge RID given the RIDs of a source and target node."""
|
|
46
|
+
if (source, target) in self.dg.edges:
|
|
59
47
|
edge_data = self.dg.get_edge_data(source, target)
|
|
60
|
-
if
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
bundle = self.cache.read(rid)
|
|
67
|
-
if bundle:
|
|
68
|
-
return bundle.validate_contents(EdgeProfile)
|
|
69
|
-
|
|
48
|
+
if edge_data:
|
|
49
|
+
return edge_data.get("rid")
|
|
50
|
+
|
|
51
|
+
return None
|
|
52
|
+
|
|
70
53
|
def get_edges(
|
|
71
54
|
self,
|
|
72
55
|
direction: Literal["in", "out"] | None = None,
|
|
@@ -74,15 +57,15 @@ class NetworkGraph:
|
|
|
74
57
|
"""Returns edges this node belongs to.
|
|
75
58
|
|
|
76
59
|
All edges returned by default, specify `direction` to restrict to incoming or outgoing edges only."""
|
|
77
|
-
|
|
60
|
+
|
|
78
61
|
edges = []
|
|
79
62
|
if direction != "in" and self.dg.out_edges:
|
|
80
63
|
out_edges = self.dg.out_edges(self.identity.rid)
|
|
81
|
-
edges.extend(
|
|
64
|
+
edges.extend(out_edges)
|
|
82
65
|
|
|
83
66
|
if direction != "out" and self.dg.in_edges:
|
|
84
67
|
in_edges = self.dg.in_edges(self.identity.rid)
|
|
85
|
-
edges.extend(
|
|
68
|
+
edges.extend(in_edges)
|
|
86
69
|
|
|
87
70
|
edge_rids = []
|
|
88
71
|
for edge in edges:
|
|
@@ -104,13 +87,15 @@ class NetworkGraph:
|
|
|
104
87
|
|
|
105
88
|
All neighboring nodes returned by default, specify `direction` to restrict to neighbors connected by incoming or outgoing edges only."""
|
|
106
89
|
|
|
107
|
-
neighbors =
|
|
90
|
+
neighbors = set()
|
|
108
91
|
for edge_rid in self.get_edges(direction):
|
|
109
|
-
|
|
92
|
+
edge_bundle = self.cache.read(edge_rid)
|
|
110
93
|
|
|
111
|
-
if not
|
|
94
|
+
if not edge_bundle:
|
|
112
95
|
logger.warning(f"Failed to find edge {edge_rid!r} in cache")
|
|
113
96
|
continue
|
|
97
|
+
|
|
98
|
+
edge_profile = edge_bundle.validate_contents(EdgeProfile)
|
|
114
99
|
|
|
115
100
|
if status and edge_profile.status != status:
|
|
116
101
|
continue
|
|
@@ -119,9 +104,9 @@ class NetworkGraph:
|
|
|
119
104
|
continue
|
|
120
105
|
|
|
121
106
|
if edge_profile.target == self.identity.rid:
|
|
122
|
-
neighbors.
|
|
107
|
+
neighbors.add(edge_profile.source)
|
|
123
108
|
elif edge_profile.source == self.identity.rid:
|
|
124
|
-
neighbors.
|
|
109
|
+
neighbors.add(edge_profile.target)
|
|
125
110
|
|
|
126
111
|
return list(neighbors)
|
|
127
112
|
|