diff --git a/veilid-python/demo/chat.py b/veilid-python/demo/chat.py new file mode 100755 index 00000000..4013dd8b --- /dev/null +++ b/veilid-python/demo/chat.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python + +"""A simple chat server using Veilid's DHT.""" + +import argparse +import asyncio +import sys + +import config + +import veilid + +QUIT = b"QUIT" + + +async def noop_callback(*args, **kwargs): + """In the real world, we'd use this to process interesting incoming events.""" + + return + + +async def chatter(rc: veilid.api.RoutingContext, key: str, send_channel: int, recv_channel: int): + """Read input, write it to the DHT, and print the response from the DHT.""" + + last_seq = -1 + + send_subkey = veilid.types.ValueSubkey(send_channel) + recv_subkey = veilid.types.ValueSubkey(recv_channel) + + # Prime the pumps. Especially when starting the conversation, this + # causes the DHT key to propagate to the network. + await rc.set_dht_value(key, send_subkey, b"Hello from the world!") + + while True: + try: + msg = input("SEND> ") + except EOFError: + # Cat got your tongue? Hang up. + print("Closing the chat.") + await rc.set_dht_value(key, send_subkey, QUIT) + return + + # Write the input message to the DHT key. + await rc.set_dht_value(key, send_subkey, msg.encode()) + + # In the real world, don't do this. People may tease you for it. + # This is meant to be easy to understand for demonstration + # purposes, not a great pattern. Instead, you'd want to use the + # callback function to handle events asynchronously. + while True: + # Try to get an updated version of the receiving subkey. + resp = await rc.get_dht_value(key, recv_subkey, True) + if resp is None: + continue + + # If the other party hasn't sent a newer message, try again. + if resp.seq == last_seq: + continue + + if resp.data == QUIT: + print("Other end closed the chat.") + return + + print(f"RECV< {resp.data.decode()}") + last_seq = resp.seq + break + + +async def start(host: str, port: int, name: str): + """Begin a conversation with a friend.""" + + conn = await veilid.json_api_connect(host, port, noop_callback) + + keys = config.read_keys() + my_key = veilid.KeyPair(keys["self"]) + + members = [ + veilid.types.DHTSchemaSMPLMember(my_key.key(), 1), + veilid.types.DHTSchemaSMPLMember(keys["peers"][name], 1), + ] + + router = await(await conn.new_routing_context()).with_privacy() + async with router: + rec = await router.create_dht_record(veilid.DHTSchema.smpl(0, members)) + print(f"New chat key: {rec.key}") + print("Give that to your friend!") + + # Close this key first. We'll reopen it for writing with our saved key. + await router.close_dht_record(rec.key) + + await router.open_dht_record(rec.key, veilid.KeyPair(keys["self"])) + + try: + # Write to the 1st subkey and read from the 2nd. + await chatter(router, rec.key, 0, 1) + finally: + await router.close_dht_record(rec.key) + await router.delete_dht_record(rec.key) + + +async def respond(host: str, port: int, key: str): + """Reply to a friend's chat.""" + + conn = await veilid.json_api_connect(host, port, noop_callback) + + keys = config.read_keys() + my_key = veilid.KeyPair(keys["self"]) + + router = await(await conn.new_routing_context()).with_privacy() + async with router: + await router.open_dht_record(key, my_key) + + # As the responder, we're writing to the 2nd subkey and reading from the 1st. + await chatter(router, key, 1, 0) + + +async def keygen(host: str, port: int): + """Generate a keypair.""" + + conn = await veilid.json_api_connect(host, port, noop_callback) + + crypto_system = await conn.get_crypto_system(veilid.CryptoKind.CRYPTO_KIND_VLD0) + async with crypto_system: + my_key = await crypto_system.generate_key_pair() + + keys = config.read_keys() + if keys["self"]: + print("You already have a keypair.") + sys.exit(1) + + keys["self"] = my_key + config.write_keys(keys) + + print(f"Your new public key is {my_key.key()}. Share it with your friends!") + + +async def add_friend(host: str, port: int, name: str, pubkey: str): + """Add a friend's public key.""" + + keys = config.read_keys() + keys["peers"][name] = pubkey + config.write_keys(keys) + + +async def clean(host: str, port: int, key: str): + """Delete a DHT key.""" + + conn = await veilid.json_api_connect(host, port, noop_callback) + + router = await(await conn.new_routing_context()).with_privacy() + async with router: + await router.close_dht_record(key) + await router.delete_dht_record(key) + + +def handle_command_line(arglist: list[str]): + """Process the command line. + + This isn't the interesting part.""" + + parser = argparse.ArgumentParser(description="Veilid chat demonstration") + parser.add_argument("--host", default="localhost", help="Address of the Veilid server host.") + parser.add_argument("--port", type=int, default=5959, help="Port of the Veilid server.") + + subparsers = parser.add_subparsers(required=True) + + cmd_start = subparsers.add_parser("start", help=start.__doc__) + cmd_start.add_argument("name", help="Your friend's name") + cmd_start.set_defaults(func=start) + + cmd_respond = subparsers.add_parser("respond", help=respond.__doc__) + cmd_respond.add_argument("key", help="The chat's DHT key") + cmd_respond.set_defaults(func=respond) + + cmd_keygen = subparsers.add_parser("keygen", help=keygen.__doc__) + cmd_keygen.set_defaults(func=keygen) + + cmd_add_friend = subparsers.add_parser("add-friend", help=add_friend.__doc__) + cmd_add_friend.add_argument("name", help="Your friend's name") + cmd_add_friend.add_argument("pubkey", help="Your friend's public key") + cmd_add_friend.set_defaults(func=add_friend) + + cmd_clean = subparsers.add_parser("clean", help=clean.__doc__) + cmd_clean.add_argument("key", help="DHT key to delete") + cmd_clean.set_defaults(func=clean) + + args = parser.parse_args(arglist) + kwargs = args.__dict__ + func = kwargs.pop("func") + + asyncio.run(func(**kwargs)) + + +if __name__ == "__main__": + handle_command_line(sys.argv[1:]) diff --git a/veilid-python/demo/config.py b/veilid-python/demo/config.py new file mode 100644 index 00000000..8479611e --- /dev/null +++ b/veilid-python/demo/config.py @@ -0,0 +1,26 @@ +"""Load and save configuration.""" + +import json +from pathlib import Path + +KEYFILE = Path(".demokeys") + + +def read_keys() -> dict: + """Load the stored keys from disk.""" + + try: + keydata = KEYFILE.read_text() + except FileNotFoundError: + return { + "self": None, + "peers": {}, + } + + return json.loads(keydata) + + +def write_keys(keydata: dict): + """Save the keys to disk.""" + + KEYFILE.write_text(json.dumps(keydata, indent=2)) diff --git a/veilid-python/pyproject.toml b/veilid-python/pyproject.toml index 345fa903..04d420aa 100644 --- a/veilid-python/pyproject.toml +++ b/veilid-python/pyproject.toml @@ -17,3 +17,9 @@ pytest-asyncio = "^0.21.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 99 + +[tool.mypy] +check_untyped_defs = true \ No newline at end of file diff --git a/veilid-python/tests/test_basic.py b/veilid-python/tests/test_basic.py index 7ede6f95..395d0a90 100644 --- a/veilid-python/tests/test_basic.py +++ b/veilid-python/tests/test_basic.py @@ -27,9 +27,7 @@ async def test_get_node_id(api_connection: veilid.VeilidAPI): @pytest.mark.asyncio async def test_fail_connect(): with pytest.raises(socket.gaierror) as exc: - await veilid.json_api_connect( - "fuahwelifuh32luhwafluehawea", 1, simple_update_callback - ) + await veilid.json_api_connect("fuahwelifuh32luhwafluehawea", 1, simple_update_callback) assert exc.value.errno == socket.EAI_NONAME diff --git a/veilid-python/tests/test_crypto.py b/veilid-python/tests/test_crypto.py index f98410ae..328bc856 100644 --- a/veilid-python/tests/test_crypto.py +++ b/veilid-python/tests/test_crypto.py @@ -12,14 +12,13 @@ async def test_best_crypto_system(api_connection: veilid.VeilidAPI): async with cs: assert await cs.default_salt_length() == 16 + @pytest.mark.asyncio async def test_get_crypto_system(api_connection: veilid.VeilidAPI): - cs: CryptoSystem = await api_connection.get_crypto_system( - veilid.CryptoKind.CRYPTO_KIND_VLD0 - ) + cs: CryptoSystem = await api_connection.get_crypto_system(veilid.CryptoKind.CRYPTO_KIND_VLD0) async with cs: assert await cs.default_salt_length() == 16 - + @pytest.mark.asyncio async def test_get_crypto_system_invalid(api_connection: veilid.VeilidAPI): @@ -45,4 +44,3 @@ async def test_hash_and_verify_password(api_connection: veilid.VeilidAPI): # Password mismatch phash2 = await cs.hash_password(b"abc1234", salt) assert not await cs.verify_password(b"abc12345", phash) - diff --git a/veilid-python/tests/test_dht.py b/veilid-python/tests/test_dht.py index 713f5675..efae87fd 100644 --- a/veilid-python/tests/test_dht.py +++ b/veilid-python/tests/test_dht.py @@ -47,7 +47,9 @@ async def test_delete_dht_record_nonexistent(api_connection: veilid.VeilidAPI): async def test_create_delete_dht_record_simple(api_connection: veilid.VeilidAPI): rc = await api_connection.new_routing_context() async with rc: - rec = await rc.create_dht_record(veilid.DHTSchema.dflt(1), veilid.CryptoKind.CRYPTO_KIND_VLD0) + rec = await rc.create_dht_record( + veilid.DHTSchema.dflt(1), veilid.CryptoKind.CRYPTO_KIND_VLD0 + ) await rc.close_dht_record(rec.key) await rc.delete_dht_record(rec.key) diff --git a/veilid-python/tests/test_routing_context.py b/veilid-python/tests/test_routing_context.py index 9b6f95f9..f9536e8c 100644 --- a/veilid-python/tests/test_routing_context.py +++ b/veilid-python/tests/test_routing_context.py @@ -34,9 +34,7 @@ async def test_routing_contexts(api_connection: veilid.VeilidAPI): rc = await (await api_connection.new_routing_context()).with_custom_privacy( veilid.SafetySelection.safe( - veilid.SafetySpec( - None, 2, veilid.Stability.RELIABLE, veilid.Sequencing.ENSURE_ORDERED - ) + veilid.SafetySpec(None, 2, veilid.Stability.RELIABLE, veilid.Sequencing.ENSURE_ORDERED) ) ) await rc.release() @@ -117,14 +115,10 @@ async def test_routing_context_app_call_loopback(): # send an app message to our own private route request = b"abcd1234" - app_call_task = asyncio.create_task( - rc.app_call(prr, request), name="app call task" - ) + app_call_task = asyncio.create_task(rc.app_call(prr, request), name="app call task") # we should get the same request back - update: veilid.VeilidUpdate = await asyncio.wait_for( - app_call_queue.get(), timeout=10 - ) + update: veilid.VeilidUpdate = await asyncio.wait_for(app_call_queue.get(), timeout=10) appcall = update.detail assert isinstance(appcall, veilid.VeilidAppCall) @@ -164,9 +158,9 @@ async def test_routing_context_app_message_loopback_big_packets(): await api.debug("purge routes") # make a routing context that uses a safety route - rc = await ( - await (await api.new_routing_context()).with_privacy() - ).with_sequencing(veilid.Sequencing.ENSURE_ORDERED) + rc = await (await (await api.new_routing_context()).with_privacy()).with_sequencing( + veilid.Sequencing.ENSURE_ORDERED + ) async with rc: # make a new local private route prl, blob = await api.new_private_route() @@ -223,14 +217,12 @@ async def test_routing_context_app_call_loopback_big_packets(): # purge routes to ensure we start fresh await api.debug("purge routes") - app_call_task = asyncio.create_task( - app_call_queue_task_handler(api), name="app call task" - ) + app_call_task = asyncio.create_task(app_call_queue_task_handler(api), name="app call task") # make a routing context that uses a safety route - rc = await ( - await (await api.new_routing_context()).with_privacy() - ).with_sequencing(veilid.Sequencing.ENSURE_ORDERED) + rc = await (await (await api.new_routing_context()).with_privacy()).with_sequencing( + veilid.Sequencing.ENSURE_ORDERED + ) async with rc: # make a new local private route prl, blob = await api.new_private_route() @@ -249,9 +241,7 @@ async def test_routing_context_app_call_loopback_big_packets(): app_call_task.cancel() -@pytest.mark.skipif( - os.getenv("NOSKIP") != "1", reason="unneeded test, only for performance check" -) +@pytest.mark.skipif(os.getenv("NOSKIP") != "1", reason="unneeded test, only for performance check") @pytest.mark.asyncio async def test_routing_context_app_message_loopback_bandwidth(): app_message_queue: asyncio.Queue = asyncio.Queue() diff --git a/veilid-python/tests/test_table_db.py b/veilid-python/tests/test_table_db.py index 9b904ae8..962ecd23 100644 --- a/veilid-python/tests/test_table_db.py +++ b/veilid-python/tests/test_table_db.py @@ -8,6 +8,7 @@ from veilid.api import CryptoSystem TEST_DB = "__pytest_db" TEST_NONEXISTENT_DB = "__pytest_nonexistent_db" + @pytest.mark.asyncio async def test_delete_table_db_nonexistent(api_connection: veilid.VeilidAPI): deleted = await api_connection.delete_table_db(TEST_NONEXISTENT_DB) @@ -25,11 +26,12 @@ async def test_open_delete_table_db(api_connection: veilid.VeilidAPI): with pytest.raises(veilid.VeilidAPIErrorGeneric) as exc: await api_connection.delete_table_db(TEST_DB) # drop the db - + # now delete should succeed deleted = await api_connection.delete_table_db(TEST_DB) assert deleted + @pytest.mark.asyncio async def test_open_twice_table_db(api_connection: veilid.VeilidAPI): # delete test db if it exists @@ -37,12 +39,12 @@ async def test_open_twice_table_db(api_connection: veilid.VeilidAPI): tdb = await api_connection.open_table_db(TEST_DB, 1) tdb2 = await api_connection.open_table_db(TEST_DB, 1) - + # delete should fail because open with pytest.raises(veilid.VeilidAPIErrorGeneric) as exc: await api_connection.delete_table_db(TEST_DB) await tdb.release() - + # delete should fail because open with pytest.raises(veilid.VeilidAPIErrorGeneric) as exc: await api_connection.delete_table_db(TEST_DB) @@ -62,7 +64,7 @@ async def test_open_twice_table_db_store_load(api_connection: veilid.VeilidAPI): async with tdb: tdb2 = await api_connection.open_table_db(TEST_DB, 1) async with tdb2: - # store into first db copy + # store into first db copy await tdb.store(b"asdf", b"1234") # load from second db copy assert await tdb.load(b"asdf") == b"1234" @@ -71,6 +73,7 @@ async def test_open_twice_table_db_store_load(api_connection: veilid.VeilidAPI): deleted = await api_connection.delete_table_db(TEST_DB) assert deleted + @pytest.mark.asyncio async def test_open_twice_table_db_store_delete_load(api_connection: veilid.VeilidAPI): # delete test db if it exists @@ -80,12 +83,11 @@ async def test_open_twice_table_db_store_delete_load(api_connection: veilid.Veil async with tdb: tdb2 = await api_connection.open_table_db(TEST_DB, 1) async with tdb2: - - # store into first db copy + # store into first db copy await tdb.store(b"asdf", b"1234") # delete from second db copy and clean up await tdb2.delete(b"asdf") - + # load from first db copy assert await tdb.load(b"asdf") == None @@ -104,24 +106,22 @@ async def test_resize_table_db(api_connection: veilid.VeilidAPI): # reopen the db with more columns should fail if it is already open with pytest.raises(veilid.VeilidAPIErrorGeneric) as exc: await api_connection.open_table_db(TEST_DB, 2) - + tdb2 = await api_connection.open_table_db(TEST_DB, 2) async with tdb2: # write something to second column - await tdb2.store(b"qwer", b"5678", col = 1) - + await tdb2.store(b"qwer", b"5678", col=1) + # reopen the db with fewer columns tdb = await api_connection.open_table_db(TEST_DB, 1) async with tdb: - - # Should fail access to second column + # Should fail access to second column with pytest.raises(veilid.VeilidAPIErrorGeneric) as exc: - await tdb.load(b"qwer", col = 1) - - # Should succeed with access to second column - assert await tdb2.load(b"qwer", col = 1) == b"5678" - + await tdb.load(b"qwer", col=1) + + # Should succeed with access to second column + assert await tdb2.load(b"qwer", col=1) == b"5678" + # now delete should succeed deleted = await api_connection.delete_table_db(TEST_DB) assert deleted - diff --git a/veilid-python/veilid/api.py b/veilid-python/veilid/api.py index b8d824cf..3bd43fe7 100644 --- a/veilid-python/veilid/api.py +++ b/veilid-python/veilid/api.py @@ -6,7 +6,6 @@ from .state import VeilidState class RoutingContext(ABC): - async def __aenter__(self) -> Self: return self @@ -23,21 +22,21 @@ class RoutingContext(ABC): pass @abstractmethod - async def with_privacy(self, release = True) -> Self: + async def with_privacy(self, release=True) -> Self: pass @abstractmethod - async def with_custom_privacy(self, safety_selection: types.SafetySelection, release = True) -> Self: + async def with_custom_privacy( + self, safety_selection: types.SafetySelection, release=True + ) -> Self: pass @abstractmethod - async def with_sequencing(self, sequencing: types.Sequencing, release = True) -> Self: + async def with_sequencing(self, sequencing: types.Sequencing, release=True) -> Self: pass @abstractmethod - async def app_call( - self, target: types.TypedKey | types.RouteId, request: bytes - ) -> bytes: + async def app_call(self, target: types.TypedKey | types.RouteId, request: bytes) -> bytes: pass @abstractmethod @@ -166,7 +165,6 @@ class TableDb(ABC): class CryptoSystem(ABC): - async def __aenter__(self) -> Self: return self @@ -183,9 +181,7 @@ class CryptoSystem(ABC): pass @abstractmethod - async def cached_dh( - self, key: types.PublicKey, secret: types.SecretKey - ) -> types.SharedSecret: + async def cached_dh(self, key: types.PublicKey, secret: types.SecretKey) -> types.SharedSecret: pass @abstractmethod @@ -211,9 +207,7 @@ class CryptoSystem(ABC): pass @abstractmethod - async def derive_shared_secret( - self, password: bytes, salt: bytes - ) -> types.SharedSecret: + async def derive_shared_secret(self, password: bytes, salt: bytes) -> types.SharedSecret: pass @abstractmethod @@ -233,9 +227,7 @@ class CryptoSystem(ABC): pass @abstractmethod - async def validate_key_pair( - self, key: types.PublicKey, secret: types.SecretKey - ) -> bool: + async def validate_key_pair(self, key: types.PublicKey, secret: types.SecretKey) -> bool: pass @abstractmethod @@ -255,9 +247,7 @@ class CryptoSystem(ABC): pass @abstractmethod - async def verify( - self, key: types.PublicKey, data: bytes, signature: types.Signature - ): + async def verify(self, key: types.PublicKey, data: bytes, signature: types.Signature): pass @abstractmethod diff --git a/veilid-python/veilid/error.py b/veilid-python/veilid/error.py index dfd7ff9a..17459fcd 100644 --- a/veilid-python/veilid/error.py +++ b/veilid-python/veilid/error.py @@ -13,6 +13,10 @@ class VeilidAPIError(Exception): def __init__(self, *args, **kwargs): super().__init__(self.label, *args, **kwargs) + def __str__(self) -> str: + args = [('label', self.label)] + sorted(vars(self).items()) + return " ".join(f"{key}={value!r}" for (key, value) in args) + @classmethod def from_json(cls, json: dict) -> Self: kind = json["kind"] diff --git a/veilid-python/veilid/json_api.py b/veilid-python/veilid/json_api.py index 6b1dd72f..bc3ff194 100644 --- a/veilid-python/veilid/json_api.py +++ b/veilid-python/veilid/json_api.py @@ -6,20 +6,45 @@ from typing import Awaitable, Callable, Optional, Self from jsonschema import exceptions, validators from . import schema -from .api import (CryptoSystem, RoutingContext, TableDb, TableDbTransaction, - VeilidAPI) +from .api import CryptoSystem, RoutingContext, TableDb, TableDbTransaction, VeilidAPI from .error import raise_api_result -from .operations import (CryptoSystemOperation, Operation, - RoutingContextOperation, TableDbOperation, - TableDbTransactionOperation) +from .operations import ( + CryptoSystemOperation, + Operation, + RoutingContextOperation, + TableDbOperation, + TableDbTransactionOperation, +) from .state import VeilidState, VeilidUpdate -from .types import (CryptoKey, CryptoKeyDistance, CryptoKind, - DHTRecordDescriptor, DHTSchema, HashDigest, KeyPair, - NewPrivateRouteResult, Nonce, OperationId, PublicKey, - RouteId, SafetySelection, SecretKey, Sequencing, SharedSecret, Signature, - Stability, Timestamp, TypedKey, TypedKeyPair, - TypedSignature, ValueData, ValueSubkey, VeilidJSONEncoder, - VeilidVersion, urlsafe_b64decode_no_pad) +from .types import ( + CryptoKey, + CryptoKeyDistance, + CryptoKind, + DHTRecordDescriptor, + DHTSchema, + HashDigest, + KeyPair, + NewPrivateRouteResult, + Nonce, + OperationId, + PublicKey, + RouteId, + SafetySelection, + SecretKey, + Sequencing, + SharedSecret, + Signature, + Stability, + Timestamp, + TypedKey, + TypedKeyPair, + TypedSignature, + ValueData, + ValueSubkey, + VeilidJSONEncoder, + VeilidVersion, + urlsafe_b64decode_no_pad, +) ############################################################## @@ -200,10 +225,7 @@ class _JsonVeilidAPI(VeilidAPI): self.writer.write(reqbytes) async def send_ndjson_request( - self, - op: Operation, - validate: Optional[Callable[[dict, dict], None]] = None, - **kwargs + self, op: Operation, validate: Optional[Callable[[dict, dict], None]] = None, **kwargs ) -> dict: # Get next id await self.lock.acquire() @@ -249,9 +271,7 @@ class _JsonVeilidAPI(VeilidAPI): return response async def control(self, args: list[str]) -> str: - return raise_api_result( - await self.send_ndjson_request(Operation.CONTROL, args=args) - ) + return raise_api_result(await self.send_ndjson_request(Operation.CONTROL, args=args)) async def get_state(self) -> VeilidState: return VeilidState.from_json( @@ -266,9 +286,7 @@ class _JsonVeilidAPI(VeilidAPI): async def new_private_route(self) -> tuple[RouteId, bytes]: return NewPrivateRouteResult.from_json( - raise_api_result( - await self.send_ndjson_request(Operation.NEW_PRIVATE_ROUTE) - ) + raise_api_result(await self.send_ndjson_request(Operation.NEW_PRIVATE_ROUTE)) ).to_tuple() async def new_custom_private_route( @@ -288,17 +306,13 @@ class _JsonVeilidAPI(VeilidAPI): 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 - ) + 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 - ) + await self.send_ndjson_request(Operation.RELEASE_PRIVATE_ROUTE, route_id=route_id) ) async def app_call_reply(self, call_id: OperationId, message: bytes): @@ -309,9 +323,7 @@ class _JsonVeilidAPI(VeilidAPI): ) async def new_routing_context(self) -> RoutingContext: - rc_id = raise_api_result( - await self.send_ndjson_request(Operation.NEW_ROUTING_CONTEXT) - ) + rc_id = raise_api_result(await self.send_ndjson_request(Operation.NEW_ROUTING_CONTEXT)) return _JsonRoutingContext(self, rc_id) async def open_table_db(self, name: str, column_count: int) -> TableDb: @@ -334,9 +346,7 @@ class _JsonVeilidAPI(VeilidAPI): return _JsonCryptoSystem(self, cs_id) async def best_crypto_system(self) -> CryptoSystem: - cs_id = raise_api_result( - await self.send_ndjson_request(Operation.BEST_CRYPTO_SYSTEM) - ) + cs_id = raise_api_result(await self.send_ndjson_request(Operation.BEST_CRYPTO_SYSTEM)) return _JsonCryptoSystem(self, cs_id) async def verify_signatures( @@ -375,27 +385,19 @@ class _JsonVeilidAPI(VeilidAPI): map( lambda x: TypedKeyPair(x), raise_api_result( - await self.send_ndjson_request( - Operation.GENERATE_KEY_PAIR, kind=kind - ) + await self.send_ndjson_request(Operation.GENERATE_KEY_PAIR, kind=kind) ), ) ) async def now(self) -> Timestamp: - return Timestamp( - raise_api_result(await self.send_ndjson_request(Operation.NOW)) - ) + return Timestamp(raise_api_result(await self.send_ndjson_request(Operation.NOW))) async def debug(self, command: str) -> str: - return raise_api_result( - await self.send_ndjson_request(Operation.DEBUG, command=command) - ) + return raise_api_result(await self.send_ndjson_request(Operation.DEBUG, command=command)) async def veilid_version_string(self) -> str: - return raise_api_result( - await self.send_ndjson_request(Operation.VEILID_VERSION_STRING) - ) + return raise_api_result(await self.send_ndjson_request(Operation.VEILID_VERSION_STRING)) async def veilid_version(self) -> VeilidVersion: v = await self.send_ndjson_request(Operation.VEILID_VERSION) @@ -424,11 +426,9 @@ class _JsonRoutingContext(RoutingContext): if not self.done: # attempt to clean up server-side anyway self.api.send_one_way_ndjson_request( - Operation.ROUTING_CONTEXT, - rc_id=self.rc_id, - rc_op=RoutingContextOperation.RELEASE + Operation.ROUTING_CONTEXT, rc_id=self.rc_id, rc_op=RoutingContextOperation.RELEASE ) - + # complain raise AssertionError("Should have released routing context before dropping object") @@ -442,11 +442,11 @@ class _JsonRoutingContext(RoutingContext): Operation.ROUTING_CONTEXT, validate=validate_rc_op, rc_id=self.rc_id, - rc_op=RoutingContextOperation.RELEASE + rc_op=RoutingContextOperation.RELEASE, ) self.done = True - async def with_privacy(self, release = True) -> Self: + async def with_privacy(self, release=True) -> Self: new_rc_id = raise_api_result( await self.api.send_ndjson_request( Operation.ROUTING_CONTEXT, @@ -459,7 +459,7 @@ class _JsonRoutingContext(RoutingContext): await self.release() return self.__class__(self.api, new_rc_id) - async def with_custom_privacy(self, safety_selection: SafetySelection, release = True) -> Self: + async def with_custom_privacy(self, safety_selection: SafetySelection, release=True) -> Self: new_rc_id = raise_api_result( await self.api.send_ndjson_request( Operation.ROUTING_CONTEXT, @@ -473,7 +473,7 @@ class _JsonRoutingContext(RoutingContext): await self.release() return self.__class__(self.api, new_rc_id) - async def with_sequencing(self, sequencing: Sequencing, release = True) -> Self: + async def with_sequencing(self, sequencing: Sequencing, release=True) -> Self: new_rc_id = raise_api_result( await self.api.send_ndjson_request( Operation.ROUTING_CONTEXT, @@ -664,7 +664,9 @@ class _JsonTableDbTransaction(TableDbTransaction): ) # complain - raise AssertionError("Should have committed or rolled back transaction before dropping object") + raise AssertionError( + "Should have committed or rolled back transaction before dropping object" + ) def is_done(self) -> bool: return self.done @@ -672,7 +674,7 @@ class _JsonTableDbTransaction(TableDbTransaction): async def commit(self): if self.done: raise AssertionError("Transaction is already done") - + raise_api_result( await self.api.send_ndjson_request( Operation.TABLE_DB_TRANSACTION, @@ -736,12 +738,9 @@ class _JsonTableDb(TableDb): def __del__(self): if not self.done: - # attempt to clean up server-side anyway self.api.send_one_way_ndjson_request( - Operation.TABLE_DB, - db_id=self.db_id, - db_op=TableDbOperation.RELEASE + Operation.TABLE_DB, db_id=self.db_id, db_op=TableDbOperation.RELEASE ) # complain @@ -757,11 +756,10 @@ class _JsonTableDb(TableDb): Operation.TABLE_DB, validate=validate_db_op, db_id=self.db_id, - db_op=TableDbOperation.RELEASE + db_op=TableDbOperation.RELEASE, ) self.done = True - async def get_column_count(self) -> int: return raise_api_result( await self.api.send_ndjson_request( @@ -859,12 +857,9 @@ class _JsonCryptoSystem(CryptoSystem): def __del__(self): if not self.done: - # attempt to clean up server-side anyway self.api.send_one_way_ndjson_request( - Operation.CRYPTO_SYSTEM, - cs_id=self.cs_id, - cs_op=CryptoSystemOperation.RELEASE + Operation.CRYPTO_SYSTEM, cs_id=self.cs_id, cs_op=CryptoSystemOperation.RELEASE ) # complain @@ -872,7 +867,7 @@ class _JsonCryptoSystem(CryptoSystem): def is_done(self) -> bool: return self.done - + async def release(self): if self.done: return @@ -880,7 +875,7 @@ class _JsonCryptoSystem(CryptoSystem): Operation.CRYPTO_SYSTEM, validate=validate_cs_op, cs_id=self.cs_id, - cs_op=CryptoSystemOperation.RELEASE + cs_op=CryptoSystemOperation.RELEASE, ) self.done = True @@ -1142,9 +1137,7 @@ class _JsonCryptoSystem(CryptoSystem): ) ) - async def crypt_no_auth( - self, body: bytes, nonce: Nonce, shared_secret: SharedSecret - ) -> bytes: + async def crypt_no_auth(self, body: bytes, nonce: Nonce, shared_secret: SharedSecret) -> bytes: return urlsafe_b64decode_no_pad( raise_api_result( await self.api.send_ndjson_request( diff --git a/veilid-python/veilid/operations.py b/veilid-python/veilid/operations.py index 297a0c77..51067803 100644 --- a/veilid-python/veilid/operations.py +++ b/veilid-python/veilid/operations.py @@ -1,6 +1,7 @@ from enum import StrEnum from typing import Self + class Operation(StrEnum): CONTROL = "Control" GET_STATE = "GetState" @@ -28,6 +29,7 @@ class Operation(StrEnum): VEILID_VERSION_STRING = "VeilidVersionString" VEILID_VERSION = "VeilidVersion" + class RoutingContextOperation(StrEnum): INVALID_ID = "InvalidId" RELEASE = "Release" @@ -45,6 +47,7 @@ class RoutingContextOperation(StrEnum): WATCH_DHT_VALUES = "WatchDhtValues" CANCEL_DHT_WATCH = "CancelDhtWatch" + class TableDbOperation(StrEnum): INVALID_ID = "InvalidId" RELEASE = "Release" @@ -55,6 +58,7 @@ class TableDbOperation(StrEnum): LOAD = "Load" DELETE = "Delete" + class TableDbTransactionOperation(StrEnum): INVALID_ID = "InvalidId" COMMIT = "Commit" @@ -62,6 +66,7 @@ class TableDbTransactionOperation(StrEnum): STORE = "Store" DELETE = "Delete" + class CryptoSystemOperation(StrEnum): INVALID_ID = "InvalidId" RELEASE = "Release" @@ -85,7 +90,8 @@ class CryptoSystemOperation(StrEnum): DECRYPT_AEAD = "DecryptAead" ENCRYPT_AEAD = "EncryptAead" CRYPT_NO_AUTH = "CryptNoAuth" - + + class RecvMessageType(StrEnum): RESPONSE = "Response" UPDATE = "Update" diff --git a/veilid-python/veilid/state.py b/veilid-python/veilid/state.py index 7d464c70..6aab549d 100644 --- a/veilid-python/veilid/state.py +++ b/veilid-python/veilid/state.py @@ -2,9 +2,18 @@ from enum import StrEnum from typing import Optional, Self from .config import VeilidConfig -from .types import (ByteCount, RouteId, Timestamp, TimestampDuration, TypedKey, - ValueData, ValueSubkey, VeilidLogLevel, OperationId, - urlsafe_b64decode_no_pad) +from .types import ( + ByteCount, + RouteId, + Timestamp, + TimestampDuration, + TypedKey, + ValueData, + ValueSubkey, + VeilidLogLevel, + OperationId, + urlsafe_b64decode_no_pad, +) class AttachmentState(StrEnum): @@ -200,9 +209,7 @@ class PeerTableData: @classmethod def from_json(cls, j: dict) -> Self: """JSON object hook""" - return cls( - j["node_ids"], j["peer_address"], PeerStats.from_json(j["peer_stats"]) - ) + return cls(j["node_ids"], j["peer_address"], PeerStats.from_json(j["peer_stats"])) class VeilidStateNetwork: @@ -276,9 +283,7 @@ class VeilidLog: message: str backtrace: Optional[str] - def __init__( - self, 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 @@ -349,9 +354,7 @@ class VeilidValueChange: count: int value: ValueData - def __init__( - self, 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 diff --git a/veilid-python/veilid/types.py b/veilid-python/veilid/types.py index 984e8cba..a9a80587 100644 --- a/veilid-python/veilid/types.py +++ b/veilid-python/veilid/types.py @@ -1,8 +1,8 @@ import base64 import json from enum import StrEnum -from typing import Any, Optional, Self, Tuple from functools import total_ordering +from typing import Any, Optional, Self, Tuple #################################################################### @@ -83,6 +83,7 @@ class SafetySelectionKind(StrEnum): UNSAFE = "Unsafe" SAFE = "Safe" + #################################################################### @@ -249,7 +250,12 @@ class VeilidVersion: return False def __eq__(self, other): - return isinstance(other, VeilidVersion) and self.data == other.data and self.seq == other.seq and self.writer == other.writer + return ( + isinstance(other, VeilidVersion) + and self.data == other.data + and self.seq == other.seq + and self.writer == other.writer + ) @property def major(self): @@ -319,8 +325,7 @@ class DHTSchema: if DHTSchemaKind(j["kind"]) == DHTSchemaKind.SMPL: return cls.smpl( j["o_cnt"], - [DHTSchemaSMPLMember.from_json(member) - for member in j["members"]], + [DHTSchemaSMPLMember.from_json(member) for member in j["members"]], ) raise Exception("Unknown DHTSchema kind", j["kind"]) @@ -346,13 +351,15 @@ class DHTRecordDescriptor: self.owner_secret = owner_secret self.schema = schema + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(key={self.key!r})>" + @classmethod def from_json(cls, j: dict) -> Self: return cls( TypedKey(j["key"]), PublicKey(j["owner"]), - None if j["owner_secret"] is None else SecretKey( - j["owner_secret"]), + None if j["owner_secret"] is None else SecretKey(j["owner_secret"]), DHTSchema.from_json(j["schema"]), ) @@ -371,6 +378,9 @@ class ValueData: self.data = data self.writer = writer + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(seq={self.seq!r}, data={self.data!r}, writer={self.writer!r})>" + def __lt__(self, other): if other is None: return True @@ -387,7 +397,12 @@ class ValueData: return False def __eq__(self, other): - return isinstance(other, ValueData) and self.data == other.data and self.seq == other.seq and self.writer == other.writer + return ( + isinstance(other, ValueData) + and self.data == other.data + and self.seq == other.seq + and self.writer == other.writer + ) @classmethod def from_json(cls, j: dict) -> Self: @@ -403,13 +418,20 @@ class ValueData: #################################################################### + class SafetySpec: preferred_route: Optional[RouteId] hop_count: int stability: Stability sequencing: Sequencing - def __init__(self, preferred_route: Optional[RouteId], hop_count: int, stability: Stability, sequencing: Sequencing): + def __init__( + self, + preferred_route: Optional[RouteId], + hop_count: int, + stability: Stability, + sequencing: Sequencing, + ): self.preferred_route = preferred_route self.hop_count = hop_count self.stability = stability @@ -417,10 +439,12 @@ class SafetySpec: @classmethod def from_json(cls, j: dict) -> Self: - return cls(RouteId(j["preferred_route"]) if "preferred_route" in j else None, - j["hop_count"], - Stability(j["stability"]), - Sequencing(j["sequencing"])) + return cls( + RouteId(j["preferred_route"]) if "preferred_route" in j else None, + j["hop_count"], + Stability(j["stability"]), + Sequencing(j["sequencing"]), + ) def to_json(self) -> dict: return self.__dict__