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
@@ -0,0 +1,222 @@
1
+ import structlog
2
+ from rid_lib.types import KoiNetEdge, KoiNetNode
3
+ from rid_lib.ext import Cache
4
+
5
+ from ..exceptions import RequestError
6
+ from ..protocol.event import EventType
7
+ from ..network.request_handler import RequestHandler
8
+ from ..network.event_queue import EventQueue
9
+ from ..network.graph import NetworkGraph
10
+ from ..identity import NodeIdentity
11
+ from .handler import (
12
+ KnowledgeHandler,
13
+ HandlerType,
14
+ STOP_CHAIN,
15
+ StopChain
16
+ )
17
+ from .knowledge_object import KnowledgeObject
18
+ from .context import HandlerContext
19
+
20
+ log = structlog.stdlib.get_logger()
21
+
22
+
23
+ class KnowledgePipeline:
24
+ handler_context: HandlerContext
25
+ cache: Cache
26
+ identity: NodeIdentity
27
+ request_handler: RequestHandler
28
+ event_queue: EventQueue
29
+ graph: NetworkGraph
30
+ knowledge_handlers: list[KnowledgeHandler]
31
+
32
+ def __init__(
33
+ self,
34
+ handler_context: HandlerContext,
35
+ cache: Cache,
36
+ request_handler: RequestHandler,
37
+ event_queue: EventQueue,
38
+ graph: NetworkGraph,
39
+ knowledge_handlers: list[KnowledgeHandler]
40
+ ):
41
+ self.handler_context = handler_context
42
+ self.cache = cache
43
+ self.request_handler = request_handler
44
+ self.event_queue = event_queue
45
+ self.graph = graph
46
+ self.knowledge_handlers = knowledge_handlers
47
+
48
+ def call_handler_chain(
49
+ self,
50
+ handler_type: HandlerType,
51
+ kobj: KnowledgeObject
52
+ ) -> KnowledgeObject | StopChain:
53
+ """Calls handlers of provided type, chaining their inputs and outputs together.
54
+
55
+ The knowledge object provided when this function is called will be passed to the first handler. A handler may return one of three types:
56
+ - `KnowledgeObject` - to modify the knowledge object for the next handler in the chain
57
+ - `None` - to keep the same knowledge object for the next handler in the chain
58
+ - `STOP_CHAIN` - to stop the handler chain and immediately exit the processing pipeline
59
+
60
+ Handlers will only be called in the chain if their handler and RID type match that of the inputted knowledge object.
61
+ """
62
+
63
+ for handler in self.knowledge_handlers:
64
+ if handler_type != handler.handler_type:
65
+ continue
66
+
67
+ if handler.rid_types and type(kobj.rid) not in handler.rid_types:
68
+ continue
69
+
70
+ if handler.event_types and kobj.event_type not in handler.event_types:
71
+ continue
72
+
73
+ log.debug(f"Calling {handler_type} handler '{handler.func.__name__}'")
74
+
75
+ resp = handler(
76
+ ctx=self.handler_context,
77
+ kobj=kobj.model_copy()
78
+ )
79
+
80
+ # stops handler chain execution
81
+ if resp is STOP_CHAIN:
82
+ log.debug(f"Handler chain stopped by {handler.func.__name__}")
83
+ return STOP_CHAIN
84
+
85
+ # kobj unmodified
86
+ elif resp is None:
87
+ continue
88
+
89
+ # kobj modified by handler
90
+ elif isinstance(resp, KnowledgeObject):
91
+ kobj = resp
92
+ log.debug(f"Knowledge object modified by {handler.func.__name__}")
93
+
94
+ else:
95
+ raise ValueError(f"Handler {handler.func.__name__} returned invalid response '{resp}'")
96
+
97
+ return kobj
98
+
99
+ def process(self, kobj: KnowledgeObject):
100
+ """Sends knowledge object through knowledge processing pipeline.
101
+
102
+ Handler chains are called in between major events in the
103
+ pipeline, indicated by their handler type. Each handler type is
104
+ guaranteed to have access to certain knowledge, and may affect a
105
+ subsequent action in the pipeline. The five handler types are as
106
+ follows:
107
+ - RID - provided RID; if event type is `FORGET`, this handler
108
+ decides whether to delete the knowledge from the cache by
109
+ setting the normalized event type to `FORGET`, otherwise this
110
+ handler decides whether to validate the manifest (and fetch it
111
+ if not provided). After processing, if event type is `FORGET`,
112
+ the manifest and contents will be retrieved from the local cache,
113
+ and indicate the last state of the knowledge before it was
114
+ deleted.
115
+ - Manifest - provided RID, manifest; decides whether to validate
116
+ the bundle (and fetch it if not provided).
117
+ - Bundle - provided RID, manifest, contents (bundle); decides
118
+ whether to write knowledge to the cache by setting the
119
+ normalized event type to `NEW` or `UPDATE`.
120
+ - Network - provided RID, manifest, contents (bundle); decides
121
+ which nodes (if any) to broadcast an event about this knowledge
122
+ to.
123
+ - Final - provided RID, manifests, contents (bundle); final
124
+ action taken after network broadcast.
125
+
126
+ The pipeline may be stopped by any point by a single handler
127
+ returning the `STOP_CHAIN` sentinel. In that case, the process
128
+ will exit immediately. Further handlers of that type and later
129
+ handler chains will not be called.
130
+ """
131
+
132
+ log.debug(f"Handling {kobj!r}")
133
+ kobj = self.call_handler_chain(HandlerType.RID, kobj)
134
+ if kobj is STOP_CHAIN: return
135
+
136
+ if kobj.event_type == EventType.FORGET:
137
+ bundle = self.cache.read(kobj.rid)
138
+ if not bundle:
139
+ log.debug("Local bundle not found")
140
+ return
141
+
142
+ # the bundle (to be deleted) attached to kobj for downstream analysis
143
+ log.debug("Adding local bundle (to be deleted) to knowledge object")
144
+ kobj.manifest = bundle.manifest
145
+ kobj.contents = bundle.contents
146
+
147
+ else:
148
+ # attempt to retrieve manifest
149
+ if not kobj.manifest:
150
+ log.debug("Manifest not found")
151
+ if not kobj.source:
152
+ return
153
+
154
+ log.debug("Attempting to fetch remote manifest from source")
155
+ try:
156
+ payload = self.request_handler.fetch_manifests(
157
+ node=kobj.source,
158
+ rids=[kobj.rid])
159
+ except RequestError:
160
+ log.debug("Failed to find manifest")
161
+ return
162
+
163
+ kobj.manifest = payload.manifests[0]
164
+
165
+ kobj = self.call_handler_chain(HandlerType.Manifest, kobj)
166
+ if kobj is STOP_CHAIN: return
167
+
168
+ # attempt to retrieve bundle
169
+ if not kobj.contents:
170
+ log.debug("Bundle not found")
171
+ if kobj.source is None:
172
+ return
173
+
174
+ log.debug("Attempting to fetch remote bundle from source")
175
+ try:
176
+ payload = self.request_handler.fetch_bundles(
177
+ node=kobj.source,
178
+ rids=[kobj.rid]
179
+ )
180
+ except RequestError:
181
+ log.debug("Failed to find bundle")
182
+ return
183
+
184
+ bundle = payload.bundles[0]
185
+
186
+ if kobj.manifest != bundle.manifest:
187
+ log.warning("Retrieved bundle contains a different manifest")
188
+
189
+ kobj.manifest = bundle.manifest
190
+ kobj.contents = bundle.contents
191
+
192
+ kobj = self.call_handler_chain(HandlerType.Bundle, kobj)
193
+ if kobj is STOP_CHAIN: return
194
+
195
+ if kobj.normalized_event_type in (EventType.UPDATE, EventType.NEW):
196
+ log.info(f"Writing to cache: {kobj!r}")
197
+ self.cache.write(kobj.bundle)
198
+
199
+ elif kobj.normalized_event_type == EventType.FORGET:
200
+ log.info(f"Deleting from cache: {kobj!r}")
201
+ self.cache.delete(kobj.rid)
202
+
203
+ else:
204
+ log.debug("Normalized event type was not set, no cache or network operations will occur")
205
+ return
206
+
207
+ if type(kobj.rid) in (KoiNetNode, KoiNetEdge):
208
+ log.debug("Change to node or edge, regenerating network graph")
209
+ self.graph.generate()
210
+
211
+ kobj = self.call_handler_chain(HandlerType.Network, kobj)
212
+ if kobj is STOP_CHAIN: return
213
+
214
+ if kobj.network_targets:
215
+ log.debug(f"Broadcasting event to {len(kobj.network_targets)} network target(s)")
216
+ else:
217
+ log.debug("No network targets set")
218
+
219
+ for node in kobj.network_targets:
220
+ self.event_queue.push(kobj.normalized_event, node)
221
+
222
+ kobj = self.call_handler_chain(HandlerType.Final, kobj)
File without changes
@@ -0,0 +1,67 @@
1
+ """Pydantic models for request and response objects in the KOI-net API."""
2
+
3
+ from typing import Annotated, Literal
4
+ from pydantic import BaseModel, Field
5
+ from rid_lib import RID, RIDType
6
+ from rid_lib.ext import Bundle, Manifest
7
+ from .event import Event
8
+ from .errors import ErrorType
9
+
10
+
11
+ # REQUEST MODELS
12
+
13
+ class PollEvents(BaseModel):
14
+ type: Literal["poll_events"] = Field("poll_events")
15
+ limit: int = 0
16
+
17
+ class FetchRids(BaseModel):
18
+ type: Literal["fetch_rids"] = Field("fetch_rids")
19
+ rid_types: list[RIDType] = []
20
+
21
+ class FetchManifests(BaseModel):
22
+ type: Literal["fetch_manifests"] = Field("fetch_manifests")
23
+ rid_types: list[RIDType] = []
24
+ rids: list[RID] = []
25
+
26
+ class FetchBundles(BaseModel):
27
+ type: Literal["fetch_bundles"] = Field("fetch_bundles")
28
+ rids: list[RID]
29
+
30
+
31
+ # RESPONSE/PAYLOAD MODELS
32
+
33
+ class RidsPayload(BaseModel):
34
+ type: Literal["rids_payload"] = Field("rids_payload")
35
+ rids: list[RID]
36
+
37
+ class ManifestsPayload(BaseModel):
38
+ type: Literal["manifests_payload"] = Field("manifests_payload")
39
+ manifests: list[Manifest]
40
+ not_found: list[RID] = []
41
+
42
+ class BundlesPayload(BaseModel):
43
+ type: Literal["bundles_payload"] = Field("bundles_payload")
44
+ bundles: list[Bundle]
45
+ not_found: list[RID] = []
46
+ deferred: list[RID] = []
47
+
48
+ class EventsPayload(BaseModel):
49
+ type: Literal["events_payload"] = Field("events_payload")
50
+ events: list[Event]
51
+
52
+
53
+ # ERROR MODELS
54
+
55
+ class ErrorResponse(BaseModel):
56
+ type: Literal["error_response"] = Field("error_response")
57
+ error: ErrorType
58
+
59
+ # TYPES
60
+
61
+ type RequestModels = EventsPayload | PollEvents | FetchRids | FetchManifests | FetchBundles
62
+ type ResponseModels = RidsPayload | ManifestsPayload | BundlesPayload | EventsPayload | ErrorResponse
63
+
64
+ type ApiModels = Annotated[
65
+ RequestModels | ResponseModels,
66
+ Field(discriminator="type")
67
+ ]
@@ -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,50 @@
1
+ from enum import StrEnum
2
+ from pydantic import BaseModel
3
+ from rid_lib import RIDType
4
+ from rid_lib.ext.bundle import Bundle
5
+ from rid_lib.ext.utils import sha256_hash
6
+ from rid_lib.types import KoiNetEdge, KoiNetNode
7
+
8
+
9
+ class EdgeStatus(StrEnum):
10
+ PROPOSED = "PROPOSED"
11
+ APPROVED = "APPROVED"
12
+
13
+ class EdgeType(StrEnum):
14
+ WEBHOOK = "WEBHOOK"
15
+ POLL = "POLL"
16
+
17
+ class EdgeProfile(BaseModel):
18
+ source: KoiNetNode
19
+ target: KoiNetNode
20
+ edge_type: EdgeType
21
+ status: EdgeStatus
22
+ rid_types: list[RIDType]
23
+
24
+
25
+ def generate_edge_bundle(
26
+ source: KoiNetNode,
27
+ target: KoiNetNode,
28
+ rid_types: list[RIDType],
29
+ edge_type: EdgeType
30
+ ) -> Bundle:
31
+ """Returns edge bundle."""
32
+
33
+ edge_rid = KoiNetEdge(sha256_hash(
34
+ str(source) + str(target)
35
+ ))
36
+
37
+ edge_profile = EdgeProfile(
38
+ source=source,
39
+ target=target,
40
+ rid_types=rid_types,
41
+ edge_type=edge_type,
42
+ status=EdgeStatus.PROPOSED
43
+ )
44
+
45
+ edge_bundle = Bundle.generate(
46
+ edge_rid,
47
+ edge_profile.model_dump()
48
+ )
49
+
50
+ return edge_bundle
@@ -0,0 +1,65 @@
1
+ import structlog
2
+ from typing import Generic, TypeVar
3
+ from pydantic import BaseModel, ConfigDict
4
+ from rid_lib.types import KoiNetNode
5
+
6
+ from .secure import PrivateKey, PublicKey
7
+ from .api_models import RequestModels, ResponseModels
8
+
9
+ log = structlog.stdlib.get_logger()
10
+
11
+
12
+ T = TypeVar("T", bound=RequestModels | ResponseModels)
13
+
14
+ class SignedEnvelope(BaseModel, Generic[T]):
15
+ payload: T
16
+ source_node: KoiNetNode
17
+ target_node: KoiNetNode
18
+ signature: str
19
+
20
+ model_config = ConfigDict(exclude_none=True)
21
+
22
+ def verify_with(self, pub_key: PublicKey):
23
+ """Verifies signed envelope with public key.
24
+
25
+ Raises `cryptography.exceptions.InvalidSignature` on failure.
26
+ """
27
+
28
+ # IMPORTANT: calling `model_dump()` loses all typing! when converting between SignedEnvelope and UnsignedEnvelope, use the Pydantic classes, not the dictionary form
29
+
30
+ unsigned_envelope = UnsignedEnvelope[T](
31
+ payload=self.payload,
32
+ source_node=self.source_node,
33
+ target_node=self.target_node
34
+ )
35
+
36
+ log.debug(f"Verifying envelope: {unsigned_envelope.model_dump_json(exclude_none=True)}")
37
+
38
+ pub_key.verify(
39
+ self.signature,
40
+ unsigned_envelope.model_dump_json(exclude_none=True).encode()
41
+ )
42
+
43
+ class UnsignedEnvelope(BaseModel, Generic[T]):
44
+ payload: T
45
+ source_node: KoiNetNode
46
+ target_node: KoiNetNode
47
+
48
+ model_config = ConfigDict(exclude_none=True)
49
+
50
+ def sign_with(self, priv_key: PrivateKey) -> SignedEnvelope[T]:
51
+ """Signs with private key and returns `SignedEnvelope`."""
52
+
53
+ log.debug(f"Signing envelope: {self.model_dump_json(exclude_none=True)}")
54
+ log.debug(f"Type: [{type(self.payload)}]")
55
+
56
+ signature = priv_key.sign(
57
+ self.model_dump_json(exclude_none=True).encode()
58
+ )
59
+
60
+ return SignedEnvelope(
61
+ payload=self.payload,
62
+ source_node=self.source_node,
63
+ target_node=self.target_node,
64
+ signature=signature
65
+ )
@@ -0,0 +1,24 @@
1
+ """Defines KOI-net protocol errors."""
2
+
3
+ from enum import StrEnum
4
+ from ..exceptions import (
5
+ ProtocolError,
6
+ UnknownNodeError,
7
+ InvalidKeyError,
8
+ InvalidSignatureError,
9
+ InvalidTargetError
10
+ )
11
+
12
+
13
+ class ErrorType(StrEnum):
14
+ UnknownNode = "unknown_node"
15
+ InvalidKey = "invalid_key"
16
+ InvalidSignature = "invalid_signature"
17
+ InvalidTarget = "invalid_target"
18
+
19
+ EXCEPTION_TO_ERROR_TYPE: dict[ProtocolError, ErrorType] = {
20
+ UnknownNodeError: ErrorType.UnknownNode,
21
+ InvalidKeyError: ErrorType.InvalidKey,
22
+ InvalidSignatureError: ErrorType.InvalidSignature,
23
+ InvalidTargetError: ErrorType.InvalidTarget
24
+ }
@@ -0,0 +1,51 @@
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
+ def __repr__(self):
19
+ return f"<Event '{self.rid}' event type: '{self.event_type}'>"
20
+
21
+ @classmethod
22
+ def from_bundle(cls, event_type: EventType, bundle: Bundle):
23
+ return cls(
24
+ rid=bundle.manifest.rid,
25
+ event_type=event_type,
26
+ manifest=bundle.manifest,
27
+ contents=bundle.contents
28
+ )
29
+
30
+ @classmethod
31
+ def from_manifest(cls, event_type: EventType, manifest: Manifest):
32
+ return cls(
33
+ rid=manifest.rid,
34
+ event_type=event_type,
35
+ manifest=manifest
36
+ )
37
+
38
+ @classmethod
39
+ def from_rid(cls, event_type: EventType, rid: RID):
40
+ return cls(
41
+ rid=rid,
42
+ event_type=event_type
43
+ )
44
+
45
+ @property
46
+ def bundle(self):
47
+ if self.manifest is not None and self.contents is not None:
48
+ return Bundle(
49
+ manifest=self.manifest,
50
+ contents=self.contents
51
+ )
@@ -0,0 +1,62 @@
1
+ from typing import NamedTuple
2
+ from pydantic import BaseModel
3
+ from .envelope import SignedEnvelope
4
+ from .consts import (
5
+ BROADCAST_EVENTS_PATH,
6
+ POLL_EVENTS_PATH,
7
+ FETCH_BUNDLES_PATH,
8
+ FETCH_MANIFESTS_PATH,
9
+ FETCH_RIDS_PATH
10
+ )
11
+ from .api_models import (
12
+ EventsPayload,
13
+ PollEvents,
14
+ FetchBundles,
15
+ BundlesPayload,
16
+ FetchManifests,
17
+ ManifestsPayload,
18
+ FetchRids,
19
+ RidsPayload
20
+ )
21
+
22
+
23
+ class Models(NamedTuple):
24
+ request: type[BaseModel]
25
+ response: type[BaseModel] | None
26
+ request_envelope: type[SignedEnvelope]
27
+ response_envelope: type[SignedEnvelope] | None
28
+
29
+
30
+ # maps API paths to request and response models
31
+ API_MODEL_MAP: dict[str, Models] = {
32
+ BROADCAST_EVENTS_PATH: Models(
33
+ request=EventsPayload,
34
+ response=None,
35
+ request_envelope=SignedEnvelope[EventsPayload],
36
+ response_envelope=None
37
+ ),
38
+ POLL_EVENTS_PATH: Models(
39
+ request=PollEvents,
40
+ response=EventsPayload,
41
+ request_envelope=SignedEnvelope[PollEvents],
42
+ response_envelope=SignedEnvelope[EventsPayload]
43
+ ),
44
+ FETCH_BUNDLES_PATH: Models(
45
+ request=FetchBundles,
46
+ response=BundlesPayload,
47
+ request_envelope=SignedEnvelope[FetchBundles],
48
+ response_envelope=SignedEnvelope[BundlesPayload]
49
+ ),
50
+ FETCH_MANIFESTS_PATH: Models(
51
+ request=FetchManifests,
52
+ response=ManifestsPayload,
53
+ request_envelope=SignedEnvelope[FetchManifests],
54
+ response_envelope=SignedEnvelope[ManifestsPayload]
55
+ ),
56
+ FETCH_RIDS_PATH: Models(
57
+ request=FetchRids,
58
+ response=RidsPayload,
59
+ request_envelope=SignedEnvelope[FetchRids],
60
+ response_envelope=SignedEnvelope[RidsPayload]
61
+ )
62
+ }
@@ -0,0 +1,18 @@
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()
18
+ public_key: str | None = None