koi-net 1.0.0b12__tar.gz → 1.0.0b14__tar.gz

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 (33) hide show
  1. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/.gitignore +1 -0
  2. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/PKG-INFO +4 -2
  3. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/pyproject.toml +5 -3
  4. koi_net-1.0.0b14/src/koi_net/config.py +95 -0
  5. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/core.py +22 -22
  6. koi_net-1.0.0b14/src/koi_net/identity.py +42 -0
  7. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/network/interface.py +16 -14
  8. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/processor/interface.py +7 -3
  9. koi_net-1.0.0b12/src/koi_net/identity.py +0 -70
  10. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/.github/workflows/publish-to-pypi.yml +0 -0
  11. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/LICENSE +0 -0
  12. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/README.md +0 -0
  13. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/examples/basic_coordinator_node.py +0 -0
  14. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/examples/basic_partial_node.py +0 -0
  15. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/examples/full_node_template.py +0 -0
  16. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/examples/partial_node_template.py +0 -0
  17. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/requirements.txt +0 -0
  18. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/__init__.py +0 -0
  19. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/network/__init__.py +0 -0
  20. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/network/graph.py +0 -0
  21. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/network/request_handler.py +0 -0
  22. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/network/response_handler.py +0 -0
  23. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/processor/__init__.py +0 -0
  24. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/processor/default_handlers.py +0 -0
  25. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/processor/handler.py +0 -0
  26. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/processor/knowledge_object.py +0 -0
  27. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/protocol/__init__.py +0 -0
  28. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/protocol/api_models.py +0 -0
  29. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/protocol/consts.py +0 -0
  30. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/protocol/edge.py +0 -0
  31. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/protocol/event.py +0 -0
  32. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/protocol/helpers.py +0 -0
  33. {koi_net-1.0.0b12 → koi_net-1.0.0b14}/src/koi_net/protocol/node.py +0 -0
@@ -2,6 +2,7 @@ rid-lib
2
2
  __pycache__
3
3
  *.json
4
4
  venv
5
+ .env
5
6
  prototypes
6
7
  .vscode
7
8
  dist/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: koi-net
3
- Version: 1.0.0b12
3
+ Version: 1.0.0b14
4
4
  Summary: Implementation of KOI-net protocol in Python
5
5
  Project-URL: Homepage, https://github.com/BlockScience/koi-net/
6
6
  Author-email: Luke Miller <luke@block.science>
@@ -30,7 +30,9 @@ Requires-Python: >=3.10
30
30
  Requires-Dist: httpx>=0.28.1
31
31
  Requires-Dist: networkx>=3.4.2
32
32
  Requires-Dist: pydantic>=2.10.6
33
- Requires-Dist: rid-lib>=3.2.1
33
+ Requires-Dist: python-dotenv>=1.1.0
34
+ Requires-Dist: rid-lib>=3.2.3
35
+ Requires-Dist: ruamel-yaml>=0.18.10
34
36
  Provides-Extra: dev
35
37
  Requires-Dist: build; extra == 'dev'
36
38
  Requires-Dist: twine>=6.0; extra == 'dev'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "koi-net"
7
- version = "1.0.0-beta.12"
7
+ version = "1.0.0-beta.14"
8
8
  description = "Implementation of KOI-net protocol in Python"
