koi-net 1.0.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.

@@ -0,0 +1,149 @@
1
+ import logging
2
+ import httpx
3
+ from rid_lib import RID
4
+ from rid_lib.ext import Cache
5
+ from rid_lib.types.koi_net_node import KoiNetNode
6
+ from ..protocol.api_models import (
7
+ RidsPayload,
8
+ ManifestsPayload,
9
+ BundlesPayload,
10
+ EventsPayload,
11
+ FetchRids,
12
+ FetchManifests,
13
+ FetchBundles,
14
+ PollEvents,
15
+ RequestModels,
16
+ ResponseModels
17
+ )
18
+ from ..protocol.consts import (
19
+ BROADCAST_EVENTS_PATH,
20
+ POLL_EVENTS_PATH,
21
+ FETCH_RIDS_PATH,
22
+ FETCH_MANIFESTS_PATH,
23
+ FETCH_BUNDLES_PATH
24
+ )
25
+ from ..protocol.node import NodeType
26
+ from .graph import NetworkGraph
27
+
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class RequestHandler:
33
+ """Handles making requests to other KOI nodes."""
34
+
35
+ cache: Cache
36
+ graph: NetworkGraph
37
+
38
+ def __init__(self, cache: Cache, graph: NetworkGraph):
39
+ self.cache = cache
40
+ self.graph = graph
41
+
42
+ def make_request(
43
+ self,
44
+ url: str,
45
+ request: RequestModels,
46
+ response_model: type[ResponseModels] | None = None
47
+ ) -> ResponseModels | None:
48
+ logger.debug(f"Making request to {url}")
49
+ resp = httpx.post(
50
+ url=url,
51
+ data=request.model_dump_json()
52
+ )
53
+ if response_model:
54
+ return response_model.model_validate_json(resp.text)
55
+
56
+ def get_url(self, node_rid: KoiNetNode, url: str) -> str:
57
+ """Retrieves URL of a node, or returns provided URL."""
58
+
59
+ if not node_rid and not url:
60
+ raise ValueError("One of 'node_rid' and 'url' must be provided")
61
+
62
+ if node_rid:
63
+ node_profile = self.graph.get_node_profile(node_rid)
64
+ if not node_profile:
65
+ raise Exception("Node not found")
66
+ if node_profile.node_type != NodeType.FULL:
67
+ raise Exception("Can't query partial node")
68
+ logger.debug(f"Resolved {node_rid!r} to {node_profile.base_url}")
69
+ return node_profile.base_url
70
+ else:
71
+ return url
72
+
73
+ def broadcast_events(
74
+ self,
75
+ node: RID = None,
76
+ url: str = None,
77
+ req: EventsPayload | None = None,
78
+ **kwargs
79
+ ) -> None:
80
+ """See protocol.api_models.EventsPayload for available kwargs."""
81
+ request = req or EventsPayload.model_validate(kwargs)
82
+ self.make_request(
83
+ self.get_url(node, url) + BROADCAST_EVENTS_PATH, request
84
+ )
85
+ logger.info(f"Broadcasted {len(request.events)} event(s) to {node or url!r}")
86
+
87
+ def poll_events(
88
+ self,
89
+ node: RID = None,
90
+ url: str = None,
91
+ req: PollEvents | None = None,
92
+ **kwargs
93
+ ) -> EventsPayload:
94
+ """See protocol.api_models.PollEvents for available kwargs."""
95
+ request = req or PollEvents.model_validate(kwargs)
96
+ resp = self.make_request(
97
+ self.get_url(node, url) + POLL_EVENTS_PATH, request,
98
+ response_model=EventsPayload
99
+ )
100
+ logger.info(f"Polled {len(resp.events)} events from {node or url!r}")
101
+ return resp
102
+
103
+ def fetch_rids(
104
+ self,
105
+ node: RID = None,
106
+ url: str = None,
107
+ req: FetchRids | None = None,
108
+ **kwargs
109
+ ) -> RidsPayload:
110
+ """See protocol.api_models.FetchRids for available kwargs."""
111
+ request = req or FetchRids.model_validate(kwargs)
112
+ resp = self.make_request(
113
+ self.get_url(node, url) + FETCH_RIDS_PATH, request,
114
+ response_model=RidsPayload
115
+ )
116
+ logger.info(f"Fetched {len(resp.rids)} RID(s) from {node or url!r}")
117
+ return resp
118
+
119
+ def fetch_manifests(
120
+ self,
121
+ node: RID = None,
122
+ url: str = None,
123
+ req: FetchManifests | None = None,
124
+ **kwargs
125
+ ) -> ManifestsPayload:
126
+ """See protocol.api_models.FetchManifests for available kwargs."""
127
+ request = req or FetchManifests.model_validate(kwargs)
128
+ resp = self.make_request(
129
+ self.get_url(node, url) + FETCH_MANIFESTS_PATH, request,
130
+ response_model=ManifestsPayload
131
+ )
132
+ logger.info(f"Fetched {len(resp.manifests)} manifest(s) from {node or url!r}")
133
+ return resp
134
+
135
+ def fetch_bundles(
136
+ self,
137
+ node: RID = None,
138
+ url: str = None,
139
+ req: FetchBundles | None = None,
140
+ **kwargs
141
+ ) -> BundlesPayload:
142
+ """See protocol.api_models.FetchBundles for available kwargs."""
143
+ request = req or FetchBundles.model_validate(kwargs)
144
+ resp = self.make_request(
145
+ self.get_url(node, url) + FETCH_BUNDLES_PATH, request,
146
+ response_model=BundlesPayload
147
+ )
148
+ logger.info(f"Fetched {len(resp.bundles)} bundle(s) from {node or url!r}")
149
+ return resp
@@ -0,0 +1,59 @@
1
+ import logging
2
+ from rid_lib import RID
3
+ from rid_lib.ext import Manifest, Cache
4
+ from rid_lib.ext.bundle import Bundle
5
+ from ..protocol.api_models import (
6
+ RidsPayload,
7
+ ManifestsPayload,
8
+ BundlesPayload,
9
+ FetchRids,
10
+ FetchManifests,
11
+ FetchBundles,
12
+ )
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ResponseHandler:
18
+ """Handles generating responses to requests from other KOI nodes."""
19
+
20
+ cache: Cache
21
+
22
+ def __init__(self, cache: Cache):
23
+ self.cache = cache
24
+
25
+ def fetch_rids(self, req: FetchRids) -> RidsPayload:
26
+ logger.info(f"Request to fetch rids, allowed types {req.rid_types}")
27
+ rids = self.cache.list_rids(req.rid_types)
28
+
29
+ return RidsPayload(rids=rids)
30
+
31
+ def fetch_manifests(self, req: FetchManifests) -> ManifestsPayload:
32
+ logger.info(f"Request to fetch manifests, allowed types {req.rid_types}, rids {req.rids}")
33
+
34
+ manifests: list[Manifest] = []
35
+ not_found: list[RID] = []
36
+
37
+ for rid in (req.rids or self.cache.list_rids(req.rid_types)):
38
+ bundle = self.cache.read(rid)
39
+ if bundle:
40
+ manifests.append(bundle.manifest)
41
+ else:
42
+ not_found.append(rid)
43
+
44
+ return ManifestsPayload(manifests=manifests, not_found=not_found)
45
+
46
+ def fetch_bundles(self, req: FetchBundles) -> BundlesPayload:
47
+ logger.info(f"Request to fetch bundles, requested rids {req.rids}")
48
+
49
+ bundles: list[Bundle] = []
50
+ not_found: list[RID] = []
51
+
52
+ for rid in req.rids:
53
+ bundle = self.cache.read(rid)
54
+ if bundle:
55
+ bundles.append(bundle)
56
+ else:
57
+ not_found.append(rid)
58
+
59
+ return BundlesPayload(bundles=bundles, not_found=not_found)
@@ -0,0 +1 @@
1
+ from .interface import ProcessorInterface
@@ -0,0 +1,220 @@
1
+ """Provides implementations of default knowledge handlers."""
2
+
3
+ import logging
4
+ from rid_lib.ext.bundle import Bundle
5
+ from rid_lib.types import KoiNetNode, KoiNetEdge
6
+ from koi_net.protocol.node import NodeType
7
+ from .interface import ProcessorInterface
8
+ from .handler import KnowledgeHandler, HandlerType, STOP_CHAIN
9
+ from .knowledge_object import KnowledgeObject, KnowledgeSource
10
+ from ..protocol.event import Event, EventType
11
+ from ..protocol.edge import EdgeProfile, EdgeStatus, EdgeType
12
+ from ..protocol.node import NodeProfile
13
+ from ..protocol.helpers import generate_edge_bundle
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # RID handlers
18
+
19
+ @KnowledgeHandler.create(HandlerType.RID)
20
+ def basic_rid_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
21
+ """Default RID handler.
22
+
23
+ Blocks external events about this node. Allows `FORGET` events if RID is known to this node.
24
+ """
25
+ if (kobj.rid == processor.identity.rid and
26
+ kobj.source == KnowledgeSource.External):
27
+ logger.debug("Don't let anyone else tell me who I am!")
28
+ return STOP_CHAIN
29
+
30
+ if kobj.event_type == EventType.FORGET:
31
+ kobj.normalized_event_type = EventType.FORGET
32
+ return kobj
33
+
34
+ # Manifest handlers
35
+
36
+ @KnowledgeHandler.create(HandlerType.Manifest)
37
+ def basic_manifest_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
38
+ """Default manifest handler.
39
+
40
+ Blocks manifests with the same hash, or aren't newer than the cached version. Sets the normalized event type to `NEW` or `UPDATE` depending on whether the RID was previously known to this node.
41
+ """
42
+ prev_bundle = processor.cache.read(kobj.rid)
43
+
44
+ if prev_bundle:
45
+ if kobj.manifest.sha256_hash == prev_bundle.manifest.sha256_hash:
46
+ logger.debug("Hash of incoming manifest is same as existing knowledge, ignoring")
47
+ return STOP_CHAIN
48
+ if kobj.manifest.timestamp <= prev_bundle.manifest.timestamp:
49
+ logger.debug("Timestamp of incoming manifest is the same or older than existing knowledge, ignoring")
50
+ return STOP_CHAIN
51
+
52
+ logger.debug("RID previously known to me, labeling as 'UPDATE'")
53
+ kobj.normalized_event_type = EventType.UPDATE
54
+
55
+ else:
56
+ logger.debug("RID previously unknown to me, labeling as 'NEW'")
57
+ kobj.normalized_event_type = EventType.NEW
58
+
59
+ return kobj
60
+
61
+
62
+ # Bundle handlers
63
+
64
+ @KnowledgeHandler.create(
65
+ handler_type=HandlerType.Bundle,
66
+ rid_types=[KoiNetEdge],
67
+ source=KnowledgeSource.External,
68
+ event_types=[EventType.NEW, EventType.UPDATE])
69
+ def edge_negotiation_handler(processor: ProcessorInterface, kobj: KnowledgeObject):
70
+ """Handles basic edge negotiation process.
71
+
72
+ Automatically approves proposed edges if they request RID types this node can provide (or KOI nodes/edges). Validates the edge type is allowed for the node type (partial nodes cannot use webhooks). If edge is invalid, a `FORGET` event is sent to the other node.
73
+ """
74
+
75
+ edge_profile = EdgeProfile.model_validate(kobj.contents)
76
+
77
+ # indicates peer subscribing to me
78
+ if edge_profile.source == processor.identity.rid:
79
+ if edge_profile.status != EdgeStatus.PROPOSED:
80
+ return
81
+
82
+ logger.debug("Handling edge negotiation")
83
+
84
+ peer_rid = edge_profile.target
85
+ peer_profile = processor.network.graph.get_node_profile(peer_rid)
86
+
87
+ if not peer_profile:
88
+ logger.warning(f"Peer {peer_rid} unknown to me")
89
+ return STOP_CHAIN
90
+
91
+ # explicitly provided event RID types and (self) node + edge objects
92
+ provided_events = (
93
+ *processor.identity.profile.provides.event,
94
+ KoiNetNode, KoiNetEdge
95
+ )
96
+
97
+
98
+ abort = False
99
+ if (edge_profile.edge_type == EdgeType.WEBHOOK and
100
+ peer_profile.node_type == NodeType.PARTIAL):
101
+ logger.debug("Partial nodes cannot use webhooks")
102
+ abort = True
103
+
104
+ if not set(edge_profile.rid_types).issubset(provided_events):
105
+ logger.debug("Requested RID types not provided by this node")
106
+ abort = True
107
+
108
+ if abort:
109
+ event = Event.from_rid(EventType.FORGET, kobj.rid)
110
+ processor.network.push_event_to(event, peer_rid, flush=True)
111
+ return STOP_CHAIN
112
+
113
+ else:
114
+ # approve edge profile
115
+ logger.debug("Approving proposed edge")
116
+ edge_profile.status = EdgeStatus.APPROVED
117
+ updated_bundle = Bundle.generate(kobj.rid, edge_profile.model_dump())
118
+
119
+ processor.handle(bundle=updated_bundle, event_type=EventType.UPDATE)
120
+ return
121
+
122
+ elif edge_profile.target == processor.identity.rid:
123
+ if edge_profile.status == EdgeStatus.APPROVED:
124
+ logger.debug("Edge approved by other node!")
125
+
126
+
127
+ # Network handlers
128
+
129
+ @KnowledgeHandler.create(HandlerType.Network, rid_types=[KoiNetNode])
130
+ def coordinator_contact(processor: ProcessorInterface, kobj: KnowledgeObject):
131
+ node_profile = kobj.bundle.validate_contents(NodeProfile)
132
+
133
+ # looking for event provider of nodes
134
+ if KoiNetNode not in node_profile.provides.event:
135
+ return
136
+
137
+ # prevents coordinators from attempting to form a self loop
138
+ if kobj.rid == processor.identity.rid:
139
+ return
140
+
141
+ # already have an edge established
142
+ if processor.network.graph.get_edge_profile(
143
+ source=kobj.rid,
144
+ target=processor.identity.rid,
145
+ ) is not None:
146
+ return
147
+
148
+ logger.info("Identified a coordinator!")
149
+ logger.info("Proposing new edge")
150
+
151
+ if processor.identity.profile.node_type == NodeType.FULL:
152
+ edge_type = EdgeType.WEBHOOK
153
+ else:
154
+ edge_type = EdgeType.POLL
155
+
156
+ # queued for processing
157
+ processor.handle(bundle=generate_edge_bundle(
158
+ source=kobj.rid,
159
+ target=processor.identity.rid,
160
+ edge_type=edge_type,
161
+ rid_types=[KoiNetNode]
162
+ ))
163
+
164
+ logger.info("Catching up on network state")
165
+
166
+ payload = processor.network.request_handler.fetch_rids(
167
+ node=kobj.rid,
168
+ rid_types=[KoiNetNode]
169
+ )
170
+ for rid in payload.rids:
171
+ if rid == processor.identity.rid:
172
+ logger.info("Skipping myself")
173
+ continue
174
+ if processor.cache.exists(rid):
175
+ logger.info(f"Skipping known RID '{rid}'")
176
+ continue
177
+
178
+ # marked as external since we are handling RIDs from another node
179
+ # will fetch remotely instead of checking local cache
180
+ processor.handle(rid=rid, source=KnowledgeSource.External)
181
+ logger.info("Done")
182
+
183
+
184
+ @KnowledgeHandler.create(HandlerType.Network)
185
+ def basic_network_output_filter(processor: ProcessorInterface, kobj: KnowledgeObject):
186
+ """Default network handler.
187
+
188
+ Allows broadcasting of all RID types this node is an event provider for (set in node profile), and other nodes have subscribed to. All nodes will also broadcast about their own (internally sourced) KOI node, and KOI edges that they are part of, regardless of their node profile configuration. Finally, nodes will also broadcast about edges to the other node involved (regardless of if they are subscribed)."""
189
+
190
+ involves_me = False
191
+ if kobj.source == KnowledgeSource.Internal:
192
+ if (type(kobj.rid) == KoiNetNode):
193
+ if (kobj.rid == processor.identity.rid):
194
+ involves_me = True
195
+
196
+ elif type(kobj.rid) == KoiNetEdge:
197
+ edge_profile = kobj.bundle.validate_contents(EdgeProfile)
198
+
199
+ if edge_profile.source == processor.identity.rid:
200
+ logger.debug(f"Adding edge target '{edge_profile.target!r}' to network targets")
201
+ kobj.network_targets.update([edge_profile.target])
202
+ involves_me = True
203
+
204
+ elif edge_profile.target == processor.identity.rid:
205
+ logger.debug(f"Adding edge source '{edge_profile.source!r}' to network targets")
206
+ kobj.network_targets.update([edge_profile.source])
207
+ involves_me = True
208
+
209
+ if (type(kobj.rid) in processor.identity.profile.provides.event or involves_me):
210
+ # broadcasts to subscribers if I'm an event provider of this RID type OR it involves me
211
+ subscribers = processor.network.graph.get_neighbors(
212
+ direction="out",
213
+ allowed_type=type(kobj.rid)
214
+ )
215
+
216
+ logger.debug(f"Updating network targets with '{type(kobj.rid)}' subscribers: {subscribers}")
217
+ kobj.network_targets.update(subscribers)
218
+
219
+ return kobj
220
+
@@ -0,0 +1,59 @@
1
+ from dataclasses import dataclass
2
+ from enum import StrEnum
3
+ from typing import Callable
4
+ from rid_lib import RIDType
5
+
6
+ from ..protocol.event import EventType
7
+ from .knowledge_object import KnowledgeSource, KnowledgeEventType
8
+
9
+
10
+ class StopChain:
11
+ """Class for a sentinel value by knowledge handlers."""
12
+ pass
13
+
14
+ STOP_CHAIN = StopChain()
15
+
16
+
17
+ class HandlerType(StrEnum):
18
+ """Types of handlers used in knowledge processing pipeline.
19
+
20
+ - RID - provided RID; if event type is `FORGET`, this handler decides whether to delete the knowledge from the cache by setting the normalized event type to `FORGET`, otherwise this handler decides whether to validate the manifest (and fetch it if not provided).
21
+ - Manifest - provided RID, manifest; decides whether to validate the bundle (and fetch it if not provided).
22
+ - Bundle - provided RID, manifest, contents (bundle); decides whether to write knowledge to the cache by setting the normalized event type to `NEW` or `UPDATE`.
23
+ - Network - provided RID, manifest, contents (bundle); decides which nodes (if any) to broadcast an event about this knowledge to. (Note, if event type is `FORGET`, the manifest and contents will be retrieved from the local cache, and indicate the last state of the knowledge before it was deleted.)
24
+ - Final - provided RID, manifests, contents (bundle); final action taken after network broadcast.
25
+ """
26
+
27
+ RID = "rid",
28
+ Manifest = "manifest",
29
+ Bundle = "bundle",
30
+ Network = "network",
31
+ Final = "final"
32
+
33
+ @dataclass
34
+ class KnowledgeHandler:
35
+ """Handles knowledge processing events of the provided types."""
36
+
37
+ func: Callable
38
+ handler_type: HandlerType
39
+ rid_types: list[RIDType] | None
40
+ source: KnowledgeSource | None = None
41
+ event_types: list[KnowledgeEventType] | None = None
42
+
43
+ @classmethod
44
+ def create(
45
+ cls,
46
+ handler_type: HandlerType,
47
+ rid_types: list[RIDType] | None = None,
48
+ source: KnowledgeSource | None = None,
49
+ event_types: list[KnowledgeEventType] | None = None
50
+ ):
51
+ """Special decorator that returns a KnowledgeHandler instead of a function.
52
+
53
+ The function symbol will redefined as a `KnowledgeHandler`, which can be passed into the `ProcessorInterface` constructor. This is used to register default handlers.
54
+ """
55
+ def decorator(func: Callable) -> KnowledgeHandler:
56
+ handler = cls(func, handler_type, rid_types, source, event_types)
57
+ return handler
58
+ return decorator
59
+