Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ rid-lib
__pycache__
*.json
venv
.env
prototypes
.vscode
dist/
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "koi-net"
version = "1.0.0-beta.12"
version = "1.0.0-beta.13"
description = "Implementation of KOI-net protocol in Python"
authors = [
{name = "Luke Miller", email = "luke@block.science"}
Expand Down
95 changes: 95 additions & 0 deletions src/koi_net/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import os
from typing import TypeVar
from ruamel.yaml import YAML
from koi_net.protocol.node import NodeProfile
from rid_lib.types import KoiNetNode
from pydantic import BaseModel, Field, PrivateAttr
from dotenv import load_dotenv


class ServerConfig(BaseModel):
host: str | None = "127.0.0.1"
port: int | None = 8000
path: str | None = None

@property
def url(self):
return f"http://{self.host}:{self.port}{self.path or ''}"

class KoiNetConfig(BaseModel):
node_name: str
node_rid: KoiNetNode | None = None
node_profile: NodeProfile

cache_directory_path: str | None = ".rid_cache"
event_queues_path: str | None = "event_queues.json"

first_contact: str | None = None

class EnvConfig(BaseModel):
def __init__(self, **kwargs):
super().__init__(**kwargs)
load_dotenv()

def __getattribute__(self, name):
value = super().__getattribute__(name)
if name in type(self).model_fields:
env_val = os.getenv(value)
if env_val is None:
raise ValueError(f"Required environment variable {value} not set")
return env_val
return value

class Config(BaseModel):
server: ServerConfig | None = Field(default_factory=ServerConfig)
koi_net: KoiNetConfig
_file_path: str = PrivateAttr(default="config.yaml")
_file_content: str | None = PrivateAttr(default=None)

@classmethod
def load_from_yaml(
cls,
file_path: str | None = None,
generate_missing: bool = True
):
yaml = YAML()

try:
with open(file_path, "r") as f:
file_content = f.read()
config_data = yaml.load(file_content)
config = cls.model_validate(config_data)
config._file_content = file_content

except FileNotFoundError:
config = cls()

config._file_path = file_path

if generate_missing:
config.koi_net.node_rid = (
config.koi_net.node_rid or KoiNetNode.generate(config.koi_net.node_name)
)
config.koi_net.node_profile.base_url = (
config.koi_net.node_profile.base_url or config.server.url
)

config.save_to_yaml()

return config

def save_to_yaml(self):
yaml = YAML()

with open(self._file_path, "w") as f:
try:
config_data = self.model_dump(mode="json")
yaml.dump(config_data, f)
except Exception as e:
if self._file_content:
f.seek(0)
f.truncate()
f.write(self._file_content)
raise e

ConfigType = TypeVar("ConfigType", bound=Config)
44 changes: 22 additions & 22 deletions src/koi_net/core.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,49 @@
import logging
from typing import Generic
import httpx
from rid_lib.ext import Cache, Bundle
from .network import NetworkInterface
from .processor import ProcessorInterface
from .processor import default_handlers
from .processor.handler import KnowledgeHandler
from .identity import NodeIdentity
from .protocol.node import NodeProfile
from .protocol.event import Event, EventType
from .config import ConfigType

logger = logging.getLogger(__name__)


class NodeInterface:

class NodeInterface(Generic[ConfigType]):
config: ConfigType
cache: Cache
identity: NodeIdentity
network: NetworkInterface
processor: ProcessorInterface
first_contact: str

use_kobj_processor_thread: bool

def __init__(
self,
name: str,
profile: NodeProfile,
identity_file_path: str = "identity.json",
event_queues_file_path: str = "event_queues.json",
cache_directory_path: str = "rid_cache",
config: ConfigType,
use_kobj_processor_thread: bool = False,
first_contact: str | None = None,

handlers: list[KnowledgeHandler] | None = None,

cache: Cache | None = None,
network: NetworkInterface | None = None,
processor: ProcessorInterface | None = None
):
self.cache = cache or Cache(cache_directory_path)
self.config: ConfigType = config
self.cache = cache or Cache(
self.config.koi_net.cache_directory_path)

self.identity = NodeIdentity(
name=name,
profile=profile,
cache=self.cache,
file_path=identity_file_path
)
self.first_contact = first_contact
config=self.config,
cache=self.cache)

self.network = network or NetworkInterface(
file_path=event_queues_file_path,
first_contact=self.first_contact,
config=self.config,
cache=self.cache,
identity=self.identity
)
Expand All @@ -58,13 +57,14 @@ def __init__(

self.use_kobj_processor_thread = use_kobj_processor_thread
self.processor = processor or ProcessorInterface(
config=self.config,
cache=self.cache,
network=self.network,
identity=self.identity,
use_kobj_processor_thread=self.use_kobj_processor_thread,
default_handlers=handlers
)

def start(self) -> None:
"""Starts a node, call this method first.

Expand All @@ -91,8 +91,8 @@ def start(self) -> None:
self.processor.flush_kobj_queue()
logger.debug("Done")

if not self.network.graph.get_neighbors() and self.first_contact:
logger.debug(f"I don't have any neighbors, reaching out to first contact {self.first_contact}")
if not self.network.graph.get_neighbors() and self.config.koi_net.first_contact:
logger.debug(f"I don't have any neighbors, reaching out to first contact {self.config.koi_net.first_contact}")

events = [
Event.from_rid(EventType.FORGET, self.identity.rid),
Expand All @@ -101,7 +101,7 @@ def start(self) -> None:

try:
self.network.request_handler.broadcast_events(
url=self.first_contact,
url=self.config.koi_net.first_contact,
events=events
)

Expand Down
46 changes: 9 additions & 37 deletions src/koi_net/identity.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,41 @@
import logging
from pydantic import BaseModel
from rid_lib.ext.bundle import Bundle
from rid_lib.ext.cache import Cache
from rid_lib.types.koi_net_node import KoiNetNode

from .config import Config
from .protocol.node import NodeProfile

logger = logging.getLogger(__name__)


class NodeIdentityModel(BaseModel):
rid: KoiNetNode
profile: NodeProfile

class NodeIdentity:
"""Represents a node's identity (RID, profile, bundle)."""

_identity: NodeIdentityModel
file_path: str
config: Config
cache: Cache

def __init__(
self,
name: str,
profile: NodeProfile,
cache: Cache,
file_path: str = "identity.json"
self,
config: Config,
cache: Cache
):
"""Initializes node identity from a name and profile.

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.

WARNING: If the name is changed, the RID will be overwritten which will have consequences for the rest of the network.
"""
self.config = config
self.cache = cache
self.file_path = file_path

self._identity = None
try:
with open(file_path, "r") as f:
self._identity = NodeIdentityModel.model_validate_json(f.read())

except FileNotFoundError:
pass

if self._identity:
if self._identity.rid.name != name:
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")
if self._identity.profile != profile:
self._identity.profile = profile
else:
self._identity = NodeIdentityModel(
rid=KoiNetNode.generate(name),
profile=profile,
)

with open(file_path, "w") as f:
f.write(self._identity.model_dump_json(indent=2))

@property
def rid(self) -> KoiNetNode:
return self._identity.rid
return self.config.koi_net.node_rid

@property
def profile(self) -> NodeProfile:
return self._identity.profile
return self.config.koi_net.node_profile

@property
def bundle(self) -> Bundle:
Expand Down
Loading