9
9
  authors = [
10
10
  {name = "Luke Miller", email = "luke@block.science"}
@@ -13,10 +13,12 @@ readme = "README.md"
13
13
  requires-python = ">=3.10"
14
14
  license = {file = "LICENSE"}
15
15
  dependencies = [
16
- "rid-lib>=3.2.1",
16
+ "rid-lib>=3.2.3",
17
17
  "networkx>=3.4.2",
18
18
  "httpx>=0.28.1",
19
- "pydantic>=2.10.6"
19
+ "pydantic>=2.10.6",
20
+ "ruamel.yaml>=0.18.10",
21
+ "python-dotenv>=1.1.0"
20
22
  ]
21
23
 
22
24
  [project.optional-dependencies]
@@ -0,0 +1,95 @@
1
+ import os
2
+ from typing import TypeVar
3
+ from ruamel.yaml import YAML
4
+ from koi_net.protocol.node import NodeProfile
5
+ from rid_lib.types import KoiNetNode
6
+ from pydantic import BaseModel, Field, PrivateAttr
7
+ from dotenv import load_dotenv
8
+
9
+
10
+ class ServerConfig(BaseModel):
11
+ host: str | None = "127.0.0.1"
12
+ port: int | None = 8000
13
+ path: str | None = "/koi-net"
14
+
15
+ @property
16
+ def url(self):
17
+ return f"http://{self.host}:{self.port}{self.path or ''}"
18
+
19
+ class KoiNetConfig(BaseModel):
20
+ node_name: str
21
+ node_rid: KoiNetNode | None = None
22
+ node_profile: NodeProfile
23
+
24
+ cache_directory_path: str | None = ".rid_cache"
25
+ event_queues_path: str | None = "event_queues.json"
26
+
27
+ first_contact: str | None = None
28
+
29
+ class EnvConfig(BaseModel):
30
+ def __init__(self, **kwargs):
31
+ super().__init__(**kwargs)
32
+ load_dotenv()
33
+
34
+ def __getattribute__(self, name):
35
+ value = super().__getattribute__(name)
36
+ if name in type(self).model_fields:
37
+ env_val = os.getenv(value)
38
+ if env_val is None:
39
+ raise ValueError(f"Required environment variable {value} not set")
40
+ return env_val
41
+ return value
42
+
43
+ class Config(BaseModel):
44
+ server: ServerConfig | None = Field(default_factory=ServerConfig)
45
+ koi_net: KoiNetConfig
46
+ _file_path: str = PrivateAttr(default="config.yaml")
47
+ _file_content: str | None = PrivateAttr(default=None)
48
+
49
+ @classmethod
50
+ def load_from_yaml(
51
+ cls,
52
+ file_path: str | None = None,
53
+ generate_missing: bool = True
54
+ ):
55
+ yaml = YAML()
56
+
57
+ try:
58
+ with open(file_path, "r") as f:
59
+ file_content = f.read()
60
+ config_data = yaml.load(file_content)
61
+ config = cls.model_validate(config_data)
62
+ config._file_content = file_content
63
+
64
+ except FileNotFoundError:
65
+ config = cls()
66
+
67
+ config._file_path = file_path
68
+
69
+ if generate_missing:
70
+ config.koi_net.node_rid = (
71
+ config.koi_net.node_rid or KoiNetNode.generate(config.koi_net.node_name)
72
+ )
73
+ config.koi_net.node_profile.base_url = (
74
+ config.koi_net.node_profile.base_url or config.server.url
75
+ )
76
+
77
+ config.save_to_yaml()
78
+
79
+ return config
80
+
81
+ def save_to_yaml(self):
82
+ yaml = YAML()
83
+
84
+ with open(self._file_path, "w") as f:
85
+ try:
86
+ config_data = self.model_dump(mode="json")
87
+ yaml.dump(config_data, f)
88
+ except Exception as e:
89
+ if self._file_content:
90
+ f.seek(0)
91
+ f.truncate()
92
+ f.write(self._file_content)
93
+ raise e
94
+
95
+ ConfigType = TypeVar("ConfigType", bound=Config)
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from typing import Generic
2
3
  import httpx
3
4
  from rid_lib.ext import Cache, Bundle
4
5
  from .network import NetworkInterface
@@ -6,45 +7,43 @@ from .processor import ProcessorInterface
6
7
  from .processor import default_handlers
7
8
  from .processor.handler import KnowledgeHandler
8
9
  from .identity import NodeIdentity
9
- from .protocol.node import NodeProfile
10
10
  from .protocol.event import Event, EventType
11
+ from .config import ConfigType
11
12
 
12
13
  logger = logging.getLogger(__name__)
13
14
 
14
15
 
15
- class NodeInterface:
16
+
17
+ class NodeInterface(Generic[ConfigType]):
18
+ config: ConfigType
16
19
  cache: Cache
17
20
  identity: NodeIdentity
18
21
  network: NetworkInterface
19
22
  processor: ProcessorInterface
20
- first_contact: str
23
+
21
24
  use_kobj_processor_thread: bool
22
25
 
23
26
  def __init__(
24
27
  self,
25
- name: str,
26
- profile: NodeProfile,
27
- identity_file_path: str = "identity.json",
28
- event_queues_file_path: str = "event_queues.json",
29
- cache_directory_path: str = "rid_cache",
28
+ config: ConfigType,
30
29
  use_kobj_processor_thread: bool = False,
31
- first_contact: str | None = None,
30
+
32
31
  handlers: list[KnowledgeHandler] | None = None,
32
+
33
33
  cache: Cache | None = None,
34
34
  network: NetworkInterface | None = None,
35
35
  processor: ProcessorInterface | None = None
36
36
  ):
37
- self.cache = cache or Cache(cache_directory_path)
37
+ self.config: ConfigType = config
38
+ self.cache = cache or Cache(
39
+ self.config.koi_net.cache_directory_path)
40
+
38
41
  self.identity = NodeIdentity(
39
- name=name,
40
- profile=profile,
41
- cache=self.cache,
42
- file_path=identity_file_path
43
- )
44
- self.first_contact = first_contact
42
+ config=self.config,
43
+ cache=self.cache)
44
+
45
45
  self.network = network or NetworkInterface(
46
- file_path=event_queues_file_path,
47
- first_contact=self.first_contact,
46
+ config=self.config,
48
47
  cache=self.cache,
49
48
  identity=self.identity
50
49
  )
