diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef707d8..7687c259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ - Improved `TypedXXX` conversion traits, including to and from `Vec` - Ensure utf8 replacement characters are never emitted in logs +- veilid-python: + - Correction of type hints + - Fixed transaction `__aexit__` to properly rollback transaction if not committed, and not raise an exception + - veilid-server: - Use `detect_address_changes: auto` by default diff --git a/veilid-python/tests/test_dht.py b/veilid-python/tests/test_dht.py index 582fce5d..75d70f9a 100644 --- a/veilid-python/tests/test_dht.py +++ b/veilid-python/tests/test_dht.py @@ -135,6 +135,8 @@ async def test_open_writer_dht_value(api_connection: veilid.VeilidAPI): key = rec.key owner = rec.owner secret = rec.owner_secret + assert secret is not None + #print(f"key:{key}") cs = await api_connection.get_crypto_system(rec.key.kind()) @@ -151,6 +153,7 @@ async def test_open_writer_dht_value(api_connection: veilid.VeilidAPI): assert vdtemp is None vdtemp = await rc.get_dht_value(key, ValueSubkey(1), False) + assert vdtemp is not None assert vdtemp.data == va assert vdtemp.seq == 0 assert vdtemp.writer == owner @@ -164,9 +167,11 @@ async def test_open_writer_dht_value(api_connection: veilid.VeilidAPI): await sync(rc, [rec]) vdtemp = await rc.get_dht_value(key, ValueSubkey(0), True) + assert vdtemp is not None assert vdtemp.data == vb vdtemp = await rc.get_dht_value(key, ValueSubkey(1), True) + assert vdtemp is not None assert vdtemp.data == va # Equal value should not trigger sequence number update @@ -191,7 +196,7 @@ async def test_open_writer_dht_value(api_connection: veilid.VeilidAPI): assert rec.key == key assert rec.owner == owner assert rec.owner_secret == secret - assert rec.schema.kind == veilid.DHTSchemaKind.DFLT + assert isinstance(rec.schema, veilid.DHTSchemaDFLT) assert rec.schema.o_cnt == 2 # Verify subkey 1 can be set before it is get but newer is available online @@ -225,7 +230,7 @@ async def test_open_writer_dht_value(api_connection: veilid.VeilidAPI): assert rec.key == key assert rec.owner == owner assert rec.owner_secret is None - assert rec.schema.kind == veilid.DHTSchemaKind.DFLT + assert isinstance(rec.schema, veilid.DHTSchemaDFLT) assert rec.schema.o_cnt == 2 # Verify subkey 1 can NOT be set because we have the wrong writer @@ -261,6 +266,7 @@ async def test_open_writer_dht_value_no_offline(api_connection: veilid.VeilidAPI key = rec.key owner = rec.owner secret = rec.owner_secret + assert secret is not None #print(f"key:{key}") cs = await api_connection.get_crypto_system(rec.key.kind()) @@ -277,6 +283,7 @@ async def test_open_writer_dht_value_no_offline(api_connection: veilid.VeilidAPI assert vdtemp is None vdtemp = await rc.get_dht_value(key, ValueSubkey(1), False) + assert vdtemp is not None assert vdtemp.data == va assert vdtemp.seq == 0 assert vdtemp.writer == owner @@ -287,12 +294,12 @@ async def test_open_writer_dht_value_no_offline(api_connection: veilid.VeilidAPI vdtemp = await rc.set_dht_value(key, ValueSubkey(0), vb, veilid.SetDHTValueOptions(None, False)) assert vdtemp is None - await sync(rc, [rec]) - vdtemp = await rc.get_dht_value(key, ValueSubkey(0), True) + assert vdtemp is not None assert vdtemp.data == vb vdtemp = await rc.get_dht_value(key, ValueSubkey(1), True) + assert vdtemp is not None assert vdtemp.data == va # Equal value should not trigger sequence number update @@ -316,7 +323,7 @@ async def test_open_writer_dht_value_no_offline(api_connection: veilid.VeilidAPI assert rec.key == key assert rec.owner == owner assert rec.owner_secret == secret - assert rec.schema.kind == veilid.DHTSchemaKind.DFLT + assert isinstance(rec.schema, veilid.DHTSchemaDFLT) assert rec.schema.o_cnt == 2 # Verify subkey 1 can be set before it is get but newer is available online @@ -348,7 +355,7 @@ async def test_open_writer_dht_value_no_offline(api_connection: veilid.VeilidAPI assert rec.key == key assert rec.owner == owner assert rec.owner_secret is None - assert rec.schema.kind == veilid.DHTSchemaKind.DFLT + assert isinstance(rec.schema, veilid.DHTSchemaDFLT) assert rec.schema.o_cnt == 2 # Verify subkey 1 can NOT be set because we have the wrong writer @@ -457,9 +464,11 @@ async def test_watch_dht_values(): upd = await asyncio.wait_for(value_change_queue.get(), timeout=10) # Server 0: Verify the update came back with the first changed subkey's data + assert isinstance(upd.detail, veilid.VeilidValueChange) assert upd.detail.key == rec0.key assert upd.detail.count == 0xFFFFFFFE assert upd.detail.subkeys == [(3, 3)] + assert upd.detail.value is not None assert upd.detail.value.data == b"BLAH BLAH" # Server 1: Now set subkey and trigger an update @@ -471,9 +480,11 @@ async def test_watch_dht_values(): upd = await asyncio.wait_for(value_change_queue.get(), timeout=10) # Server 0: Verify the update came back with the first changed subkey's data + assert isinstance(upd.detail, veilid.VeilidValueChange) assert upd.detail.key == rec0.key assert upd.detail.count == 0xFFFFFFFD assert upd.detail.subkeys == [(4, 4)] + assert upd.detail.value is not None assert upd.detail.value.data == b"BZORT" # Server 0: Cancel some subkeys we don't care about @@ -489,9 +500,11 @@ async def test_watch_dht_values(): upd = await asyncio.wait_for(value_change_queue.get(), timeout=10) # Server 0: Verify only one update came back + assert isinstance(upd.detail, veilid.VeilidValueChange) assert upd.detail.key == rec0.key assert upd.detail.count == 0xFFFFFFFC assert upd.detail.subkeys == [(4, 4)] + assert upd.detail.value is not None assert upd.detail.value.data == b"BZORT BZORT" # Server 0: Now we should NOT get any other update @@ -512,6 +525,7 @@ async def test_watch_dht_values(): upd = await asyncio.wait_for(value_change_queue.get(), timeout=10) # Server 0: Verify only one update came back + assert isinstance(upd.detail, veilid.VeilidValueChange) assert upd.detail.key == rec0.key assert upd.detail.count == 0 assert upd.detail.subkeys == [] @@ -618,6 +632,7 @@ async def test_watch_many_dht_values(): # Server 0: Wait for the update try: upd = await asyncio.wait_for(value_change_queue.get(), timeout=10) + assert isinstance(upd.detail, veilid.VeilidValueChange) missing_records.remove(upd.detail.key) except: # Dump which records didn't get updates @@ -689,6 +704,7 @@ async def _run_test_schema_limit(api_connection: veilid.VeilidAPI, open_record: desc1 = await rc.open_dht_record(desc.key) for n in range(count): vd0 = await rc.get_dht_value(desc1.key, ValueSubkey(n)) + assert vd0 is not None assert vd0.data == test_data print(f' {n}') @@ -728,7 +744,7 @@ async def test_schema_limit_dflt(api_connection: veilid.VeilidAPI): @pytest.mark.asyncio async def test_schema_limit_smpl(api_connection: veilid.VeilidAPI): - async def open_record(rc: veilid.RoutingContext, count: int) -> tuple[veilid.TypedKey, veilid.PublicKey, veilid.SecretKey]: + async def open_record(rc: veilid.RoutingContext, count: int) -> tuple[veilid.DHTRecordDescriptor, veilid.KeyPair]: cs = await api_connection.best_crypto_system() async with cs: writer_keypair = await cs.generate_key_pair() @@ -817,6 +833,7 @@ async def test_dht_integration_writer_reader(): desc1 = await rc1.open_dht_record(desc.key) vd1 = await rc1.get_dht_value(desc1.key, ValueSubkey(0)) + assert vd1 is not None assert vd1.data == TEST_DATA await rc1.close_dht_record(desc1.key) @@ -877,9 +894,11 @@ async def test_dht_write_read_local(): desc1 = await rc0.open_dht_record(desc0.key) vd0 = await rc0.get_dht_value(desc1.key, ValueSubkey(0), force_refresh=True) + assert vd0 is not None assert vd0.data == TEST_DATA vd1 = await rc0.get_dht_value(desc1.key, ValueSubkey(1), force_refresh=True) + assert vd1 is not None assert vd1.data == TEST_DATA2 await rc0.close_dht_record(desc1.key) @@ -1082,8 +1101,8 @@ async def sync_win( max(0, int(cur_cols/2) - int(WIDTH/2))) win.border(0,0,0,0) win.addstr(1, 1, "syncing records to the network", curses.color_pair(0)) - for n, rr in enumerate(records): - key = rr.key + for n, rd in enumerate(records): + key = rd.key win.addstr(n+2, GRAPHWIDTH+1, key, curses.color_pair(0)) if key in donerecords: diff --git a/veilid-python/veilid/api.py b/veilid-python/veilid/api.py index 523a4296..66b335d5 100644 --- a/veilid-python/veilid/api.py +++ b/veilid-python/veilid/api.py @@ -49,7 +49,7 @@ class RoutingContext(ABC): 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, message: bytes) -> bytes: pass @abstractmethod @@ -132,7 +132,7 @@ class TableDbTransaction(ABC): async def __aexit__(self, *excinfo): self.ref_count -= 1 if self.ref_count == 0 and not self.is_done(): - await self.release() + await self.rollback() @abstractmethod def is_done(self) -> bool: diff --git a/veilid-python/veilid/state.py b/veilid-python/veilid/state.py index c28256f7..a9ec0c2e 100644 --- a/veilid-python/veilid/state.py +++ b/veilid-python/veilid/state.py @@ -522,7 +522,7 @@ class VeilidAppCall: message: bytes call_id: OperationId - def __init__(self, sender: Optional[TypedKey], route_id: Optional[TypedKey], message: bytes, call_id: OperationId): + def __init__(self, sender: Optional[TypedKey], route_id: Optional[RouteId], message: bytes, call_id: OperationId): self.sender = sender self.route_id = route_id self.message = message diff --git a/veilid-python/veilid/types.py b/veilid-python/veilid/types.py index 343bcd09..08762cd5 100644 --- a/veilid-python/veilid/types.py +++ b/veilid-python/veilid/types.py @@ -1,8 +1,9 @@ import base64 import json +from abc import ABC, abstractmethod from enum import StrEnum from functools import total_ordering -from typing import Any, Optional, Self +from typing import Any, Optional, Self, final #################################################################### @@ -267,9 +268,9 @@ class VeilidVersion: def __eq__(self, other): return ( isinstance(other, VeilidVersion) - and self.data == other.data - and self.seq == other.seq - and self.writer == other.writer + and self._major == other._major + and self._minor == other._minor + and self._patch == other._patch ) @property @@ -323,26 +324,19 @@ class DHTSchemaSMPLMember: return self.__dict__ -class DHTSchema: +class DHTSchema(ABC): kind: DHTSchemaKind - def __init__(self, kind: DHTSchemaKind, **kwargs): + def __init__(self, kind: DHTSchemaKind): self.kind = kind - for k, v in kwargs.items(): - setattr(self, k, v) @classmethod def dflt(cls, o_cnt: int) -> Self: - assert isinstance(o_cnt, int) - return cls(DHTSchemaKind.DFLT, o_cnt=o_cnt) + return DHTSchemaDFLT(o_cnt=o_cnt) # type: ignore @classmethod def smpl(cls, o_cnt: int, members: list[DHTSchemaSMPLMember]) -> Self: - assert isinstance(o_cnt, int) - assert isinstance(members, list) - for m in members: - assert isinstance(m, DHTSchemaSMPLMember) - return cls(DHTSchemaKind.SMPL, o_cnt=o_cnt, members=members) + return DHTSchemaSMPL(o_cnt=o_cnt, members=members) # type: ignore @classmethod def from_json(cls, j: dict) -> Self: @@ -358,6 +352,53 @@ class DHTSchema: def to_json(self) -> dict: return self.__dict__ +@final +class DHTSchemaDFLT(DHTSchema): + o_cnt: int + + def __init__( + self, + o_cnt: int + ): + super().__init__(DHTSchemaKind.DFLT) + + assert isinstance(o_cnt, int) + self.o_cnt = o_cnt + + + @classmethod + def from_json(cls, j: dict) -> Self: + if DHTSchemaKind(j["kind"]) == DHTSchemaKind.DFLT: + return cls(j["o_cnt"]) + raise Exception("Invalid DHTSchemaDFLT") + + +@final +class DHTSchemaSMPL(DHTSchema): + o_cnt: int + members: list[DHTSchemaSMPLMember] + + def __init__( + self, + o_cnt: int, + members: list[DHTSchemaSMPLMember] + ): + super().__init__(DHTSchemaKind.SMPL) + + assert isinstance(o_cnt, int) + assert isinstance(members, list) + for m in members: + assert isinstance(m, DHTSchemaSMPLMember) + + self.o_cnt = o_cnt + self.members = members + + @classmethod + def from_json(cls, j: dict) -> Self: + if DHTSchemaKind(j["kind"]) == DHTSchemaKind.SMPL: + return cls(j["o_cnt"], + [DHTSchemaSMPLMember.from_json(member) for member in j["members"]]) + raise Exception("Invalid DHTSchemaSMPL") class DHTRecordDescriptor: key: TypedKey @@ -381,6 +422,8 @@ class DHTRecordDescriptor: return f"<{self.__class__.__name__}(key={self.key!r}, owner={self.owner!r}, owner_secret={self.owner_secret!r}, schema={self.schema!r})>" def owner_key_pair(self) -> Optional[KeyPair]: + if self.owner_secret is None: + return None return KeyPair.from_parts(self.owner, self.owner_secret) @classmethod @@ -435,7 +478,7 @@ class SetDHTValueOptions: writer: Optional[KeyPair] allow_offline: Optional[bool] - def __init__(self, writer: Optional[KeyPair], allow_offline: Optional[bool] = None): + def __init__(self, writer: Optional[KeyPair] = None, allow_offline: Optional[bool] = None): self.writer = writer self.allow_offline = allow_offline @@ -534,22 +577,20 @@ class SafetySpec: def to_json(self) -> dict: return self.__dict__ +class SafetySelection(ABC): -class SafetySelection: - kind: SafetySelectionKind - - def __init__(self, kind: SafetySelectionKind, **kwargs): - self.kind = kind - for k, v in kwargs.items(): - setattr(self, k, v) + @property + @abstractmethod + def kind(self) -> SafetySelectionKind: + pass @classmethod def unsafe(cls, sequencing: Sequencing = Sequencing.PREFER_ORDERED) -> Self: - return cls(SafetySelectionKind.UNSAFE, sequencing=sequencing) + return SafetySelectionUnsafe(sequencing=sequencing) # type: ignore @classmethod def safe(cls, safety_spec: SafetySpec) -> Self: - return cls(SafetySelectionKind.SAFE, safety_spec=safety_spec) + return SafetySelectionSafe(safety_spec=safety_spec) # type: ignore @classmethod def from_json(cls, j: dict) -> Self: @@ -559,10 +600,48 @@ class SafetySelection: return cls.unsafe(Sequencing(j["Unsafe"])) raise Exception("Invalid SafetySelection") + @abstractmethod def to_json(self) -> dict: - if self.kind == SafetySelectionKind.UNSAFE: - return {"Unsafe": self.sequencing} - elif self.kind == SafetySelectionKind.SAFE: - return {"Safe": self.safety_spec.to_json()} - else: - raise Exception("Invalid SafetySelection") + pass + +@final +class SafetySelectionUnsafe(SafetySelection): + sequencing: Sequencing + + def __init__(self, sequencing: Sequencing = Sequencing.PREFER_ORDERED): + assert isinstance(sequencing, Sequencing) + self.sequencing = sequencing + + @property + def kind(self): + return SafetySelectionKind.UNSAFE + + @classmethod + def from_json(cls, j: dict) -> Self: + if "Unsafe" in j: + return cls(Sequencing(j["Unsafe"])) + raise Exception("Invalid SafetySelectionUnsafe") + + def to_json(self) -> dict: + return {"Unsafe": self.sequencing} + +@final +class SafetySelectionSafe(SafetySelection): + safety_spec: SafetySpec + + def __init__(self, safety_spec: SafetySpec): + assert isinstance(safety_spec, SafetySpec) + self.safety_spec = safety_spec + + @property + def kind(self): + return SafetySelectionKind.SAFE + + @classmethod + def from_json(cls, j: dict) -> Self: + if "Safe" in j: + return cls(SafetySpec.from_json(j["Safe"])) + raise Exception("Invalid SafetySelectionUnsafe") + + def to_json(self) -> dict: + return {"Safe": self.safety_spec.to_json()}