diff --git a/veilid-core/proto/veilid.capnp b/veilid-core/proto/veilid.capnp index b7f1a359..59af6722 100644 --- a/veilid-core/proto/veilid.capnp +++ b/veilid-core/proto/veilid.capnp @@ -230,7 +230,7 @@ struct NodeInfo @0xe125d847e3f9f419 { outboundProtocols @1 :ProtocolTypeSet; # protocols that can go outbound addressTypes @2 :AddressTypeSet; # address types supported envelopeSupport @3 :List(UInt8); # supported rpc envelope/receipt versions - cryptoSupport @4 :List(CryptoKind); # cryptography systems supported + cryptoSupport @4 :List( ); # cryptography systems supported dialInfoDetailList @5 :List(DialInfoDetail); # inbound dial info details for this node } @@ -534,7 +534,8 @@ struct Answer @0xacacb8b6988c1058 { appCallA @2 :OperationAppCallA; getValueA @3 :OperationGetValueA; setValueA @4 :OperationSetValueA; - watchValueA @5 :OperationWatchValueA; + watchValueA @5 :OperationWatchValueA; + # #[cfg(feature="unstable-blockstore")] #supplyBlockA @6 :OperationSupplyBlockA; #findBlockA @7 :OperationFindBlockA; diff --git a/veilid-python/veilid_python/__init__.py b/veilid-python/veilid_python/__init__.py index cc512528..e69de29b 100644 --- a/veilid-python/veilid_python/__init__.py +++ b/veilid-python/veilid_python/__init__.py @@ -1 +0,0 @@ -from .error import * diff --git a/veilid-python/veilid_python/api.py b/veilid-python/veilid_python/api.py index abd9a10f..356866d8 100644 --- a/veilid-python/veilid_python/api.py +++ b/veilid-python/veilid_python/api.py @@ -1,11 +1,211 @@ from abc import ABC, abstractmethod -from .state import VeilidState +from typing import Self + +from .state import * +from .config import * +from .error import * +from .types import * + +class RoutingContext(ABC): + @abstractmethod + async def with_privacy(self) -> Self: + pass + @abstractmethod + async def with_custom_privacy(self, stability: Stability) -> Self: + pass + @abstractmethod + async def with_sequencing(self, sequencing: Sequencing) -> Self: + pass + @abstractmethod + async def app_call(self, target: TypedKey | RouteId, request: bytes) -> bytes: + pass + @abstractmethod + async def app_message(self, target: TypedKey | RouteId, message: bytes): + pass + @abstractmethod + async def create_dht_record(self, kind: CryptoKind, schema: DHTSchema) -> DHTRecordDescriptor: + pass + @abstractmethod + async def open_dht_record(self, key: TypedKey, writer: Optional[KeyPair]) -> DHTRecordDescriptor: + pass + @abstractmethod + async def close_dht_record(self, key: TypedKey): + pass + @abstractmethod + async def delete_dht_record(self, key: TypedKey): + pass + @abstractmethod + async def get_dht_value(self, key: TypedKey, subkey: ValueSubkey, force_refresh: bool) -> Optional[ValueData]: + pass + @abstractmethod + async def set_dht_value(self, key: TypedKey, subkey: ValueSubkey, data: bytes) -> Optional[ValueData]: + pass + @abstractmethod + async def watch_dht_values(self, key: TypedKey, subkeys: list[(ValueSubkey, ValueSubkey)], expiration: Timestamp, count: int) -> Timestamp: + pass + @abstractmethod + async def cancel_dht_values(self, key: TypedKey, subkeys: list[(ValueSubkey, ValueSubkey)]) -> bool: + pass + + +class TableDBTransaction(ABC): + @abstractmethod + async def commit(self): + pass + @abstractmethod + async def rollback(self): + pass + @abstractmethod + async def store(self, col: int, key: bytes, value: bytes): + pass + @abstractmethod + async def delete(self, col: int, key: bytes): + pass + +class TableDB(ABC): + @abstractmethod + async def get_column_count(self) -> int: + pass + @abstractmethod + async def get_keys(self, col: int) -> list[str]: + pass + @abstractmethod + async def transact(self) -> TableDBTransaction: + pass + @abstractmethod + async def store(self, col: int, key: bytes, value: bytes): + pass + @abstractmethod + async def load(self, col: int, key: bytes) -> Optional[bytes]: + pass + @abstractmethod + async def delete(self, col: int, key: bytes) -> Optional[bytes]: + pass + +class CryptoSystem(ABC): + @abstractmethod + async def cached_dh(self, key: PublicKey, secret: SecretKey) -> SharedSecret: + pass + @abstractmethod + async def compute_dh(self, key: PublicKey, secret: SecretKey) -> SharedSecret: + pass + @abstractmethod + async def random_bytes(self, len: int) -> bytes: + pass + @abstractmethod + async def default_salt_length(self) -> int: + pass + @abstractmethod + async def hash_password(self, password: bytes, salt: bytes) -> str: + pass + @abstractmethod + async def verify_password(self, password: bytes, password_hash: str) -> bool: + pass + @abstractmethod + async def derive_shared_secret(self, password: bytes, salt: bytes) -> SharedSecret: + pass + @abstractmethod + async def random_nonce(self) -> Nonce: + pass + @abstractmethod + async def random_shared_secret(self) -> SharedSecret: + pass + @abstractmethod + async def generate_key_pair(self) -> KeyPair: + pass + @abstractmethod + async def generate_hash(self, data: bytes) -> HashDigest: + pass + @abstractmethod + async def validate_key_pair(self, key: PublicKey, secret: SecretKey) -> bool: + pass + @abstractmethod + async def validate_hash(self, data: bytes, hash_digest: HashDigest) -> bool: + pass + @abstractmethod + async def distance(self, key1: CryptoKey, key2: CryptoKey) -> CryptoKeyDistance: + pass + @abstractmethod + async def sign(self, key: PublicKey, secret: SecretKey, data: bytes) -> Signature: + pass + @abstractmethod + async def verify(self, key: PublicKey, data: bytes, signature: Signature): + pass + @abstractmethod + async def aead_overhead(self) -> int: + pass + @abstractmethod + async def decrypt_aead(self, body: bytes, nonce: Nonce, shared_secret: SharedSecret, associated_data: Optional[bytes]) -> bytes: + pass + @abstractmethod + async def encrypt_aead(self, body: bytes, nonce: Nonce, shared_secret: SharedSecret, associated_data: Optional[bytes]) -> bytes: + pass + @abstractmethod + async def crypt_no_auth(self, body: bytes, nonce: Nonce, shared_secret: SharedSecret) -> bytes: + pass + class VeilidAPI(ABC): @abstractmethod - def control(self, args: list[str]) -> str: + async def control(self, args: list[str]) -> str: pass @abstractmethod - def get_state(self) -> VeilidState: + async def get_state(self) -> VeilidState: + pass + @abstractmethod + async def attach(self): + pass + @abstractmethod + async def detach(self): + pass + @abstractmethod + async def new_private_route(self) -> NewPrivateRouteResult: + pass + @abstractmethod + async def new_custom_private_route(self, kinds: list[CryptoKind], stability: Stability, sequencing: Sequencing) -> NewPrivateRouteResult: + pass + @abstractmethod + async def import_remote_private_route(self, blob: bytes) -> RouteId: + pass + @abstractmethod + async def release_private_route(self, route_id: RouteId): + pass + @abstractmethod + async def app_call_reply(self, call_id: OperationId, message: bytes): + pass + @abstractmethod + async def new_routing_context(self) -> RoutingContext: + pass + @abstractmethod + async def open_table_db(self, name: str, column_count: int) -> TableDB: + pass + @abstractmethod + async def delete_table_db(self, name: str): + pass + @abstractmethod + async def get_crypto_system(self, kind: CryptoKind) -> CryptoSystem: + pass + @abstractmethod + async def best_crypto_system(self) -> CryptoSystem: + pass + @abstractmethod + async def verify_signatures(self, node_ids: list[TypedKey], data: bytes, signatures: list[TypedSignature]) -> list[TypedKey]: + pass + @abstractmethod + async def generate_signatures(self, data: bytes, key_pairs: list[TypedKeyPair]) -> list[TypedSignature]: + pass + @abstractmethod + async def generate_key_pair(self, kind: CryptoKind) -> list[TypedKeyPair]: + pass + @abstractmethod + async def now(self) -> Timestamp: + pass + @abstractmethod + async def debug(self, command: str) -> str: + pass + @abstractmethod + async def veilid_version_string(self) -> str: + pass + @abstractmethod + async def veilid_version(self) -> VeilidVersion: pass - \ No newline at end of file diff --git a/veilid-python/veilid_python/config.py b/veilid-python/veilid_python/config.py index ba520cc6..d44bc11a 100644 --- a/veilid-python/veilid_python/config.py +++ b/veilid-python/veilid_python/config.py @@ -1,5 +1,6 @@ from typing import Self, Optional from enum import StrEnum +from json import dumps class VeilidConfigLogLevel(StrEnum): OFF = 'Off' @@ -38,6 +39,8 @@ class VeilidConfigCapabilities: j['protocol_accept_ws'], j['protocol_connect_wss'], j['protocol_accept_wss']) + def to_json(self) -> dict: + return self.__dict__ class VeilidConfigProtectedStore: allow_insecure_fallback: bool @@ -61,6 +64,8 @@ class VeilidConfigProtectedStore: def from_json(j: dict) -> Self: return VeilidConfigProtectedStore(j['allow_insecure_fallback'], j['always_use_insecure_storage'], j['directory'], j['delete'], j['device_encryption_key_password'], j['new_device_encryption_key_password']) + def to_json(self) -> dict: + return self.__dict__ class VeilidConfigTableStore: directory: str @@ -73,6 +78,8 @@ class VeilidConfigTableStore: @staticmethod def from_json(j: dict) -> Self: return VeilidConfigTableStore(j['directory'], j['delete']) + def to_json(self) -> dict: + return self.__dict__ class VeilidConfigBlockStore: directory: str @@ -85,6 +92,8 @@ class VeilidConfigBlockStore: @staticmethod def from_json(j: dict) -> Self: return VeilidConfigBlockStore(j['directory'], j['delete']) + def to_json(self) -> dict: + return self.__dict__ class VeilidConfigRoutingTable: node_id: list[str] @@ -119,6 +128,8 @@ class VeilidConfigRoutingTable: j['limit_attached_strong'], j['limit_attached_good'], j['limit_attached_weak']) + def to_json(self) -> dict: + return self.__dict__ class VeilidConfigRPC: @@ -151,6 +162,286 @@ class VeilidConfigRPC: j['timeout_ms'], j['max_route_hop_count'], j['default_route_hop_count']) + def to_json(self) -> dict: + return self.__dict__ + +class VeilidConfigDHT: + max_find_node_count: int + resolve_node_timeout_ms: int + resolve_node_count: int + resolve_node_fanout: int + get_value_timeout_ms: int + get_value_count: int + get_value_fanout: int + set_value_timeout_ms: int + set_value_count: int + set_value_fanout: int + min_peer_count: int + min_peer_refresh_time_ms: int + validate_dial_info_receipt_time_ms: int + local_subkey_cache_size: int + local_max_subkey_cache_memory_mb: int + remote_subkey_cache_size: int + remote_max_records: int + remote_max_subkey_cache_memory_mb: int + remote_max_storage_space_mb: int + + def __init__(self, max_find_node_count: int, resolve_node_timeout_ms: int, resolve_node_count: int, + resolve_node_fanout: int, get_value_timeout_ms: int, get_value_count: int, get_value_fanout: int, + set_value_timeout_ms: int, set_value_count: int, set_value_fanout: int, + min_peer_count: int, min_peer_refresh_time_ms: int, validate_dial_info_receipt_time_ms: int, + local_subkey_cache_size: int, local_max_subkey_cache_memory_mb: int, + remote_subkey_cache_size: int, remote_max_records: int, remote_max_subkey_cache_memory_mb: int, remote_max_storage_space_mb: int): + + self.max_find_node_count = max_find_node_count + self.resolve_node_timeout_ms =resolve_node_timeout_ms + self.resolve_node_count = resolve_node_count + self.resolve_node_fanout = resolve_node_fanout + self.get_value_timeout_ms = get_value_timeout_ms + self.get_value_count = get_value_count + self.get_value_fanout = get_value_fanout + self.set_value_timeout_ms = set_value_timeout_ms + self.set_value_count = set_value_count + self.set_value_fanout = set_value_fanout + self.min_peer_count = min_peer_count + self.min_peer_refresh_time_ms = min_peer_refresh_time_ms + self.validate_dial_info_receipt_time_ms = validate_dial_info_receipt_time_ms + self.local_subkey_cache_size = local_subkey_cache_size + self.local_max_subkey_cache_memory_mb = local_max_subkey_cache_memory_mb + self.remote_subkey_cache_size = remote_subkey_cache_size + self.remote_max_records = remote_max_records + self.remote_max_subkey_cache_memory_mb = remote_max_subkey_cache_memory_mb + self.remote_max_storage_space_mb = remote_max_storage_space_mb + + @staticmethod + def from_json(j: dict) -> Self: + return VeilidConfigDHT( + j['max_find_node_count'], + j['resolve_node_timeout_ms'], + j['resolve_node_count'], + j['resolve_node_fanout'], + j['get_value_timeout_ms'], + j['get_value_count'], + j['get_value_fanout'], + j['set_value_timeout_ms'], + j['set_value_count'], + j['set_value_fanout'], + j['min_peer_count'], + j['min_peer_refresh_time_ms'], + j['validate_dial_info_receipt_time_ms'], + j['local_subkey_cache_size'], + j['local_max_subkey_cache_memory_mb'], + j['remote_subkey_cache_size'], + j['remote_max_records'], + j['remote_max_subkey_cache_memory_mb'], + j['remote_max_storage_space_mb']) + def to_json(self) -> dict: + return self.__dict__ + +class VeilidConfigTLS: + certificate_path: str + private_key_path: str + connection_initial_timeout_ms: int + + def __init__(self, certificate_path: str, private_key_path: str, connection_initial_timeout_ms: int): + self.certificate_path = certificate_path + self.private_key_path = private_key_path + self.connection_initial_timeout_ms = connection_initial_timeout_ms + + @staticmethod + def from_json(j: dict) -> Self: + return VeilidConfigTLS( + j['certificate_path'], + j['private_key_path'], + j['connection_initial_timeout_ms']) + def to_json(self) -> dict: + return self.__dict__ + +class VeilidConfigHTTPS: + enabled: bool + listen_address: str + path: str + url: Optional[str] + + def __init__(self, enabled: bool, listen_address: str, path: str, url: Optional[str]): + self.enabled = enabled + self.listen_address = listen_address + self.path = path + self.url = url + + @staticmethod + def from_json(j: dict) -> Self: + return VeilidConfigHTTPS( + j['enabled'], + j['listen_address'], + j['path'], + j['url']) + def to_json(self) -> dict: + return self.__dict__ + +class VeilidConfigHTTP: + enabled: bool + listen_address: str + path: str + url: Optional[str] + + def __init__(self, enabled: bool, listen_address: str, path: str, url: Optional[str]): + self.enabled = enabled + self.listen_address = listen_address + self.path = path + self.url = url + + @staticmethod + def from_json(j: dict) -> Self: + return VeilidConfigHTTP( + j['enabled'], + j['listen_address'], + j['path'], + j['url']) + def to_json(self) -> dict: + return self.__dict__ + +class VeilidConfigApplication: + https: VeilidConfigHTTPS + http: VeilidConfigHTTP + + def __init__(self, https: VeilidConfigHTTPS, http: VeilidConfigHTTP): + self.https = https + self.http = http + + @staticmethod + def from_json(j: dict) -> Self: + return VeilidConfigApplication( + VeilidConfigHTTPS.from_json(j['https']), + VeilidConfigHTTP.from_json(j['http'])) + def to_json(self) -> dict: + return self.__dict__ + + +class VeilidConfigUDP: + enabled: bool + socket_pool_size: int + listen_address: str + public_address: Optional[str] + + def __init__(self, enabled: bool, socket_pool_size: int, listen_address: str, public_address: Optional[str]): + self.enabled = enabled + self.socket_pool_size = socket_pool_size + self.listen_address = listen_address + self.public_address = public_address + + @staticmethod + def from_json(j: dict) -> Self: + return VeilidConfigUDP( + j['enabled'], + j['socket_pool_size'], + j['listen_address'], + j['public_address']) + def to_json(self) -> dict: + return self.__dict__ + +class VeilidConfigTCP: + connect: bool + listen: bool + max_connections: int + listen_address: str + public_address: Optional[str] + + def __init__(self, connect: bool, listen: bool, max_connections: int, listen_address: str, public_address: Optional[str]): + self.connect = connect + self.listen = listen + self.max_connections = max_connections + self.listen_address = listen_address + self.public_address = public_address + + @staticmethod + def from_json(j: dict) -> Self: + return VeilidConfigTCP( + j['connect'], + j['listen'], + j['max_connections'], + j['listen_address'], + j['public_address']) + def to_json(self) -> dict: + return self.__dict__ + +class VeilidConfigWS: + connect: bool + listen: bool + max_connections: int + listen_address: str + path: str + url: Optional[str] + + def __init__(self, connect: bool, listen: bool, max_connections: int, listen_address: str, path: str, url: Optional[str]): + self.connect = connect + self.listen = listen + self.max_connections = max_connections + self.listen_address = listen_address + self.path = path + self.url = url + + @staticmethod + def from_json(j: dict) -> Self: + return VeilidConfigWS( + j['connect'], + j['listen'], + j['max_connections'], + j['listen_address'], + j['path'], + j['url']) + def to_json(self) -> dict: + return self.__dict__ + +class VeilidConfigWSS: + connect: bool + listen: bool + max_connections: int + listen_address: str + path: str + url: Optional[str] + + def __init__(self, connect: bool, listen: bool, max_connections: int, listen_address: str, path: str, url: Optional[str]): + self.connect = connect + self.listen = listen + self.max_connections = max_connections + self.listen_address = listen_address + self.path = path + self.url = url + + @staticmethod + def from_json(j: dict) -> Self: + return VeilidConfigWSS( + j['connect'], + j['listen'], + j['max_connections'], + j['listen_address'], + j['path'], + j['url']) + def to_json(self) -> dict: + return self.__dict__ + +class VeilidConfigProtocol: + udp: VeilidConfigUDP + tcp: VeilidConfigTCP + ws: VeilidConfigWS + wss: VeilidConfigWSS + + def __init__(self, udp: VeilidConfigUDP, tcp: VeilidConfigTCP, ws: VeilidConfigWS, wss: VeilidConfigWSS): + self.udp = udp + self.tcp = tcp + self.ws = ws + self.wss = wss + + @staticmethod + def from_json(j: dict) -> Self: + return VeilidConfigProtocol( + VeilidConfigUDP.from_json(j['udp']), + VeilidConfigTCP.from_json(j['tcp']), + VeilidConfigWS.from_json(j['ws']), + VeilidConfigWSS.from_json(j['wss'])) + def to_json(self) -> dict: + return self.__dict__ class VeilidConfigNetwork: @@ -221,6 +512,8 @@ class VeilidConfigNetwork: VeilidConfigTLS.from_json(j['tls']), VeilidConfigApplication.from_json(j['application']), VeilidConfigProtocol.from_json(j['protocol'])) + def to_json(self) -> dict: + return self.__dict__ class VeilidConfig: program_name: str @@ -245,10 +538,14 @@ class VeilidConfig: @staticmethod def from_json(j: dict) -> Self: + '''JSON object hook''' return VeilidConfig(j['program_name'], j['namespace'], VeilidConfigCapabilities.from_json(j['capabilities']), VeilidConfigProtectedStore.from_json(j['protected_store']), VeilidConfigTableStore.from_json(j['table_store']), VeilidConfigBlockStore.from_json(j['block_store']), VeilidConfigNetwork.from_json(j['network'])) - + def to_json(self) -> dict: + return self.__dict__ + + \ No newline at end of file diff --git a/veilid-python/veilid_python/error.py b/veilid-python/veilid_python/error.py index d3ded7da..07a242f4 100644 --- a/veilid-python/veilid_python/error.py +++ b/veilid-python/veilid_python/error.py @@ -1,4 +1,4 @@ -from typing import Self +from typing import Self, Any class VeilidAPIError(Exception): """Veilid API error exception base class""" @@ -131,3 +131,12 @@ class VeilidAPIErrorGeneric(VeilidAPIError): def __init__(self, message: str): super().__init__("Generic") self.message = message + + +def raise_api_result(api_result: dict) -> Any: + if "value" in api_result: + return api_result["value"] + elif "error" in api_result: + raise VeilidAPIError.from_json(api_result["error"]) + else: + raise ValueError("Invalid format for ApiResult") \ No newline at end of file diff --git a/veilid-python/veilid_python/json_api.py b/veilid-python/veilid_python/json_api.py new file mode 100644 index 00000000..b68d3a76 --- /dev/null +++ b/veilid-python/veilid_python/json_api.py @@ -0,0 +1,314 @@ +import json; +import asyncio; +from typing import Callable, Awaitable + +from .api import *; +from .state import * +from .config import * +from .error import * +from .types import * +from .operations import * + +class _JsonRoutingContext(RoutingContext): + api: VeilidAPI + rc_id: int + + def __init__(self, api: VeilidAPI, rc_id: int): + self.api = api + self.rc_id = rc_id + + async def with_privacy(self) -> Self: + new_rc_id = raise_api_result(await self.send_ndjson_request(Operation.ROUTING_CONTEXT, + rc_id = self.rc_id, + rc_op = RoutingContextOperation.WITH_PRIVACY)) + return _JsonRoutingContext(self.api, new_rc_id) + + async def with_custom_privacy(self, stability: Stability) -> Self: + new_rc_id = raise_api_result(await self.send_ndjson_request(Operation.ROUTING_CONTEXT, + rc_id = self.rc_id, + rc_op = RoutingContextOperation.WITH_CUSTOM_PRIVACY, + stability = stability)) + return _JsonRoutingContext(self.api, new_rc_id) + async def with_sequencing(self, sequencing: Sequencing) -> Self: + new_rc_id = raise_api_result(await self.send_ndjson_request(Operation.ROUTING_CONTEXT, + rc_id = self.rc_id, + rc_op = RoutingContextOperation.WITH_SEQUENCING, + sequencing = sequencing)) + return _JsonRoutingContext(self.api, new_rc_id) + async def app_call(self, target: TypedKey | RouteId, request: bytes) -> bytes: + return urlsafe_b64decode_no_pad(raise_api_result(await self.send_ndjson_request(Operation.ROUTING_CONTEXT, + rc_id = self.rc_id, + rc_op = RoutingContextOperation.APP_CALL, + target = target, + request = request))) + async def app_message(self, target: TypedKey | RouteId, message: bytes): + raise_api_result(await self.send_ndjson_request(Operation.ROUTING_CONTEXT, + rc_id = self.rc_id, + rc_op = RoutingContextOperation.APP_MESSAGE, + target = target, + message = message)) + async def create_dht_record(self, kind: CryptoKind, schema: DHTSchema) -> DHTRecordDescriptor: + return DHTRecordDescriptor.from_json(raise_api_result(await self.send_ndjson_request(Operation.ROUTING_CONTEXT, + rc_id = self.rc_id, + rc_op = RoutingContextOperation.CREATE_DHT_RECORD, + kind = kind, + schema = schema))) + async def open_dht_record(self, key: TypedKey, writer: Optional[KeyPair]) -> DHTRecordDescriptor: + pass + async def close_dht_record(self, key: TypedKey): + pass + async def delete_dht_record(self, key: TypedKey): + pass + async def get_dht_value(self, key: TypedKey, subkey: ValueSubkey, force_refresh: bool) -> Optional[ValueData]: + pass + async def set_dht_value(self, key: TypedKey, subkey: ValueSubkey, data: bytes) -> Optional[ValueData]: + pass + async def watch_dht_values(self, key: TypedKey, subkeys: list[(ValueSubkey, ValueSubkey)], expiration: Timestamp, count: int) -> Timestamp: + pass + async def cancel_dht_values(self, key: TypedKey, subkeys: list[(ValueSubkey, ValueSubkey)]) -> bool: + pass + + +class _JsonTableDBTransaction(TableDBTransaction): + async def commit(self): + pass + async def rollback(self): + pass + async def store(self, col: int, key: bytes, value: bytes): + pass + async def delete(self, col: int, key: bytes): + pass + +class _JsonTableDB(TableDB): + async def get_column_count(self) -> int: + pass + async def get_keys(self, col: int) -> list[str]: + pass + async def transact(self) -> TableDBTransaction: + pass + async def store(self, col: int, key: bytes, value: bytes): + pass + async def load(self, col: int, key: bytes) -> Optional[bytes]: + pass + async def delete(self, col: int, key: bytes) -> Optional[bytes]: + pass + +class _JsonCryptoSystem(CryptoSystem): + async def cached_dh(self, key: PublicKey, secret: SecretKey) -> SharedSecret: + pass + async def compute_dh(self, key: PublicKey, secret: SecretKey) -> SharedSecret: + pass + async def random_bytes(self, len: int) -> bytes: + pass + async def default_salt_length(self) -> int: + pass + async def hash_password(self, password: bytes, salt: bytes) -> str: + pass + async def verify_password(self, password: bytes, password_hash: str) -> bool: + pass + async def derive_shared_secret(self, password: bytes, salt: bytes) -> SharedSecret: + pass + async def random_nonce(self) -> Nonce: + pass + async def random_shared_secret(self) -> SharedSecret: + pass + async def generate_key_pair(self) -> KeyPair: + pass + async def generate_hash(self, data: bytes) -> HashDigest: + pass + async def validate_key_pair(self, key: PublicKey, secret: SecretKey) -> bool: + pass + async def validate_hash(self, data: bytes, hash_digest: HashDigest) -> bool: + pass + async def distance(self, key1: CryptoKey, key2: CryptoKey) -> CryptoKeyDistance: + pass + async def sign(self, key: PublicKey, secret: SecretKey, data: bytes) -> Signature: + pass + async def verify(self, key: PublicKey, data: bytes, signature: Signature): + pass + async def aead_overhead(self) -> int: + pass + async def decrypt_aead(self, body: bytes, nonce: Nonce, shared_secret: SharedSecret, associated_data: Optional[bytes]) -> bytes: + pass + async def encrypt_aead(self, body: bytes, nonce: Nonce, shared_secret: SharedSecret, associated_data: Optional[bytes]) -> bytes: + pass + async def crypt_no_auth(self, body: bytes, nonce: Nonce, shared_secret: SharedSecret) -> bytes: + pass + + +class _JsonVeilidAPI(VeilidAPI): + reader: asyncio.StreamReader + writer: asyncio.StreamWriter + update_callback: Callable[[VeilidUpdate], Awaitable] + handle_recv_messages_task: Optional[asyncio.Task] + # Shared Mutable State + lock: asyncio.Lock + next_id: int + in_flight_requests: dict + + def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, update_callback: Callable[[VeilidUpdate], Awaitable]): + self.reader = reader + self.writer = writer + self.update_callback = update_callback + self.handle_recv_messages_task = None + self.lock = asyncio.Lock() + self.next_id = 1 + self.in_flight_requests = dict() + + @staticmethod + async def connect(host: str, port: int, update_callback: Callable[[VeilidUpdate], Awaitable]) -> Self: + reader, writer = await asyncio.open_connection(host, port) + veilid_api = _JsonVeilidAPI(reader, writer, update_callback) + veilid_api.handle_recv_messages_task = asyncio.create_task(veilid_api.handle_recv_messages(), name = "JsonVeilidAPI.handle_recv_messages") + return veilid_api + + async def handle_recv_message_response(self, j: dict): + id = j['id'] + await self.lock.acquire() + try: + # Get and remove the in-flight request + reqfuture = self.in_flight_requests.pop(id, None) + finally: + self.lock.release() + # Resolve the request's future to the response json + reqfuture.set_result(j) + + async def handle_recv_messages(self): + # Read lines until we're done + try: + while True: + linebytes = await self.reader.readline() + if not linebytes.endswith(b'\n'): + break + + # Parse line as ndjson + j = json.loads(linebytes.strip()) + + # Process the message + if j['type'] == "Response": + await self.handle_recv_message_response(j) + elif j['type'] == "Update": + await self.update_callback(VeilidUpdate.from_json(j)) + except: + pass + finally: + self.reader = None + self.writer.close() + await self.writer.wait_closed() + self.writer = None + + async def allocate_request_future(self, id: int) -> asyncio.Future: + reqfuture = asyncio.get_running_loop().create_future() + + await self.lock.acquire() + try: + self.in_flight_requests[id] = reqfuture + finally: + self.lock.release() + + return reqfuture + + async def cancel_request_future(self, id: int): + await self.lock.acquire() + try: + reqfuture = self.in_flight_requests.pop(id, None) + reqfuture.cancel() + finally: + self.lock.release() + + async def send_ndjson_request(self, op: Operation, **kwargs) -> dict: + + # Get next id + await self.lock.acquire() + try: + id = self.next_id + self.next_id += 1 + finally: + self.lock.release() + + # Make NDJSON string for request + req = { "id": id, "op": op } + for k, v in kwargs.items(): + setattr(req, k, v) + reqstr = VeilidJSONEncoder.dumps(req) + "\n" + reqbytes = reqstr.encode() + + # Allocate future for request + reqfuture = await self.allocate_request_future(id) + + # Send to socket + try: + self.writer.write(reqbytes) + await self.writer.drain() + finally: + # Send failed, release future + self.cancel_request_future(id) + + # Wait for response + response = await reqfuture + + return response + + async def control(self, args: list[str]) -> str: + return raise_api_result(await self.send_ndjson_request(Operation.CONTROL, args = args)) + async def get_state(self) -> VeilidState: + return VeilidState.from_json(raise_api_result(await self.send_ndjson_request(Operation.GET_STATE))) + async def attach(self): + raise_api_result(await self.send_ndjson_request(Operation.ATTACH)) + async def detach(self): + raise_api_result(await self.send_ndjson_request(Operation.DETACH)) + async def new_private_route(self) -> NewPrivateRouteResult: + return NewPrivateRouteResult.from_json(raise_api_result(await self.send_ndjson_request(Operation.NEW_PRIVATE_ROUTE))) + async def new_custom_private_route(self, kinds: list[CryptoKind], stability: Stability, sequencing: Sequencing) -> NewPrivateRouteResult: + return NewPrivateRouteResult.from_json(raise_api_result( + await self.send_ndjson_request(Operation.NEW_CUSTOM_PRIVATE_ROUTE, + kinds = kinds, + stability = stability, + sequencing = sequencing) + )) + async def import_remote_private_route(self, blob: bytes) -> RouteId: + return RouteId(raise_api_result( + await self.send_ndjson_request(Operation.IMPORT_REMOTE_PRIVATE_ROUTE, + blob = blob) + )) + async def release_private_route(self, route_id: RouteId): + raise_api_result( + await self.send_ndjson_request(Operation.RELEASE_PRIVATE_ROUTE, + route_id = route_id) + ) + async def app_call_reply(self, call_id: OperationId, message: bytes): + raise_api_result( + await self.send_ndjson_request(Operation.APP_CALL_REPLY, + call_id = call_id, + message = message) + ) + async def new_routing_context(self) -> RoutingContext: + rc_id = raise_api_result(await self.send_ndjson_request(Operation.NEW_ROUTING_CONTEXT)) + return RoutingContext(self, rc_id) + + async def open_table_db(self, name: str, column_count: int) -> TableDB: + pass + async def delete_table_db(self, name: str): + pass + async def get_crypto_system(self, kind: CryptoKind) -> CryptoSystem: + pass + async def best_crypto_system(self) -> CryptoSystem: + pass + async def verify_signatures(self, node_ids: list[TypedKey], data: bytes, signatures: list[TypedSignature]) -> list[TypedKey]: + pass + async def generate_signatures(self, data: bytes, key_pairs: list[TypedKeyPair]) -> list[TypedSignature]: + pass + async def generate_key_pair(self, kind: CryptoKind) -> list[TypedKeyPair]: + pass + async def now(self) -> Timestamp: + pass + async def debug(self, command: str) -> str: + pass + async def veilid_version_string(self) -> str: + pass + async def veilid_version(self) -> VeilidVersion: + pass + + +def json_api_connect(host:str, port:int) -> VeilidAPI: + return _JsonVeilidAPI.connect(host, port) \ No newline at end of file diff --git a/veilid-python/veilid_python/operations.py b/veilid-python/veilid_python/operations.py new file mode 100644 index 00000000..20dd11eb --- /dev/null +++ b/veilid-python/veilid_python/operations.py @@ -0,0 +1,91 @@ +from enum import StrEnum +from typing import Self + +class Operation(StrEnum): + CONTROL = "Control" + GET_STATE = "GetState" + ATTACH = "Attach" + DETACH = "Detach" + NEW_PRIVATE_ROUTE = "NewPrivateRoute" + NEW_CUSTOM_PRIVATE_ROUTE = "NewCustomPrivateRoute" + IMPORT_REMOTE_PRIVATE_ROUTE = "ImportRemotePrivateRoute" + RELEASE_PRIVATE_ROUTE = "ReleasePrivateRoute" + APP_CALL_REPLY = "AppCallReply" + NEW_ROUTING_CONTEXT = "NewRoutingContext" + ROUTING_CONTEXT = "RoutingContext" + OPEN_TABLE_DB = "OpenTableDb" + DELETE_TABLE_DB = "DeleteTableDb" + TABLE_DB = "TableDb" + TABLE_DB_TRANSACTION = "TableDbTransaction" + GET_CRYPTO_SYSTEM = "GetCryptoSystem" + BEST_CRYPTO_SYSTEM = "BestCryptoSystem" + CRYPTO_SYSTEM = "CryptoSystem" + VERIFY_SIGNATURES = "VerifySignatures" + GENERATE_SIGNATURES = "GenerateSignatures" + GENERATE_KEY_PAIR = "GenerateKeyPair" + NOW = "Now" + DEBUG = "Debug" + VEILID_VERSION_STRING = "VeilidVersionString" + VEILID_VERSION = "VeilidVersion" + +class RoutingContextOperation(StrEnum): + INVALID_ID = "InvalidId" + RELEASE = "Release" + WITH_PRIVACY = "WithPrivacy" + WITH_CUSTOM_PRIVACY = "WithCustomPrivacy" + WITH_SEQUENCING = "WithSequencing" + APP_CALL = "AppCall" + APP_MESSAGE = "AppMessage" + CREATE_DHT_RECORD = "CreateDhtRecord" + OPEN_DHT_RECORD = "OpenDhtRecord" + CLOSE_DHT_RECORD = "CloseDhtRecord" + DELETE_DHT_RECORD = "DeleteDhtRecord" + GET_DHT_VALUE = "GetDhtValue" + SET_DHT_VALUE = "SetDhtValue" + WATCH_DHT_VALUES = "WatchDhtValues" + CANCEL_DHT_WATCH = "CancelDhtWatch" + +class TableDbOperation(StrEnum): + INVALID_ID = "InvalidId" + RELEASE = "Release" + GET_COLUMN_COUNT = "GetColumnCount" + GET_KEYS = "GetKeys" + TRANSACT = "Transact" + STORE = "Store" + LOAD = "Load" + DELETE = "Delete" + +class TableDBTransactionOperation(StrEnum): + INVALID_ID = "InvalidId" + COMMIT = "Commit" + ROLLBACK = "Rollback" + STORE = "Store" + DELETE = "Delete" + +class CryptoSystemOperation(StrEnum): + INVALID_ID = "InvalidId" + RELEASE = "Release" + CACHED_DH = "CachedDh" + COMPUTE_DH = "ComputeDh" + RANDOM_BYTES = "RandomBytes" + DEFAULT_SALT_LENGTH = "DefaultSaltLength" + HASH_PASSWORD = "HashPassword" + VERIFY_PASSWORD = "VerifyPassword" + DERIVE_SHARED_SECRET = "DeriveSharedSecret" + RANDOM_NONCE = "RandomNonce" + RANDOM_SHARED_SECRET = "RandomSharedSecret" + GENERATE_KEY_PAIR = "GenerateKeyPair" + GENERATE_HASH = "GenerateHash" + VALIDATE_KEY_PAIR = "ValidateKeyPair" + VALIDATE_HASH = "ValidateHash" + DISTANCE = "Distance" + SIGN = "Sign" + VERIFY = "Verify" + AEAD_OVERHEAD = "AeadOverhead" + DECRYPT_AEAD = "DecryptAead" + ENCRYPT_AEAD = "EncryptAead" + CRYPT_NO_AUTH = "CryptNoAuth" + +class RecvMessageType(StrEnum): + RESPONSE = "Response" + UPDATE = "Update" diff --git a/veilid-python/veilid_python/request.py b/veilid-python/veilid_python/request.py deleted file mode 100644 index 19d59ae2..00000000 --- a/veilid-python/veilid_python/request.py +++ /dev/null @@ -1,4 +0,0 @@ -class Request: - - def __init__(self, id: int): - self.id = id \ No newline at end of file diff --git a/veilid-python/veilid_python/state.py b/veilid-python/veilid_python/state.py index f5ac2abb..a5fd9cd8 100644 --- a/veilid-python/veilid_python/state.py +++ b/veilid-python/veilid_python/state.py @@ -1,7 +1,8 @@ from typing import Self, Optional from enum import StrEnum -from .types import Timestamp, TimestampDuration, ByteCount -from .config import VeilidConfig + +from .types import * +from .config import * class AttachmentState(StrEnum): DETACHED = 'Detached' @@ -25,6 +26,7 @@ class VeilidStateAttachment: @staticmethod def from_json(j: dict) -> Self: + '''JSON object hook''' return VeilidStateAttachment( AttachmentState(j['state']), j['public_internet_ready'], @@ -54,6 +56,7 @@ class RPCStats: @staticmethod def from_json(j: dict) -> Self: + '''JSON object hook''' return RPCStats( j['messages_sent'], j['messages_rcvd'], @@ -76,6 +79,7 @@ class LatencyStats: @staticmethod def from_json(j: dict) -> Self: + '''JSON object hook''' return LatencyStats( TimestampDuration(j['fastest']), TimestampDuration(j['average']), @@ -96,6 +100,7 @@ class TransferStats: @staticmethod def from_json(j: dict) -> Self: + '''JSON object hook''' return TransferStats( ByteCount(j['total']), ByteCount(j['maximum']), @@ -113,6 +118,7 @@ class TransferStatsDownUp: @staticmethod def from_json(j: dict) -> Self: + '''JSON object hook''' return TransferStatsDownUp( TransferStats.from_json(j['down']), TransferStats.from_json(j['up'])) @@ -131,6 +137,7 @@ class PeerStats: @staticmethod def from_json(j: dict) -> Self: + '''JSON object hook''' return PeerStats( j['time_added'], RPCStats.from_json(j['rpc_stats']), @@ -149,6 +156,7 @@ class PeerTableData: @staticmethod def from_json(j: dict) -> Self: + '''JSON object hook''' return PeerTableData( j['node_ids'], j['peer_address'], @@ -168,6 +176,7 @@ class VeilidStateNetwork: @staticmethod def from_json(j: dict) -> Self: + '''JSON object hook''' return VeilidStateNetwork( j['started'], ByteCount(j['bps_down']), @@ -182,10 +191,10 @@ class VeilidStateConfig: @staticmethod def from_json(j: dict) -> Self: + '''JSON object hook''' return VeilidStateConfig( j['config']) - class VeilidState: attachment: VeilidStateAttachment network: VeilidStateNetwork @@ -198,8 +207,143 @@ class VeilidState: @staticmethod def from_json(j: dict) -> Self: + '''JSON object hook''' return VeilidState( VeilidStateAttachment.from_json(j['attachment']), VeilidStateNetwork.from_json(j['network']), VeilidStateConfig.from_json(j['config'])) +class VeilidLog: + log_level: VeilidLogLevel + message: str + backtrace: Optional[str] + + def __init__(self, log_level: VeilidLogLevel, message: str, backtrace: Optional[str]): + self.log_level = log_level + self.message = message + self.backtrace = backtrace + + @staticmethod + def from_json(j: dict) -> Self: + '''JSON object hook''' + return VeilidLog( + VeilidLogLevel(j['attachment']), + j['message'], + j['backtrace']) + +class VeilidAppMessage: + sender: Optional[TypedKey] + message: bytes + + def __init__(self, sender: Optional[TypedKey], message: bytes): + self.sender = sender + self.message = message + + @staticmethod + def from_json(j: dict) -> Self: + '''JSON object hook''' + return VeilidAppMessage( + None if j['sender'] is None else TypedKey(j['sender']), + urlsafe_b64decode_no_pad(j['message'])) + +class VeilidAppCall: + sender: Optional[TypedKey] + message: bytes + operation_id: str + + def __init__(self, sender: Optional[TypedKey], message: bytes, operation_id: str): + self.sender = sender + self.message = message + self.operation_id = operation_id + + @staticmethod + def from_json(j: dict) -> Self: + '''JSON object hook''' + return VeilidAppCall( + None if j['sender'] is None else TypedKey(j['sender']), + urlsafe_b64decode_no_pad(j['message']), + j['operation_id']) + +class VeilidRouteChange: + dead_routes: list[RouteId] + dead_remote_routes: list[RouteId] + + def __init__(self, dead_routes: list[RouteId], dead_remote_routes: list[RouteId]): + self.dead_routes = dead_routes + self.dead_remote_routes = dead_remote_routes + + @staticmethod + def from_json(j: dict) -> Self: + '''JSON object hook''' + return VeilidRouteChange( + map(lambda x: RouteId(x), j['dead_routes']), + map(lambda x: RouteId(x), j['dead_remote_routes'])) + +class VeilidValueChange: + key: TypedKey + subkeys: list[ValueSubkey] + count: int + value: ValueData + + def __init__(self, key: TypedKey, subkeys: list[ValueSubkey], count: int, value: ValueData): + self.key = key + self.subkeys = subkeys + self.count = count + self.value = value + + @staticmethod + def from_json(j: dict) -> Self: + '''JSON object hook''' + return VeilidValueChange( + TypedKey(j['key']), + map(lambda x: ValueSubkey(x), j['subkeys']), + j['count'], + ValueData.from_json(j['value'])) + + +class VeilidUpdateKind(StrEnum): + LOG = "Log" + APP_MESSAGE = "AppMessage" + APP_CALL = "AppCall" + ATTACHMENT = "Attachment" + NETWORK = "Network" + CONFIG = "Config" + ROUTE_CHANGE = "RouteChange" + VALUE_CHANGE = "ValueChange" + SHUTDOWN = "Shutdown" + +class VeilidUpdate: + kind: VeilidUpdateKind + detail: Optional[VeilidLog | VeilidAppMessage | VeilidAppCall | VeilidStateAttachment | VeilidStateNetwork | VeilidStateConfig | VeilidRouteChange | VeilidValueChange] + + def __init__(self, kind: VeilidUpdateKind, detail: Optional[VeilidLog | VeilidAppMessage | VeilidAppCall | VeilidStateAttachment | VeilidStateNetwork | VeilidStateConfig | VeilidRouteChange | VeilidValueChange]): + self.kind = kind + self.detail = detail + + @staticmethod + def from_json(j: dict) -> Self: + '''JSON object hook''' + kind = VeilidUpdateKind(j['kind']) + detail = None + match kind: + case VeilidUpdateKind.LOG: + detail = VeilidLog.from_json(j) + case VeilidUpdateKind.APP_MESSAGE: + detail = VeilidAppMessage.from_json(j) + case VeilidUpdateKind.APP_CALL: + detail = VeilidAppCall.from_json(j) + case VeilidUpdateKind.ATTACHMENT: + detail = VeilidStateAttachment.from_json(j) + case VeilidUpdateKind.NETWORK: + detail = VeilidStateNetwork.from_json(j) + case VeilidUpdateKind.CONFIG: + detail = VeilidStateConfig.from_json(j) + case VeilidUpdateKind.ROUTE_CHANGE: + detail = VeilidRouteChange.from_json(j) + case VeilidUpdateKind.VALUE_CHANGE: + detail = VeilidValueChange.from_json(j) + case VeilidUpdateKind.SHUTDOWN: + detail = None + case _: + raise ValueError("Unknown VeilidUpdateKind") + diff --git a/veilid-python/veilid_python/types.py b/veilid-python/veilid_python/types.py index e1938e04..9b87670c 100644 --- a/veilid-python/veilid_python/types.py +++ b/veilid-python/veilid_python/types.py @@ -1,4 +1,38 @@ import time +import json +import base64 + +from enum import StrEnum +from typing import Self, Optional, Any + +def urlsafe_b64encode_no_pad(b: bytes) -> str: + """ + Removes any `=` used as padding from the encoded string. + """ + encoded = str(base64.urlsafe_b64encode(b)) + return encoded.rstrip("=") + + +def urlsafe_b64decode_no_pad(s: str) -> bytes: + """ + Adds back in the required padding before decoding. + """ + padding = 4 - (len(s) % 4) + string = string + ("=" * padding) + return base64.urlsafe_b64decode(s) + +class VeilidJSONEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, bytes): + return urlsafe_b64encode_no_pad(o) + if hasattr(o, "to_json") and callable(o.to_json): + return o.to_json() + return json.JSONEncoder.default(self, o) + + @staticmethod + def dumps(req: Any, *args, **kwargs) -> str: + return json.dumps(req, cls = VeilidJSONEncoder, *args, **kwargs) + class Timestamp(int): pass @@ -7,4 +41,195 @@ class TimestampDuration(int): pass class ByteCount(int): - pass \ No newline at end of file + pass + +class OperationId(int): + pass + +class RouteId(str): + pass + +class TypedKey(str): + pass + +class TypedKeyPair(str): + pass + +class TypedSignature(str): + pass + +class CryptoKey(str): + pass + +class CryptoKeyDistance(str): + pass + +class PublicKey(CryptoKey): + pass + +class SecretKey(CryptoKey): + pass + +class SharedSecret(CryptoKey): + pass + +class Signature(str): + pass + +class Nonce(str): + pass + +class KeyPair(str): + pass + +class HashDigest(CryptoKey): + pass + +class ValueSubkey(int): + pass + +class ValueSeqNum(int): + pass + +class VeilidVersion: + _major: int + _minor: int + _patch: int + def __init__(self, major: int, minor: int, patch: int): + self._major = major + self._minor = minor + self._patch = patch + @property + def major(self): + return self._major + @property + def minor(self): + return self._minor + @property + def patch(self): + return self._patch + +class VeilidLogLevel(StrEnum): + ERROR = 'Error' + WARN = 'Warn' + INFO = 'Info' + DEBUG = 'Debug' + TRACE = 'Trace' + +class NewPrivateRouteResult: + route_id: RouteId + blob: bytes + + def __init__(self, route_id: RouteId, blob: bytes): + self.route_id = route_id + self.blob = blob + + @staticmethod + def from_json(j: dict) -> Self: + return NewPrivateRouteResult( + RouteId(j['route_id']), + urlsafe_b64decode_no_pad(j['blob'])) + +class CryptoKind(StrEnum): + CRYPTO_KIND_NONE = "NONE" + CRYPTO_KIND_VLD0 = "VLD0" + +class Stability(StrEnum): + LOW_LATENCY = "LowLatency" + RELIABLE = "Reliable" + +class Sequencing(StrEnum): + NO_PREFERENCE = "NoPreference" + PREFER_ORDERED = "PreferOrdered" + ENSURE_ORDERED = "EnsureOrdered" + +class DHTSchemaKind(StrEnum): + DFLT = "DFLT" + SMPL = "SMPL" + +class DHTSchemaSMPLMember: + m_key: PublicKey + m_cnt: int + def __init__(self, m_key: PublicKey, m_cnt: int): + self.m_key = m_key + self.m_cnt = m_cnt + @staticmethod + def from_json(j: dict) -> Self: + return DHTSchemaSMPLMember( + PublicKey(j['m_key']), + j['m_cnt']) + def to_json(self) -> dict: + return self.__dict__ + +class DHTSchema: + kind: DHTSchemaKind + + def __init__(self, kind: DHTSchemaKind, **kwargs): + self.kind = kind + for k, v in kwargs.items(): + setattr(self, k, v) + + @staticmethod + def dflt(o_cnt: int) -> Self: + Self(DHTSchemaKind.DFLT, o_cnt = o_cnt) + + @staticmethod + def smpl(o_cnt: int, members: list[DHTSchemaSMPLMember]) -> Self: + Self(DHTSchemaKind.SMPL, o_cnt = o_cnt, members = members) + + @staticmethod + def from_json(j: dict) -> Self: + if DHTSchemaKind(j['kind']) == DHTSchemaKind.DFLT: + return DHTSchema.dflt(j['o_cnt']) + if DHTSchemaKind(j['kind']) == DHTSchemaKind.SMPL: + return DHTSchema.smpl( + j['o_cnt'], + map(lambda x: DHTSchemaSMPLMember.from_json(x), j['members'])) + raise Exception("Unknown DHTSchema kind", j['kind']) + + def to_json(self) -> dict: + return self.__dict__ + +class DHTRecordDescriptor: + key: TypedKey + owner: PublicKey + owner_secret: Optional[SecretKey] + schema: DHTSchema + + def __init__(self, key: TypedKey, owner: PublicKey, owner_secret: Optional[SecretKey], schema: DHTSchema): + self.key = key + self.owner = owner + self.owner_secret = owner_secret + self.schema = schema + + @staticmethod + def from_json(j: dict) -> Self: + DHTRecordDescriptor( + TypedKey(j['key']), + PublicKey(j['owner']), + None if j['owner_secret'] is None else SecretKey(j['owner_secret']), + DHTSchema.from_json(j['schema'])) + + def to_json(self) -> dict: + return self.__dict__ + +class ValueData: + seq: ValueSeqNum + data: bytes + writer: PublicKey + + def __init__(self, seq: ValueSeqNum, data: bytes, writer: PublicKey): + self.seq = seq + self.data = data + self.writer = writer + + @staticmethod + def from_json(j: dict) -> Self: + DHTRecordDescriptor( + ValueSeqNum(j['seq']), + urlsafe_b64decode_no_pad(j['data']), + PublicKey(j['writer'])) + + def to_json(self) -> dict: + return self.__dict__ +