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,300 @@
1
+ import logging
2
+ import queue
3
+ import threading
4
+ from typing import Callable, Generic
5
+ from rid_lib.core import RID, RIDType
6
+ from rid_lib.ext import Bundle, Cache, Manifest
7
+ from rid_lib.types.koi_net_edge import KoiNetEdge
8
+ from rid_lib.types.koi_net_node import KoiNetNode
9
+ from ..identity import NodeIdentity
10
+ from ..network import NetworkInterface
11
+ from ..protocol.event import Event, EventType
12
+ from ..config import NodeConfig
13
+ from .handler import (
14
+ KnowledgeHandler,
15
+ HandlerType,
16
+ STOP_CHAIN,
17
+ StopChain
18
+ )
19
+ from .knowledge_object import (
20
+ KnowledgeObject,
21
+ KnowledgeSource,
22
+ KnowledgeEventType
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class ProcessorInterface():
29
+ """Provides access to this node's knowledge processing pipeline."""
30
+
31
+ config: NodeConfig
32
+ cache: Cache
33
+ network: NetworkInterface
34
+ identity: NodeIdentity
35
+ handlers: list[KnowledgeHandler]
36
+ kobj_queue: queue.Queue[KnowledgeObject]
37
+ use_kobj_processor_thread: bool
38
+ worker_thread: threading.Thread | None = None
39
+
40
+ def __init__(
41
+ self,
42
+ config: NodeConfig,
43
+ cache: Cache,
44
+ network: NetworkInterface,
45
+ identity: NodeIdentity,
46
+ use_kobj_processor_thread: bool,
47
+ default_handlers: list[KnowledgeHandler] = []
48
+ ):
49
+ self.config = config
50
+ self.cache = cache
51
+ self.network = network
52
+ self.identity = identity
53
+ self.use_kobj_processor_thread = use_kobj_processor_thread
54
+ self.handlers: list[KnowledgeHandler] = default_handlers
55
+ self.kobj_queue = queue.Queue()
56
+
57
+ if self.use_kobj_processor_thread:
58
+ self.worker_thread = threading.Thread(
59
+ target=self.kobj_processor_worker,
60
+ daemon=True
61
+ )
62
+
63
+ def add_handler(self, handler: KnowledgeHandler):
64
+ self.handlers.append(handler)
65
+
66
+ def register_handler(
67
+ self,
68
+ handler_type: HandlerType,
69
+ rid_types: list[RIDType] | None = None,
70
+ source: KnowledgeSource | None = None,
71
+ event_types: list[KnowledgeEventType] | None = None
72
+ ):
73
+ """Assigns decorated function as handler for this processor."""
74
+ def decorator(func: Callable) -> Callable:
75
+ handler = KnowledgeHandler(func, handler_type, rid_types, source, event_types)
76
+ self.add_handler(handler)
77
+ return func
78
+ return decorator
79
+
80
+ def call_handler_chain(
81
+ self,
82
+ handler_type: HandlerType,
83
+ kobj: KnowledgeObject
84
+ ) -> KnowledgeObject | StopChain:
85
+ """Calls handlers of provided type, chaining their inputs and outputs together.
86
+
87
+ The knowledge object provided when this function is called will be passed to the first handler. A handler may return one of three types:
88
+ - `KnowledgeObject` - to modify the knowledge object for the next handler in the chain
89
+ - `None` - to keep the same knowledge object for the next handler in the chain
90
+ - `STOP_CHAIN` - to stop the handler chain and immediately exit the processing pipeline
91
+
92
+ Handlers will only be called in the chain if their handler and RID type match that of the inputted knowledge object.
93
+ """
94
+
95
+ for handler in self.handlers:
96
+ if handler_type != handler.handler_type:
97
+ continue
98
+
99
+ if handler.rid_types and type(kobj.rid) not in handler.rid_types:
100
+ continue
101
+
102
+ if handler.source and handler.source != kobj.source:
103
+ continue
104
+
105
+ if handler.event_types and kobj.event_type not in handler.event_types:
106
+ continue
107
+
108
+ logger.debug(f"Calling {handler_type} handler '{handler.func.__name__}'")
109
+ resp = handler.func(self, kobj.model_copy())
110
+
111
+ # stops handler chain execution
112
+ if resp is STOP_CHAIN:
113
+ logger.debug(f"Handler chain stopped by {handler.func.__name__}")
114
+ return STOP_CHAIN
115
+ # kobj unmodified
116
+ elif resp is None:
117
+ continue
118
+ # kobj modified by handler
119
+ elif isinstance(resp, KnowledgeObject):
120
+ kobj = resp
121
+ logger.debug(f"Knowledge object modified by {handler.func.__name__}")
122
+ else:
123
+ raise ValueError(f"Handler {handler.func.__name__} returned invalid response '{resp}'")
124
+
125
+ return kobj
126
+
127
+
128
+ def process_kobj(self, kobj: KnowledgeObject) -> None:
129
+ """Sends provided knowledge obejct through knowledge processing pipeline.
130
+
131
+ Handler chains are called in between major events in the pipeline, indicated by their handler type. Each handler type is guaranteed to have access to certain knowledge, and may affect a subsequent action in the pipeline. The five handler types are as follows:
132
+ - 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).
133
+ - Manifest - provided RID, manifest; decides whether to validate the bundle (and fetch it if not provided).
134
+ - Bundle - provided RID, manifest, contents (bundle); decides whether to write knowledge to the cache by setting the normalized event type to `NEW` or `UPDATE`.
135
+ - 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.)
136
+ - Final - provided RID, manifests, contents (bundle); final action taken after network broadcast.
137
+
138
+ The pipeline may be stopped by any point by a single handler returning the `STOP_CHAIN` sentinel. In that case, the process will exit immediately. Further handlers of that type and later handler chains will not be called.
139
+ """
140
+
141
+ logger.debug(f"Handling {kobj!r}")
142
+ kobj = self.call_handler_chain(HandlerType.RID, kobj)
143
+ if kobj is STOP_CHAIN: return
144
+
145
+ if kobj.event_type == EventType.FORGET:
146
+ bundle = self.cache.read(kobj.rid)
147
+ if not bundle:
148
+ logger.debug("Local bundle not found")
149
+ return
150
+
151
+ # the bundle (to be deleted) attached to kobj for downstream analysis
152
+ logger.debug("Adding local bundle (to be deleted) to knowledge object")
153
+ kobj.manifest = bundle.manifest
154
+ kobj.contents = bundle.contents
155
+
156
+ else:
157
+ # attempt to retrieve manifest
158
+ if not kobj.manifest:
159
+ logger.debug("Manifest not found")
160
+ if kobj.source == KnowledgeSource.External:
161
+ logger.debug("Attempting to fetch remote manifest")
162
+ manifest = self.network.fetch_remote_manifest(kobj.rid)
163
+
164
+ elif kobj.source == KnowledgeSource.Internal:
165
+ logger.debug("Attempting to read manifest from cache")
166
+ bundle = self.cache.read(kobj.rid)
167
+ if bundle:
168
+ manifest = bundle.manifest
169
+ else:
170
+ manifest = None
171
+ return
172
+
173
+ if not manifest:
174
+ logger.debug("Failed to find manifest")
175
+ return
176
+
177
+ kobj.manifest = manifest
178
+
179
+ kobj = self.call_handler_chain(HandlerType.Manifest, kobj)
180
+ if kobj is STOP_CHAIN: return
181
+
182
+ # attempt to retrieve bundle
183
+ if not kobj.bundle:
184
+ logger.debug("Bundle not found")
185
+ if kobj.source == KnowledgeSource.External:
186
+ logger.debug("Attempting to fetch remote bundle")
187
+ bundle = self.network.fetch_remote_bundle(kobj.rid)
188
+
189
+ elif kobj.source == KnowledgeSource.Internal:
190
+ logger.debug("Attempting to read bundle from cache")
191
+ bundle = self.cache.read(kobj.rid)
192
+
193
+ if not bundle:
194
+ logger.debug("Failed to find bundle")
195
+ return
196
+
197
+ if kobj.manifest != bundle.manifest:
198
+ logger.warning("Retrieved bundle contains a different manifest")
199
+
200
+ kobj.manifest = bundle.manifest
201
+ kobj.contents = bundle.contents
202
+
203
+ kobj = self.call_handler_chain(HandlerType.Bundle, kobj)
204
+ if kobj is STOP_CHAIN: return
205
+
206
+ if kobj.normalized_event_type in (EventType.UPDATE, EventType.NEW):
207
+ logger.info(f"Writing to cache: {kobj!r}")
208
+ self.cache.write(kobj.bundle)
209
+
210
+ elif kobj.normalized_event_type == EventType.FORGET:
211
+ logger.info(f"Deleting from cache: {kobj!r}")
212
+ self.cache.delete(kobj.rid)
213
+
214
+ else:
215
+ logger.debug("Normalized event type was never set, no cache or network operations will occur")
216
+ return
217
+
218
+ if type(kobj.rid) in (KoiNetNode, KoiNetEdge):
219
+ logger.debug("Change to node or edge, regenerating network graph")
220
+ self.network.graph.generate()
221
+
222
+ kobj = self.call_handler_chain(HandlerType.Network, kobj)
223
+ if kobj is STOP_CHAIN: return
224
+
225
+ if kobj.network_targets:
226
+ logger.debug(f"Broadcasting event to {len(kobj.network_targets)} network target(s)")
227
+ else:
228
+ logger.debug("No network targets set")
229
+
230
+ for node in kobj.network_targets:
231
+ self.network.push_event_to(kobj.normalized_event, node)
232
+ self.network.flush_webhook_queue(node)
233
+
234
+ kobj = self.call_handler_chain(HandlerType.Final, kobj)
235
+
236
+ def flush_kobj_queue(self):
237
+ """Flushes all knowledge objects from queue and processes them.
238
+
239
+ NOTE: ONLY CALL THIS METHOD IN SINGLE THREADED NODES, OTHERWISE THIS WILL CAUSE RACE CONDITIONS.
240
+ """
241
+ if self.use_kobj_processor_thread:
242
+ logger.warning("You are using a worker thread, calling this method can cause race conditions!")
243
+
244
+ while not self.kobj_queue.empty():
245
+ kobj = self.kobj_queue.get()
246
+ logger.debug(f"Dequeued {kobj!r}")
247
+
248
+ try:
249
+ self.process_kobj(kobj)
250
+ finally:
251
+ self.kobj_queue.task_done()
252
+ logger.debug("Done")
253
+
254
+ def kobj_processor_worker(self, timeout=0.1):
255
+ while True:
256
+ try:
257
+ kobj = self.kobj_queue.get(timeout=timeout)
258
+ logger.debug(f"Dequeued {kobj!r}")
259
+
260
+ try:
261
+ self.process_kobj(kobj)
262
+ finally:
263
+ self.kobj_queue.task_done()
264
+ logger.debug("Done")
265
+
266
+ except queue.Empty:
267
+ pass
268
+
269
+ except Exception as e:
270
+ logger.warning(f"Error processing kobj: {e}")
271
+
272
+ def handle(
273
+ self,
274
+ rid: RID | None = None,
275
+ manifest: Manifest | None = None,
276
+ bundle: Bundle | None = None,
277
+ event: Event | None = None,
278
+ kobj: KnowledgeObject | None = None,
279
+ event_type: KnowledgeEventType = None,
280
+ source: KnowledgeSource = KnowledgeSource.Internal
281
+ ):
282
+ """Queues provided knowledge to be handled by processing pipeline.
283
+
284
+ Knowledge may take the form of an RID, manifest, bundle, event, or knowledge object (with an optional event type for RID, manifest, or bundle objects). All objects will be normalized into knowledge objects and queued. If `flush` is `True`, the queue will be flushed immediately after adding the new knowledge.
285
+ """
286
+ if rid:
287
+ _kobj = KnowledgeObject.from_rid(rid, event_type, source)
288
+ elif manifest:
289
+ _kobj = KnowledgeObject.from_manifest(manifest, event_type, source)
290
+ elif bundle:
291
+ _kobj = KnowledgeObject.from_bundle(bundle, event_type, source)
292
+ elif event:
293
+ _kobj = KnowledgeObject.from_event(event, source)
294
+ elif kobj:
295
+ _kobj = kobj
296
+ else:
297
+ raise ValueError("One of 'rid', 'manifest', 'bundle', 'event', or 'kobj' must be provided")
298
+
299
+ self.kobj_queue.put(_kobj)
300
+ logger.debug(f"Queued {_kobj!r}")
@@ -0,0 +1,123 @@
1
+ from enum import StrEnum
2
+ from pydantic import BaseModel
3
+ from rid_lib import RID
4
+ from rid_lib.ext import Manifest
5
+ from rid_lib.ext.bundle import Bundle
6
+ from rid_lib.types.koi_net_node import KoiNetNode
7
+ from ..protocol.event import Event, EventType
8
+
9
+
10
+ type KnowledgeEventType = EventType | None
11
+
12
+ class KnowledgeSource(StrEnum):
13
+ Internal = "INTERNAL"
14
+ External = "EXTERNAL"
15
+
16
+ class KnowledgeObject(BaseModel):
17
+ """A normalized knowledge representation for internal processing.
18
+
19
+ Capable of representing an RID, manifest, bundle, or event. Contains three additional fields use for decision making in the knowledge processing pipeline.
20
+
21
+ The source indicates whether this object was generated by this node, or sourced from another node in the network.
22
+
23
+ The normalized event type indicates how the knowledge object is viewed from the perspective of this node, and what cache actions will take place. `NEW`, `UPDATE` -> cache write, `FORGET` -> cache delete, `None` -> no cache action.
24
+
25
+ The network targets indicate other nodes in the network this knowledge object will be sent to. The event sent to them will be constructed from this knowledge object's RID, manifest, contents, and normalized event type.
26
+
27
+ Constructors are provided to create a knowledge object from an RID, manifest, bundle, or event.
28
+ """
29
+ rid: RID
30
+ manifest: Manifest | None = None
31
+ contents: dict | None = None
32
+ event_type: KnowledgeEventType = None
33
+ normalized_event_type: KnowledgeEventType = None
34
+ source: KnowledgeSource
35
+ network_targets: set[KoiNetNode] = set()
36
+
37
+ def __repr__(self):
38
+ return f"<KObj '{self.rid}' event type: '{self.event_type}' -> '{self.normalized_event_type}', source: '{self.source}'>"
39
+
40
+ @classmethod
41
+ def from_rid(
42
+ cls,
43
+ rid: RID,
44
+ event_type: KnowledgeEventType = None,
45
+ source: KnowledgeSource = KnowledgeSource.Internal
46
+ ) -> "KnowledgeObject":
47
+ return cls(
48
+ rid=rid,
49
+ event_type=event_type,
50
+ source=source
51
+ )
52
+
53
+ @classmethod
54
+ def from_manifest(
55
+ cls,
56
+ manifest: Manifest,
57
+ event_type: KnowledgeEventType = None,
58
+ source: KnowledgeSource = KnowledgeSource.Internal
59
+ ) -> "KnowledgeObject":
60
+ return cls(
61
+ rid=manifest.rid,
62
+ manifest=manifest,
63
+ event_type=event_type,
64
+ source=source
65
+ )
66
+
67
+ @classmethod
68
+ def from_bundle(
69
+ cls,
70
+ bundle: Bundle,
71
+ event_type: KnowledgeEventType = None,
72
+ source: KnowledgeSource = KnowledgeSource.Internal
73
+ ) -> "KnowledgeObject":
74
+ return cls(
75
+ rid=bundle.rid,
76
+ manifest=bundle.manifest,
77
+ contents=bundle.contents,
78
+ event_type=event_type,
79
+ source=source
80
+ )
81
+
82
+ @classmethod
83
+ def from_event(
84
+ cls,
85
+ event: Event,
86
+ source: KnowledgeSource = KnowledgeSource.Internal
87
+ ) -> "KnowledgeObject":
88
+ return cls(
89
+ rid=event.rid,
90
+ manifest=event.manifest,
91
+ contents=event.contents,
92
+ event_type=event.event_type,
93
+ source=source
94
+ )
95
+
96
+ @property
97
+ def bundle(self):
98
+ if self.manifest is None or self.contents is None:
99
+ return
100
+
101
+ return Bundle(
102
+ manifest=self.manifest,
103
+ contents=self.contents
104
+ )
105
+
106
+ @property
107
+ def normalized_event(self):
108
+ if self.normalized_event_type is None:
109
+ raise ValueError("Internal event's normalized event type is None, cannot convert to Event")
110
+
111
+ elif self.normalized_event_type == EventType.FORGET:
112
+ return Event(
113
+ rid=self.rid,
114
+ event_type=EventType.FORGET
115
+ )
116
+
117
+ else:
118
+ return Event(
119
+ rid=self.rid,
120
+ event_type=self.normalized_event_type,
121
+ manifest=self.manifest,
122
+ contents=self.contents
123
+ )
File without changes
@@ -0,0 +1,47 @@
1
+ """Pydantic models for request and response/payload objects in the KOI-net API."""
2
+
3
+ from pydantic import BaseModel
4
+ from rid_lib import RID, RIDType
5
+ from rid_lib.ext import Bundle, Manifest
6
+ from .event import Event
7
+
8
+
9
+ # REQUEST MODELS
10
+
11
+ class PollEvents(BaseModel):
12
+ rid: RID
13
+ limit: int = 0
14
+
15
+ class FetchRids(BaseModel):
16
+ rid_types: list[RIDType] = []
17
+
18
+ class FetchManifests(BaseModel):
19
+ rid_types: list[RIDType] = []
20
+ rids: list[RID] = []
21
+
22
+ class FetchBundles(BaseModel):
23
+ rids: list[RID]
24
+
25
+
26
+ # RESPONSE/PAYLOAD MODELS
27
+
28
+ class RidsPayload(BaseModel):
29
+ rids: list[RID]
30
+
31
+ class ManifestsPayload(BaseModel):
32
+ manifests: list[Manifest]
33
+ not_found: list[RID] = []
34
+
35
+ class BundlesPayload(BaseModel):
36
+ bundles: list[Bundle]
37
+ not_found: list[RID] = []
38
+ deferred: list[RID] = []
39
+
40
+ class EventsPayload(BaseModel):
41
+ events: list[Event]
42
+
43
+
44
+ # TYPES
45
+
46
+ type RequestModels = EventsPayload | PollEvents | FetchRids | FetchManifests | FetchBundles
47
+ type ResponseModels = RidsPayload | ManifestsPayload | BundlesPayload | EventsPayload
@@ -0,0 +1,7 @@
1
+ """API paths for KOI-net protocol."""
2
+
3
+ BROADCAST_EVENTS_PATH = "/events/broadcast"
4
+ POLL_EVENTS_PATH = "/events/poll"
5
+ FETCH_RIDS_PATH = "/rids/fetch"
6
+ FETCH_MANIFESTS_PATH = "/manifests/fetch"
7
+ FETCH_BUNDLES_PATH = "/bundles/fetch"
@@ -0,0 +1,20 @@
1
+ from enum import StrEnum
2
+ from pydantic import BaseModel
3
+ from rid_lib import RIDType
4
+ from rid_lib.types import KoiNetNode
5
+
6
+
7
+ class EdgeStatus(StrEnum):
8
+ PROPOSED = "PROPOSED"
9
+ APPROVED = "APPROVED"
10
+
11
+ class EdgeType(StrEnum):
12
+ WEBHOOK = "WEBHOOK"
13
+ POLL = "POLL"
14
+
15
+ class EdgeProfile(BaseModel):
16
+ source: KoiNetNode
17
+ target: KoiNetNode
18
+ edge_type: EdgeType
19
+ status: EdgeStatus
20
+ rid_types: list[RIDType]
@@ -0,0 +1,54 @@
1
+ from enum import StrEnum
2
+ from pydantic import BaseModel
3
+ from rid_lib import RID
4
+ from rid_lib.ext import Manifest, Bundle
5
+
6
+
7
+ class EventType(StrEnum):
8
+ NEW = "NEW"
9
+ UPDATE = "UPDATE"
10
+ FORGET = "FORGET"
11
+
12
+ class Event(BaseModel):
13
+ rid: RID
14
+ event_type: EventType
15
+ manifest: Manifest | None = None
16
+ contents: dict | None = None
17
+
18
+ class Config:
19
+ exclude_none = True
20
+
21
+ def __repr__(self):
22
+ return f"<Event '{self.rid}' event type: '{self.event_type}'>"
23
+
24
+ @classmethod
25
+ def from_bundle(cls, event_type: EventType, bundle: Bundle):
26
+ return cls(
27
+ rid=bundle.manifest.rid,
28
+ event_type=event_type,
29
+ manifest=bundle.manifest,
30
+ contents=bundle.contents
31
+ )
32
+
33
+ @classmethod
34
+ def from_manifest(cls, event_type: EventType, manifest: Manifest):
35
+ return cls(
36
+ rid=manifest.rid,
37
+ event_type=event_type,
38
+ manifest=manifest
39
+ )
40
+
41
+ @classmethod
42
+ def from_rid(cls, event_type: EventType, rid: RID):
43
+ return cls(
44
+ rid=rid,
45
+ event_type=event_type
46
+ )
47
+
48
+ @property
49
+ def bundle(self):
50
+ if self.manifest is not None and self.contents is not None:
51
+ return Bundle(
52
+ manifest=self.manifest,
53
+ contents=self.contents
54
+ )
@@ -0,0 +1,25 @@
1
+ from rid_lib.core import RIDType
2
+ from rid_lib.ext.bundle import Bundle
3
+ from rid_lib.types import KoiNetEdge
4
+ from rid_lib.types.koi_net_node import KoiNetNode
5
+ from .edge import EdgeProfile, EdgeStatus, EdgeType
6
+
7
+ def generate_edge_bundle(
8
+ source: KoiNetNode,
9
+ target: KoiNetNode,
10
+ rid_types: list[RIDType],
11
+ edge_type: EdgeType
12
+ ) -> Bundle:
13
+ edge_rid = KoiNetEdge.generate(source, target)
14
+ edge_profile = EdgeProfile(
15
+ source=source,
16
+ target=target,
17
+ rid_types=rid_types,
18
+ edge_type=edge_type,
19
+ status=EdgeStatus.PROPOSED
20
+ )
21
+ edge_bundle = Bundle.generate(
22
+ edge_rid,
23
+ edge_profile.model_dump()
24
+ )
25
+ return edge_bundle
@@ -0,0 +1,17 @@
1
+ from enum import StrEnum
2
+ from pydantic import BaseModel
3
+ from rid_lib import RIDType
4
+
5
+
6
+ class NodeType(StrEnum):
7
+ FULL = "FULL"
8
+ PARTIAL = "PARTIAL"
9
+
10
+ class NodeProvides(BaseModel):
11
+ event: list[RIDType] = []
12
+ state: list[RIDType] = []
13
+
14
+ class NodeProfile(BaseModel):
15
+ base_url: str | None = None
16
+ node_type: NodeType
17
+ provides: NodeProvides = NodeProvides()