Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filters and reverse ordering in Noter and Notifier #816

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion src/keri/app/notifying.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is an appropriate approach for filtering messages in LMDB. It requires deserializing the data and analyzing it and it hard codes the fields you can search for.

If you look at the Seeker class in KERIA you'll see an approach to creating LMDB indexes that are more flexible and more performant than this approach.

"""
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.
Expand Down Expand Up @@ -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)
31 changes: 31 additions & 0 deletions src/keri/db/dbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'.'):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have a method that implements something similar to this and is called getIoValsAllPreBackIter. This name should match.

"""
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''):
"""
Expand Down
72 changes: 72 additions & 0 deletions tests/app/test_notifying.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand Down
7 changes: 7 additions & 0 deletions tests/db/test_dbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 )]
Expand Down
Loading