koi-net 1.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of koi-net might be problematic. Click here for more details.
- koi_net/__init__.py +1 -0
- koi_net/behaviors/handshaker.py +68 -0
- koi_net/behaviors/profile_monitor.py +23 -0
- koi_net/behaviors/sync_manager.py +68 -0
- koi_net/build/artifact.py +209 -0
- koi_net/build/assembler.py +60 -0
- koi_net/build/comp_order.py +6 -0
- koi_net/build/comp_type.py +7 -0
- koi_net/build/consts.py +18 -0
- koi_net/build/container.py +46 -0
- koi_net/cache.py +81 -0
- koi_net/config/core.py +113 -0
- koi_net/config/full_node.py +45 -0
- koi_net/config/loader.py +60 -0
- koi_net/config/partial_node.py +26 -0
- koi_net/config/proxy.py +20 -0
- koi_net/core.py +78 -0
- koi_net/effector.py +147 -0
- koi_net/entrypoints/__init__.py +2 -0
- koi_net/entrypoints/base.py +8 -0
- koi_net/entrypoints/poller.py +43 -0
- koi_net/entrypoints/server.py +85 -0
- koi_net/exceptions.py +107 -0
- koi_net/identity.py +20 -0
- koi_net/log_system.py +133 -0
- koi_net/network/__init__.py +0 -0
- koi_net/network/error_handler.py +63 -0
- koi_net/network/event_buffer.py +91 -0
- koi_net/network/event_queue.py +31 -0
- koi_net/network/graph.py +123 -0
- koi_net/network/request_handler.py +244 -0
- koi_net/network/resolver.py +152 -0
- koi_net/network/response_handler.py +130 -0
- koi_net/processor/__init__.py +0 -0
- koi_net/processor/context.py +36 -0
- koi_net/processor/handler.py +61 -0
- koi_net/processor/knowledge_handlers.py +302 -0
- koi_net/processor/knowledge_object.py +135 -0
- koi_net/processor/kobj_queue.py +51 -0
- koi_net/processor/pipeline.py +222 -0
- koi_net/protocol/__init__.py +0 -0
- koi_net/protocol/api_models.py +67 -0
- koi_net/protocol/consts.py +7 -0
- koi_net/protocol/edge.py +50 -0
- koi_net/protocol/envelope.py +65 -0
- koi_net/protocol/errors.py +24 -0
- koi_net/protocol/event.py +51 -0
- koi_net/protocol/model_map.py +62 -0
- koi_net/protocol/node.py +18 -0
- koi_net/protocol/secure.py +167 -0
- koi_net/secure_manager.py +115 -0
- koi_net/workers/__init__.py +2 -0
- koi_net/workers/base.py +26 -0
- koi_net/workers/event_worker.py +111 -0
- koi_net/workers/kobj_worker.py +51 -0
- koi_net-1.2.4.dist-info/METADATA +485 -0
- koi_net-1.2.4.dist-info/RECORD +59 -0
- koi_net-1.2.4.dist-info/WHEEL +4 -0
- koi_net-1.2.4.dist-info/licenses/LICENSE +21 -0
koi_net/exceptions.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""KOI-net library exceptions.
|
|
2
|
+
|
|
3
|
+
Exception hierarchy map:
|
|
4
|
+
- `KoiNetError`
|
|
5
|
+
- `BuildError`
|
|
6
|
+
- `RequestError`
|
|
7
|
+
- `ClientError`
|
|
8
|
+
- `SelfRequestError`
|
|
9
|
+
- `PartialNodeQueryError`
|
|
10
|
+
- `NodeNotFoundError`
|
|
11
|
+
- `TransportError`
|
|
12
|
+
- `ServerError`
|
|
13
|
+
- `RemoteProtocolError`
|
|
14
|
+
- `RemoteUnknownNodeError`
|
|
15
|
+
- `RemoteInvalidKeyError`
|
|
16
|
+
- `RemoteInvalidSignatureError`
|
|
17
|
+
- `RemoteInvalidTargetError`
|
|
18
|
+
- `ProtocolError`
|
|
19
|
+
- `UnknownNodeError`
|
|
20
|
+
- `InvalidKeyError`
|
|
21
|
+
- `InvalidSignatureError`
|
|
22
|
+
- `InvalidTargetError`
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# BASE EXCEPTION
|
|
27
|
+
class KoiNetError(Exception):
|
|
28
|
+
"""Base exception."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
# BUILD ERRORS
|
|
32
|
+
class BuildError(KoiNetError):
|
|
33
|
+
"""Raised when errors occur in build process."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
# NETWORK REQUEST ERRORS
|
|
37
|
+
class RequestError(KoiNetError):
|
|
38
|
+
"""Base for network request errors."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
# CLIENT ERRORS
|
|
42
|
+
class ClientError(RequestError):
|
|
43
|
+
"""Raised when this node makes an invalid request."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
class SelfRequestError(ClientError):
|
|
47
|
+
"""Raised when this node tries to request itself."""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
class PartialNodeQueryError(ClientError):
|
|
51
|
+
"""Raised when this node attempts to query a partial node."""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
class NodeNotFoundError(ClientError):
|
|
55
|
+
"""Raised when this node cannot find a node's URL."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
class TransportError(RequestError):
|
|
59
|
+
"""Raised when a transport error occurs during a request."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
# SERVER ERRORS
|
|
63
|
+
class ServerError(RequestError):
|
|
64
|
+
"""Raised when an server error occurs during a request."""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
# PROTOCOL ERRORS
|
|
68
|
+
class RemoteProtocolError(ServerError):
|
|
69
|
+
"""Base for protocol errors raised by peer node."""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
class RemoteUnknownNodeError(RemoteProtocolError):
|
|
73
|
+
"""Raised by peer node when this node is unknown."""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
class RemoteInvalidKeyError(RemoteProtocolError):
|
|
77
|
+
"""Raised by peer node when this node's public key doesn't match their RID."""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
class RemoteInvalidSignatureError(RemoteProtocolError):
|
|
81
|
+
"""Raised by peer node when this node's envelope signature is invalid."""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
class RemoteInvalidTargetError(RemoteProtocolError):
|
|
85
|
+
"""Raised by peer node when this node's envelope target is not it's RID."""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ProtocolError(KoiNetError):
|
|
90
|
+
"""Base for protocol errors raised by this node."""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
class UnknownNodeError(ProtocolError):
|
|
94
|
+
"""Raised when peer node is unknown."""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
class InvalidKeyError(ProtocolError):
|
|
98
|
+
"""Raised when peer node's public key doesn't match their RID."""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
class InvalidSignatureError(ProtocolError):
|
|
102
|
+
"""Raised when peer node's envelope signature is invalid."""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
class InvalidTargetError(ProtocolError):
|
|
106
|
+
"""Raised when peer node's target is not this node."""
|
|
107
|
+
pass
|
koi_net/identity.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from rid_lib.types import KoiNetNode
|
|
2
|
+
from .config.core import NodeConfig
|
|
3
|
+
from .protocol.node import NodeProfile
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NodeIdentity:
|
|
7
|
+
"""Represents a node's identity (RID, profile)."""
|
|
8
|
+
|
|
9
|
+
config: NodeConfig
|
|
10
|
+
|
|
11
|
+
def __init__(self, config: NodeConfig):
|
|
12
|
+
self.config = config
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def rid(self) -> KoiNetNode:
|
|
16
|
+
return self.config.koi_net.node_rid
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def profile(self) -> NodeProfile:
|
|
20
|
+
return self.config.koi_net.node_profile
|
koi_net/log_system.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Configures and initializes the logging system."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import logging
|
|
5
|
+
from logging.handlers import RotatingFileHandler
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
import structlog
|
|
9
|
+
import colorama
|
|
10
|
+
|
|
11
|
+
LOG_FILE_PATH = "log.ndjson"
|
|
12
|
+
MAX_LOG_SIZE = 10 * 1024 ** 2 # 10MB
|
|
13
|
+
MAX_LOG_BACKUPS = 5
|
|
14
|
+
LOG_ENCODING = "utf-8"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
shared_log_processors = [
|
|
18
|
+
structlog.stdlib.add_logger_name,
|
|
19
|
+
structlog.stdlib.add_log_level,
|
|
20
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
21
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
22
|
+
structlog.processors.UnicodeDecoder(),
|
|
23
|
+
structlog.processors.CallsiteParameterAdder({
|
|
24
|
+
structlog.processors.CallsiteParameter.MODULE,
|
|
25
|
+
structlog.processors.CallsiteParameter.FUNC_NAME
|
|
26
|
+
}),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
console_renderer = structlog.dev.ConsoleRenderer(
|
|
30
|
+
columns=[
|
|
31
|
+
# Render the timestamp without the key name in yellow.
|
|
32
|
+
structlog.dev.Column(
|
|
33
|
+
"timestamp",
|
|
34
|
+
structlog.dev.KeyValueColumnFormatter(
|
|
35
|
+
key_style=None,
|
|
36
|
+
value_style=colorama.Style.DIM,
|
|
37
|
+
reset_style=colorama.Style.RESET_ALL,
|
|
38
|
+
value_repr=lambda t: datetime.fromisoformat(t).strftime("%Y-%m-%d %H:%M:%S"),
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
structlog.dev.Column(
|
|
42
|
+
"level",
|
|
43
|
+
structlog.dev.LogLevelColumnFormatter(
|
|
44
|
+
level_styles={
|
|
45
|
+
level: colorama.Style.BRIGHT + color
|
|
46
|
+
for level, color in {
|
|
47
|
+
"critical": colorama.Fore.RED,
|
|
48
|
+
"exception": colorama.Fore.RED,
|
|
49
|
+
"error": colorama.Fore.RED,
|
|
50
|
+
"warn": colorama.Fore.YELLOW,
|
|
51
|
+
"warning": colorama.Fore.YELLOW,
|
|
52
|
+
"info": colorama.Fore.GREEN,
|
|
53
|
+
"debug": colorama.Fore.GREEN,
|
|
54
|
+
"notset": colorama.Back.RED,
|
|
55
|
+
}.items()
|
|
56
|
+
},
|
|
57
|
+
reset_style=colorama.Style.RESET_ALL,
|
|
58
|
+
width=9
|
|
59
|
+
)
|
|
60
|
+
),
|
|
61
|
+
# Render the event without the key name in bright magenta.
|
|
62
|
+
|
|
63
|
+
# Default formatter for all keys not explicitly mentioned. The key is
|
|
64
|
+
# cyan, the value is green.
|
|
65
|
+
structlog.dev.Column(
|
|
66
|
+
"path",
|
|
67
|
+
structlog.dev.KeyValueColumnFormatter(
|
|
68
|
+
key_style=None,
|
|
69
|
+
value_style=colorama.Fore.MAGENTA,
|
|
70
|
+
reset_style=colorama.Style.RESET_ALL,
|
|
71
|
+
value_repr=str,
|
|
72
|
+
width=30
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
structlog.dev.Column(
|
|
76
|
+
"event",
|
|
77
|
+
structlog.dev.KeyValueColumnFormatter(
|
|
78
|
+
key_style=None,
|
|
79
|
+
value_style=colorama.Fore.WHITE,
|
|
80
|
+
reset_style=colorama.Style.RESET_ALL,
|
|
81
|
+
value_repr=str,
|
|
82
|
+
width=30
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
structlog.dev.Column(
|
|
86
|
+
"",
|
|
87
|
+
structlog.dev.KeyValueColumnFormatter(
|
|
88
|
+
key_style=colorama.Fore.BLUE,
|
|
89
|
+
value_style=colorama.Fore.GREEN,
|
|
90
|
+
reset_style=colorama.Style.RESET_ALL,
|
|
91
|
+
value_repr=str,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
99
|
+
console_handler.setFormatter(
|
|
100
|
+
structlog.stdlib.ProcessorFormatter(
|
|
101
|
+
processor=console_renderer,
|
|
102
|
+
foreign_pre_chain=shared_log_processors
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
file_handler = RotatingFileHandler(
|
|
107
|
+
filename=LOG_FILE_PATH,
|
|
108
|
+
maxBytes=MAX_LOG_SIZE,
|
|
109
|
+
backupCount=MAX_LOG_BACKUPS,
|
|
110
|
+
encoding=LOG_ENCODING
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
file_handler.setFormatter(
|
|
114
|
+
structlog.stdlib.ProcessorFormatter(
|
|
115
|
+
processor=structlog.processors.JSONRenderer(),
|
|
116
|
+
foreign_pre_chain=shared_log_processors
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
logging.basicConfig(
|
|
121
|
+
level=logging.DEBUG,
|
|
122
|
+
handlers=[
|
|
123
|
+
file_handler,
|
|
124
|
+
console_handler
|
|
125
|
+
])
|
|
126
|
+
|
|
127
|
+
structlog.configure(
|
|
128
|
+
processors=shared_log_processors + [
|
|
129
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
|
|
130
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
131
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
132
|
+
cache_logger_on_first_use=True,
|
|
133
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import structlog
|
|
2
|
+
from rid_lib.types import KoiNetNode
|
|
3
|
+
from ..behaviors.handshaker import Handshaker
|
|
4
|
+
from ..protocol.errors import ErrorType
|
|
5
|
+
from ..protocol.event import EventType
|
|
6
|
+
from ..processor.kobj_queue import KobjQueue
|
|
7
|
+
|
|
8
|
+
log = structlog.stdlib.get_logger()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ErrorHandler:
|
|
12
|
+
"""Handles network and protocol errors that may occur during requests."""
|
|
13
|
+
timeout_counter: dict[KoiNetNode, int]
|
|
14
|
+
kobj_queue: KobjQueue
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
kobj_queue: KobjQueue,
|
|
19
|
+
handshaker: Handshaker
|
|
20
|
+
):
|
|
21
|
+
self.kobj_queue = kobj_queue
|
|
22
|
+
self.handshaker = handshaker
|
|
23
|
+
self.timeout_counter = {}
|
|
24
|
+
|
|
25
|
+
def reset_timeout_counter(self, node: KoiNetNode):
|
|
26
|
+
"""Reset's a node timeout counter to zero."""
|
|
27
|
+
self.timeout_counter[node] = 0
|
|
28
|
+
|
|
29
|
+
def handle_connection_error(self, node: KoiNetNode):
|
|
30
|
+
"""Drops nodes after timing out three times.
|
|
31
|
+
|
|
32
|
+
TODO: Need a better heuristic for network state. For example, if
|
|
33
|
+
a node lost connection to the internet, it would quickly forget
|
|
34
|
+
all other nodes.
|
|
35
|
+
"""
|
|
36
|
+
self.timeout_counter.setdefault(node, 0)
|
|
37
|
+
self.timeout_counter[node] += 1
|
|
38
|
+
|
|
39
|
+
log.debug(f"{node} has timed out {self.timeout_counter[node]} time(s)")
|
|
40
|
+
|
|
41
|
+
if self.timeout_counter[node] > 3:
|
|
42
|
+
log.debug(f"Exceeded time out limit, forgetting node")
|
|
43
|
+
self.kobj_queue.push(rid=node, event_type=EventType.FORGET)
|
|
44
|
+
|
|
45
|
+
def handle_protocol_error(
|
|
46
|
+
self,
|
|
47
|
+
error_type: ErrorType,
|
|
48
|
+
node: KoiNetNode
|
|
49
|
+
):
|
|
50
|
+
"""Handles protocol errors that occur during network requests.
|
|
51
|
+
|
|
52
|
+
Attempts handshake when this node is unknown to target.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
log.info(f"Handling protocol error {error_type} for node {node!r}")
|
|
56
|
+
match error_type:
|
|
57
|
+
case ErrorType.UnknownNode:
|
|
58
|
+
log.info("Peer doesn't know me, attempting handshake...")
|
|
59
|
+
self.handshaker.handshake_with(node)
|
|
60
|
+
|
|
61
|
+
case ErrorType.InvalidKey: ...
|
|
62
|
+
case ErrorType.InvalidSignature: ...
|
|
63
|
+
case ErrorType.InvalidTarget: ...
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from typing import Generator
|
|
4
|
+
from rid_lib.types import KoiNetNode
|
|
5
|
+
|
|
6
|
+
from ..protocol.event import Event
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EventBuffer:
|
|
10
|
+
"""Stores outgoing events sent to other nodes."""
|
|
11
|
+
|
|
12
|
+
buffers: dict[KoiNetNode, list[Event]]
|
|
13
|
+
start_time: dict[KoiNetNode, float]
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.buffers = {}
|
|
17
|
+
self.start_time = {}
|
|
18
|
+
|
|
19
|
+
def push(self, node: KoiNetNode, event: Event):
|
|
20
|
+
"""Pushes event to specified node.
|
|
21
|
+
|
|
22
|
+
Sets start time to now if unset.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
self.start_time.setdefault(node, time.time())
|
|
26
|
+
|
|
27
|
+
event_buf = self.buffers.setdefault(node, [])
|
|
28
|
+
event_buf.append(event)
|
|
29
|
+
|
|
30
|
+
def buf_len(self, node: KoiNetNode):
|
|
31
|
+
"""Returns the length of a node's event buffer."""
|
|
32
|
+
return len(self.buffers.get(node, []))
|
|
33
|
+
|
|
34
|
+
def flush(self, node: KoiNetNode, limit: int = 0) -> list[Event]:
|
|
35
|
+
"""Flushes all (or limit) events for a node.
|
|
36
|
+
|
|
37
|
+
Resets start time.
|
|
38
|
+
"""
|
|
39
|
+
self.start_time.pop(node, None)
|
|
40
|
+
|
|
41
|
+
if node not in self.buffers:
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
event_buf = self.buffers[node]
|
|
45
|
+
|
|
46
|
+
if limit and len(event_buf) > limit:
|
|
47
|
+
flushed_events = event_buf[:limit]
|
|
48
|
+
self.buffers[node] = event_buf[limit:]
|
|
49
|
+
else:
|
|
50
|
+
flushed_events = event_buf.copy()
|
|
51
|
+
del self.buffers[node]
|
|
52
|
+
|
|
53
|
+
return flushed_events
|
|
54
|
+
|
|
55
|
+
@contextmanager
|
|
56
|
+
def safe_flush(
|
|
57
|
+
self,
|
|
58
|
+
node: KoiNetNode,
|
|
59
|
+
limit: int = 0,
|
|
60
|
+
force_flush: bool = False
|
|
61
|
+
) -> Generator[list[Event], None, None]:
|
|
62
|
+
"""Context managed safe flush, only commits on successful exit.
|
|
63
|
+
|
|
64
|
+
Exceptions will result in buffer rollback to the previous state.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
self.start_time.pop(node, None)
|
|
68
|
+
|
|
69
|
+
if node not in self.buffers:
|
|
70
|
+
yield []
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
event_buf = self.buffers[node].copy()
|
|
74
|
+
in_place = limit and len(event_buf) > limit
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
if in_place:
|
|
78
|
+
yield event_buf[:limit]
|
|
79
|
+
self.buffers[node] = event_buf[limit:]
|
|
80
|
+
else:
|
|
81
|
+
yield event_buf.copy()
|
|
82
|
+
self.buffers.pop(node, None)
|
|
83
|
+
|
|
84
|
+
except Exception:
|
|
85
|
+
# if force, flushes buffers and reraises exception
|
|
86
|
+
if force_flush:
|
|
87
|
+
if in_place:
|
|
88
|
+
self.buffers[node] = event_buf[limit:]
|
|
89
|
+
else:
|
|
90
|
+
self.buffers.pop(node, None)
|
|
91
|
+
raise
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import structlog
|
|
2
|
+
from queue import Queue
|
|
3
|
+
|
|
4
|
+
from rid_lib.types import KoiNetNode
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from ..protocol.event import Event
|
|
8
|
+
|
|
9
|
+
log = structlog.stdlib.get_logger()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class QueuedEvent(BaseModel):
|
|
13
|
+
event: Event
|
|
14
|
+
target: KoiNetNode
|
|
15
|
+
|
|
16
|
+
class EventQueue:
|
|
17
|
+
"""Queue for outgoing network events."""
|
|
18
|
+
q: Queue[QueuedEvent]
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.q = Queue()
|
|
22
|
+
|
|
23
|
+
def push(self, event: Event, target: KoiNetNode):
|
|
24
|
+
"""Pushes event to queue of specified node.
|
|
25
|
+
|
|
26
|
+
Event will be sent to webhook or poll queue by the event worker
|
|
27
|
+
depending on the node type and edge type of the specified node.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
self.q.put(QueuedEvent(target=target, event=event))
|
|
31
|
+
|
koi_net/network/graph.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import structlog
|
|
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
|
+
|
|
10
|
+
log = structlog.stdlib.get_logger()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NetworkGraph:
|
|
14
|
+
"""Graph functions for this node's view of its network."""
|
|
15
|
+
|
|
16
|
+
cache: Cache
|
|
17
|
+
identity: NodeIdentity
|
|
18
|
+
dg: nx.DiGraph
|
|
19
|
+
|
|
20
|
+
def __init__(self, cache: Cache, identity: NodeIdentity):
|
|
21
|
+
self.cache = cache
|
|
22
|
+
self.dg = nx.DiGraph()
|
|
23
|
+
self.identity = identity
|
|
24
|
+
|
|
25
|
+
def start(self):
|
|
26
|
+
self.generate()
|
|
27
|
+
|
|
28
|
+
def generate(self):
|
|
29
|
+
"""Generates directed graph from cached KOI nodes and edges."""
|
|
30
|
+
log.debug("Generating network graph")
|
|
31
|
+
self.dg.clear()
|
|
32
|
+
for rid in self.cache.list_rids():
|
|
33
|
+
if type(rid) == KoiNetNode:
|
|
34
|
+
self.dg.add_node(rid)
|
|
35
|
+
log.debug(f"Added node {rid!r}")
|
|
36
|
+
|
|
37
|
+
elif type(rid) == KoiNetEdge:
|
|
38
|
+
edge_bundle = self.cache.read(rid)
|
|
39
|
+
if not edge_bundle:
|
|
40
|
+
log.warning(f"Failed to load {rid!r}")
|
|
41
|
+
continue
|
|
42
|
+
edge_profile = edge_bundle.validate_contents(EdgeProfile)
|
|
43
|
+
self.dg.add_edge(edge_profile.source, edge_profile.target, rid=rid)
|
|
44
|
+
log.debug(f"Added edge {rid!r} ({edge_profile.source} -> {edge_profile.target})")
|
|
45
|
+
log.debug("Done")
|
|
46
|
+
|
|
47
|
+
def get_edge(
|
|
48
|
+
self,
|
|
49
|
+
source: KoiNetNode,
|
|
50
|
+
target: KoiNetNode
|
|
51
|
+
) -> KoiNetEdge | None:
|
|
52
|
+
"""Returns edge RID given the RIDs of a source and target node."""
|
|
53
|
+
if (source, target) in self.dg.edges:
|
|
54
|
+
edge_data = self.dg.get_edge_data(source, target)
|
|
55
|
+
if edge_data:
|
|
56
|
+
return edge_data.get("rid")
|
|
57
|
+
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def get_edges(
|
|
61
|
+
self,
|
|
62
|
+
direction: Literal["in", "out"] | None = None,
|
|
63
|
+
) -> list[KoiNetEdge]:
|
|
64
|
+
"""Returns edges this node belongs to.
|
|
65
|
+
|
|
66
|
+
All edges returned by default, specify `direction` to restrict
|
|
67
|
+
to incoming or outgoing edges only.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
edges = []
|
|
71
|
+
if (direction is None or direction == "out") and self.dg.out_edges:
|
|
72
|
+
out_edges = self.dg.out_edges(self.identity.rid)
|
|
73
|
+
edges.extend(out_edges)
|
|
74
|
+
|
|
75
|
+
if (direction is None or direction == "in") and self.dg.in_edges:
|
|
76
|
+
in_edges = self.dg.in_edges(self.identity.rid)
|
|
77
|
+
edges.extend(in_edges)
|
|
78
|
+
|
|
79
|
+
edge_rids = []
|
|
80
|
+
for edge in edges:
|
|
81
|
+
edge_data = self.dg.get_edge_data(*edge)
|
|
82
|
+
if not edge_data: continue
|
|
83
|
+
edge_rid = edge_data.get("rid")
|
|
84
|
+
if not edge_rid: continue
|
|
85
|
+
edge_rids.append(edge_rid)
|
|
86
|
+
|
|
87
|
+
return edge_rids
|
|
88
|
+
|
|
89
|
+
def get_neighbors(
|
|
90
|
+
self,
|
|
91
|
+
direction: Literal["in", "out"] | None = None,
|
|
92
|
+
status: EdgeStatus | None = None,
|
|
93
|
+
allowed_type: RIDType | None = None
|
|
94
|
+
) -> list[KoiNetNode]:
|
|
95
|
+
"""Returns neighboring nodes this node shares an edge with.
|
|
96
|
+
|
|
97
|
+
All neighboring nodes returned by default, specify `direction`
|
|
98
|
+
to restrict to neighbors connected by incoming or outgoing edges
|
|
99
|
+
only.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
neighbors = set()
|
|
103
|
+
for edge_rid in self.get_edges(direction):
|
|
104
|
+
edge_bundle = self.cache.read(edge_rid)
|
|
105
|
+
|
|
106
|
+
if not edge_bundle:
|
|
107
|
+
log.warning(f"Failed to find edge {edge_rid!r} in cache")
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
edge_profile = edge_bundle.validate_contents(EdgeProfile)
|
|
111
|
+
|
|
112
|
+
if status and edge_profile.status != status:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
if allowed_type and allowed_type not in edge_profile.rid_types:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if edge_profile.target == self.identity.rid:
|
|
119
|
+
neighbors.add(edge_profile.source)
|
|
120
|
+
elif edge_profile.source == self.identity.rid:
|
|
121
|
+
neighbors.add(edge_profile.target)
|
|
122
|
+
|
|
123
|
+
return list(neighbors)
|