@@ -58,13 +57,14 @@ class NodeInterface:
58
57
 
59
58
  self.use_kobj_processor_thread = use_kobj_processor_thread
60
59
  self.processor = processor or ProcessorInterface(
60
+ config=self.config,
61
61
  cache=self.cache,
62
62
  network=self.network,
63
63
  identity=self.identity,
64
64
  use_kobj_processor_thread=self.use_kobj_processor_thread,
65
65
  default_handlers=handlers
66
66
  )
67
-
67
+
68
68
  def start(self) -> None:
69
69
  """Starts a node, call this method first.
70
70
 
@@ -91,8 +91,8 @@ class NodeInterface:
91
91
  self.processor.flush_kobj_queue()
92
92
  logger.debug("Done")
93
93
 
94
- if not self.network.graph.get_neighbors() and self.first_contact:
95
- logger.debug(f"I don't have any neighbors, reaching out to first contact {self.first_contact}")
94
+ if not self.network.graph.get_neighbors() and self.config.koi_net.first_contact:
95
+ logger.debug(f"I don't have any neighbors, reaching out to first contact {self.config.koi_net.first_contact}")
96
96
 
97
97
  events = [
98
98
  Event.from_rid(EventType.FORGET, self.identity.rid),
@@ -101,7 +101,7 @@ class NodeInterface:
101
101
 
102
102
  try:
103
103
  self.network.request_handler.broadcast_events(
104
- url=self.first_contact,
104
+ url=self.config.koi_net.first_contact,
105
105
  events=events
106
106
  )
107
107
 
@@ -0,0 +1,42 @@
1
+ import logging
2
+ from rid_lib.ext.bundle import Bundle
3
+ from rid_lib.ext.cache import Cache
4
+ from rid_lib.types.koi_net_node import KoiNetNode
5
+
6
+ from .config import Config
7
+ from .protocol.node import NodeProfile
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class NodeIdentity:
13
+ """Represents a node's identity (RID, profile, bundle)."""
14
+
15
+ config: Config
16
+ cache: Cache
17
+
18
+ def __init__(
19
+ self,
20
+ config: Config,
21
+ cache: Cache
22
+ ):
23
+ """Initializes node identity from a name and profile.
24
+
25
+ Attempts to read identity from storage. If it doesn't already exist, a new RID is generated from the provided name, and that RID and profile are written to storage. Changes to the name or profile will update the stored identity.
26
+
27
+ WARNING: If the name is changed, the RID will be overwritten which will have consequences for the rest of the network.
28
+ """
29
+ self.config = config
30
+ self.cache = cache
31
+
32
+ @property
33
+ def rid(self) -> KoiNetNode:
34
+ return self.config.koi_net.node_rid
35
+
36
+ @property
37
+ def profile(self) -> NodeProfile:
38
+ return self.config.koi_net.node_profile
39
+
40
+ @property
41
+ def bundle(self) -> Bundle:
42
+ return self.cache.read(self.rid)
@@ -1,11 +1,13 @@
1
1
  import logging
2
2
  from queue import Queue
3
+ from typing import Generic
3
4
  import httpx
4
5
  from pydantic import BaseModel
5
6
  from rid_lib import RID
6
7
  from rid_lib.core import RIDType
7
8
  from rid_lib.ext import Cache
