From 6be8d8f3089a003e832c9a4008bb19d23d3e6b28 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Fri, 9 Aug 2024 15:53:16 -0600 Subject: [PATCH 1/6] fix comments --- src/keri/core/coring.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/keri/core/coring.py b/src/keri/core/coring.py index be89859d..f4facb5a 100644 --- a/src/keri/core/coring.py +++ b/src/keri/core/coring.py @@ -3417,13 +3417,11 @@ def _derive(clas, sad: dict, *, raw, proto, kind, sad, version = sizeify(ked=sad, kind=kind) ser = dict(sad) - if ignore: + if ignore: # delete ignore fields in said calculation from ser dict for f in ignore: del ser[f] - # string now has correct size - # sad as 'v' verision string then use its kind otherwise passed in kind - cpa = clas._serialize(ser, kind=kind) # raw pos arg class + cpa = clas._serialize(ser, kind=kind) # serialize ser return (Diger._digest(ser=cpa, code=code), sad) # raw digest and sad From 8c038c9f18d6b29769bedb3eafdea465018e5d08 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Sat, 10 Aug 2024 16:23:51 -0600 Subject: [PATCH 2/6] refactor Bexter to add class Method so external Labeler can decode. --- src/keri/core/coring.py | 101 +++++++++++++++++++++++++++++++++++--- tests/core/test_coring.py | 23 +++++++-- 2 files changed, 111 insertions(+), 13 deletions(-) diff --git a/src/keri/core/coring.py b/src/keri/core/coring.py index f4facb5a..973f5e50 100644 --- a/src/keri/core/coring.py +++ b/src/keri/core/coring.py @@ -2013,7 +2013,7 @@ class Tagger(Matter): composable (bool): True when .qb64b and .qb2 are 24 bit aligned and round trip Properties: - tag (str): B64 primitive without prepad (alias of .soft) + tag (str): B64 .soft portion of code but without prepad Inherited Hidden: (See Matter) @@ -2660,14 +2660,15 @@ def _rawify(self, bext): raw = decodeB64(base)[ls:] # convert and remove leader return raw # raw binary equivalent of text - @property - def bext(self): - """ - Property bext: Base64 text value portion of qualified b64 str - Returns the value portion of .qb64 with text code and leader removed + @classmethod + def _decode(cls, raw, code): + """Returns decoded raw as B64 str aka bext value + + Returns: + bext (str): decoded raw as B64 str aka bext value """ - _, _, _, _, ls = self.Sizes[self.code] - bext = encodeB64(bytes([0] * ls) + self.raw) + _, _, _, _, ls = cls.Sizes[code] + bext = encodeB64(bytes([0] * ls) + raw) ws = 0 if ls == 0 and bext: if bext[0] == ord(b'A'): # strip leading 'A' zero pad @@ -2677,6 +2678,24 @@ def bext(self): return bext.decode('utf-8')[ws:] + @property + def bext(self): + """ + Property bext: Base64 text value portion of qualified b64 str + Returns the value portion of .qb64 with text code and leader removed + """ + return self._decode(raw=self.raw, code=self.code) + #_, _, _, _, ls = self.Sizes[self.code] + #bext = encodeB64(bytes([0] * ls) + self.raw) + #ws = 0 + #if ls == 0 and bext: + #if bext[0] == ord(b'A'): # strip leading 'A' zero pad + #ws = 1 + #else: + #ws = (ls + 1) % 4 + #return bext.decode('utf-8')[ws:] + + class Pather(Bexter): """ Pather is a subclass of Bexter that provides SAD Path language specific functionality @@ -2909,6 +2928,72 @@ def _resolve(self, val, ptr): return self._resolve(cur, ptr) +class Labeler(Matter): + """ + Labeler is subclass of Matter for CESR native field map labels and/or generic + textual field values. Labeler auto sizes the instance code to minimize + the total encoded size of associated field label or textual field value. + + + + Attributes: + + Inherited Properties: + (See Matter) + + + Properties: + label (str): base value without encoding + + Inherited Hidden: + (See Matter) + + Hidden: + _label (str): base value without encoding + + Methods: + + """ + + + def __init__(self, label='', code=None, **kwa): + """ + Inherited Parameters: + (see Matter) + + Parameters: + label (str | bytes): base value before encoding + + """ + self._label = None + if label: + if hasattr(label, "decode"): # make label a str + label = label.decode("utf-8") + + if not Reb64.match(label.encode("utf-8")): + raise InvalidSoftError(f"Non Base64 chars in {tag=}.") + + self._label = label + + + super(Labeler, self).__init__(code=code, **kwa) + + + + if self.code not in LabelDex: + raise InvalidCodeError(f"Invalid code={self.code} for Labeler.") + + @property + def label(self): + """Returns: + label (str): base value without encoding + + getter for ._label. Makes ._label read only + + """ + return self._label + + class Verfer(Matter): """ diff --git a/tests/core/test_coring.py b/tests/core/test_coring.py index 9261b016..d4b52f67 100644 --- a/tests/core/test_coring.py +++ b/tests/core/test_coring.py @@ -40,7 +40,7 @@ from keri.core import coring from keri.core.coring import (Saids, Sadder, Tholder, Seqner, NumDex, Number, Dater, Bexter, Texter, - TagDex, Tagger, Ilker, Traitor, + TagDex, Tagger, Ilker, Traitor, Labeler, Verser, Versage, ) from keri.core.coring import Kindage, Kinds from keri.core.coring import (Sizage, MtrDex, Matter) @@ -4270,6 +4270,19 @@ def test_pather(): """ Done Test """ +def test_labeler(): + """ + Test Labeler subclass of Matter + """ + + + with pytest.raises(EmptyMaterialError): + labeler = Labeler() # defaults + + + """ Done Test """ + + def test_verfer(): """ Test the support functionality for verifier subclass of crymat @@ -5390,14 +5403,14 @@ def test_tholder(): test_traitor() test_verser() test_diger() - #test_texter() + test_texter() + test_bexter() + test_labeler() #test_prodex() - #test_indexer() test_number() #test_seqner() #test_siger() #test_nexter() #test_tholder() - #test_labels() - #test_prefixer() + test_prefixer() From c1798db6653be6265f12a69c57817fcdbf9e69d7 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Sat, 10 Aug 2024 16:32:30 -0600 Subject: [PATCH 3/6] Finished refactor Bexter so rawify and derawify can be called externally as static and class methods --- src/keri/core/coring.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/keri/core/coring.py b/src/keri/core/coring.py index 973f5e50..dd36a691 100644 --- a/src/keri/core/coring.py +++ b/src/keri/core/coring.py @@ -2646,7 +2646,8 @@ def __init__(self, raw=None, qb64b=None, qb64=None, qb2=None, raise ValidationError("Invalid code = {} for Bexter." "".format(self.code)) - def _rawify(self, bext): + @staticmethod + def _rawify(bext): """Returns raw value equivalent of Base64 text. Suitable for variable sized matter @@ -2661,7 +2662,7 @@ def _rawify(self, bext): return raw # raw binary equivalent of text @classmethod - def _decode(cls, raw, code): + def _derawify(cls, raw, code): """Returns decoded raw as B64 str aka bext value Returns: @@ -2684,16 +2685,7 @@ def bext(self): Property bext: Base64 text value portion of qualified b64 str Returns the value portion of .qb64 with text code and leader removed """ - return self._decode(raw=self.raw, code=self.code) - #_, _, _, _, ls = self.Sizes[self.code] - #bext = encodeB64(bytes([0] * ls) + self.raw) - #ws = 0 - #if ls == 0 and bext: - #if bext[0] == ord(b'A'): # strip leading 'A' zero pad - #ws = 1 - #else: - #ws = (ls + 1) % 4 - #return bext.decode('utf-8')[ws:] + return self._derawify(raw=self.raw, code=self.code) class Pather(Bexter): From 90c30a7841a15ac69aa6fb167199c697419e84b7 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Sun, 11 Aug 2024 14:23:01 -0600 Subject: [PATCH 4/6] Started creating Labeler class functionality --- src/keri/core/coring.py | 111 ++++++++++++++++++++++++++++++++------- src/keri/help/helping.py | 4 +- 2 files changed, 95 insertions(+), 20 deletions(-) diff --git a/src/keri/core/coring.py b/src/keri/core/coring.py index dd36a691..691ae0a9 100644 --- a/src/keri/core/coring.py +++ b/src/keri/core/coring.py @@ -550,6 +550,46 @@ def __iter__(self): TagDex = TagCodex() # Make instance +@dataclass(frozen=True) +class LabelCodex: + """ + LabelCodex is codex of. + + Only provide defined codes. + Undefined are left out so that inclusion(exclusion) via 'in' operator works. + """ + Tag1: str = '0J' # 1 B64 char tag with 1 pre pad + Tag2: str = '0K' # 2 B64 char tag + Tag3: str = 'X' # 3 B64 char tag + Tag4: str = '1AAF' # 4 B64 char tag + Tag5: str = '0L' # 5 B64 char tag with 1 pre pad + Tag6: str = '0M' # 6 B64 char tag + Tag7: str = 'Y' # 7 B64 char tag + Tag8: str = '1AAN' # 8 B64 char tag + Tag9: str = '0N' # 9 B64 char tag with 1 pre pad + Tag10: str = '0O' # 10 B64 char tag + StrB64_L0: str = '4A' # String Base64 Only Leader Size 0 + StrB64_L1: str = '5A' # String Base64 Only Leader Size 1 + StrB64_L2: str = '6A' # String Base64 Only Leader Size 2 + StrB64_Big_L0: str = '7AAA' # String Base64 Only Big Leader Size 0 + StrB64_Big_L1: str = '8AAA' # String Base64 Only Big Leader Size 1 + StrB64_Big_L2: str = '9AAA' # String Base64 Only Big Leader Size 2 + Label1: str = 'V' # Label1 1 bytes for label lead size 1 + Label2: str = 'W' # Label2 2 bytes for label lead size 0 + Bytes_L0: str = '4B' # Byte String lead size 0 + Bytes_L1: str = '5B' # Byte String lead size 1 + Bytes_L2: str = '6B' # Byte String lead size 2 + Bytes_Big_L0: str = '7AAB' # Byte String big lead size 0 + Bytes_Big_L1: str = '8AAB' # Byte String big lead size 1 + Bytes_Big_L2: str = '9AAB' # Byte String big lead size 2 + + def __iter__(self): + return iter(astuple(self)) + + +LabelDex = LabelCodex() # Make instance + + @dataclass(frozen=True) class PreCodex: @@ -2059,16 +2099,13 @@ def __init__(self, tag='', soft='', code=None, **kwa): """ if tag: - if hasattr(tag, "decode"): # make tag str - tag = tag.decode("utf-8") - if not Reb64.match(tag.encode("utf-8")): + if hasattr(tag, "encode"): # make tag bytes for regex + tag = tag.encode("utf-8") + + if not Reb64.match(tag): raise InvalidSoftError(f"Non Base64 chars in {tag=}.") - # TagDex tags appear in order of size 1 to 10, at indices 0 to 9 - codes = astuple(TagDex) - l = len(tag) # soft not empty so l > 0 - if l > len(codes): - raise InvalidSoftError("Oversized tag={soft}.") - code = codes[l-1] # get code for for tag of len where (index = len - 1) + + code = self._codify(tag=tag) soft = tag @@ -2077,6 +2114,27 @@ def __init__(self, tag='', soft='', code=None, **kwa): if (not self._special(self.code)) or self.code not in TagDex: raise InvalidCodeError(f"Invalid code={self.code} for Tagger.") + + @staticmethod + def _codify(tag): + """Returns code for tag when tag is appropriately sized Base64 + + Parameters: + tag (str | bytes): Base64 value + + Returns: + code (str): derivation code for tag + + """ + # TagDex tags appear in order of size 1 to 10, at indices 0 to 9 + codes = astuple(TagDex) + l = len(tag) + if l < 1 or l > len(codes): + raise InvalidSoftError(f"Invalid {tag=} size {l=}, empty or oversized.") + return codes[l-1] # return code at index = len - 1 + + + @property def tag(self): """Returns: @@ -2948,7 +3006,7 @@ class Labeler(Matter): """ - def __init__(self, label='', code=None, **kwa): + def __init__(self, label='', raw=None, code=None, soft=None, **kwa): """ Inherited Parameters: (see Matter) @@ -2958,17 +3016,34 @@ def __init__(self, label='', code=None, **kwa): """ self._label = None - if label: - if hasattr(label, "decode"): # make label a str - label = label.decode("utf-8") - - if not Reb64.match(label.encode("utf-8")): - raise InvalidSoftError(f"Non Base64 chars in {tag=}.") - self._label = label + if label: + if hasattr(label, "encode"): # make label bytes + label = label.encode("utf-8") + + if Reb64.match(label): # candidate for Base64 compact encoding + try: + code = Tagger._codify(tag=label) + soft = label + except InvalidSoftError as ex: # too big + if label[0] != b'A': # use Bexter code + code = MtrDex.StrB64_L0 + raw = Bexter.rawify(label) + else: # use Texter code + code = MtrDex.Bytes_L0 + raw = label + else: + if len(label) == 1: + code = MtrDex.Label1 + elif len(label) == 2: + code = MtrDex.Label2 + else: + code = MtrDex.Bytes_L0 + raw = label + self._label = label.decode() # convert bytes to str - super(Labeler, self).__init__(code=code, **kwa) + super(Labeler, self).__init__(raw=raw, code=code, soft=soft, **kwa) diff --git a/src/keri/help/helping.py b/src/keri/help/helping.py index 5333887e..63bac1a9 100644 --- a/src/keri/help/helping.py +++ b/src/keri/help/helping.py @@ -264,9 +264,9 @@ def fromIso8601(dts): B64_CHARS = tuple(B64ChrByIdx.values()) # tuple of characters in Base64 -B64REX = rb'^[0-9A-Za-z_-]*\Z' # [A-Za-z0-9\-\_] +B64REX = rb'^[0-9A-Za-z_-]*\Z' # [A-Za-z0-9\-\_] bytes Reb64 = re.compile(B64REX) # compile is faster -# Usage: if Reb64.match(bext): or if not Reb64.match(bext): +# Usage: if Reb64.match(bext): or if not Reb64.match(bext): bext is bytes def intToB64(i, l=1): From 56535e32c9ae6609b8064bd6695a25b05cfe6ae9 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Sun, 11 Aug 2024 14:29:43 -0600 Subject: [PATCH 5/6] more refactor Labeler --- src/keri/core/coring.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/keri/core/coring.py b/src/keri/core/coring.py index 691ae0a9..53a7ba2d 100644 --- a/src/keri/core/coring.py +++ b/src/keri/core/coring.py @@ -3046,10 +3046,11 @@ def __init__(self, label='', raw=None, code=None, soft=None, **kwa): super(Labeler, self).__init__(raw=raw, code=code, soft=soft, **kwa) - if self.code not in LabelDex: raise InvalidCodeError(f"Invalid code={self.code} for Labeler.") + # need way to extract label from raw and code + @property def label(self): """Returns: From 227f9b0cf74577fea6edd4854696004d86d0e459 Mon Sep 17 00:00:00 2001 From: Samuel M Smith Date: Sun, 11 Aug 2024 20:12:09 -0600 Subject: [PATCH 6/6] Labeler finished with unit tests --- src/keri/core/coring.py | 43 +++++---- tests/core/test_coring.py | 184 +++++++++++++++++++++++++++++++++++++- 2 files changed, 204 insertions(+), 23 deletions(-) diff --git a/src/keri/core/coring.py b/src/keri/core/coring.py index 53a7ba2d..99fe7e18 100644 --- a/src/keri/core/coring.py +++ b/src/keri/core/coring.py @@ -2999,7 +2999,6 @@ class Labeler(Matter): (See Matter) Hidden: - _label (str): base value without encoding Methods: @@ -3015,8 +3014,6 @@ def __init__(self, label='', raw=None, code=None, soft=None, **kwa): label (str | bytes): base value before encoding """ - self._label = None - if label: if hasattr(label, "encode"): # make label bytes label = label.encode("utf-8") @@ -3025,41 +3022,49 @@ def __init__(self, label='', raw=None, code=None, soft=None, **kwa): try: code = Tagger._codify(tag=label) soft = label + except InvalidSoftError as ex: # too big - if label[0] != b'A': # use Bexter code - code = MtrDex.StrB64_L0 - raw = Bexter.rawify(label) - else: # use Texter code - code = MtrDex.Bytes_L0 + if label[0] != ord(b'A'): # use Bexter code + code = LabelDex.StrB64_L0 + raw = Bexter._rawify(label) + + else: # use Texter code since ambiguity if starts with 'A' + code = LabelDex.Bytes_L0 raw = label + else: if len(label) == 1: - code = MtrDex.Label1 + code = LabelDex.Label1 + elif len(label) == 2: - code = MtrDex.Label2 + code = LabelDex.Label2 + else: - code = MtrDex.Bytes_L0 - raw = label + code = LabelDex.Bytes_L0 - self._label = label.decode() # convert bytes to str + raw = label super(Labeler, self).__init__(raw=raw, code=code, soft=soft, **kwa) - if self.code not in LabelDex: raise InvalidCodeError(f"Invalid code={self.code} for Labeler.") - # need way to extract label from raw and code + @property def label(self): - """Returns: + """Extracts and returns label from .code and .soft or .code and .raw + + Returns: label (str): base value without encoding + """ + if self.code in TagDex: # tag + return self.soft # soft part of code - getter for ._label. Makes ._label read only + if self.code in BexDex: # bext + return Bexter._derawify(raw=self.raw, code=self.code) # derawify - """ - return self._label + return self.raw.decode() # everything else is just raw as str diff --git a/tests/core/test_coring.py b/tests/core/test_coring.py index d4b52f67..e4118949 100644 --- a/tests/core/test_coring.py +++ b/tests/core/test_coring.py @@ -33,14 +33,15 @@ from keri.kering import Protocols, Protocolage, Ilkage, Ilks, TraitDex from keri.help import helping -from keri.help.helping import (sceil, intToB64, intToB64b, b64ToInt, codeB64ToB2, codeB2ToB64, +from keri.help.helping import (sceil, intToB64, intToB64b, b64ToInt, + codeB64ToB2, codeB2ToB64, B64_CHARS, Reb64, nabSextets) from keri import core from keri.core import coring from keri.core.coring import (Saids, Sadder, Tholder, Seqner, NumDex, Number, Dater, Bexter, Texter, - TagDex, Tagger, Ilker, Traitor, Labeler, + TagDex, Tagger, Ilker, Traitor, Labeler, LabelDex, Verser, Versage, ) from keri.core.coring import Kindage, Kinds from keri.core.coring import (Sizage, MtrDex, Matter) @@ -4274,11 +4275,186 @@ def test_labeler(): """ Test Labeler subclass of Matter """ - - with pytest.raises(EmptyMaterialError): labeler = Labeler() # defaults + # test taggable label + label = 'z' + raw = b'' + code = LabelDex.Tag1 + qb64 = '0J_z' + qb2 = decodeB64(qb64) + + labeler = Labeler(label=label) + assert labeler.label == label + assert labeler.code == code + assert labeler.soft == label + assert labeler.raw == raw + assert labeler.qb64 == qb64 + assert labeler.qb2 == qb2 + + labeler = Labeler(raw=raw, code=code, soft=label) + assert labeler.label == label + + labeler = Labeler(qb64=qb64) + assert labeler.label == label + + labeler = Labeler(qb2=qb2) + assert labeler.label == label + + + # Test all sizes taggable labels + labels = ('A', 'AB', 'ABC', 'ABCD', 'ABCDE', 'ABCDEF', 'ABCDEFG', 'ABCDEFGH', + 'ABCDEFGHI', 'ABCDEFGHIJ') + + raw = b'' + for i, label in enumerate(labels): + code = astuple(LabelDex)[i] + xs = Matter._xtraSize(code) + qb64 = code + ('_' * xs) + label + qb2 = decodeB64(qb64) + + labeler = Labeler(label=label) + assert labeler.label == label + assert labeler.code == code + assert labeler.soft == label + assert labeler.raw == raw + assert labeler.qb64 == qb64 + assert labeler.qb2 == qb2 + + labeler = Labeler(raw=raw, code=code, soft=label) + assert labeler.label == label + + labeler = Labeler(qb64=qb64) + assert labeler.label == label + + labeler = Labeler(qb2=qb2) + assert labeler.label == label + + + # test bextable labels + label = 'zyxwvutsrqponm' + code = LabelDex.StrB64_L1 + qb64 = '5AAEAAzyxwvutsrqponm' + qb2 = decodeB64(qb64) + raw = qb2[4:] # skip 3 for code and 1 for lead pad + + labeler = Labeler(label=label) + assert labeler.label == label + assert labeler.code == code + rs = (len(label) + len(label) % 4) // 4 + assert labeler.soft == intToB64(rs, 2) == 'AE' + assert labeler.raw == raw + assert labeler.qb64 == qb64 + assert labeler.qb2 == qb2 + + labeler = Labeler(raw=raw, code=code, soft=label) + assert labeler.label == label + + labeler = Labeler(qb64=qb64) + assert labeler.label == label + + labeler = Labeler(qb2=qb2) + assert labeler.label == label + + # test textable labels + # fixed size short + label = '@' + code = LabelDex.Label1 + raw = label.encode() + qb64 = 'VABA' + qb2 = decodeB64(qb64) # b'T\x00@' + + labeler = Labeler(label=label) + assert labeler.label == label + assert labeler.code == code + assert labeler.soft == '' + assert labeler.raw == raw + assert labeler.qb64 == qb64 + assert labeler.qb2 == qb2 + + labeler = Labeler(raw=raw, code=code) + assert labeler.label == label + + labeler = Labeler(qb64=qb64) + assert labeler.label == label + + labeler = Labeler(qb2=qb2) + assert labeler.label == label + + label = '!$' + code = LabelDex.Label2 + raw = label.encode() + qb64 = 'WCEk' + qb2 = decodeB64(qb64) # b'X!$' + + labeler = Labeler(label=label) + assert labeler.label == label + assert labeler.code == code + assert labeler.soft == '' + assert labeler.raw == raw + assert labeler.qb64 == qb64 + assert labeler.qb2 == qb2 + + labeler = Labeler(raw=raw, code=code) + assert labeler.label == label + + labeler = Labeler(qb64=qb64) + assert labeler.label == label + + labeler = Labeler(qb2=qb2) + assert labeler.label == label + + + # variable sized + label = '#yxwvutsrqponm' + code = LabelDex.Bytes_L1 + raw = label.encode() + qb64 = '5BAFACN5eHd2dXRzcnFwb25t' + qb2 = decodeB64(qb64) + + + labeler = Labeler(label=label) + assert labeler.label == label + assert labeler.code == code + assert labeler.soft == 'AF' + assert labeler.raw == raw + assert labeler.qb64 == qb64 + assert labeler.qb2 == qb2 + + labeler = Labeler(raw=raw, code=code) + assert labeler.label == label + + labeler = Labeler(qb64=qb64) + assert labeler.label == label + + labeler = Labeler(qb2=qb2) + assert labeler.label == label + + # test base64 that starts with 'A' get encoded as textable, is not bextable + label = 'Ayxwvutsrqponm' + code = LabelDex.Bytes_L1 + raw = label.encode() + qb64 = '5BAFAEF5eHd2dXRzcnFwb25t' + qb2 = decodeB64(qb64) + + + labeler = Labeler(label=label) + assert labeler.label == label + assert labeler.code == code + assert labeler.soft == 'AF' + assert labeler.raw == raw + assert labeler.qb64 == qb64 + assert labeler.qb2 == qb2 + + labeler = Labeler(raw=raw, code=code) + assert labeler.label == label + + labeler = Labeler(qb64=qb64) + assert labeler.label == label + + labeler = Labeler(qb2=qb2) + assert labeler.label == label """ Done Test """