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.

Files changed (59) hide show
  1. koi_net/__init__.py +1 -0
  2. koi_net/behaviors/handshaker.py +68 -0
  3. koi_net/behaviors/profile_monitor.py +23 -0
  4. koi_net/behaviors/sync_manager.py +68 -0
  5. koi_net/build/artifact.py +209 -0
  6. koi_net/build/assembler.py +60 -0
  7. koi_net/build/comp_order.py +6 -0
  8. koi_net/build/comp_type.py +7 -0
  9. koi_net/build/consts.py +18 -0
  10. koi_net/build/container.py +46 -0
  11. koi_net/cache.py +81 -0
  12. koi_net/config/core.py +113 -0
  13. koi_net/config/full_node.py +45 -0
  14. koi_net/config/loader.py +60 -0
  15. koi_net/config/partial_node.py +26 -0
  16. koi_net/config/proxy.py +20 -0
  17. koi_net/core.py +78 -0
  18. koi_net/effector.py +147 -0
  19. koi_net/entrypoints/__init__.py +2 -0
  20. koi_net/entrypoints/base.py +8 -0
  21. koi_net/entrypoints/poller.py +43 -0
  22. koi_net/entrypoints/server.py +85 -0
  23. koi_net/exceptions.py +107 -0
  24. koi_net/identity.py +20 -0
  25. koi_net/log_system.py +133 -0
  26. koi_net/network/__init__.py +0 -0
  27. koi_net/network/error_handler.py +63 -0
  28. koi_net/network/event_buffer.py +91 -0
  29. koi_net/network/event_queue.py +31 -0
  30. koi_net/network/graph.py +123 -0
  31. koi_net/network/request_handler.py +244 -0
  32. koi_net/network/resolver.py +152 -0
  33. koi_net/network/response_handler.py +130 -0
  34. koi_net/processor/__init__.py +0 -0
  35. koi_net/processor/context.py +36 -0
  36. koi_net/processor/handler.py +61 -0
  37. koi_net/processor/knowledge_handlers.py +302 -0
  38. koi_net/processor/knowledge_object.py +135 -0
  39. koi_net/processor/kobj_queue.py +51 -0
  40. koi_net/processor/pipeline.py +222 -0
  41. koi_net/protocol/__init__.py +0 -0
  42. koi_net/protocol/api_models.py +67 -0
  43. koi_net/protocol/consts.py +7 -0
  44. koi_net/protocol/edge.py +50 -0
  45. koi_net/protocol/envelope.py +65 -0
  46. koi_net/protocol/errors.py +24 -0
  47. koi_net/protocol/event.py +51 -0
  48. koi_net/protocol/model_map.py +62 -0
  49. koi_net/protocol/node.py +18 -0
  50. koi_net/protocol/secure.py +167 -0
  51. koi_net/secure_manager.py +115 -0
  52. koi_net/workers/__init__.py +2 -0
  53. koi_net/workers/base.py +26 -0
  54. koi_net/workers/event_worker.py +111 -0
  55. koi_net/workers/kobj_worker.py +51 -0
  56. koi_net-1.2.4.dist-info/METADATA +485 -0
  57. koi_net-1.2.4.dist-info/RECORD +59 -0
  58. koi_net-1.2.4.dist-info/WHEEL +4 -0
  59. 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
+
@@ -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)