8
9
  from rid_lib.types import KoiNetNode
10
+
9
11
  from .graph import NetworkGraph
10
12
  from .request_handler import RequestHandler
11
13
  from .response_handler import ResponseHandler
@@ -13,6 +15,7 @@ from ..protocol.node import NodeType
13
15
  from ..protocol.edge import EdgeType
14
16
  from ..protocol.event import Event
15
17
  from ..identity import NodeIdentity
18
+ from ..config import Config, ConfigType
16
19
 
17
20
  logger = logging.getLogger(__name__)
18
21
 
@@ -23,33 +26,30 @@ class EventQueueModel(BaseModel):
23
26
 
24
27
  type EventQueue = dict[RID, Queue[Event]]
25
28
 
26
- class NetworkInterface:
29
+ class NetworkInterface(Generic[ConfigType]):
27
30
  """A collection of functions and classes to interact with the KOI network."""
28
31
 
32
+ config: ConfigType
29
33
  identity: NodeIdentity
30
34
  cache: Cache
31
- first_contact: str | None
32
35
  graph: NetworkGraph
33
36
  request_handler: RequestHandler
34
37
  response_handler: ResponseHandler
35
38
  poll_event_queue: EventQueue
36
39
  webhook_event_queue: EventQueue
37
- event_queues_file_path: str
38
40
 
39
41
  def __init__(
40
42
  self,
41
- file_path: str,
42
- first_contact: str | None,
43
+ config: ConfigType,
43
44
  cache: Cache,
44
45
  identity: NodeIdentity
45
46
  ):
47
+ self.config = config
46
48
  self.identity = identity
47
49
  self.cache = cache
48
- self.first_contact = first_contact
49
50
  self.graph = NetworkGraph(cache, identity)
50
51
  self.request_handler = RequestHandler(cache, self.graph)
51
52
  self.response_handler = ResponseHandler(cache)
52
- self.event_queues_file_path = file_path
53
53
 
54
54
  self.poll_event_queue = dict()
55
55
  self.webhook_event_queue = dict()
@@ -58,7 +58,7 @@ class NetworkInterface:
58
58
  def _load_event_queues(self):
59
59
  """Loads event queues from storage."""
60
60
  try:
61
- with open(self.event_queues_file_path, "r") as f:
61
+ with open(self.config.koi_net.event_queues_path, "r") as f:
62
62
  queues = EventQueueModel.model_validate_json(f.read())
63
63
 
64
64
  for node in queues.poll.keys():
@@ -92,7 +92,7 @@ class NetworkInterface:
92
92
  if len(events_model.poll) == 0 and len(events_model.webhook) == 0:
93
93
  return
94
94
 
95
- with open(self.event_queues_file_path, "w") as f:
95
+ with open(self.config.koi_net.event_queues_path, "w") as f:
96
96
  f.write(events_model.model_dump_json(indent=2))
97
97
 
98
98
  def push_event_to(self, event: Event, node: KoiNetNode, flush=False):
@@ -171,10 +171,12 @@ class NetworkInterface:
171
171
 
172
172
  try:
173
173
  self.request_handler.broadcast_events(node, events=events)
174
+ return True
174
175
  except httpx.ConnectError:
175
- logger.warning("Broadcast failed, requeuing events")
176
+ logger.warning("Broadcast failed, dropping node")
176
177
  for event in events:
177
178
  self.push_event_to(event, node)
179
+ return False
178
180
 
179
181
  def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
180
182
  """Returns list of node RIDs which provide state for the specified RID type."""
@@ -238,18 +240,18 @@ class NetworkInterface:
238
240
 
239
241
  neighbors = self.graph.get_neighbors()
240
242
 
241
- if not neighbors and self.first_contact:
243
+ if not neighbors and self.config.koi_net.first_contact:
242
244
  logger.debug("No neighbors found, polling first contact")
243
245
  try:
244
246
  payload = self.request_handler.poll_events(
245
- url=self.first_contact,
247
+ url=self.config.koi_net.first_contact,
246
248
  rid=self.identity.rid
247
249
  )
248
250
  if payload.events:
249
- logger.debug(f"Received {len(payload.events)} events from '{self.first_contact}'")
251
+ logger.debug(f"Received {len(payload.events)} events from '{self.config.koi_net.first_contact}'")
250
252
  return payload.events
251
253
  except httpx.ConnectError:
