If you want an OOP approach, you might try using the following prototype:
#! /usr/bin/env python3
from datetime import datetime
from pathlib import Path
from subprocess import run, CalledProcessError
from sys import intern
from uuid import UUID
DATABASE = Path(r'C:\Users\Stephen Chappell\Documents\Accounts Live.kdbx')
PASSWORD = r'password'
def main():
"""Get KeePass entries, check each one, and change if required."""
db = KPScript(DATABASE, PASSWORD)
for entry in filter(needs_expiration, db.list_entries()):
print('Updating:', {key: entry['S'][key] for key in
'Title UserName Password URL'.split()})
# Add 5 years to the last modification
# time for the expiry date and format.
last_modified = entry['TLM']
expiry_time = last_modified.replace(last_modified.year + 5)
db.edit_entry(entry['UUID'], True, expiry_time)
def needs_expiration(entry):
"""Check if an entry needs its expiration set."""
return not entry['EXP'] and \
entry['GRPN'] not in {'Recycle Bin', 'Templates'}
def _field_to_datetime(value):
"""Convert a timestamp into a datetime object."""
return datetime.strptime(value, KPScript.DATETIME_FORMAT)
class KPScript:
"""Reference: https://keepass.info/help/v2_dev/scr_sc_index.html"""
EXE = Path(r'C:\Program Files\KeePass Password Safe 2\KPScript.exe')
TIMEOUT = 2
DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
FIELD_TYPES = {
'UUID': UUID,
'GRPU': UUID,
'GRPN': intern,
'S': lambda s: s,
'TC': _field_to_datetime,
'TLM': _field_to_datetime,
'TE': _field_to_datetime,
'EXP': {'True': True, 'False': False}.__getitem__
}
def __init__(self, db_path, password=None, keyfile=None):
"""Initialize the attributes of a KPScript instance."""
self.__db_path = db_path
self.__password = password
self.__keyfile = keyfile
def __do_command(self, name, kwargs=None):
"""Help run whatever command is specified by name."""
args = [self.EXE, f'-c:{name}', self.__db_path]
if self.__password is not None:
args.append(f'-pw:{self.__password}')
if self.__keyfile is not None:
args.append(f'-keyfile:{self.__keyfile}')
if kwargs is not None:
for key, value in kwargs.items():
args.append(f'-{key}:{value}')
try:
completed_process = run(
args,
capture_output=True,
timeout=self.TIMEOUT,
check=True,
text=True
)
except CalledProcessError as error:
print('Error:', error.stdout)
raise error
else:
return completed_process.stdout.splitlines()
def list_groups(self):
"""This command lists all groups in a format that easily
machine-readable."""
...
def list_entries(self):
"""This command lists all entries in a format that easily
machine-readable."""
def parse_line():
nonlocal line_key, line_value
try:
new_line_key, new_line_value = line.split(': ', 1)
if new_line_key not in self.FIELD_TYPES.keys() | {'OK'}:
raise ValueError()
except ValueError:
append_value()
else:
line_key, line_value = new_line_key, new_line_value
if line_key == 'S':
parse_s()
else:
entry[line_key] = line_value
def parse_s():
nonlocal s_key, s_value
try:
s_key, s_value = line_value.split(' = ', 1)
except ValueError:
raise SyntaxError(line_value)
else:
s_field = entry.setdefault('S', {})
s_field[s_key] = s_value
def append_value():
if line_key == 'S':
entry['S'][s_key] += '\n' + line
else:
entry[line_key] += '\n' + line
def complete_entry():
return entry.keys() == self.FIELD_TYPES.keys()
def transform():
for key, value in self.FIELD_TYPES.items():
entry[key] = value(entry[key])
return entry
entry = {}
line_key = line_value = ''
s_key = s_value = ''
for line in self.__do_command('ListEntries'):
if line:
parse_line()
elif complete_entry():
yield transform()
entry = {}
else:
append_value()
if len(entry) != 1 or 'OK' not in entry:
raise ValueError('last entry is invalid')
def get_entry_string(self):
"""Retrieves the value of an entry string field."""
...
def add_entry(self):
"""This command adds an entry to the database."""
...
def edit_entry(self, uuid, expires=None, expiry_time=None):
"""This command edits existing entries."""
kwargs = {'refx-UUID': uuid.hex}
if expires is not None:
kwargs['setx-Expires'] = str(bool(expires)).lower()
if expiry_time is not None:
kwargs['setx-ExpiryTime'] = expiry_time.strftime(
self.DATETIME_FORMAT)
self.__do_command('EditEntry', kwargs)
def move_entry(self):
"""This command moves one or more existing entries."""
...
def delete_entry(self):
"""This command deletes one or more existing entries."""
...
def delete_all_entries(self):
"""This command deletes all entries (in all subgroups)."""
...
def import_(self):
"""This command imports a file into the database."""
...
def export(self):
"""This command exports (parts of) the database."""
...
def sync(self):
"""This command synchronizes the database with another one."""
...
def change_master_key(self):
"""This command changes the master key of the database."""
...
def detach_bins(self):
"""This command saves all entry attachments (into the directory of the
database) and removes them from the database."""
...
def gen_pw(self):
"""Generates passwords."""
...
def estimate_quality(self):
"""Estimates the quality (in bits) of the password specified via the
-text: parameter."""
...
if __name__ == '__main__':
main()