Merge branch 'tek/python-chat-demo' into 'main'
Python chat demo A Python based demonstration app which facilitates a 1:1 conversation between two parties. Routing privacy is enabled in this initial version. See merge request veilid/veilid!106
This commit is contained in:
		
							
								
								
									
										195
									
								
								veilid-python/demo/chat.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										195
									
								
								veilid-python/demo/chat.py
									
									
									
									
									
										Executable file
									
								
							| @@ -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:]) | ||||
							
								
								
									
										26
									
								
								veilid-python/demo/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								veilid-python/demo/config.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||
| @@ -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 | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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"] | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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__ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user