From 8cde184183d07f9231bb6c0bf6ea639c877987be Mon Sep 17 00:00:00 2001 From: Rodolfo Miranda Date: Wed, 10 Jul 2024 09:50:58 -0300 Subject: [PATCH] notification filters --- src/keri/app/notifying.py | 86 ++++++++++++++++++++++++++++++++++++- src/keri/db/dbing.py | 31 +++++++++++++ tests/app/test_notifying.py | 72 +++++++++++++++++++++++++++++++ tests/db/test_dbing.py | 7 +++ 4 files changed, 195 insertions(+), 1 deletion(-) diff --git a/src/keri/app/notifying.py b/src/keri/app/notifying.py index 371815f9b..71aba2aa6 100644 --- a/src/keri/app/notifying.py +++ b/src/keri/app/notifying.py @@ -188,6 +188,20 @@ def getItemIter(self, keys: Union[str, Iterable] = b""): for key, val in self.db.getAllItemIter(db=self.sdb, key=self._tokey(keys), split=False): yield self._tokeys(key), self.klas(raw=bytes(val)) + def getItemRvsdIter(self, keys: Union[str, Iterable] = b""): + """ Return reversed iterator over the all the items in subdb + + Parameters: + keys (tuple): of key strs to be combined in order to form key + + Returns: + iterator: of tuples of keys tuple and val serdering.SerderKERI for + each entry in db + + """ + for key, val in self.db.getAllItemRvsdIter(db=self.sdb, key=self._tokey(keys), split=False): + yield self._tokeys(key), self.klas(raw=bytes(val)) + def cntAll(self): """ Return count over the all the items in subdb @@ -323,7 +337,7 @@ def getNoteCnt(self): """ return self.notes.cntAll() - + def getNotes(self, start=0, end=25): """ Returns list of tuples (note, cigar) of notes for controller of agent @@ -353,7 +367,49 @@ def getNotes(self, start=0, end=25): break return notes + + def getFltNotes(self, start=0, end=25, rvsd=False, read=None, route=None): + """ + Returns list of tuples (note, cigar) of filtered notes for controller of agent + + Parameters: + start (int): number of item to start + end (int): number of last item to return + reversed (bool): reverse order of notes + read (bool): filter by read status + route (str): filter by route if provided in attrs + + """ + if hasattr(start, "isoformat"): + start = start.isoformat() + + notes = [] + it = self.notes.getItemIter(keys=()) if not rvsd else self.notes.getItemRvsdIter(keys=()) + idx = 0 + + for ((_, _), note) in it: + if (read is None or note.read == read) and (route is None or ('r' in note.attrs and note.attrs['r'] == route)): + if idx >= start and (end == -1 or idx <= end): + cig = self.ncigs.get(keys=(note.rid,)) + notes.append((note, cig)) + idx += 1 + + return notes + + def getFltNoteCnt(self, read=None, route=None): + """ + Return count over filtered Notes + Returns: + int: count of all filtered items + + """ + it = self.notes.getItemIter(keys=()) + count = 0 + for ((_, _), note) in it: + if (read is None or note.read == read) and (route is None or ('r' in note.attrs and note.attrs['r'] == route)): + count += 1 + return count class Notifier: """ Class for sending notifications to the controller of an agent. @@ -497,3 +553,31 @@ def getNotes(self, start=0, end=24): notes.append(note) return notes + + def getFltNotes(self, start=0, end=24, rvsd=False, read=None, route=None): + """ + Returns list of tuples (note, cigar) of notes for controller of agent + + Parameters: + start (int): number of item to start + end (int): number of last item to return + + """ + notesigs = self.noter.getFltNotes(start, end, rvsd, read, route) + notes = [] + for note, cig in notesigs: + if not self.hby.signator.verify(ser=note.raw, cigar=cig): + raise kering.ValidationError("note stored without valid signature") + + notes.append(note) + + return notes + def getFltNoteCnt(self, read=None, route=None): + """ + Return count over the all Notes + + Returns: + int: count of all items + + """ + return self.noter.getFltNoteCnt(read, route) diff --git a/src/keri/db/dbing.py b/src/keri/db/dbing.py index 1dc1208bc..b91794f8b 100644 --- a/src/keri/db/dbing.py +++ b/src/keri/db/dbing.py @@ -581,6 +581,37 @@ def getAllItemIter(self, db, key=b'', split=True, sep=b'.'): splits = (bytes(key), val) yield tuple(splits) + def getAllItemRvsdIter(self, db, key=b'', split=True, sep=b'.'): + """ + Returns iterator of item duple (key, val), at each key over all + keys in db in reverser order. If split is true then the key is split at sep and instead + of returing duple it results tuple with one entry for each key split + as well as the value. + + Works for both dupsort==False and dupsort==True + + Raises StopIteration Error when empty. + + Parameters: + db is opened named sub db with dupsort=False + key is key location in db to resume replay, + If empty then last key in database + split (bool): True means split key at sep before returning + sep (bytes): separator char for key + """ + with self.env.begin(db=db, write=False, buffers=True) as txn: + cursor = txn.cursor() + if key == b'' or not cursor.set_key(key): # moves to val at key = key, first if empty + cursor.last() + + for key, val in cursor.iterprev(): # return key, val at cursor + if split: + splits = bytes(key).split(sep) + splits.append(val) + else: + splits = (bytes(key), val) + yield tuple(splits) + def getTopItemIter(self, db, key=b''): """ diff --git a/tests/app/test_notifying.py b/tests/app/test_notifying.py index b02bd91c2..d779ba40d 100644 --- a/tests/app/test_notifying.py +++ b/tests/app/test_notifying.py @@ -168,6 +168,46 @@ def test_noter(): cnt = noter.getNoteCnt() assert cnt == 13 + # test reversed iteration + notes = noter.getFltNotes(rvsd=True) + assert notes[0][0].attrs['a'] == 9 + assert notes[1][0].attrs['a'] == 8 + assert notes[2][0].attrs['a'] == 7 + assert notes[3][0].attrs['a'] == 6 + assert notes[4][0].attrs['a'] == 5 + assert notes[5][0].attrs['a'] == 4 + assert notes[9][0].attrs['a'] == 0 + + # test reversed and paginated iteration + notes = noter.getFltNotes(start=5, end=7, rvsd=True) + assert notes[0][0].attrs['a'] == 4 + assert notes[1][0].attrs['a'] == 3 + assert notes[2][0].attrs['a'] == 2 + + # test filter by route + note = notifying.notice(attrs=dict(r='/multisig/rev')) + assert noter.add(note, cig) is True + notes = noter.getFltNotes(route='/multisig/rev') + assert notes[0][0].attrs['r'] == '/multisig/rev' + assert len(notes) == 1 + + cnt = noter.getFltNoteCnt(route='/multisig/rev') + assert cnt == 1 + + # test filter by read status + note = notifying.notice(attrs=dict(a=11), read=True) + assert noter.add(note, cig) is True + note = notifying.notice(attrs=dict(a=12), read=True) + assert noter.add(note, cig) is True + notes = noter.getFltNotes(read=True) + assert notes[0][0].read == True + assert notes[0][0].attrs['a'] == 11 + assert notes[1][0].read == True + assert notes[1][0].attrs['a'] == 12 + assert len(notes) == 2 + + cnt = noter.getFltNoteCnt(read=True) + assert cnt == 2 def test_notifier(): with habbing.openHby(name="test") as hby: @@ -208,6 +248,38 @@ def test_notifier(): notes = notifier.getNotes() assert len(notes) == 3 + # test reversed iteration + notes = notifier.getFltNotes(rvsd=True) + assert notes[0].attrs['a'] == 3 + assert notes[1].attrs['a'] == 2 + assert notes[2].attrs['a'] == 1 + + # test reversed and paginated iteration + notes = notifier.getFltNotes(start=1, end=2, rvsd=True) + assert notes[0].attrs['a'] == 2 + assert notes[1].attrs['a'] == 1 + + # test filter by read status + notes = notifier.getNotes() + assert notifier.mar(notes[1].rid) is True + assert notifier.mar(notes[2].rid) is True + notes = notifier.getFltNotes(read=True) + assert notes[0].attrs['a'] == 2 + assert notes[1].attrs['a'] == 3 + + cnt = notifier.getFltNoteCnt(read=True) + assert cnt == 2 + + # test filter by route + assert notifier.add(attrs=dict(r='/multisig/rev')) is True + notes = notifier.getFltNotes(route='/multisig/rev') + assert notes[0].attrs['r'] == '/multisig/rev' + assert len(notes) == 1 + + cnt = notifier.getFltNoteCnt(route='/multisig/rev') + assert cnt == 1 + + payload = dict(a=1, b=2, c=3) dt = helping.fromIso8601("2022-07-08T15:01:05.453632") cig = coring.Cigar(qb64="AABr1EJXI1sTuI51TXo4F1JjxIJzwPeCxa-Cfbboi7F4Y4GatPEvK629M7G_5c86_Ssvwg8POZWNMV-WreVqBECw") diff --git a/tests/db/test_dbing.py b/tests/db/test_dbing.py index c88cca4f8..e569b69bf 100644 --- a/tests/db/test_dbing.py +++ b/tests/db/test_dbing.py @@ -277,6 +277,13 @@ def test_lmdber(): in dber.getAllItemIter(db=db)] == [(b'a', b'1', b'wow'), (b'a', b'2', b'wee'), (b'b', b'1', b'woo')] + + # Test reversed getAllItemRvsdIter + assert [(bytes(pre), bytes(num), bytes(val)) for pre, num, val + in dber.getAllItemRvsdIter(db=db)] == [(b'b', b'1', b'woo'), + (b'a', b'2', b'wee'), + (b'a', b'1', b'wow')] + assert dber.delTopVal(db, key=b"a.") items = [ (key, bytes(val)) for key, val in dber.getTopItemIter(db=db )]