Retrieve data from db result before returning from Context Manager

SQLAlchemy's connection is a Context Manager and if we return a result from
code wrapped in a Context Manager, its cursor might already be closed.
This commit is contained in:
Piotr F. Mieszkowski 2024-01-20 18:52:47 +01:00
parent 8d2bf403a7
commit bfd3541b18
5 changed files with 30 additions and 15 deletions

View file

@ -41,6 +41,9 @@ def sub_queue(args):
elif args.list:
for k in queue.fetch_keys():
print(f'- {k.id}: {k.email}')
elif args.to_delete:
for k in queue.fetch_keys_to_delete():
print(f'- {k.id}: {k.email}')
else:
cnt = queue.count_keys()
if cnt is None:
@ -125,6 +128,8 @@ def main():
help='delete specified email from the queue')
cmd_queue.add_argument('-l', '--list', action='store_true',
help='list keys in the queue')
cmd_queue.add_argument('-d', '--to-delete', action='store_true',
help='list keys to be deleted')
cmd_queue.set_defaults(operation=sub_queue)
cmd_identities = sub_commands.add_parser('identities',

View file

@ -12,15 +12,22 @@ This definition includes:
import sqlalchemy
# Values for lacre_keys.status column:
# - ST_DEFAULT: initial state;
# - ST_IMPORTED: key has been successfully processed by cron job;
# - ST_TO_BE_DELETED: key can be deleted.
ST_DEFAULT, ST_IMPORTED, ST_TO_BE_DELETED = range(3)
# lacre_keys.confirmed is set to an empty string when a key is confirmed by the user.
CO_CONFIRMED = ''
_meta = sqlalchemy.MetaData()
LACRE_KEYS = sqlalchemy.Table('lacre_keys', _meta,
sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True),
sqlalchemy.Column('email', sqlalchemy.String(256)),
sqlalchemy.Column('email', sqlalchemy.String(256), index=True),
# ASCII-armored key
sqlalchemy.Column('publickey', sqlalchemy.Text),
# Empty string means this key has been confirmed.
sqlalchemy.Column('confirm', sqlalchemy.String(32)),
# Status: see ST_* constants at the top of the file.
sqlalchemy.Column('status', sqlalchemy.Integer),

View file

@ -139,33 +139,37 @@ class KeyConfirmationQueue:
def fetch_keys(self, /, max_keys=None):
"""Runs a query to retrieve at most `keys_read_max` keys and returns db result."""
max_keys = max_keys or self.keys_read_max
LOG.debug('Row limit: %d', max_keys)
selq = select(self._keys.c.publickey, self._keys.c.id, self._keys.c.email) \
.where(and_(self._keys.c.status == db.ST_DEFAULT, self._keys.c.confirm == "")) \
.where(and_(self._keys.c.status == db.ST_DEFAULT, self._keys.c.confirm == db.CO_CONFIRMED)) \
.limit(max_keys)
LOG.debug('Retrieving keys to be processed: %s -- %s', selq, selq.compile().params)
with self._engine.connect() as conn:
return conn.execute(selq)
return [e for e in conn.execute(selq)]
def count_keys(self):
selq = select(func.count(self._keys.c.id))
selq = select(func.count(self._keys.c.id)) \
.where(and_(self._keys.c.status == db.ST_DEFAULT, self._keys.c.confirm == db.CO_CONFIRMED))
LOG.debug('Counting all keys: %s -- %s', selq, selq.compile().params)
try:
with self._engine.connect() as conn:
c = [cnt for cnt in conn.execute(selq)]
# Result is an iterable of tuples:
return c[0][0]
res = conn.execute(selq)
# This is a 1-element tuple.
return res.one_or_none()[0]
except OperationalError:
LOG.exception('Cannot count keys')
return None
def fetch_keys_to_delete(self):
seldel = select(self._keys.c.email, self._keys.c.id).where(self._keys.c.status == db.ST_TO_BE_DELETED).limit(self.keys_read_max)
seldel = select(self._keys.c.email, self._keys.c.id) \
.where(self._keys.c.status == db.ST_TO_BE_DELETED) \
.limit(self.keys_read_max)
with self._engine.connect() as conn:
return conn.execute(seldel)
return [e for e in conn.execute(seldel)]
def delete_keys(self, row_id, /, email=None):
"""Remove key from the database."""
@ -178,7 +182,7 @@ class KeyConfirmationQueue:
with self._engine.connect() as conn:
LOG.debug('Deleting public keys associated with confirmed email: %s', delq)
return conn.execute(delq)
conn.execute(delq)
def delete_key_by_email(self, email):
"""Remove keys linked to the given email from the database."""

View file

@ -90,8 +90,8 @@ AQgHiHgEGBYIACAWIQQZz0tH7MnEevqE1L2W85/aDjG7ZwUCYdTF4AIbDAAKCRCW\n\
OjjB6xRD0Q2FN+alsNGCtdutAs18AZ5l33RMzws=\n\
=wWoq\n\
-----END PGP PUBLIC KEY BLOCK-----\
", "status": 0, "confirm": "", "time": None},
{"id": 3, "email": "cecil@lacre.io", "publickey": "RUBBISH", "status": 0, "confirm": "", "time": None}
", "status": 1, "confirm": "", "time": None},
{"id": 3, "email": "cecil@lacre.io", "publickey": "RUBBISH", "status": 2, "confirm": "", "time": None}
])
conn.execute(identities.insert(), [

View file

@ -94,8 +94,7 @@ try:
notify("PGP key deleted", "keyDeleted.md", email)
LOG.info('Cleaning up after a round of key confirmation')
stat2_result_set = key_queue.fetch_keys_to_delete()
for email, row_id in stat2_result_set:
for email, row_id in key_queue.fetch_keys_to_delete():
LOG.debug('Removing key from keyring: %s', email)
GnuPG.delete_key(key_dir, email)