252
- logger.debug(f"Failed to reach first contact '{self.first_contact}'")
254
+ logger.debug(f"Failed to reach first contact '{self.config.koi_net.first_contact}'")
253
255
 
254
256
  events = []
255
257
  for node_rid in neighbors:
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import queue
3
3
  import threading
4
- from typing import Callable
4
+ from typing import Callable, Generic
5
5
  from rid_lib.core import RID, RIDType
6
6
  from rid_lib.ext import Bundle, Cache, Manifest
7
7
  from rid_lib.types.koi_net_edge import KoiNetEdge
@@ -9,6 +9,7 @@ from rid_lib.types.koi_net_node import KoiNetNode
9
9
  from ..identity import NodeIdentity
10
10
  from ..network import NetworkInterface
11
11
  from ..protocol.event import Event, EventType
12
+ from ..config import Config
12
13
  from .handler import (
13
14
  KnowledgeHandler,
14
15
  HandlerType,
@@ -24,9 +25,10 @@ from .knowledge_object import (
24
25
  logger = logging.getLogger(__name__)
25
26
 
26
27
 
27
- class ProcessorInterface:
28
+ class ProcessorInterface():
28
29
  """Provides access to this node's knowledge processing pipeline."""
29
30
 
31
+ config: Config
30
32
  cache: Cache
31
33
  network: NetworkInterface
32
34
  identity: NodeIdentity
@@ -36,13 +38,15 @@ class ProcessorInterface:
36
38
  worker_thread: threading.Thread | None = None
37
39
 
38
40
  def __init__(
39
- self,
41
+ self,
42
+ config: Config,
40
43
  cache: Cache,
41
44
  network: NetworkInterface,
42
45
  identity: NodeIdentity,
43
46
  use_kobj_processor_thread: bool,
44
47
  default_handlers: list[KnowledgeHandler] = []
45
48
  ):
49
+ self.config = config
46
50
  self.cache = cache
47
51
  self.network = network
48
52
  self.identity = identity
@@ -1,70 +0,0 @@
1
- import logging
2
- from pydantic import BaseModel
3
- from rid_lib.ext.bundle import Bundle
4
- from rid_lib.ext.cache import Cache
5
- from rid_lib.types.koi_net_node import KoiNetNode
6
- from .protocol.node import NodeProfile
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
-
11
- class NodeIdentityModel(BaseModel):
12
- rid: KoiNetNode
13
- profile: NodeProfile
14
-
15
- class NodeIdentity:
16
- """Represents a node's identity (RID, profile, bundle)."""
17
-
18
- _identity: NodeIdentityModel
19
- file_path: str
20
- cache: Cache
21
-
22
- def __init__(
23
- self,
24
- name: str,
25
- profile: NodeProfile,
26
- cache: Cache,
27
- file_path: str = "identity.json"
28
- ):
29
- """Initializes node identity from a name and profile.
30
-
31
- Attempts to read identity from storage. If it doesn't already exist, a new RID is generated from the provided name, and that RID and profile are written to storage. Changes to the name or profile will update the stored identity.
32
-
33
- WARNING: If the name is changed, the RID will be overwritten which will have consequences for the rest of the network.
34
- """
35
- self.cache = cache
36
- self.file_path = file_path
37
-
38
- self._identity = None
39
- try:
40
- with open(file_path, "r") as f:
41
- self._identity = NodeIdentityModel.model_validate_json(f.read())
42
-
43
- except FileNotFoundError:
44
- pass
45
-
46
- if self._identity:
47
- if self._identity.rid.name != name:
48
- logger.warning("Node name changed which will change this node's RID, if you really want to do this manually delete the identity JSON file")
49
- if self._identity.profile != profile:
50
- self._identity.profile = profile
51
- else:
52
- self._identity = NodeIdentityModel(
53
- rid=KoiNetNode.generate(name),
54
- profile=profile,
55
- )
56
-
57
- with open(file_path, "w") as f:
58
- f.write(self._identity.model_dump_json(indent=2))
59
-
60
- @property
61
- def rid(self) -> KoiNetNode:
62
- return self._identity.rid
63
-
64
- @property
65
- def profile(self) -> NodeProfile:
66
- return self._identity.profile
67
-
68
- @property
69
- def bundle(self) -> Bundle:
70
- return self.cache.read(self.rid)
File without changes
File without changes
File without changes